feat(redesign): roll subpage hero across all pages + Aurora Button + JinjaEditor + pulse fix

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 <Type>' 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.
This commit is contained in:
2026-04-25 02:52:01 +03:00
parent 9733e5c122
commit d662b50925
22 changed files with 529 additions and 76 deletions
+1 -4
View File
@@ -21,6 +21,7 @@
matrixBotsCache, targetsCache,
} from '$lib/stores/caches.svelte';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import { topbarAction } from '$lib/stores/topbar-action.svelte';
import { providerDefaultIcon } from '$lib/grid-items';
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
@@ -622,10 +623,6 @@
title={t('common.language')} aria-label={t('common.language')}>
<span class="topbar-locale font-mono">{getLocale().toUpperCase()}</span>
</button>
<a href="/notification-trackers" class="topbar-cta">
<NavIcon name="mdiPlus" size={14} />
<span>{t('dashboard.newTracker')}</span>
</a>
</div>
</div>
+19 -1
View File
@@ -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 @@
}
</script>
<PageHeader title={t('actions.title')} description={t('actions.description')}>
<PageHeader
title={t('actions.title')}
emphasis={t('actions.titleEmphasis')}
description={t('actions.description')}
crumb="Routing · Automation"
count={actions.length}
countLabel={t('actions.countLabel')}
pills={headerPills}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('actions.addAction')}
</Button>
+8 -1
View File
@@ -86,7 +86,14 @@
}
</script>
<PageHeader title={t('emailBot.title')} description={t('emailBot.description')}>
<PageHeader
title={t('emailBot.title')}
emphasis={t('emailBot.titleEmphasis')}
description={t('emailBot.description')}
crumb="Operators · Bots"
count={emailBots.length}
countLabel={t('emailBot.countLabel')}
>
<Button size="sm" onclick={() => { showEmailForm ? (showEmailForm = false, editingEmail = null) : openNewEmail(); }}>
{showEmailForm ? t('common.cancel') : t('emailBot.addBot')}
</Button>
+8 -1
View File
@@ -84,7 +84,14 @@
}
</script>
<PageHeader title={t('matrixBot.title')} description={t('matrixBot.description')}>
<PageHeader
title={t('matrixBot.title')}
emphasis={t('matrixBot.titleEmphasis')}
description={t('matrixBot.description')}
crumb="Operators · Bots"
count={matrixBots.length}
countLabel={t('matrixBot.countLabel')}
>
<Button size="sm" onclick={() => { showMatrixForm ? (showMatrixForm = false, editingMatrix = null) : openNewMatrix(); }}>
{showMatrixForm ? t('common.cancel') : t('matrixBot.addBot')}
</Button>
@@ -285,7 +285,14 @@
}
</script>
<PageHeader title={t('telegramBot.title')} description={t('telegramBot.description')}>
<PageHeader
title={t('telegramBot.title')}
emphasis={t('telegramBot.titleEmphasis')}
description={t('telegramBot.description')}
crumb="Operators · Bots"
count={bots.length}
countLabel={t('telegramBot.countLabel')}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('telegramBot.addBot')}
</Button>
@@ -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 @@
}
</script>
<PageHeader title={t('commandConfig.title')} description={t('commandConfig.description')}>
<PageHeader
title={t('commandConfig.title')}
emphasis={t('commandConfig.titleEmphasis')}
description={t('commandConfig.description')}
crumb="Routing · Commands"
count={configs.length}
countLabel={t('commandConfig.countLabel')}
pills={headerPills}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('commandConfig.newConfig')}
</Button>
@@ -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 @@
}
</script>
<PageHeader title={t('cmdTemplateConfig.title')} description={t('cmdTemplateConfig.description')}>
<button onclick={() => { 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">
<PageHeader
title={t('cmdTemplateConfig.title')}
emphasis={t('cmdTemplateConfig.titleEmphasis')}
description={t('cmdTemplateConfig.description')}
crumb="Routing · Commands"
count={configs.length}
countLabel={t('cmdTemplateConfig.countLabel')}
pills={headerPills}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('cmdTemplateConfig.newConfig')}
</button>
</Button>
</PageHeader>
{#if !loaded}<Loading />{:else}
@@ -1,7 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { slide } from 'svelte/transition';
import { api } from '$lib/api';
import { topbarAction } from '$lib/stores/topbar-action.svelte';
import { t } from '$lib/i18n';
import { providersCache, telegramBotsCache, commandConfigsCache } from '$lib/stores/caches.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
@@ -72,7 +73,24 @@
.filter((c: any) => !globalProviderFilter.providerType || c.provider_type === globalProviderFilter.providerType)
.map((c: any) => ({ value: c.id, label: c.name, icon: c.icon || 'mdiCog', desc: c.provider_type })));
onMount(load);
onMount(() => {
topbarAction.set({
label: t('commandTracker.newTracker'),
onclick: () => { showForm ? (showForm = false, editing = null) : openNew(); },
});
load();
});
onDestroy(() => topbarAction.clear());
const headerPills = $derived.by(() => {
const pills: Array<{ label: string; tone: 'mint' | 'sky' | 'citrus' }> = [];
const armed = trackers.filter((tr: { enabled?: boolean }) => tr.enabled).length;
const paused = trackers.length - armed;
if (armed > 0) pills.push({ label: `${armed} ${t('notificationTracker.armed')}`, tone: 'mint' });
if (paused > 0) pills.push({ label: `${paused} ${t('notificationTracker.paused')}`, tone: 'citrus' });
return pills;
});
async function load() {
try {
[allCmdTrackers] = await Promise.all([
@@ -226,7 +244,15 @@
}
</script>
<PageHeader title={t('commandTracker.title')} description={t('commandTracker.description')}>
<PageHeader
title={t('commandTracker.title')}
emphasis={t('commandTracker.titleEmphasis')}
description={t('commandTracker.description')}
crumb="Routing · Commands"
count={trackers.length}
countLabel={t('dashboard.trackersShort')}
pills={headerPills}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('commandTracker.newTracker')}
</Button>
@@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { api, parseDate } from '$lib/api';
import { topbarAction } from '$lib/stores/topbar-action.svelte';
import { t, getLocale } from '$lib/i18n';
import { providersCache, targetsCache, trackingConfigsCache, templateConfigsCache, capabilitiesCache } from '$lib/stores/caches.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
@@ -126,7 +127,25 @@
return base;
});
onMount(load);
onMount(() => {
topbarAction.set({
label: t('notificationTracker.newTracker'),
onclick: () => { showForm ? (showForm = false, editing = null) : openNew(); },
});
load();
});
onDestroy(() => topbarAction.clear());
const headerPills = $derived.by(() => {
const pills: Array<{ label: string; tone: 'mint' | 'sky' | 'coral' | 'citrus' }> = [];
const armed = notificationTrackers.filter(t => t.enabled).length;
const paused = notificationTrackers.length - armed;
if (armed > 0) pills.push({ label: `${armed} ${t('notificationTracker.armed')}`, tone: 'mint' });
if (paused > 0) pills.push({ label: `${paused} ${t('notificationTracker.paused')}`, tone: 'citrus' });
const providerCount = new Set(notificationTrackers.map(t => t.provider_id)).size;
if (providerCount > 0) pills.push({ label: `${providerCount} ${providerCount === 1 ? t('providers.typeSingular') : t('providers.typePlural')}`, tone: 'sky' });
return pills;
});
async function load() {
loadError = '';
@@ -416,7 +435,15 @@
}
</script>
<PageHeader title={t('notificationTracker.title')} description={t('notificationTracker.description')}>
<PageHeader
title={t('notificationTracker.title')}
emphasis={t('notificationTracker.titleEmphasis')}
description={t('notificationTracker.description')}
crumb="Routing · Notification"
count={notificationTrackers.length}
countLabel={t('dashboard.trackersShort')}
pills={headerPills}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('notificationTracker.cancel') : t('notificationTracker.newTracker')}
</Button>
+10 -1
View File
@@ -19,6 +19,8 @@
const gridItemSources: Record<string, () => 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);
+6 -1
View File
@@ -93,7 +93,12 @@
}
</script>
<PageHeader title={t('settings.title')} description={t('settings.description')} />
<PageHeader
title={t('settings.title')}
emphasis={t('settings.titleEmphasis')}
description={t('settings.description')}
crumb="System · Configuration"
/>
{#if !loaded}
<Loading />
@@ -292,7 +292,12 @@
}
</script>
<PageHeader title={t('backup.title')} description={t('backup.description')} />
<PageHeader
title={t('backup.title')}
emphasis={t('backup.titleEmphasis')}
description={t('backup.description')}
crumb="System · Maintenance"
/>
{#if !loaded}
<Loading />
+26 -4
View File
@@ -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 @@
}
</script>
<PageHeader title={activeType ? `${t('targets.title')} ${activeType.charAt(0).toUpperCase() + activeType.slice(1)}` : t('targets.title')} description={activeType ? t(TYPE_DESC_KEYS[activeType]) : t('targets.description')}>
<button onclick={() => { 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">
<PageHeader
title={activeType ? activeType.charAt(0).toUpperCase() + activeType.slice(1) : t('targets.title')}
emphasis={activeType ? t('targets.titleEmphasis') : t('targets.titleEmphasisAll')}
description={activeType ? t(TYPE_DESC_KEYS[activeType]) : t('targets.description')}
crumb="Routing · Targets"
count={targets.length}
countLabel={t('dashboard.targetsShort')}
pills={headerPills}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('targets.cancel') : t('targets.addTarget')}
</button>
</Button>
</PageHeader>
{#if !loaded}<Loading />{:else}
@@ -1,7 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { slide } from 'svelte/transition';
import { api, getBlockedBy, type BlockedByDetail } from '$lib/api';
import { topbarAction } from '$lib/stores/topbar-action.svelte';
import BlockedByModal from '$lib/components/BlockedByModal.svelte';
import { t } from '$lib/i18n';
import { sanitizePreview } from '$lib/sanitize';
@@ -207,7 +208,22 @@
]},
]);
onMount(load);
onMount(() => {
topbarAction.set({
label: t('templateConfig.newConfig'),
onclick: () => { showForm ? (showForm = false, editing = null) : openNew(); },
});
load();
});
onDestroy(() => topbarAction.clear());
const headerPills = $derived.by(() => {
const pills: Array<{ label: string; tone: 'sky' }> = [];
const types = new Set(allTemplateConfigs.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 {
[, varsRef] = await Promise.all([
@@ -379,7 +395,15 @@
}
</script>
<PageHeader title={t('templateConfig.title')} description={t('templateConfig.description')}>
<PageHeader
title={t('templateConfig.title')}
emphasis={t('templateConfig.titleEmphasis')}
description={t('templateConfig.description')}
crumb="Routing · Notification"
count={allTemplateConfigs.length}
countLabel={t('templateConfig.countLabel')}
pills={headerPills}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('templateConfig.newConfig')}
</Button>
@@ -1,7 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import { slide } from 'svelte/transition';
import { api, getBlockedBy, type BlockedByDetail } from '$lib/api';
import { topbarAction } from '$lib/stores/topbar-action.svelte';
import BlockedByModal from '$lib/components/BlockedByModal.svelte';
import { t } from '$lib/i18n';
import { trackingConfigsCache } from '$lib/stores/caches.svelte';
@@ -190,7 +191,21 @@
let form: Record<string, any> = $state(defaultForm());
let descriptor = $derived(getDescriptor(form.provider_type));
onMount(load);
onMount(() => {
topbarAction.set({
label: t('trackingConfig.newConfig'),
onclick: () => { showForm ? (showForm = false, editing = null) : openNew(); },
});
load();
});
onDestroy(() => topbarAction.clear());
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 trackingConfigsCache.fetch(true); }
catch (err: any) { error = err.message || t('common.loadError'); snackError(error); }
@@ -248,7 +263,15 @@
}
</script>
<PageHeader title={t('trackingConfig.title')} description={t('trackingConfig.description')}>
<PageHeader
title={t('trackingConfig.title')}
emphasis={t('trackingConfig.titleEmphasis')}
description={t('trackingConfig.description')}
crumb="Routing · Notification"
count={configs.length}
countLabel={t('trackingConfig.countLabel')}
pills={headerPills}
>
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('trackingConfig.newConfig')}
</Button>
+8 -1
View File
@@ -89,7 +89,14 @@
}
</script>
<PageHeader title={t('users.title')} description={t('users.description')}>
<PageHeader
title={t('users.title')}
emphasis={t('users.titleEmphasis')}
description={t('users.description')}
crumb="System · Access"
count={users.length}
countLabel={t('users.countLabel')}
>
<Button size="sm" onclick={() => showForm = !showForm}>
{showForm ? t('users.cancel') : t('users.addUser')}
</Button>