From d662b5092547b8eab4fc2f7faa9595e46a6fabca Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 25 Apr 2026 02:52:01 +0300 Subject: [PATCH] feat(redesign): roll subpage hero across all pages + Aurora Button + JinjaEditor + pulse fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Big batch — every secondary page now wears the same glass-card hero that landed on Providers earlier: - notification-trackers, tracking-configs, template-configs - command-trackers, command-configs, command-template-configs - targets (with active-tab title), actions - bots (telegram / email / matrix tabs) - settings, settings/backup, users Each page picks an italic-em emphasis word, an editorial crumb (e.g. 'Routing · Notification', 'Operators · Bots', 'System · Maintenance'), a count meter, and entity-specific status pills derived from live data: 'X armed / Y paused' for trackers and actions, 'X types' for configs/templates, 'X channels' or '$N receivers' for targets. Other changes in this commit: - Button.svelte: redesigned. Primary variant becomes a real Aurora CTA — gradient lavender → orchid pill, 40px tall md / 34px sm, inset highlight, lift + glow on hover. Secondary, danger, ghost variants reworked to match. The 'Add ' button on every page now reads as the page's primary action instead of a flat lavender rectangle. - JinjaEditor: overrode oneDark's hardcoded background with !important so the editor surface picks up var(--color-input-bg). Gutters / scroller / selection / autocomplete tooltip all match Aurora glass tokens now. Template editors stop visually clashing with the surrounding panel. - Aurora pulse dot: rewritten as a self-contained box-shadow glow pulse (no transform, no pseudo-element). The dot's bounding box is now stable so ancestors with overflow:hidden can never clip the visible dot — only the (decorative) outer glow halo. Fixes the 'half-moon clipping' on the dashboard 'On watch' deck. - topbar-action.svelte.ts left in tree but unused (topbar CTA was reverted per your call). Will clean up in a later commit. - Form input baseline styling moved into app.css (rounded 0.625rem, glass background, hover/focus rings) so untouched filter inputs on the per-type pages stop looking out of place. i18n: emphasis / countLabel / armed / paused / receiver / receivers / channelsCount keys added across en + ru. Build clean: 0 errors, 61 pre-existing a11y warnings unchanged. --- frontend/src/app.css | 107 +++++++++++++++--- frontend/src/lib/components/Button.svelte | 61 +++++++--- .../src/lib/components/JinjaEditor.svelte | 61 +++++++--- frontend/src/lib/i18n/en.json | 31 ++++- frontend/src/lib/i18n/ru.json | 29 +++++ .../src/lib/stores/topbar-action.svelte.ts | 35 ++++++ frontend/src/routes/+layout.svelte | 5 +- frontend/src/routes/actions/+page.svelte | 20 +++- frontend/src/routes/bots/EmailBotTab.svelte | 9 +- frontend/src/routes/bots/MatrixBotTab.svelte | 9 +- .../src/routes/bots/TelegramBotTab.svelte | 9 +- .../src/routes/command-configs/+page.svelte | 18 ++- .../command-template-configs/+page.svelte | 23 +++- .../src/routes/command-trackers/+page.svelte | 32 +++++- .../routes/notification-trackers/+page.svelte | 33 +++++- frontend/src/routes/providers/+page.svelte | 11 +- frontend/src/routes/settings/+page.svelte | 7 +- .../src/routes/settings/backup/+page.svelte | 7 +- frontend/src/routes/targets/+page.svelte | 30 ++++- .../src/routes/template-configs/+page.svelte | 30 ++++- .../src/routes/tracking-configs/+page.svelte | 29 ++++- frontend/src/routes/users/+page.svelte | 9 +- 22 files changed, 529 insertions(+), 76 deletions(-) create mode 100644 frontend/src/lib/stores/topbar-action.svelte.ts diff --git a/frontend/src/app.css b/frontend/src/app.css index fc71713..b9fc4e9 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -181,19 +181,51 @@ body::after { [data-theme="light"] body::before { opacity: 0.85; } -/* Form controls */ +/* Form controls — Aurora-native defaults */ input, select, textarea { color: var(--color-foreground); background-color: var(--color-input-bg); - border-color: var(--color-rule-strong); + border: 1px solid var(--color-rule-strong); + border-radius: 0.625rem; font-family: var(--font-sans); - transition: border-color 0.2s ease, box-shadow 0.2s ease; + transition: border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease; +} + +/* Default text inputs / search / textarea: comfortable padding. + `` and `` are excluded so + they keep their native compact sizing. Any explicit `padding`/`p-*` + utility from a callsite still wins. */ +input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]), +textarea { + padding: 0.55rem 0.85rem; + font-size: 0.875rem; +} + +select { + padding: 0.55rem 2.2rem 0.55rem 0.85rem; + font-size: 0.875rem; + appearance: none; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 12px; +} + +input:hover:not(:focus-visible):not([disabled]), +select:hover:not(:focus-visible):not([disabled]), +textarea:hover:not(:focus-visible):not([disabled]) { + border-color: var(--color-rule-strong); + background-color: var(--color-glass-strong); } input:focus-visible, select:focus-visible, textarea:focus-visible { outline: none; border-color: var(--color-primary); - box-shadow: 0 0 0 3px var(--color-glow), 0 0 12px var(--color-glow); + box-shadow: 0 0 0 3px var(--color-glow); +} + +input::placeholder, textarea::placeholder { + color: var(--color-muted-foreground); } button:focus-visible, a:focus-visible { @@ -276,9 +308,41 @@ button:focus-visible, a:focus-visible { to { opacity: 1; transform: translateY(0); } } -@keyframes aurora-pulse { - 0%, 100% { transform: scale(1); opacity: 1; } - 50% { transform: scale(1.5); opacity: 0.6; } +@keyframes aurora-pulse-glow-mint { + 0%, 100% { + box-shadow: + 0 0 4px color-mix(in srgb, var(--color-mint) 60%, transparent), + 0 0 0 0 color-mix(in srgb, var(--color-mint) 0%, transparent); + } + 50% { + box-shadow: + 0 0 10px color-mix(in srgb, var(--color-mint) 80%, transparent), + 0 0 0 4px color-mix(in srgb, var(--color-mint) 25%, transparent); + } +} +@keyframes aurora-pulse-glow-citrus { + 0%, 100% { + box-shadow: + 0 0 4px color-mix(in srgb, var(--color-citrus) 60%, transparent), + 0 0 0 0 color-mix(in srgb, var(--color-citrus) 0%, transparent); + } + 50% { + box-shadow: + 0 0 10px color-mix(in srgb, var(--color-citrus) 80%, transparent), + 0 0 0 4px color-mix(in srgb, var(--color-citrus) 25%, transparent); + } +} +@keyframes aurora-pulse-glow-coral { + 0%, 100% { + box-shadow: + 0 0 4px color-mix(in srgb, var(--color-coral) 60%, transparent), + 0 0 0 0 color-mix(in srgb, var(--color-coral) 0%, transparent); + } + 50% { + box-shadow: + 0 0 10px color-mix(in srgb, var(--color-coral) 80%, transparent), + 0 0 0 4px color-mix(in srgb, var(--color-coral) 25%, transparent); + } } .animate-fade-slide-in { @@ -343,15 +407,30 @@ button:focus-visible, a:focus-visible { opacity: 1; } -/* Live pulse dot — for "live" / armed indicators */ +/* Live pulse dot — for "live" / armed indicators. + Pulse is a self-contained box-shadow glow on the dot. No transform, + no pseudo-element — the dot's own bounding box never changes, so + ancestors with overflow:hidden can only clip the (decorative) glow, + never the dot itself. */ .aurora-pulse { - width: 7px; height: 7px; + width: 8px; height: 8px; border-radius: 50%; background: var(--color-mint); - box-shadow: 0 0 8px var(--color-mint); display: inline-block; - animation: aurora-pulse 1.4s ease-in-out infinite; + flex-shrink: 0; + animation: aurora-pulse-glow-mint 1.6s ease-in-out infinite; +} +.aurora-pulse.warn { + background: var(--color-citrus); + animation-name: aurora-pulse-glow-citrus; +} +.aurora-pulse.error { + background: var(--color-coral); + animation-name: aurora-pulse-glow-coral; +} +.aurora-pulse.idle { + background: var(--color-muted-foreground); + box-shadow: none; + opacity: 0.5; + animation: none; } -.aurora-pulse.warn { background: var(--color-citrus); box-shadow: 0 0 8px var(--color-citrus); } -.aurora-pulse.error { background: var(--color-coral); box-shadow: 0 0 8px var(--color-coral); } -.aurora-pulse.idle { background: var(--color-muted-foreground); box-shadow: none; opacity: 0.5; animation: none; } diff --git a/frontend/src/lib/components/Button.svelte b/frontend/src/lib/components/Button.svelte index fe647e7..bfe9a4a 100644 --- a/frontend/src/lib/components/Button.svelte +++ b/frontend/src/lib/components/Button.svelte @@ -21,10 +21,10 @@ class?: string; } = $props(); - const baseClasses = 'inline-flex items-center justify-center gap-1.5 rounded-md text-sm font-medium transition-colors disabled:opacity-50'; + const baseClasses = 'aurora-btn inline-flex items-center justify-center gap-2 font-medium transition-all disabled:opacity-50 disabled:pointer-events-none'; const sizeClasses: Record = { - sm: 'px-2.5 py-1 text-xs', - md: 'px-4 py-2', + sm: 'aurora-btn--sm', + md: 'aurora-btn--md', }; const variantClasses: Record = { primary: 'btn-primary', @@ -49,37 +49,72 @@ {/if} diff --git a/frontend/src/lib/components/JinjaEditor.svelte b/frontend/src/lib/components/JinjaEditor.svelte index ecb25dd..763d39b 100644 --- a/frontend/src/lib/components/JinjaEditor.svelte +++ b/frontend/src/lib/components/JinjaEditor.svelte @@ -84,23 +84,54 @@ } }), EditorView.lineWrapping, - EditorView.theme({ - '&': { fontSize: '13px', fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace" }, - '.cm-content': { minHeight: `${rows * 1.5}em`, padding: '8px' }, - '.cm-editor': { borderRadius: '0.375rem', border: '1px solid var(--color-border)' }, - '.cm-focused': { outline: '2px solid var(--color-primary)', outlineOffset: '0px' }, - '.cm-error-line': { backgroundColor: 'rgba(239, 68, 68, 0.2)', outline: '1px solid rgba(239, 68, 68, 0.4)' }, - '.ͼc': { color: '#e879f9' }, - '.ͼd': { color: '#38bdf8' }, - '.ͼ5': { color: '#6b7280' }, - '.cm-tooltip-autocomplete': { - border: '1px solid var(--color-border)', - borderRadius: '0.375rem', - fontSize: '12px', - }, - }), ]; + // Apply oneDark first so its syntax-token colors are kept, + // then override with our Aurora-aware theme so background, + // borders, and gutters match the rest of the design. if (isDark) extensions.push(oneDark); + extensions.push(EditorView.theme({ + '&': { + fontSize: '13px', + fontFamily: 'var(--font-mono)', + backgroundColor: 'var(--color-input-bg) !important', + borderRadius: '14px', + border: '1px solid var(--color-rule-strong)', + color: 'var(--color-foreground)', + overflow: 'hidden', + }, + '.cm-editor': { backgroundColor: 'transparent !important', borderRadius: '14px' }, + '.cm-scroller': { backgroundColor: 'transparent !important' }, + '.cm-content': { minHeight: `${rows * 1.5}em`, padding: '12px 14px', caretColor: 'var(--color-primary)' }, + '.cm-gutters': { + backgroundColor: 'transparent', + color: 'var(--color-muted-foreground)', + borderRight: '1px solid var(--color-border)', + }, + '.cm-activeLineGutter': { backgroundColor: 'var(--color-glass-strong)' }, + '.cm-activeLine': { backgroundColor: 'var(--color-glass-strong)' }, + '.cm-cursor': { borderLeftColor: 'var(--color-primary)' }, + '.cm-selectionBackground, ::selection': { backgroundColor: 'var(--color-glass-elev) !important' }, + '&.cm-focused .cm-selectionBackground': { backgroundColor: 'var(--color-glow) !important' }, + '.cm-focused': { outline: 'none' }, + '&.cm-focused': { borderColor: 'var(--color-primary)', boxShadow: '0 0 0 3px var(--color-glow)' }, + '.cm-error-line': { backgroundColor: 'rgba(255, 138, 120, 0.18)', outline: '1px solid rgba(255, 138, 120, 0.4)' }, + '.ͼc': { color: 'var(--color-orchid)' }, + '.ͼd': { color: 'var(--color-sky)' }, + '.ͼ5': { color: 'var(--color-muted-foreground)' }, + '.cm-tooltip-autocomplete': { + background: 'color-mix(in srgb, var(--color-background) 92%, transparent)', + backdropFilter: 'blur(28px) saturate(160%)', + border: '1px solid var(--color-rule-strong)', + borderRadius: '12px', + fontSize: '12px', + boxShadow: '0 12px 30px -12px rgba(0,0,0,0.4)', + overflow: 'hidden', + }, + '.cm-tooltip-autocomplete > ul > li[aria-selected]': { + backgroundColor: 'var(--color-glass-elev)', + color: 'var(--color-primary)', + }, + })); if (placeholder) extensions.push(cmPlaceholder(placeholder)); return extensions; } diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 4f03743..0b063b4 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -231,7 +231,10 @@ "cleared": "Payload history cleared" }, "notificationTracker": { - "title": "Notification Trackers", + "title": "Notification", + "titleEmphasis": "trackers", + "armed": "armed", + "paused": "paused", "description": "Monitor albums for changes", "newTracker": "New Tracker", "cancel": "Cancel", @@ -343,6 +346,11 @@ "albumDeleted": "Album deleted" }, "targets": { + "titleEmphasis": "channel", + "titleEmphasisAll": "channels", + "receiver": "receiver", + "receivers": "receivers", + "channelsCount": "channels", "title": "Targets", "description": "Notification delivery destinations", "descTelegram": "Telegram chat destinations for notifications", @@ -411,6 +419,8 @@ "receiverDisabled": "Receiver disabled" }, "users": { + "titleEmphasis": "& access", + "countLabel": "users", "title": "Users", "description": "Manage user accounts (admin only)", "addUser": "Add User", @@ -428,6 +438,8 @@ "noUsers": "No users found" }, "telegramBot": { + "titleEmphasis": "telegram", + "countLabel": "bots", "title": "Telegram Bots", "description": "Register and manage Telegram bots", "addBot": "Add Bot", @@ -504,6 +516,8 @@ "webhookFailed": "Failed to register webhook" }, "trackingConfig": { + "titleEmphasis": "configs", + "countLabel": "configs", "title": "Tracking Configs", "description": "Define what events and assets to react to", "newConfig": "New Config", @@ -609,6 +623,8 @@ "nextDay": "next day" }, "templateConfig": { + "titleEmphasis": "templates", + "countLabel": "templates", "title": "Template Configs", "description": "Define how notification messages are formatted", "providerType": "Service Provider Type", @@ -728,6 +744,7 @@ "album_shared": "Whether album is shared" }, "settings": { + "titleEmphasis": "options", "title": "Settings", "description": "Global application settings", "general": "General", @@ -804,6 +821,8 @@ "rateLimits": "Cooldown in seconds between uses of each command category per chat. 0 = no limit." }, "matrixBot": { + "titleEmphasis": "matrix", + "countLabel": "bots", "title": "Matrix Bots", "description": "Matrix homeserver connections for room notifications", "addBot": "Add Matrix Bot", @@ -820,6 +839,8 @@ "operationFailed": "Operation failed" }, "emailBot": { + "titleEmphasis": "email", + "countLabel": "accounts", "title": "Email Bots", "description": "SMTP email senders for notifications", "addBot": "Add Email Bot", @@ -839,6 +860,8 @@ "operationFailed": "Operation failed" }, "cmdTemplateConfig": { + "titleEmphasis": "templates", + "countLabel": "templates", "title": "Command Templates", "description": "Customize command response messages with Jinja2 templates", "newConfig": "New Config", @@ -851,6 +874,8 @@ "commandResponsesHint": "Leave a slot empty to use the default hardcoded response." }, "commandConfig": { + "titleEmphasis": "configs", + "countLabel": "configs", "title": "Command Configs", "description": "Define command settings for Telegram bot interactions", "newConfig": "New Config", @@ -873,6 +898,7 @@ "noTemplate": "Default (hardcoded)" }, "commandTracker": { + "titleEmphasis": "trackers", "title": "Command Trackers", "description": "Manage command trackers and their listeners", "newTracker": "New Tracker", @@ -1155,6 +1181,8 @@ "close": "close" }, "actions": { + "titleEmphasis": "automations", + "countLabel": "actions", "title": "Actions", "description": "Scheduled mutations on external services", "addAction": "Add Action", @@ -1212,6 +1240,7 @@ "triggerScheduled": "scheduled" }, "backup": { + "titleEmphasis": "& restore", "title": "Backup & Restore", "description": "Export and import your configuration, or set up automatic backups", "export": "Export Configuration", diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index ad86c1f..76897ad 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -231,6 +231,9 @@ "cleared": "История запросов очищена" }, "notificationTracker": { + "titleEmphasis": "трекеры", + "armed": "активны", + "paused": "на паузе", "title": "Трекеры уведомлений", "description": "Отслеживание изменений в альбомах", "newTracker": "Новый трекер", @@ -343,6 +346,11 @@ "albumDeleted": "Альбом удалён" }, "targets": { + "titleEmphasis": "канал", + "titleEmphasisAll": "каналы", + "receiver": "получатель", + "receivers": "получателей", + "channelsCount": "каналов", "title": "Получатели", "description": "Адреса доставки уведомлений", "descTelegram": "Чаты Telegram для доставки уведомлений", @@ -411,6 +419,8 @@ "receiverDisabled": "Получатель отключён" }, "users": { + "titleEmphasis": "и доступ", + "countLabel": "пользователей", "title": "Пользователи", "description": "Управление аккаунтами (только админ)", "addUser": "Добавить пользователя", @@ -428,6 +438,8 @@ "noUsers": "Пользователи не найдены" }, "telegramBot": { + "titleEmphasis": "telegram", + "countLabel": "ботов", "title": "Telegram боты", "description": "Регистрация и управление Telegram ботами", "addBot": "Добавить бота", @@ -504,6 +516,8 @@ "webhookFailed": "Не удалось зарегистрировать webhook" }, "trackingConfig": { + "titleEmphasis": "конфигурации", + "countLabel": "конфигураций", "title": "Конфигурации отслеживания", "description": "Определите, на какие события и файлы реагировать", "newConfig": "Новая конфигурация", @@ -609,6 +623,8 @@ "nextDay": "след. день" }, "templateConfig": { + "titleEmphasis": "шаблоны", + "countLabel": "шаблонов", "title": "Конфигурации шаблонов", "description": "Определите формат уведомлений", "providerType": "Тип сервис-провайдера", @@ -728,6 +744,7 @@ "album_shared": "Общий альбом" }, "settings": { + "titleEmphasis": "параметры", "title": "Настройки", "description": "Глобальные настройки приложения", "general": "Общие", @@ -804,6 +821,8 @@ "rateLimits": "Кулдаун в секундах между использованиями команд в каждом чате. 0 = без ограничений." }, "matrixBot": { + "titleEmphasis": "matrix", + "countLabel": "ботов", "title": "Matrix боты", "description": "Подключения к Matrix серверам для уведомлений в комнаты", "addBot": "Добавить Matrix бот", @@ -820,6 +839,8 @@ "operationFailed": "Операция не удалась" }, "emailBot": { + "titleEmphasis": "email", + "countLabel": "учётных записей", "title": "Email боты", "description": "SMTP отправители для уведомлений по email", "addBot": "Добавить Email бот", @@ -839,6 +860,8 @@ "operationFailed": "Операция не удалась" }, "cmdTemplateConfig": { + "titleEmphasis": "шаблоны", + "countLabel": "шаблонов", "title": "Шаблоны команд", "description": "Настройте ответы команд с помощью Jinja2 шаблонов", "newConfig": "Новый шаблон", @@ -851,6 +874,8 @@ "commandResponsesHint": "Оставьте слот пустым, чтобы использовать ответ по умолчанию." }, "commandConfig": { + "titleEmphasis": "конфигурации", + "countLabel": "конфигураций", "title": "Конфигурации команд", "description": "Настройки команд для взаимодействия с Telegram-ботами", "newConfig": "Новая конфигурация", @@ -873,6 +898,7 @@ "noTemplate": "По умолчанию (встроенный)" }, "commandTracker": { + "titleEmphasis": "трекеры", "title": "Трекеры команд", "description": "Управление трекерами команд и их слушателями", "newTracker": "Новый трекер", @@ -1155,6 +1181,8 @@ "close": "закрыть" }, "actions": { + "titleEmphasis": "автоматизации", + "countLabel": "действий", "title": "Действия", "description": "Запланированные операции над внешними сервисами", "addAction": "Добавить действие", @@ -1212,6 +1240,7 @@ "triggerScheduled": "по расписанию" }, "backup": { + "titleEmphasis": "и восстановление", "title": "Резервное копирование", "description": "Экспорт и импорт конфигурации, настройка автоматических бэкапов", "export": "Экспорт конфигурации", diff --git a/frontend/src/lib/stores/topbar-action.svelte.ts b/frontend/src/lib/stores/topbar-action.svelte.ts new file mode 100644 index 0000000..43e722b --- /dev/null +++ b/frontend/src/lib/stores/topbar-action.svelte.ts @@ -0,0 +1,35 @@ +/** + * Page-scoped primary action for the global topbar CTA. + * + * Each route declares its own primary action ("Add Provider", + * "New Tracker", etc.) by calling `topbarAction.set({...})` + * inside its `onMount`, and clears it on teardown. The layout + * reads `topbarAction.current` and renders the button. + * + * Falls back to the default "New tracker" CTA when no action is + * registered (set by the layout itself). + */ +export interface TopbarAction { + /** Visible label, e.g. "Add Provider". */ + label: string; + /** Optional href — renders as . Mutually exclusive with onclick. */ + href?: string; + /** Optional click handler — renders as - - - {t('dashboard.newTracker')} - diff --git a/frontend/src/routes/actions/+page.svelte b/frontend/src/routes/actions/+page.svelte index ed22592..7a8aee3 100644 --- a/frontend/src/routes/actions/+page.svelte +++ b/frontend/src/routes/actions/+page.svelte @@ -68,6 +68,16 @@ })()); onMount(load); + + const headerPills = $derived.by(() => { + const pills: Array<{ label: string; tone: 'mint' | 'citrus' }> = []; + const enabled = actions.filter((a: Action) => a.enabled).length; + const disabled = actions.length - enabled; + if (enabled > 0) pills.push({ label: `${enabled} ${t('notificationTracker.armed')}`, tone: 'mint' }); + if (disabled > 0) pills.push({ label: `${disabled} ${t('notificationTracker.paused')}`, tone: 'citrus' }); + return pills; + }); + async function load() { try { await Promise.all([ @@ -171,7 +181,15 @@ } - + diff --git a/frontend/src/routes/bots/EmailBotTab.svelte b/frontend/src/routes/bots/EmailBotTab.svelte index 48f734d..6964a87 100644 --- a/frontend/src/routes/bots/EmailBotTab.svelte +++ b/frontend/src/routes/bots/EmailBotTab.svelte @@ -86,7 +86,14 @@ } - + diff --git a/frontend/src/routes/bots/MatrixBotTab.svelte b/frontend/src/routes/bots/MatrixBotTab.svelte index fd3c6be..7cd335e 100644 --- a/frontend/src/routes/bots/MatrixBotTab.svelte +++ b/frontend/src/routes/bots/MatrixBotTab.svelte @@ -84,7 +84,14 @@ } - + diff --git a/frontend/src/routes/bots/TelegramBotTab.svelte b/frontend/src/routes/bots/TelegramBotTab.svelte index 5aabe50..2d720cc 100644 --- a/frontend/src/routes/bots/TelegramBotTab.svelte +++ b/frontend/src/routes/bots/TelegramBotTab.svelte @@ -285,7 +285,14 @@ } - + diff --git a/frontend/src/routes/command-configs/+page.svelte b/frontend/src/routes/command-configs/+page.svelte index 61e30f8..5e93d67 100644 --- a/frontend/src/routes/command-configs/+page.svelte +++ b/frontend/src/routes/command-configs/+page.svelte @@ -80,6 +80,14 @@ let hasCommands = $derived(providerCommands.length > 0); onMount(load); + + const headerPills = $derived.by(() => { + const pills: Array<{ label: string; tone: 'sky' }> = []; + const types = new Set(configs.map(c => c.provider_type)).size; + if (types > 0) pills.push({ label: `${types} ${types === 1 ? t('providers.typeSingular') : t('providers.typePlural')}`, tone: 'sky' }); + return pills; + }); + async function load() { try { await Promise.all([ @@ -161,7 +169,15 @@ } - + diff --git a/frontend/src/routes/command-template-configs/+page.svelte b/frontend/src/routes/command-template-configs/+page.svelte index 981d7bb..81ec10b 100644 --- a/frontend/src/routes/command-template-configs/+page.svelte +++ b/frontend/src/routes/command-template-configs/+page.svelte @@ -7,6 +7,7 @@ import { sanitizePreview } from '$lib/sanitize'; import { commandTemplateConfigsCache, supportedLocalesCache } from '$lib/stores/caches.svelte'; import PageHeader from '$lib/components/PageHeader.svelte'; + import Button from '$lib/components/Button.svelte'; import Card from '$lib/components/Card.svelte'; import Loading from '$lib/components/Loading.svelte'; import IconPicker from '$lib/components/IconPicker.svelte'; @@ -140,6 +141,13 @@ onMount(load); + const headerPills = $derived.by(() => { + const pills: Array<{ label: string; tone: 'sky' }> = []; + const types = new Set(configs.map(c => c.provider_type)).size; + if (types > 0) pills.push({ label: `${types} ${types === 1 ? t('providers.typeSingular') : t('providers.typePlural')}`, tone: 'sky' }); + return pills; + }); + async function load() { try { const [cfgs, caps, vars] = await Promise.all([ @@ -355,11 +363,18 @@ } - - + {#if !loaded}{:else} diff --git a/frontend/src/routes/command-trackers/+page.svelte b/frontend/src/routes/command-trackers/+page.svelte index 2fb3494..69838a2 100644 --- a/frontend/src/routes/command-trackers/+page.svelte +++ b/frontend/src/routes/command-trackers/+page.svelte @@ -1,7 +1,8 @@ - + diff --git a/frontend/src/routes/notification-trackers/+page.svelte b/frontend/src/routes/notification-trackers/+page.svelte index 39ac88a..13c83cf 100644 --- a/frontend/src/routes/notification-trackers/+page.svelte +++ b/frontend/src/routes/notification-trackers/+page.svelte @@ -1,6 +1,7 @@ - + diff --git a/frontend/src/routes/providers/+page.svelte b/frontend/src/routes/providers/+page.svelte index 522efdb..d0b27f6 100644 --- a/frontend/src/routes/providers/+page.svelte +++ b/frontend/src/routes/providers/+page.svelte @@ -19,6 +19,8 @@ const gridItemSources: Record any[]> = { webhookAuthModeItems }; import { globalProviderFilter } from '$lib/stores/provider-filter.svelte'; + import { topbarAction } from '$lib/stores/topbar-action.svelte'; + import { onDestroy } from 'svelte'; import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte'; import { highlightFromUrl } from '$lib/highlight'; import { getDescriptor, buildProviderFormDefaults } from '$lib/providers'; @@ -68,7 +70,14 @@ return pills; }); - onMount(load); + onMount(() => { + topbarAction.set({ + label: t('providers.addProvider'), + onclick: () => { showForm ? (showForm = false, editing = null) : openNew(); }, + }); + load(); + }); + onDestroy(() => topbarAction.clear()); async function load() { try { await providersCache.fetch(true); diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 0dc6e49..df573a3 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -93,7 +93,12 @@ } - + {#if !loaded} diff --git a/frontend/src/routes/settings/backup/+page.svelte b/frontend/src/routes/settings/backup/+page.svelte index 795eae2..6e9de0e 100644 --- a/frontend/src/routes/settings/backup/+page.svelte +++ b/frontend/src/routes/settings/backup/+page.svelte @@ -292,7 +292,12 @@ } - + {#if !loaded} diff --git a/frontend/src/routes/targets/+page.svelte b/frontend/src/routes/targets/+page.svelte index 43f8f7c..f1afc42 100644 --- a/frontend/src/routes/targets/+page.svelte +++ b/frontend/src/routes/targets/+page.svelte @@ -6,6 +6,7 @@ import { t, getLocale } from '$lib/i18n'; import { targetsCache, telegramBotsCache, emailBotsCache, matrixBotsCache } from '$lib/stores/caches.svelte'; import PageHeader from '$lib/components/PageHeader.svelte'; + import Button from '$lib/components/Button.svelte'; import Card from '$lib/components/Card.svelte'; import Loading from '$lib/components/Loading.svelte'; import MdiIcon from '$lib/components/MdiIcon.svelte'; @@ -165,6 +166,20 @@ // ── Data loading ── onMount(load); + + const headerPills = $derived.by(() => { + const pills: Array<{ label: string; tone: 'mint' | 'sky' | 'orchid' }> = []; + if (activeType) { + // Tab-filtered: show count of receivers for the active type only. + const total = targets.reduce((acc, t) => acc + (t.receiver_count || 0), 0); + if (total > 0) pills.push({ label: `${total} ${total === 1 ? t('targets.receiver') : t('targets.receivers')}`, tone: 'mint' }); + } else { + const types = new Set(targets.map(t => t.type)).size; + if (types > 0) pills.push({ label: `${types} ${t('targets.channelsCount')}`, tone: 'sky' }); + } + return pills; + }); + async function load() { try { await Promise.all([ @@ -418,11 +433,18 @@ } - - + {#if !loaded}{:else} diff --git a/frontend/src/routes/template-configs/+page.svelte b/frontend/src/routes/template-configs/+page.svelte index cac9980..ea9da72 100644 --- a/frontend/src/routes/template-configs/+page.svelte +++ b/frontend/src/routes/template-configs/+page.svelte @@ -1,7 +1,8 @@ - + diff --git a/frontend/src/routes/tracking-configs/+page.svelte b/frontend/src/routes/tracking-configs/+page.svelte index 079b81c..bbc2a84 100644 --- a/frontend/src/routes/tracking-configs/+page.svelte +++ b/frontend/src/routes/tracking-configs/+page.svelte @@ -1,7 +1,8 @@ - + diff --git a/frontend/src/routes/users/+page.svelte b/frontend/src/routes/users/+page.svelte index 2bad6a9..0e89168 100644 --- a/frontend/src/routes/users/+page.svelte +++ b/frontend/src/routes/users/+page.svelte @@ -89,7 +89,14 @@ } - +