ui: plan canvas ink extent + the keyboard-first round

Plan canvas (owner report: arrows clipped at the viewBox edge in dist —
measured 14px/20px cuts in the owner's real plans): blockLayout now
unions node boxes with every edge path's control-point hull (a bezier
never leaves it; the layout calls edgePathD with the renderer's exact
inputs). Dagre's node extent stays the floor, so ordinary plans render
byte-identical; the viewBox origin shifts only when ink bulges past
zero. Verified on both real chats; pins use the owner's real
linear-algebra tree (golden-real-data).

Keyboard-first (owner's law: Enter always submits, arrows always cycle;
text fields the only exception):
- Enter guard split (enter-submits): a focused BUTTON keeps native
  Enter only when keyboard-walked (:focus-visible) or inside an open
  overlay (menus/dialogs); pointer-clicked transcript buttons submit,
  preventDefault suppressing the re-activation underneath.
- Focus hygiene (focus-hygiene): pointer clicks on transient controls
  (agent/user thinking toggles, copy, mermaid expand, plan chip) blur
  to body — the state clicking empty space produces.
- Session search owns the walk: Up/Down wrap-cycle the filtered rows,
  Enter opens the walked row or the first match (quick-open), then
  blurs and keeps the query. The Alt-arrow session cycle is RETIRED.
- Registry: model-menu walk/pick entries removed (obvious in-field
  navigation, not settings material); question-dock keys stay
  displayOnly per owner ruling.

Gates: 1489/1489, typecheck, lint. Verified live end-to-end.
This commit is contained in:
amosblomqvist
2026-10-10 09:43:12 +03:00
parent 643e7596f0
commit 26bc305d66
14 changed files with 403 additions and 72 deletions
+32
View File
@@ -0,0 +1,32 @@
"use client";
/**
* FOCUS HYGIENE — transient transcript controls don't keep focus
* (owner, keyboard round 2026-10-10). A pointer click on a thinking
* toggle, a copy button, a plan chip used to LEAVE focus on that
* button; the learner's next Enter then re-activated the toggle
* instead of submitting (see enter-submits.ts for the other half of
* the fix). Here we restore, after pointer activation, exactly the
* state clicking empty transcript space produces: focus falls back to
* the body — Enter submits, arrows cycle the question rows, nothing
* re-toggles. Deliberately NOT a focus-into-a-text-field: a focused
* field would own the arrows as caret moves, against the arrows-always
* law (owner, 2026-10-10).
*
* Keyboard-activated clicks are untouched: e.detail === 0 marks the
* synthesized click of Enter/Space on a keyboard-focused button — the
* keyboard user keeps their focus.
*/
/** Attach as (or compose into) a transient control's onClick: blurs the
* control after POINTER clicks (keyboard clicks keep focus). Node-safe
* duck-typing — no HTMLElement global needed (the test harness runs
* bare node with linkedom). */
export function blurAfterPointerClick(e: {
detail: number;
currentTarget: EventTarget | null;
}): void {
if (e.detail === 0) return;
const el = e.currentTarget as { blur?: unknown } | null;
if (el && typeof el.blur === "function") (el.blur as () => void).call(el);
}