feat(keys): the conversation list walks with j/k; the cheat sheet names keys in the reader's language

E11 S5 — the HU/EN sweep and the keyboard-only pass over every module.

- The keyboard cheat sheet printed "Space" and "Menu" on a Hungarian sheet: a keycap that carries a
  WORD (not a legend every layout prints alike, like Esc, Ctrl or Enter) is now named by the caller in
  the reader's language (`formatChord(chord, words)`, `keys.cap.*`: Szóköz / Menü).
- The Plex player's ⌫/Esc line said "Stop & back to feed", but the player goes back to wherever it
  was opened from (Home, a Plex page): "Stop & go back" / "Leállítás és vissza".
- Messages: the conversation list is a keyboard list (NavGrid + navItemAttrs, the name button is the
  open element; the pop-out button keeps its own Enter). D1 covers every list whose rows open one
  thing; this one was missed in S4. Notification cards have no single open action and stay out, like
  the download queue.

Sweep, measured: every static `t("…")` key used in the source exists in both languages (the only
misses are template prefixes); the HU values equal to EN are proper nouns and loanwords (Admin, Plex,
Trakt, Shorts). Palette in Hungarian: "téma" makes "Téma: Midnight" the active row ahead of the channel
"Topik - mindig témában", live region "7 találat".

Keyboard-only pass in Chrome, Hungarian UI — 1024×768: every `g` chord lands on its module, `j` rings
the first item (solid 2px accent) on Home, Feed, Channels, Playlists, Plex and Downloads, `s` focuses
the search on Feed, Channels, Playlists and Plex, no page scrolls sideways. 3440×1440: `j` → → ↓
walks Home, Feed (8 columns: 0 → 1 → 9), Channels, Playlists, Plex and Downloads, no sideways scroll.
The help sheet lists every line translated; inside the Plex player "Szóköz / K" and "⌫ / Esc
Leállítás és vissza". Messages: a real `j` rings the row, Enter opens the thread.
This commit is contained in:
2026-09-13 07:00:51 +02:00
parent b5d37ca142
commit 8731508645
9 changed files with 127 additions and 30 deletions
+3 -2
View File
@@ -149,8 +149,9 @@ describe("the page-level rows", () => {
});
it("render as the keys to press, step by step", () => {
expect(formatChord("g h")).toBe("G H");
expect(formatChord("g ,")).toBe("G ,");
const words = { Space: "Space", ContextMenu: "Menu" };
expect(formatChord("g h", words)).toBe("G H");
expect(formatChord("g ,", words)).toBe("G ,");
});
// `g h` shares Home's line with Shift+W; leaving out an unreachable chord must not take that with it.
+13 -5
View File
@@ -213,11 +213,19 @@ describe("helpLines", () => {
});
describe("formatChord", () => {
const words = { Space: "Szóköz", ContextMenu: "Menü" };
it("renders glyphs and upper-cases the letter", () => {
expect(formatChord("ArrowLeft")).toBe("←");
expect(formatChord("Backspace")).toBe("⌫");
expect(formatChord("Shift+ArrowRight")).toBe("⇧ →");
expect(formatChord("k")).toBe("K");
expect(formatChord("Space")).toBe("Space");
expect(formatChord("ArrowLeft", words)).toBe("←");
expect(formatChord("Backspace", words)).toBe("⌫");
expect(formatChord("Shift+ArrowRight", words)).toBe("⇧ →");
expect(formatChord("k", words)).toBe("K");
});
// A key whose cap carries a word is named in the reader's language: the Hungarian sheet said "Space".
it("names a word key in the caller's language", () => {
expect(formatChord("Space", words)).toBe("Szóköz");
expect(formatChord("ContextMenu", words)).toBe("Menü");
expect(formatChord("Shift+F10", words)).toBe("⇧ F10");
});
});
+19 -14
View File
@@ -195,7 +195,7 @@ export const SHORTCUTS = [
{ id: "stats", scope: "plexPlayer", keys: ["d"], labelKey: "keys.stats" },
// Two rows, one help line: ⌫ is the plain "stop & out", Escape is the layer-aware cascade (defer to
// anything above → cancel auto-skip → close the topmost player overlay → close). Sharing a labelKey
// keeps them a single "⌫ Esc Stop & back to feed" row in the sheet.
// keeps them a single "⌫ Esc Stop & go back" row in the sheet.
{ id: "back", scope: "plexPlayer", keys: ["Backspace"], labelKey: "keys.back" },
{
id: "escape",
@@ -558,28 +558,33 @@ const GLYPHS: Record<string, string> = {
ArrowUp: "↑",
ArrowDown: "↓",
Backspace: "⌫",
ContextMenu: "Menu", // the dedicated menu key; "ContextMenu" is a DOM name, not a keycap
Escape: "Esc",
Shift: "⇧",
};
/** The keys a keycap names with a WORD rather than a legend every layout prints alike (Esc, Ctrl,
* Enter): the caller supplies them in the reader's language — a Hungarian sheet said "Space". */
export type KeycapWords = Readonly<Record<"Space" | "ContextMenu", string>>;
const isMac = (): boolean =>
typeof navigator !== "undefined" && /mac|iphone|ipad/i.test(navigator.userAgent);
/** A chord as the help sheet shows it: `Mod+Shift+z` → `Ctrl ⇧ Z` (`⌘ ⇧ Z` on a Mac), and a
* sequence step by step: `g h` → `G H`. */
export function formatChord(chord: string): string {
return stepsOf(chord).map(formatStep).join(" ");
}
function formatStep(chord: string): string {
return chord
.split("+")
.map((part, i, all) => {
if (part.toLowerCase() === "mod") return isMac() ? "⌘" : "Ctrl";
if (i < all.length - 1) return GLYPHS[part] ?? part;
return GLYPHS[part] ?? (part.length === 1 ? part.toUpperCase() : part);
})
export function formatChord(chord: string, words: KeycapWords): string {
const name = (part: string): string | undefined =>
GLYPHS[part] ?? words[part as keyof KeycapWords];
return stepsOf(chord)
.map((step) =>
step
.split("+")
.map((part, i, all) => {
if (part.toLowerCase() === "mod") return isMac() ? "⌘" : "Ctrl";
if (i < all.length - 1) return name(part) ?? part;
return name(part) ?? (part.length === 1 ? part.toUpperCase() : part);
})
.join(" "),
)
.join(" ");
}