feat(frontend): keyboard roving-focus and bulk-action keys on the file grid

Arrow keys move a focus ring across the grid (column count derived from
the layout, scrolling the focused card into view and pulling the next
page near the end). Enter opens the focused file; Space/x select; e edits
tags (opening the sheet and focusing its tag filter); p adds to a pool;
Del moves to trash — each falling back to the focused card when nothing
is selected. / opens the filter and focuses its search. The ring only
appears once keyboard navigation starts and is dismissed on pointer use,
so it never distracts mouse users. Escape layering is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 17:30:13 +03:00
parent 49de9fe42b
commit 9a20cc1c84
2 changed files with 148 additions and 13 deletions
@@ -11,6 +11,8 @@
index: number;
selected?: boolean;
selectionMode?: boolean;
/** Roving keyboard-focus ring (shown only during keyboard navigation). */
focused?: boolean;
onTap?: (e: MouseEvent) => void;
/** Called when long-press fires; receives the pointerType of the gesture. */
onLongPress?: (pointerType: string) => void;
@@ -21,6 +23,7 @@
index,
selected = false,
selectionMode = false,
focused = false,
onTap,
onLongPress
}: Props = $props();
@@ -108,6 +111,7 @@
class="card"
class:loaded={!!imgSrc}
class:selected
class:focused
data-file-index={index}
onpointerdown={onPointerDown}
onpointermove={onPointerMoveInternal}
@@ -215,6 +219,12 @@
background-color: color-mix(in srgb, var(--color-accent) 35%, transparent);
}
.card.focused {
outline: 3px solid var(--color-accent);
outline-offset: -3px;
z-index: 1;
}
.check {
position: absolute;
top: 6px;