Fix wave: dropdown keyboard/focus, appearance resets, inline key editors, content-sized slider

- MenuShell: after placement, focus lands in the search when the menu has
  one, else the body (tabIndex -1) — the lists' mount-time focus ran during
  the hidden measuring pass and silently failed, so no dropdown ever opened
  focused. The body owns the arrow machine for searchless menus:
  ArrowDown/Up cycle options with wrap (first/last entry from the body,
  native-select convention), Home/End, Enter activates, preventDefault so
  arrows never scroll the page.
- Appearance: the CSS-var applier only ever setProperty'd — a later write
  omitting a var left the stale inline value forever (empty background
  never reset, serif toggle-back looked dead). appliedNamesRef tracks what
  was set; staleVarNames (pure, exported) returns what to removeProperty.
- CommitInput: controlled — after a commit the field snaps to the stored
  record, so the clamp on a -5 prose size is visible (12), Esc reverts.
- Keys: Add-provider dropdown closes on pick; the paste row is inline
  (label | input | Save | Cancel); existing-row editors go inline with an
  explicit Cancel beside Save/Delete — folding is visible, not Esc-only.
- Graph context: readout is content-sized (tabular-nums) — no fixed-width
  dead gap when "off"; the dictation strip fills whatever space remains.

Gates: tsc clean, 866/866 tests (new staleVarNames suite), lint 0, build ok.
Live-verified via Playwright: dropdown focus+arrow cycles+wrap+Enter pick,
background set→empty reset, serif round-trip, visible clamp to 12, provider
pick closes menu, inline editor + Cancel + Esc fold, slider width 270→277px.
This commit is contained in:
amosblomqvist
2026-09-23 00:57:28 +03:00
parent b18294948d
commit 22266973f3
6 changed files with 224 additions and 51 deletions
+20 -5
View File
@@ -9,6 +9,7 @@
* control together) and the control itself locks — the row owns the dim.
*/
import { useState } from "react";
import { cn } from "@/lib/utils";
/** A section block: pronounced mono heading + content. */
@@ -165,7 +166,10 @@ export function CommitInput({
testid,
type = "text",
}: {
/** "" = unset (the factory default). */
/** "" = unset (the factory default). Mirrors the STORED record —
* after a clamped/normalized commit this flows back and the field
* snaps to the stored truth (owner repro: a negative size kept
* displaying "-5" while 12 was stored). */
value: string;
placeholder?: string;
onCommit: (next: string) => void;
@@ -173,21 +177,32 @@ export function CommitInput({
testid?: string;
type?: "text" | "number";
}) {
// The draft exists only while editing; a blur shows the record's value
// again — commit-on-blur/Enter (owner ruling), the field honest about
// what landed.
const [draft, setDraft] = useState(value);
const [editing, setEditing] = useState(false);
return (
<input
type={type}
defaultValue={value}
value={editing ? draft : value}
placeholder={placeholder}
aria-label={ariaLabel}
data-testid={testid}
onBlur={(e) => {
const next = e.target.value.trim();
onChange={(e) => {
setEditing(true);
setDraft(e.target.value);
}}
onBlur={() => {
if (!editing) return;
setEditing(false);
const next = draft.trim();
if (next !== value) onCommit(next);
}}
onKeyDown={(e) => {
if (e.key === "Enter") e.currentTarget.blur();
if (e.key === "Escape") {
e.currentTarget.value = value;
setEditing(false);
e.currentTarget.blur();
}
}}