feat: IconGridSelect, CrossLink, SearchPalette components + entity crosslinks

New components:
- IconGridSelect: Visual grid selector replacing <select> dropdowns,
  with icon + label cells, fixed-position popup, smart placement
- CrossLink: Inline clickable badge for cross-entity navigation,
  hover highlights primary, used on entity cards
- SearchPalette: Ctrl+K global command palette, searches all entity
  types via cached data, grouped results, keyboard navigation

Integration:
- Targets: type selector uses IconGridSelect (4-column grid with icons)
- Targets: bot crosslink on telegram/email/matrix target cards
- Command Trackers: provider and config badges → CrossLinks
- Command Trackers: listener bot name → CrossLink
- Command Configs: template config shown as CrossLink on card
- Notification Trackers: provider CrossLink added to card
- Layout: SearchPalette mounted globally

Infrastructure:
- Added CommandConfig, CommandTemplateConfig, CommandTracker types
- Added notificationTrackersCache, commandTrackersCache to caches
- Added allCaches map and fetchAllCaches() for search palette
- Added searchPalette i18n keys (EN/RU)
This commit is contained in:
2026-03-21 23:44:12 +03:00
parent 563716fa76
commit 06b24638cb
12 changed files with 764 additions and 26 deletions
+34 -7
View File
@@ -14,9 +14,34 @@
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
import Hint from '$lib/components/Hint.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import CrossLink from '$lib/components/CrossLink.svelte';
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import type { NotificationTarget, TelegramBot, TelegramChat, EmailBot, MatrixBot } from '$lib/types';
function getBotName(target: any): string | null {
if (target.type === 'telegram' && target.config?.bot_id) {
const bot = telegramBots.find(b => b.id === target.config.bot_id);
return bot?.name || null;
}
if (target.type === 'email' && target.config?.email_bot_id) {
const bot = emailBots.find(b => b.id === target.config.email_bot_id);
return bot?.name || null;
}
if (target.type === 'matrix' && target.config?.matrix_bot_id) {
const bot = matrixBots.find(b => b.id === target.config.matrix_bot_id);
return bot?.name || null;
}
return null;
}
function getBotHref(target: any): string {
if (target.type === 'telegram') return '/telegram-bots';
if (target.type === 'email') return '/telegram-bots?tab=email';
if (target.type === 'matrix') return '/telegram-bots?tab=matrix';
return '/telegram-bots';
}
const ALL_TYPES = ['telegram', 'webhook', 'email', 'discord', 'slack', 'ntfy', 'matrix'] as const;
type TargetType = typeof ALL_TYPES[number];
const TYPE_ICONS: Record<string, string> = {
@@ -28,6 +53,12 @@
discord: 'targets.descDiscord', slack: 'targets.descSlack', ntfy: 'targets.descNtfy', matrix: 'targets.descMatrix',
};
const typeGridItems = $derived(ALL_TYPES.map(tt => ({
value: tt,
icon: TYPE_ICONS[tt] || 'mdiTarget',
label: tt.charAt(0).toUpperCase() + tt.slice(1),
})));
let allTargets = $derived(targetsCache.items);
let activeType = $derived(page.url.searchParams.get('type') as TargetType | null);
let targets = $derived(activeType ? allTargets.filter(t => t.type === activeType) : allTargets);
@@ -189,13 +220,8 @@
<form onsubmit={save} class="space-y-4">
{#if !activeType}
<div>
<label for="tgt-type" class="block text-sm font-medium mb-1">{t('targets.type')}</label>
<select id="tgt-type" bind:value={formType}
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
{#each ALL_TYPES as tt}
<option value={tt}>{tt.charAt(0).toUpperCase() + tt.slice(1)}</option>
{/each}
</select>
<label class="block text-sm font-medium mb-1">{t('targets.type')}</label>
<IconGridSelect items={typeGridItems} bind:value={formType} columns={4} />
</div>
{/if}
<div>
@@ -380,6 +406,7 @@
<p class="font-medium">{target.name}</p>
{#if !activeType}<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{target.type}</span>{/if}
{#if target.receiver_count}<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{target.receiver_count} receiver(s)</span>{/if}
{#if getBotName(target)}<CrossLink href={getBotHref(target)} icon="mdiRobot" label={getBotName(target)} />{/if}
</div>
<p class="text-sm text-[var(--color-muted-foreground)]">
{#if target.type === 'telegram'}