feat: EntitySelect palette-style entity picker, replace select dropdowns

New EntitySelect component: modal palette with search, keyboard nav,
current-value indicator (left border), smooth overlay. Replaces native
<select> dropdowns for entity selection.

Replaced selects:
- Notification Trackers: provider selector
- Command Trackers: provider + command config selectors
- Command Configs: response template selector
- Targets: telegram bot, email bot, matrix bot selectors

Added reactive $effect watchers for loadCollections and loadBotChats
since EntitySelect uses bind:value instead of onchange.
This commit is contained in:
2026-03-22 00:21:57 +03:00
parent 86115f5c75
commit a3a1fe3d75
5 changed files with 359 additions and 46 deletions
@@ -13,6 +13,7 @@
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import CrossLink from '$lib/components/CrossLink.svelte';
import EntitySelect from '$lib/components/EntitySelect.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import { highlightFromUrl } from '$lib/highlight';
import type { ServiceProvider, TelegramBot } from '$lib/types';
@@ -21,6 +22,9 @@
let providers = $derived(providersCache.items);
let commandConfigs = $derived(commandConfigsCache.items);
let telegramBots = $derived(telegramBotsCache.items);
const providerItems = $derived(providers.map(p => ({ value: p.id, label: p.name, icon: p.icon || 'mdiServer', desc: p.type })));
const configItems = $derived(filteredConfigs().map((c: any) => ({ value: c.id, label: c.name, icon: c.icon || 'mdiCog', desc: c.provider_type })));
const botItems = $derived(telegramBots.map(b => ({ value: b.id, label: b.name, icon: b.icon || 'mdiRobot', desc: b.bot_username ? `@${b.bot_username}` : '' })));
let loaded = $state(false);
let showForm = $state(false);
let editing = $state<number | null>(null);
@@ -191,25 +195,13 @@
</div>
<div>
<label for="trk-provider" class="block text-sm font-medium mb-1">{t('commandTracker.provider')}</label>
<select id="trk-provider" bind:value={form.provider_id} required
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
<option value={0} disabled>{t('commandTracker.selectProvider')}</option>
{#each providers as p}
<option value={p.id}>{p.name} ({p.type})</option>
{/each}
</select>
<label class="block text-sm font-medium mb-1">{t('commandTracker.provider')}</label>
<EntitySelect items={providerItems} bind:value={form.provider_id} placeholder={t('commandTracker.selectProvider')} />
</div>
<div>
<label for="trk-config" class="block text-sm font-medium mb-1">{t('commandTracker.commandConfig')}</label>
<select id="trk-config" bind:value={form.command_config_id} required
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
<option value={0} disabled>{t('commandTracker.selectCommandConfig')}</option>
{#each filteredConfigs() as c}
<option value={c.id}>{c.name}</option>
{/each}
</select>
<label class="block text-sm font-medium mb-1">{t('commandTracker.commandConfig')}</label>
<EntitySelect items={configItems} bind:value={form.command_config_id} placeholder={t('commandTracker.selectCommandConfig')} />
</div>
<button type="submit" disabled={submitting}