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:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user