feat: add IconGrid, EntityPicker controls and enhance search panel
CI / lint-and-check (push) Failing after 4m56s
CI / test (push) Has been skipped
CI / docker-build (push) Has been skipped

Port icon grid and entity picker patterns from wled-screen-controller.
IconGrid replaces plain <select> elements with visual icon grids for
known item sets (widget type, icon type, healthcheck method, permission
level). EntityPicker replaces search dropdowns with a command-palette
style overlay with keyboard navigation and filtering.

Enhance SearchDialog with keyboard navigation (arrow keys, Enter,
Escape), grouped results with section headers, active highlight,
and a footer with shortcut hints.
This commit is contained in:
2026-03-25 11:58:21 +03:00
parent 54a30ca4ca
commit 8d7847889e
11 changed files with 744 additions and 195 deletions
@@ -1,5 +1,9 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import IconGrid from '$lib/components/ui/IconGrid.svelte';
import EntityPicker from '$lib/components/ui/EntityPicker.svelte';
import type { IconGridItem } from '$lib/components/ui/IconGrid.svelte';
import type { EntityPickerItem } from '$lib/components/ui/EntityPicker.svelte';
interface Props {
sectionId: string;
@@ -31,6 +35,26 @@
let statusLabel = $state('');
let statusAppIds = $state<string[]>([]);
const widgetTypeItems: IconGridItem[] = [
{ value: 'app', icon: '🖥️', label: 'App' },
{ value: 'bookmark', icon: '🔖', label: 'Bookmark' },
{ value: 'note', icon: '📝', label: 'Note' },
{ value: 'embed', icon: '🧩', label: 'Embed' },
{ value: 'status', icon: '📊', label: 'Status' }
];
const noteFormatItems: IconGridItem[] = [
{ value: 'markdown', icon: '📝', label: 'Markdown' },
{ value: 'text', icon: '📄', label: 'Plain Text' }
];
const appPickerItems: EntityPickerItem[] = $derived(
apps.map((app) => ({
value: app.id,
label: app.name
}))
);
function resetForm() {
selectedWidgetType = 'app';
selectedAppId = '';
@@ -94,40 +118,30 @@
</script>
<div class="mb-3 rounded-lg border border-border bg-muted/50 p-3">
<!-- Widget Type Selector -->
<!-- Widget Type Selector (Icon Grid) -->
<div class="mb-3">
<label for="widget-type-{sectionId}" class="mb-1 block text-sm font-medium text-foreground">
<label class="mb-1 block text-sm font-medium text-foreground">
Widget Type
</label>
<select
id="widget-type-{sectionId}"
<IconGrid
items={widgetTypeItems}
bind:value={selectedWidgetType}
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
>
<option value="app">App</option>
<option value="bookmark">Bookmark</option>
<option value="note">Note</option>
<option value="embed">Embed</option>
<option value="status">Status</option>
</select>
columns={5}
/>
</div>
<!-- Type-specific config forms -->
{#if selectedWidgetType === 'app'}
<div>
<label for="widget-app-{sectionId}" class="mb-1 block text-sm font-medium text-foreground">
<label class="mb-1 block text-sm font-medium text-foreground">
{$t('widget.select_app')}
</label>
<select
id="widget-app-{sectionId}"
<EntityPicker
items={appPickerItems}
bind:value={selectedAppId}
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
>
<option value="">{$t('widget.choose_app')}</option>
{#each apps as app (app.id)}
<option value={app.id}>{app.name}</option>
{/each}
</select>
placeholder={$t('widget.choose_app')}
searchPlaceholder={$t('widget.choose_app')}
/>
</div>
{:else if selectedWidgetType === 'bookmark'}
<div class="grid gap-3 sm:grid-cols-2">
@@ -177,15 +191,12 @@
{:else if selectedWidgetType === 'note'}
<div class="space-y-3">
<div>
<label for="note-format-{sectionId}" class="mb-1 block text-sm font-medium text-foreground">Format</label>
<select
id="note-format-{sectionId}"
<label class="mb-1 block text-sm font-medium text-foreground">Format</label>
<IconGrid
items={noteFormatItems}
bind:value={noteFormat}
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
>
<option value="markdown">Markdown</option>
<option value="text">Plain Text</option>
</select>
columns={2}
/>
</div>
<div>
<label for="note-content-{sectionId}" class="mb-1 block text-sm font-medium text-foreground">Content</label>