feat(audio-player): keyboard shortcuts — declarative keymap, dialog, media keys (plan §2)
Space/Enter (body-focus only), H/L + arrows skip, J/K + PgDn/PgUp chapter steps, B bookmark capture (note box auto-focuses, Enter saves even when blank, Shift+Enter inserts a newline, Esc cancels), Alt+arrows / Alt+H/L speed steps, up/down volume with a brief OSD, M mute, [ ] skip-duration cycle, ? shortcut dialog, Esc closes the top-most surface (help → capture → note edit → drawers → dropdowns). audio-keys.ts is the ONE declarative table: resolveShortcut() drives the document keydown dispatcher and visibleKeymap() renders the templ dialog (x-for over the exposed keymap — templ cannot import TS). Hardware media keys ride the existing MediaSession handlers; the Media* keydown codes resolve through the same table to the SAME methods, so both layers stay one behavior. Guards mirror the ebook reader: typing in any INPUT/TEXTAREA/contentEditable never triggers shortcuts (bookmark notes included), a focused button keeps Space/Enter for itself, and the focused scrubber keeps its arrows (its own @change seeks — a native arrow nudge moves 0.08s, not a 10s skip). SKIP_SECONDS consolidates to one const beside SPEEDS (the templ skip dropdown now iterates skipChoices; [ ] cycles via cycleSkip()) — the same one-source pattern as the §5 speed-list consolidation. Unit: audio-keys.test.ts (resolution, Alt disambiguation, media keys, keymap integrity and conflict-freedom, dialog visibility) and cycleSkip tests. Live: 39 Playwright checks on the KJV folder book covering every binding, the guards, dialog and OSD; probe user created and deleted.
This commit is contained in:
1 parent
db96ea91cd
commit
295c4b0664
6 files changed
+569
-22
No files matched your search
+19
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { SPEEDS, normalizeSpeed, stepSpeed } from "./audio";
|
||||
import { SPEEDS, normalizeSpeed, stepSpeed, SKIP_SECONDS, cycleSkip } from "./audio";
|
||||
|
||||
describe("SPEEDS", () => {
|
||||
it("is the agreed list from the player plan (0.5 through 3)", () => {
|
||||
@@ -53,3 +53,21 @@ describe("stepSpeed", () => {
|
||||
expect(stepSpeed(2.2, -1)).toBe(0.75);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SKIP_SECONDS + cycleSkip", () => {
|
||||
it("is the agreed skip list", () => {
|
||||
expect(SKIP_SECONDS).toEqual([1, 5, 10, 15, 30]);
|
||||
});
|
||||
|
||||
it("cycles forward and backward, clamping at the ends", () => {
|
||||
expect(cycleSkip(10, 1)).toBe(15);
|
||||
expect(cycleSkip(10, -1)).toBe(5);
|
||||
expect(cycleSkip(1, -1)).toBe(1);
|
||||
expect(cycleSkip(30, 1)).toBe(30);
|
||||
});
|
||||
|
||||
it("anchors unknown currents at 10s before stepping", () => {
|
||||
expect(cycleSkip(7, 1)).toBe(15);
|
||||
expect(cycleSkip(7, -1)).toBe(5);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user