feat(redesign): project mockup richness onto live dashboard

Bringing the Aurora dashboard mockup to feature parity in the real app:

- NavIcon component: thin-stroke SVG icon set covering ~30 nav-tier
  glyphs (dashboard, server, target, radar, bots, console, email,
  matrix, webhook, slack, bullhorn, settings, theme, search, logout,
  chevrons, plus/arrow). Falls back to MdiIcon for unknown names so
  every existing usage keeps working.
- layout.svelte: sidebar nav swapped from MdiIcon to NavIcon — dropping
  the dense filled glyphs in favour of soft 1.6px outlines that match
  the mockup. Main container max-width bumped from 5xl (1024px) to
  7xl (1280px) so the dashboard finally gets to breathe.
- +page.svelte (dashboard): full rewrite of the markup to project
  every mockup section onto live data:
    * Editorial Hero — 'Tonight, everything is flowing.' with gradient
      italic emphasis, live/attention pill driven by failure count,
      and a big mono throughput meter (24h events, armed/total,
      providers, targets) on the right.
    * Stats — kept the 4-card grid, refined accent palette per card.
    * Two-column row — Signal stream (events with full routing trail:
      tracker → provider, gradient avatar tile per event-type) on the
      left, On-watch provider deck (per-provider activity bar fill +
      live/idle pulse, derived from recent_events) on the right.
    * Pulse panel wrapping the existing EventChart with the new title
      treatment.
    * Active wires — Sankey-style 'Tracker → Target' route summary
      derived from notificationTrackers.tracker_targets + targetsCache
      with event counts per route.
    * Compose band — gradient call-to-action strip with new-tracker CTA.
- i18n: en.json + ru.json get the full set of new dashboard keys
  (hero copy, panel titles, compose band, wires labels).

