feat(e10): put every keyboard shortcut in one table

Six surfaces each owned a private keydown listener with its own key
literals and its own idea of "am I typing?", and they had drifted:
PlexPlayer matched `case "a"` without checking modifiers, so Ctrl+A
(select all) cycled the audio track, and its typing guard missed
<select> and contentEditable where the other two surfaces covered both.

lib/keymap.ts is a TABLE, not a dispatcher. Each surface keeps its own
listener and closures - PlexPlayer's refs are not worth re-plumbing -
but the chords, the modifier rules and the typing guard live in one
place, and dispatch() binds a scope's rows to handlers through a mapped
type, so a row without a handler (or a handler without a row) is a tsc
error rather than a comment someone forgets.

Matching is now exact on modifiers, which is what kills the Ctrl+A bug
and lets Shift+arrow be its own row beside the bare arrow. Escape opts
out of the typing guard (allowInInput) so it stays the way out of a
player's own subtitle-offset field.

The player's shortcut sheet renders helpLines() instead of a hand-copied
array beside the handler - the two used to be edited separately, which
is how a sheet ends up documenting a key that no longer exists. Labels
move to the new `keys` i18n namespace (EN + HU), and a test asserts
every row is named in both.
This commit is contained in:
2026-09-05 20:58:17 +02:00
parent 928eac4c89
commit 12822a9e04
10 changed files with 663 additions and 226 deletions
+13 -15
View File
@@ -1,10 +1,12 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Redo2, Undo2 } from "lucide-react";
import { dispatch } from "../../lib/keymap";
// Reusable undo/redo buttons with Ctrl/Cmd+Z (undo) and Ctrl/Cmd+Y or Ctrl/Cmd+Shift+Z
// (redo) shortcuts. Pairs with useUndoable but takes plain props, so it works with any
// undo source. The shortcuts ignore keystrokes while a text field is focused.
// undo source. The chords and the typing guard come from the shared keymap (`undo` scope), so
// they are listed in the help sheet with everything else instead of only living here.
export default function UndoToolbar({
canUndo,
canRedo,
@@ -20,22 +22,18 @@ export default function UndoToolbar({
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (!(e.ctrlKey || e.metaKey)) return;
const tgt = e.target as HTMLElement | null;
if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable))
return;
const k = e.key.toLowerCase();
if (k === "z" && !e.shiftKey) {
if (canUndo) {
e.preventDefault();
dispatch(e, "undo", {
undo: (ev) => {
if (!canUndo) return;
ev.preventDefault();
onUndo();
}
} else if (k === "y" || (k === "z" && e.shiftKey)) {
if (canRedo) {
e.preventDefault();
},
redo: (ev) => {
if (!canRedo) return;
ev.preventDefault();
onRedo();
}
}
},
});
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);