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 @@
-
+
{ showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('commandTracker.newTracker')}
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 @@
-
+
{ showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('notificationTracker.cancel') : t('notificationTracker.newTracker')}
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 @@
}
-
- { showForm ? (showForm = false, editing = null) : openNew(); }}
- class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
+
+ { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('targets.cancel') : t('targets.addTarget')}
-
+
{#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 @@
-
+
{ showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('templateConfig.newConfig')}
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 @@
-
+
{ showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('trackingConfig.newConfig')}
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 @@
}
-
+
showForm = !showForm}>
{showForm ? t('users.cancel') : t('users.addUser')}