feat(redesign): subpage hero header + iconpicker portal + tighter gaps

Three threads bundled:

- PageHeader.svelte upgraded to a glass-card subpage hero matching
  the dashboard hero language, scaled down. New optional props (all
  backward-compatible — old callers keep working): emphasis (italic
  gradient word appended to title), crumb (uppercase mono kicker),
  count + countLabel (right-side mono meter), pills (status chips
  with tones: mint / sky / orchid / coral / citrus / primary).
- Providers page wired up first as the test surface: pulls live
  online/offline/checking counts from the existing health probe and
  shows a type-count pill. Locale keys added (en + ru) for the new
  copy ('Service / providers' wordmark, longer description).
- IconPicker dropdown was suffering the same backdrop-filter
  containing-block bug as IconGridSelect — repositioned popups
  rendered inside any glass form panel got clipped or floated to
  the bottom of the page. Now portals to <body> via the shared
  $lib/portal action and uses Aurora glass styling end-to-end
  (solid surface, gradient-active cell, glass-strong search input).
- Layout gaps tightened to match the mockup:
    * sidebar→content horizontal gap is now 18px flat (was 50px:
      the 18px shell-gap PLUS another 32px wrapper padding on each
      child of main). Dropped px-4/md:px-8 from the topbar wrapper
      and the per-page content wrapper — main's children sit flush
      at the column boundary.
    * topbar→content vertical gap reduced to 12px (was 16px / pt-4).

Build clean, 0 errors.
This commit is contained in:
2026-04-25 02:15:34 +03:00
parent 46a4a6ee29
commit 9733e5c122
6 changed files with 378 additions and 52 deletions
+23 -1
View File
@@ -54,6 +54,20 @@
let health = $state<Record<number, boolean | null>>({});
// Status pill row for the page header — derived from health probes.
const headerPills = $derived.by(() => {
const onlineCount = Object.values(health).filter(v => v === true).length;
const offlineCount = Object.values(health).filter(v => v === false).length;
const checkingCount = Math.max(0, providers.length - onlineCount - offlineCount);
const typeCount = new Set(providers.map(p => p.type)).size;
const pills: Array<{ label: string; tone: 'mint' | 'sky' | 'coral' | 'citrus' }> = [];
if (onlineCount > 0) pills.push({ label: `${onlineCount} ${t('providers.online')}`, tone: 'mint' });
if (offlineCount > 0) pills.push({ label: `${offlineCount} ${t('providers.offline')}`, tone: 'coral' });
if (checkingCount > 0 && providers.length > 0) pills.push({ label: `${checkingCount} ${t('providers.checking')}`, tone: 'citrus' });
if (typeCount > 0) pills.push({ label: `${typeCount} ${typeCount === 1 ? t('providers.typeSingular') : t('providers.typePlural')}`, tone: 'sky' });
return pills;
});
onMount(load);
async function load() {
try {
@@ -146,7 +160,15 @@
}
</script>
<PageHeader title={t('providers.title')} description={t('providers.description')}>
<PageHeader
title={t('providers.title')}
emphasis={t('providers.titleEmphasis')}
description={t('providers.description')}
crumb="Service · Connections"
count={providers.length}
countLabel={t('dashboard.providersShort')}
pills={headerPills}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('providers.cancel') : t('providers.addProvider')}
</Button>