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
@@ -11,8 +11,15 @@
import EmptyState from '$lib/components/EmptyState.svelte';
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import CrossLink from '$lib/components/CrossLink.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
function templateName(id: number | null): string {
if (!id) return '';
const tpl = cmdTemplateConfigs.find((c: any) => c.id === id);
return tpl?.name || `#${id}`;
}
let configs = $derived(commandConfigsCache.items);
let cmdTemplateConfigs = $derived(commandTemplateConfigsCache.items);
let loaded = $state(false);
@@ -243,10 +250,15 @@
</span>
<span class="text-xs text-[var(--color-muted-foreground)]">{cfg.locale?.toUpperCase()}</span>
</div>
<p class="text-xs text-[var(--color-muted-foreground)] mt-0.5">
{t('commandConfig.responseMode')}: {cfg.response_mode === 'media' ? t('commandConfig.modeMedia') : t('commandConfig.modeText')}
&middot; {t('commandConfig.defaultCount')}: {cfg.default_count}
</p>
<div class="flex items-center gap-2 mt-0.5">
<span class="text-xs text-[var(--color-muted-foreground)]">
{t('commandConfig.responseMode')}: {cfg.response_mode === 'media' ? t('commandConfig.modeMedia') : t('commandConfig.modeText')}
&middot; {t('commandConfig.defaultCount')}: {cfg.default_count}
</span>
{#if cfg.command_template_config_id}
<CrossLink href="/command-template-configs" icon="mdiCodeBracesBox" label={templateName(cfg.command_template_config_id)} />
{/if}
</div>
</div>
<div class="flex items-center gap-1">
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => editConfig(cfg)} />