feat(frontend): keyboard nav for the viewer, tag editor and filter

Viewer: j/k mirror the arrow keys, and `e` scrolls to the (lazy) Tags
section and drops the cursor into its filter, forcing the load so focus
lands even before the section is reached.

Tag picker & filter bar: from the search input, ↓/↑ highlight a
suggestion and Enter adds it (focus stays for chaining); with the input
empty ←/→ walk the added tags/tokens and Del removes the focused one. The
filter bar also inserts an operator token on & | ! ( ), applies on
Ctrl+Enter, resets on Ctrl+Backspace and closes on Esc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 17:40:03 +03:00
parent 9a20cc1c84
commit 3a0dbc9ba7
3 changed files with 172 additions and 6 deletions
@@ -184,17 +184,44 @@
}
// ---- Keyboard ----
let tagsSection = $state<HTMLElement>();
let pendingTagFocus = false;
function handleKeydown(e: KeyboardEvent) {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
if (e.key === 'ArrowLeft') {
if (e.key === 'ArrowLeft' || e.key === 'k') {
if (prevId) onNavigate(prevId);
} else if (e.key === 'ArrowRight') {
} else if (e.key === 'ArrowRight' || e.key === 'j') {
if (nextId) onNavigate(nextId);
} else if (e.key === 'e') {
e.preventDefault();
jumpToTags();
} else if (e.key === 'Escape') {
onClose();
}
}
// Scroll the (lazily loaded) Tags section into view and drop the cursor into
// its filter. Forces the load so the focus lands even before the user reaches
// the section by scrolling.
function jumpToTags() {
tagsVisible = true;
tagsSection?.scrollIntoView({ behavior: 'smooth', block: 'start' });
pendingTagFocus = true;
focusTagInput();
}
function focusTagInput() {
requestAnimationFrame(() => tagsSection?.querySelector<HTMLInputElement>('input')?.focus());
}
$effect(() => {
if (tagsLoaded && pendingTagFocus) {
pendingTagFocus = false;
focusTagInput();
}
});
// ---- Helpers ----
function formatDatetime(iso: string | null | undefined): string {
if (!iso) return '—';
@@ -344,7 +371,7 @@
</button>
<!-- Tags (loaded lazily on scroll) -->
<section class="section" use:tagsSentinel>
<section class="section" use:tagsSentinel bind:this={tagsSection}>
<div class="field-label">Tags</div>
{#if tagsLoaded}
<TagPicker {fileTags} onAdd={addTag} onRemove={removeTag} />