core/workbench-ui/src/app/KeyboardHelp.tsx
Shay c54a942871 feat(workbench): interaction substrate — list nav, virtualization, panel chrome, inspector resize, palette verbs (Wave R R0d)
- useListNavigation: shared j/k/arrows/Home/End/Enter keyboard spine,
  container or window scope, input guard, roving tabindex. Replaces
  ProposalsRoute's bespoke window listener (one pattern app-wide).
- VirtualizedList over @tanstack/react-virtual composing the hook;
  evidence lists render O(viewport).
- Panel: standard header/toolbar/body chrome for routes.
- Inspector resize: Shell center cell becomes SplitPane(main, inspector)
  when open; width persists via guarded storage (SplitPane id).
- Palette action verbs: Command gains kind (navigate|action); Shell
  registers Toggle inspector + Copy evidence link; EvalsRoute registers
  'Run eval lane <lane>' per read-only lane (same POST /evals/run).
- shortcutRegistry: binding sites register what they handle while
  mounted; KeyboardHelp renders FROM the registry — advertising an
  unbound shortcut is structurally impossible. j/k, /, Enter rows
  (removed in R0a as false) return as live registry entries.
- Kbd primitive (front-loaded from R1); SearchInput registers '/';
  SearchInput wired into Proposals + Replay lists with client filters.
- Preview entries for Kbd/Panel/VirtualizedList.

Verified: build green; vitest 33 files / 245 tests EXIT=0 (~5.5s);
playwright 12/12.
2026-06-12 12:48:02 -07:00

56 lines
2.1 KiB
TypeScript

import * as Dialog from "@radix-ui/react-dialog";
import { Kbd } from "../design/components/primitives/Kbd";
import { useShortcuts } from "./shortcutRegistry";
/**
* Keyboard help overlay — registry-driven (Wave R brief R0d).
*
* Rows render from the live shortcut registry, where binding sites register
* exactly what they handle while mounted. Advertising an unimplemented
* shortcut is structurally impossible: there is no hand-maintained list to
* drift. (R0a removed three false rows by hand; this removes the failure
* mode itself.)
*/
export function KeyboardHelp({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const shortcuts = useShortcuts();
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/55" />
<Dialog.Content
className="fixed left-1/2 top-[18vh] w-[min(400px,calc(100vw-32px))] -translate-x-1/2 rounded-lg border border-[var(--color-border-strong)] bg-[var(--color-surface-overlay)] p-4 shadow-[var(--shadow-overlay)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)]"
aria-label="Keyboard shortcuts"
>
<Dialog.Title className="mb-3 text-sm font-semibold text-[var(--color-text-primary)]">
Keyboard Shortcuts
</Dialog.Title>
<Dialog.Description className="sr-only">
Keyboard shortcuts currently active in the workbench.
</Dialog.Description>
<dl className="m-0 grid gap-0">
{shortcuts.map((s) => (
<div
key={s.id}
className="flex items-center gap-3 border-b border-[var(--color-border-subtle)] py-2 last:border-b-0"
>
<dt className="m-0 w-20 shrink-0">
<Kbd>{s.keys}</Kbd>
</dt>
<dd className="m-0 text-sm text-[var(--color-text-secondary)]">
{s.action}
</dd>
</div>
))}
</dl>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}