From 9ba8d625bc9612596b1f2ef757015a9cf0a1e3ee Mon Sep 17 00:00:00 2001 From: dtoro Date: Fri, 3 Jul 2026 13:22:31 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E2=8C=98K=20command=20palette?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bits-ui Command in a dialog: jump to sections, heaps, folders (from the already-warm sidebar queries), plus dark-mode and shortcut-overlay actions. Global ⌘K binding in the layout works from any route and while inputs hold focus. Co-Authored-By: Claude Fable 5 --- .../components/layout/CommandPalette.svelte | 200 ++++++++++++++++++ web/src/lib/stores/view.svelte.ts | 11 + web/src/routes/+layout.svelte | 14 ++ 3 files changed, 225 insertions(+) create mode 100644 web/src/lib/components/layout/CommandPalette.svelte diff --git a/web/src/lib/components/layout/CommandPalette.svelte b/web/src/lib/components/layout/CommandPalette.svelte new file mode 100644 index 0000000..e078cbb --- /dev/null +++ b/web/src/lib/components/layout/CommandPalette.svelte @@ -0,0 +1,200 @@ + + + + { + if (!o) closePalette(); + }} +> + + + + + + + + + No matches. + + + + + Go to + + + {#each SECTIONS as s (s.label)} + + + {s.label} + + {/each} + + + + {#if (heapsQuery.data ?? []).length > 0} + + + Heaps + + + {#each heapsQuery.data ?? [] as heap (heap.UID)} + + + {heap.Title} + {#if heap.PhotoCount} + + {heap.PhotoCount} + + {/if} + + {/each} + + + {/if} + + {#if folderEntries.length > 0} + + + Folders + + + {#each folderEntries as folder (folder.Path)} + + + {folder.Path} + + {/each} + + + {/if} + + + + Actions + + + {#each ACTIONS as a (a.label)} + + + {a.label} + + {/each} + + + + + + + + diff --git a/web/src/lib/stores/view.svelte.ts b/web/src/lib/stores/view.svelte.ts index 28f919a..24a553a 100644 --- a/web/src/lib/stores/view.svelte.ts +++ b/web/src/lib/stores/view.svelte.ts @@ -83,6 +83,8 @@ export const view = $state<{ previewOpen: boolean; /** Ephemeral: true while the keyboard-shortcuts overlay is open. */ shortcutsOpen: boolean; + /** Ephemeral: true while the ⌘K command palette is open. */ + paletteOpen: boolean; metadataSections: Record; }>({ rightSidebarCollapsed: initial.rightSidebarCollapsed ?? false, @@ -110,6 +112,7 @@ export const view = $state<{ tagsBrowserCollapsed: initial.tagsBrowserCollapsed ?? false, previewOpen: false, shortcutsOpen: false, + paletteOpen: false, metadataSections: initial.metadataSections && typeof initial.metadataSections === 'object' ? { ...initial.metadataSections } @@ -175,6 +178,14 @@ export function closeShortcuts(): void { view.shortcutsOpen = false; } +export function togglePalette(): void { + view.paletteOpen = !view.paletteOpen; +} + +export function closePalette(): void { + view.paletteOpen = false; +} + export function setThumbnailSize(size: ThumbnailSize): void { view.thumbnailSize = size; persist(); diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index b552a0f..aba87cc 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -21,6 +21,8 @@ import PreviewModal from '$lib/components/preview/PreviewModal.svelte'; import MoveToFolderDialog from '$lib/components/layout/MoveToFolderDialog.svelte'; import ShortcutsDialog from '$lib/components/layout/ShortcutsDialog.svelte'; + import CommandPalette from '$lib/components/layout/CommandPalette.svelte'; + import { togglePalette } from '$lib/stores/view.svelte'; let { children } = $props(); @@ -59,8 +61,18 @@ else stopIndexerWatch(); }); + // ⌘K lives at the window level (not gridKeyNav) so the palette opens + // from any route and even while a form field holds focus. + function onGlobalKey(e: KeyboardEvent) { + if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) { + e.preventDefault(); + togglePalette(); + } + } + + Mulimage @@ -125,6 +137,8 @@ + + {:else} {@render children?.()} {/if}