Files
siftlode/frontend/src/components/ui/KeyboardHelp.tsx
T
peter 8731508645 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.
2026-09-13 07:00:51 +02:00

73 lines
3.3 KiB
TypeScript

import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import Modal from "./Modal";
import {
closeKeyboardHelp,
formatChord,
helpLines,
isPageLevelScope,
useActiveScopes,
useHelpConditionValues,
useKeyboardHelpOpen,
useKeyboardHelpOverLayer,
useNavigablePages,
} from "../../lib/keymap";
// The app's one keyboard cheat sheet. Mounted once by App; the global `h` that opens it lives in
// `useGlobalKeymap`, beside the palette's Mod+K, so one listener owns the whole `global` scope.
//
// It renders the shared SHORTCUTS table filtered to the scopes that are live right now — the Plex
// player's keys while you are in the player, the app-wide ones underneath — so there is a single
// answer to "what can I press here" and nothing to keep in sync. Before this, the Plex player had a
// cheat sheet nobody else did, listing an array hand-copied from its own handler.
//
// `h` rather than the conventional `?`: on a Hungarian layout `?` is a Shift chord, and `h` is the
// key this app's reader already has in their fingers from the player.
//
// Deliberately NOT a new overlay primitive: `Modal` already portals into
// `document.fullscreenElement ?? document.body` (so this works over a fullscreen player), registers
// a layer so Escape closes it before the player underneath, traps focus and handles browser Back.
export default function KeyboardHelp() {
const { t } = useTranslation();
const open = useKeyboardHelpOpen();
const scopes = useActiveScopes();
const conditions = useHelpConditionValues();
const overLayer = useKeyboardHelpOverLayer();
const pages = useNavigablePages();
if (!open) return null;
// A `when` row is listed only where it applies — "Previous / next episode" inside a movie would be
// a promise the player does not keep.
// Opened over a player or a dialog, the page-level keys (`s`, `g …`, `j`/`k`) are quiet — listing
// them there would promise keys that do nothing until the overlay closes. And a `g` chord is listed
// only for a module this account can actually reach.
// The context-menu key goes too: every `useContextMenu` is a page list, and with an overlay up the
// focus Shift+F10 needs is inside the overlay (pre-existing since E10, found in S4 UAT).
const live = overLayer
? scopes.filter((s) => !isPageLevelScope(s) && s !== "contextMenu")
: scopes;
const lines = helpLines(live, pages).filter((l) => !l.when || !!conditions[l.when]);
const words = { Space: t("keys.cap.space"), ContextMenu: t("keys.cap.menu") };
return (
<Modal title={t("keys.title")} onClose={closeKeyboardHelp} maxWidth="max-w-md">
<dl className="grid grid-cols-[auto_1fr] items-center gap-x-4 gap-y-2.5 text-sm">
{lines.map((line) => (
<Fragment key={line.labelKey}>
<dt className="flex flex-wrap gap-1">
{line.chords.map((chord) => (
<kbd
key={chord}
className="rounded-sm border border-border bg-surface px-1.5 py-0.5 text-xs font-mono whitespace-nowrap"
>
{formatChord(chord, words)}
</kbd>
))}
</dt>
<dd className="text-muted">{t(line.labelKey)}</dd>
</Fragment>
))}
</dl>
</Modal>
);
}