feat: collapsible chart, paginator controls, localized template slots

- Dashboard chart collapsible with state persisted in localStorage
- Events per page user-controlled (5/10/20/50) via select, persisted
- Paginator rendered both above and below event list (shared snippet)
- Removed viewport-based page size calculation
- Template slot descriptions localized (templateSlot.* i18n keys)
- Preview As target selector expanded: email, discord, slack added
- Tighter event item spacing
This commit is contained in:
2026-03-24 23:36:41 +03:00
parent 21d8ef712a
commit 337276113d
5 changed files with 188 additions and 61 deletions
@@ -44,6 +44,12 @@
let slotErrorLines = $state<Record<string, number | null>>({});
let slotErrorTypes = $state<Record<string, string>>({});
let validateTimers: Record<string, ReturnType<typeof setTimeout>> = {};
/** Clean up validate timers on unmount */
onMount(() => {
return () => {
for (const timer of Object.values(validateTimers)) clearTimeout(timer);
};
});
let dateFormatPreview = $state<Record<string, string | null>>({});
let expandedSlots = $state<Set<string>>(new Set());
let slotFilter = $state('');
@@ -166,14 +172,20 @@
);
// Group slots into event messages vs scheduled messages based on slot name prefix
function slotDescription(s: {name: string, description: string}): string {
const key = `templateSlot.${s.name}`;
const localized = t(key);
return localized !== key ? localized : s.description;
}
let templateSlots = $derived([
{ group: 'eventMessages', slots: notificationSlots
.filter(s => s.name.startsWith('message_'))
.map(s => ({ key: s.name, label: s.name.replace('message_', '').replace(/_/g, ' '), description: s.description, rows: s.name === 'message_assets_added' ? 10 : 3, isDateFormat: false }))
.map(s => ({ key: s.name, label: s.name.replace('message_', '').replace(/_/g, ' '), description: slotDescription(s), rows: s.name === 'message_assets_added' ? 10 : 3, isDateFormat: false }))
},
{ group: 'scheduledMessages', slots: notificationSlots
.filter(s => !s.name.startsWith('message_'))
.map(s => ({ key: s.name, label: s.name.replace(/_/g, ' '), description: s.description, rows: 6, isDateFormat: false }))
.map(s => ({ key: s.name, label: s.name.replace(/_/g, ' '), description: slotDescription(s), rows: 6, isDateFormat: false }))
},
{ group: 'settings', slots: [
{ key: 'date_format', label: 'dateFormat', description: 'Date+time format', rows: 1, isDateFormat: true },
@@ -418,7 +430,7 @@
<p class="font-medium">{config.name}</p>
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{config.provider_type}</span>
{#if config.user_id === 0}
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">System</span>
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{t('common.system')}</span>
{/if}
</div>
{#if config.description}