feat(frontend): group targets by bot, redesign backup settings
Targets page: collapse targets under a per-bot header (BotGroupHeader) with a count chip and an "Open bot" cross-link. Receivers are hidden by default and expand per group; non-bot types fall back to a "Direct delivery" group. Telegram "Add receiver" now opens the EntitySelect chat palette directly instead of an inline form — EntitySelect grew a bindable `open` flag, `showTrigger`, and an `onclose` cancel signal. Backup settings page: split the monolithic +page into focused panels (BackupHero, BackupLedger, ExportPanel, ImportPanel, PendingStrip, ScheduleCassette) and introduce a stepwise export/import flow with category groups, secrets handling, conflict policy, and validation gating. New i18n keys in both locales cover the bot grouping labels and the backup step copy.
This commit is contained in:
@@ -20,7 +20,10 @@
|
||||
noneLabel = '—',
|
||||
disabled = false,
|
||||
size = 'default',
|
||||
open = $bindable(false),
|
||||
showTrigger = true,
|
||||
onselect,
|
||||
onclose,
|
||||
}: {
|
||||
items: EntityItem[];
|
||||
value: string | number | null;
|
||||
@@ -29,10 +32,12 @@
|
||||
noneLabel?: string;
|
||||
disabled?: boolean;
|
||||
size?: 'sm' | 'default';
|
||||
open?: boolean;
|
||||
showTrigger?: boolean;
|
||||
onselect?: (value: string | number | null) => void;
|
||||
onclose?: () => void;
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let query = $state('');
|
||||
let highlightIdx = $state(0);
|
||||
let inputEl = $state<HTMLInputElement | undefined>();
|
||||
@@ -52,24 +57,37 @@
|
||||
return [...result, ...matching];
|
||||
});
|
||||
|
||||
// Focus input whenever the palette transitions to open (covers both internal
|
||||
// trigger clicks and external programmatic opening via bind:open).
|
||||
let wasOpen = false;
|
||||
$effect(() => {
|
||||
if (open && !wasOpen) {
|
||||
query = '';
|
||||
highlightIdx = Math.max(0, filtered.findIndex(i => String(i.value) === String(value)));
|
||||
requestAnimationFrame(() => inputEl?.focus());
|
||||
}
|
||||
wasOpen = open;
|
||||
});
|
||||
|
||||
function openPalette() {
|
||||
if (disabled) return;
|
||||
open = true;
|
||||
query = '';
|
||||
highlightIdx = Math.max(0, filtered.findIndex(i => String(i.value) === String(value)));
|
||||
requestAnimationFrame(() => inputEl?.focus());
|
||||
}
|
||||
|
||||
// Called when the user dismisses the palette (overlay click or ESC).
|
||||
// Selection uses its own quiet-close path so onclose stays a true "cancel" signal.
|
||||
function closePalette() {
|
||||
open = false;
|
||||
query = '';
|
||||
onclose?.();
|
||||
}
|
||||
|
||||
function selectItem(item: EntityItem) {
|
||||
if (item.disabled) return;
|
||||
value = item.value || null;
|
||||
onselect?.(value);
|
||||
closePalette();
|
||||
open = false;
|
||||
query = '';
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
@@ -106,21 +124,23 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Trigger button -->
|
||||
<button type="button" class="es-trigger" class:es-sm={size === 'sm'} onclick={openPalette}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
style="opacity: {disabled ? 0.5 : 1}; cursor: {disabled ? 'default' : 'pointer'};">
|
||||
{#if selected}
|
||||
{#if selected.icon}
|
||||
<span class="es-trigger-icon"><MdiIcon name={selected.icon} size={16} /></span>
|
||||
<!-- Trigger button (hidden when the parent drives `open` via bind:open) -->
|
||||
{#if showTrigger}
|
||||
<button type="button" class="es-trigger" class:es-sm={size === 'sm'} onclick={openPalette}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
style="opacity: {disabled ? 0.5 : 1}; cursor: {disabled ? 'default' : 'pointer'};">
|
||||
{#if selected}
|
||||
{#if selected.icon}
|
||||
<span class="es-trigger-icon"><MdiIcon name={selected.icon} size={16} /></span>
|
||||
{/if}
|
||||
<span class="es-trigger-label">{selected.label}</span>
|
||||
{:else}
|
||||
<span class="es-trigger-label es-trigger-none">{placeholder}</span>
|
||||
{/if}
|
||||
<span class="es-trigger-label">{selected.label}</span>
|
||||
{:else}
|
||||
<span class="es-trigger-label es-trigger-none">{placeholder}</span>
|
||||
{/if}
|
||||
<span class="es-trigger-arrow"><MdiIcon name="mdiChevronDown" size={14} /></span>
|
||||
</button>
|
||||
<span class="es-trigger-arrow"><MdiIcon name="mdiChevronDown" size={14} /></span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Palette overlay — portalled to <body> to escape backdrop-filter ancestors -->
|
||||
{#if open}
|
||||
|
||||
Reference in New Issue
Block a user