Build: 0 errors, 57 pre-existing warnings unchanged.
This commit is contained in:
2026-04-25 01:27:08 +03:00
parent d9ef3c6cc3
commit d3210fd5ea
5 changed files with 1223 additions and 241 deletions
@@ -0,0 +1,92 @@
<script lang="ts">
/**
* Thin-stroke SVG icon set for navigation surfaces.
*
* Mirrors the visual language of the Aurora design mockups — soft outline
* glyphs at 1.6px stroke. Falls back to MdiIcon for any name we don't
* have a hand-drawn version of, so the existing navEntries config keeps
* working unchanged.
*/
import MdiIcon from './MdiIcon.svelte';
interface Props {
name: string;
size?: number;
}
const { name, size = 18 }: Props = $props();
</script>
{#if name === 'mdiViewDashboard'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l3-9 4 18 3-9h4"/></svg>
{:else if name === 'mdiServer'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>
{:else if name === 'mdiBellOutline' || name === 'mdiBell'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10 21a2 2 0 0 0 4 0"/></svg>
{:else if name === 'mdiConsoleLine'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9l3 3-3 3M13 15h4"/></svg>
{:else if name === 'mdiRobotOutline' || name === 'mdiRobot'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="6" width="16" height="14" rx="3"/><circle cx="9" cy="12" r="1.2"/><circle cx="15" cy="12" r="1.2"/><path d="M8 17c1.5 1 6.5 1 8 0M12 3v3"/></svg>
{:else if name === 'mdiTarget'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>
{:else if name === 'mdiCogOutline' || name === 'mdiCog'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
{:else if name === 'mdiRadar'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="8"/><path d="M12 4v3M12 17v3M4 12h3M17 12h3"/></svg>
{:else if name === 'mdiFileDocumentEdit'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="M14 2v6h6"/><path d="M18 14l3 3-5 5h-3v-3z"/></svg>
{:else if name === 'mdiCodeBracesBox'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 8a2 2 0 0 0-2 2v1.5a1 1 0 0 1-1 1 1 1 0 0 1 1 1V15a2 2 0 0 0 2 2M15 8a2 2 0 0 1 2 2v1.5a1 1 0 0 0 1 1 1 1 0 0 0-1 1V15a2 2 0 0 1-2 2"/></svg>
{:else if name === 'mdiPlayCircleOutline'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M10 9l5 3-5 3z" fill="currentColor"/></svg>
{:else if name === 'mdiSendCircle' || name === 'mdiSend'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4z"/></svg>
{:else if name === 'mdiEmailOutline'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 7 9-7"/></svg>
{:else if name === 'mdiMatrix'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="3" height="18"/><rect x="18" y="3" width="3" height="18"/><path d="M6 6h2M6 18h2M16 6h2M16 18h2"/></svg>
{:else if name === 'mdiWebhook'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="18" r="3"/><circle cx="18" cy="18" r="3"/><circle cx="12" cy="5" r="3"/><path d="M12 8l-4 7M15 18H9M16 8l4 7"/></svg>
{:else if name === 'mdiChat'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
{:else if name === 'mdiSlack'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3" width="3" height="9" rx="1.5"/><rect x="14" y="9" width="7" height="3" rx="1.5"/><rect x="12" y="14" width="3" height="7" rx="1.5"/><rect x="3" y="12" width="7" height="3" rx="1.5"/></svg>
{:else if name === 'mdiBullhorn'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11v3a1 1 0 0 0 1 1h3l5 4V6L7 10H4a1 1 0 0 0-1 1z"/><path d="M16 8a5 5 0 0 1 0 8M19 5a9 9 0 0 1 0 14"/></svg>
{:else if name === 'mdiBackupRestore'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/><path d="M12 7v5l3 2"/></svg>
{:else if name === 'mdiAccountGroup'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.5"/><path d="M2 21a7 7 0 0 1 14 0"/><circle cx="17" cy="6" r="3"/><path d="M22 18a5 5 0 0 0-5-5"/></svg>
{:else if name === 'mdiChevronRight'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>
{:else if name === 'mdiChevronLeft'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>
{:else if name === 'mdiChevronDown'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
{:else if name === 'mdiMagnify'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
{:else if name === 'mdiLogout'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>
{:else if name === 'mdiKeyVariant'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4"/><path d="M11 13l9-9 2 2-2 2 2 2-3 3-2-2"/></svg>
{:else if name === 'mdiApi'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M7 16V9a2 2 0 1 1 4 0v7M7 13h4M14 9v7M17 9v7"/></svg>
{:else if name === 'mdiWeatherNight'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
{:else if name === 'mdiWeatherSunny'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M5.6 18.4L7 17M17 7l1.4-1.4"/></svg>
{:else if name === 'mdiDesktopTowerMonitor'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="14" height="10" rx="1"/><path d="M9 14v3M6 17h6"/><rect x="18" y="4" width="4" height="16" rx="1"/></svg>
{:else if name === 'mdiFilterOff'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l18 18M22 3H6l3.4 4.4M14 13v8l-4-2v-4"/></svg>
{:else if name === 'mdiDotsHorizontal'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="currentColor"><circle cx="6" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="18" cy="12" r="1.6"/></svg>
{:else if name === 'mdiPulse'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l3-9 4 18 3-9h4"/></svg>
{:else if name === 'mdiPlus'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
{:else if name === 'mdiArrowRight'}
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
{:else}
<MdiIcon {name} {size} />
{/if}
+33 -1
View File
@@ -103,7 +103,39 @@
"last14days": "Last 14 days",
"event": "event",
"events": "events",
"noChartData": "No event data yet"
"noChartData": "No event data yet",
"live": "Live",
"attention": "Attention",
"heroPrefix": "Tonight,",
"heroEmphasis": "everything",
"heroSuffix": "is flowing.",
"heroSummary": "{providers} providers listening, {armed} of {total} trackers armed, {throughput} events dispatched across {targets} targets in 24h.",
"throughput24h": "throughput · 24h",
"eventsShort": "events",
"armedShort": "armed",
"providersShort": "providers",
"targetsShort": "targets",
"trackersShort": "trackers",
"streamTitle": "Signal",
"streamEmphasis": "stream",
"eventsLabel": "events",
"onWatchTitle": "On",
"onWatchEmphasis": "watch",
"noProviders": "No providers yet.",
"addProvider": "Add provider",
"addProviderHint": "Connect a service to start tracking",
"pulseTitle": "Pulse",
"pulseEmphasis": "· last 14 days",
"pulseSub": "Events grouped by day",
"wiresTitle": "Active",
"wiresEmphasis": "wires",
"wiresSub": "routes",
"composeTitle": "Pick a source. Choose a channel.",
"composeEmphasis": "Compose the wire.",
"composeSub": "Walk from provider → tracker → template → target. Or paste a webhook URL and we'll infer the rest.",
"viewTrackers": "View trackers",
"newTracker": "New tracker",
"eventsTotal": "Events"
},
"providers": {
"title": "Providers",
+33 -1
View File
@@ -103,7 +103,39 @@
"last14days": "Последние 14 дней",
"event": "событие",
"events": "событий",
"noChartData": "Нет данных о событиях"
"noChartData": "Нет данных о событиях",
"live": "В эфире",
"attention": "Внимание",
"heroPrefix": "Сегодня",
"heroEmphasis": "всё",
"heroSuffix": "идёт по плану.",
"heroSummary": "{providers} провайдеров на связи, {armed} из {total} трекеров активны, {throughput} событий доставлено в {targets} каналов за сутки.",
"throughput24h": "пропускная способность · 24ч",
"eventsShort": "событий",
"armedShort": "активны",
"providersShort": "провайдеров",
"targetsShort": "каналов",
"trackersShort": "трекеров",
"streamTitle": "Поток",
"streamEmphasis": "сигналов",
"eventsLabel": "событий",
"onWatchTitle": "На",
"onWatchEmphasis": "слежении",
"noProviders": "Пока нет провайдеров.",
"addProvider": "Добавить",
"addProviderHint": "Подключите сервис, чтобы начать слежение",
"pulseTitle": "Пульс",
"pulseEmphasis": "· 14 дней",
"pulseSub": "События по дням",
"wiresTitle": "Активные",
"wiresEmphasis": "линии",
"wiresSub": "маршрутов",
"composeTitle": "Выберите источник, выберите канал.",
"composeEmphasis": "Свяжите.",
"composeSub": "Проведите путь от провайдера → трекер → шаблон → цель. Или вставьте webhook URL — остальное мы определим сами.",
"viewTrackers": "К трекерам",
"newTracker": "Новый трекер",
"eventsTotal": "Событий"
},
"providers": {
"title": "Провайдеры",
+22 -22
View File
@@ -10,7 +10,7 @@
import { t, getLocale, setLocale } from '$lib/i18n';
import { getTheme, initTheme, setTheme, type Theme } from '$lib/theme.svelte';
import Modal from '$lib/components/Modal.svelte';
import MdiIcon from '$lib/components/MdiIcon.svelte';
import NavIcon from '$lib/components/NavIcon.svelte';
import Snackbar from '$lib/components/Snackbar.svelte';
import SearchPalette from '$lib/components/SearchPalette.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
@@ -359,7 +359,7 @@
<div>
<h1 class="brand-mark">
{#if globalProviderFilter.provider}
<span class="brand-mark__icon" style="color: var(--color-primary);"><MdiIcon name={providerDefaultIcon(globalProviderFilter.provider)} size={16} /></span>
<span class="brand-mark__icon" style="color: var(--color-primary);"><NavIcon name={providerDefaultIcon(globalProviderFilter.provider)} size={16} /></span>
{/if}
<span class="brand-mark__notify">Notify</span> <em>Bridge</em>
</h1>
@@ -372,7 +372,7 @@
<button onclick={toggleSidebar}
class="sidebar-icon-btn flex items-center justify-center w-8 h-8 rounded-lg transition-all duration-200"
title={collapsed ? t('common.expand') : t('common.collapse')}>
<MdiIcon name={collapsed ? 'mdiChevronRight' : 'mdiChevronLeft'} size={18} />
<NavIcon name={collapsed ? 'mdiChevronRight' : 'mdiChevronLeft'} size={18} />
</button>
</div>
@@ -387,7 +387,7 @@
}}
class="provider-filter-btn flex items-center justify-center w-full py-1.5 rounded-lg text-sm transition-all duration-200"
title={globalProviderFilter.provider?.name || t('common.allProviders')}>
<MdiIcon name={globalProviderFilter.provider ? providerDefaultIcon(globalProviderFilter.provider) : 'mdiFilterOff'} size={16} />
<NavIcon name={globalProviderFilter.provider ? providerDefaultIcon(globalProviderFilter.provider) : 'mdiFilterOff'} size={16} />
</button>
{:else}
<IconGridSelect items={providerFilterItems} bind:value={providerFilterValue} columns={Math.min(providerFilterItems.length, 3)} compact />
@@ -400,7 +400,7 @@
<button onclick={() => openSearch?.()}
class="search-btn flex items-center gap-2 w-full {collapsed ? 'justify-center px-2' : 'px-2.5'} py-1.5 rounded-lg text-sm transition-all duration-200"
title={t('searchPalette.placeholder')}>
<MdiIcon name="mdiMagnify" size={16} />
<NavIcon name="mdiMagnify" size={16} />
{#if !collapsed}
<span class="flex-1 text-left text-xs">{t('searchPalette.placeholder')}</span>
<kbd class="text-[0.6rem] font-mono px-1 py-0.5 rounded" style="background: var(--color-background); border: 1px solid var(--color-border);">{isMac ? '⌘' : 'Ctrl '}K</kbd>
@@ -421,11 +421,11 @@
{#if isGroupActive(entry) && !expandedGroups[entry.key]}
<div class="active-indicator" style="position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 3px; height: 60%; border-radius: 0 3px 3px 0; background: var(--color-primary); box-shadow: 0 0 8px var(--color-glow-strong);"></div>
{/if}
<MdiIcon name={entry.icon} size={18} />
<NavIcon name={entry.icon} size={18} />
{#if !collapsed}
<span class="truncate flex-1">{t(entry.key)}</span>
<span class="nav-chevron" style="display: inline-flex; transition: transform 0.2s ease; transform: rotate({expandedGroups[entry.key] ? '90deg' : '0deg'});">
<MdiIcon name="mdiChevronRight" size={14} />
<NavIcon name="mdiChevronRight" size={14} />
</span>
{/if}
</button>
@@ -440,7 +440,7 @@
{#if isActive(child.href)}
<div class="active-indicator" style="position: absolute; left: -13px; top: 50%; transform: translateY(-50%); width: 3px; height: 60%; border-radius: 0 3px 3px 0; background: var(--color-primary); box-shadow: 0 0 8px var(--color-glow-strong);"></div>
{/if}
<MdiIcon name={child.icon} size={15} />
<NavIcon name={child.icon} size={15} />
<span class="truncate flex-1">{t(child.key)}</span>
{#if child.countKey && navCounts[child.countKey]}
<span class="nav-badge-sm">{navCounts[child.countKey]}</span>
@@ -459,7 +459,7 @@
{#if isActive(entry.href)}
<div class="active-indicator" style="position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 3px; height: 60%; border-radius: 0 3px 3px 0; background: var(--color-primary); box-shadow: 0 0 8px var(--color-glow-strong);"></div>
{/if}
<MdiIcon name={entry.icon} size={18} />
<NavIcon name={entry.icon} size={18} />
{#if !collapsed}
<span class="truncate flex-1">{t(entry.key)}</span>
{#if entry.countKey && navCounts[entry.countKey]}
@@ -483,12 +483,12 @@
<button onclick={cycleTheme}
class="footer-pill flex items-center justify-center {collapsed ? 'w-8 h-8' : 'px-2.5 py-1'} rounded-lg text-xs transition-all duration-200"
title={t('common.theme')}>
<MdiIcon name={theme.resolved === 'dark' ? 'mdiWeatherNight' : theme.current === 'system' ? 'mdiDesktopTowerMonitor' : 'mdiWeatherSunny'} size={14} />
<NavIcon name={theme.resolved === 'dark' ? 'mdiWeatherNight' : theme.current === 'system' ? 'mdiDesktopTowerMonitor' : 'mdiWeatherSunny'} size={14} />
</button>
<a href="/docs" target="_blank" rel="noopener noreferrer"
class="footer-pill flex items-center justify-center {collapsed ? 'w-8 h-8' : 'px-2.5 py-1'} rounded-lg text-xs transition-all duration-200"
title={t('common.apiDocs')}>
<MdiIcon name="mdiApi" size={14} />
<NavIcon name="mdiApi" size={14} />
</a>
</div>
@@ -498,7 +498,7 @@
<button onclick={logout}
class="sidebar-icon-btn w-full flex justify-center py-2 rounded-lg transition-all duration-200"
title={t('nav.logout')}>
<MdiIcon name="mdiLogout" size={16} />
<NavIcon name="mdiLogout" size={16} />
</button>
{:else}
<div class="px-1.5">
@@ -515,12 +515,12 @@
<button onclick={logout}
class="sidebar-icon-btn p-1.5 rounded-lg transition-all duration-200"
title={t('nav.logout')}>
<MdiIcon name="mdiLogout" size={15} />
<NavIcon name="mdiLogout" size={15} />
</button>
</div>
<button onclick={() => showPasswordForm = true}
class="change-pwd-link text-[0.7rem] mt-1.5 transition-colors duration-200 flex items-center gap-1">
<MdiIcon name="mdiKeyVariant" size={12} />
<NavIcon name="mdiKeyVariant" size={12} />
{t('common.changePassword')}
</button>
</div>
@@ -535,18 +535,18 @@
<a href={item.href} aria-label={t(item.key)}
class="flex flex-col items-center gap-0.5 px-2 py-1.5 text-xs rounded-lg transition-all duration-200"
style="color: {isActive(item.href) ? 'var(--color-primary)' : 'var(--color-muted-foreground)'};">
<MdiIcon name={item.icon} size={20} />
<NavIcon name={item.icon} size={20} />
</a>
{/each}
<button onclick={() => openSearch?.()} aria-label={t('searchPalette.placeholder')}
class="flex flex-col items-center gap-0.5 px-2 py-1.5 text-xs rounded-lg transition-all duration-200"
style="color: var(--color-muted-foreground);">
<MdiIcon name="mdiMagnify" size={20} />
<NavIcon name="mdiMagnify" size={20} />
</button>
<button onclick={() => mobileMoreOpen = !mobileMoreOpen} aria-label={t('nav.more')}
class="flex flex-col items-center gap-0.5 px-2 py-1.5 text-xs rounded-lg transition-all duration-200"
style="color: {mobileMoreOpen ? 'var(--color-primary)' : 'var(--color-muted-foreground)'};">
<MdiIcon name="mdiDotsHorizontal" size={20} />
<NavIcon name="mdiDotsHorizontal" size={20} />
</button>
</nav>
@@ -567,7 +567,7 @@
<div>
<div class="flex items-center gap-1.5 px-1 pb-1.5 text-[0.65rem] font-semibold uppercase tracking-wider"
style="color: var(--color-muted-foreground);">
<MdiIcon name={entry.icon} size={13} />
<NavIcon name={entry.icon} size={13} />
<span>{t(entry.key)}</span>
</div>
<div class="grid grid-cols-3 gap-2">
@@ -576,7 +576,7 @@
class="flex flex-col items-center gap-1 p-3 rounded-lg transition-all duration-200 relative"
style="color: {isActive(child.href) ? 'var(--color-primary)' : 'var(--color-muted-foreground)'}; background: {isActive(child.href) ? 'var(--color-sidebar-active)' : 'transparent'};"
>
<MdiIcon name={child.icon} size={20} />
<NavIcon name={child.icon} size={20} />
<span class="text-xs text-center leading-tight">{t(child.key)}</span>
{#if child.countKey && navCounts[child.countKey]}
<span class="nav-badge-sm" style="position: absolute; top: 0.25rem; right: 0.25rem;">{navCounts[child.countKey]}</span>
@@ -590,7 +590,7 @@
class="flex items-center gap-2 p-3 rounded-lg transition-all duration-200 relative"
style="color: {isActive(entry.href) ? 'var(--color-primary)' : 'var(--color-muted-foreground)'}; background: {isActive(entry.href) ? 'var(--color-sidebar-active)' : 'transparent'};"
>
<MdiIcon name={entry.icon} size={18} />
<NavIcon name={entry.icon} size={18} />
<span class="text-sm flex-1">{t(entry.key)}</span>
{#if entry.countKey && navCounts[entry.countKey]}
<span class="nav-badge">{navCounts[entry.countKey]}</span>
@@ -602,7 +602,7 @@
<button onclick={() => { closeMobileMore(); logout(); }}
class="flex items-center gap-2 p-3 w-full rounded-lg transition-all duration-200"
style="color: var(--color-muted-foreground);">
<MdiIcon name="mdiLogout" size={18} />
<NavIcon name="mdiLogout" size={18} />
<span class="text-sm">{t('nav.logout')}</span>
</button>
</div>
@@ -614,7 +614,7 @@
<main class="flex-1 overflow-auto md:pb-0"
style="padding-bottom: calc(4rem + env(safe-area-inset-bottom, 0px));">
{#key page.url.pathname}
<div class="max-w-5xl mx-auto p-4 md:p-8" in:fade={{ duration: 200, delay: 50 }}>
<div class="max-w-7xl mx-auto p-4 md:p-8" in:fade={{ duration: 200, delay: 50 }}>
{@render children()}
</div>
{/key}
File diff suppressed because it is too large Load Diff