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:
@@ -13,10 +13,23 @@
|
||||
* What the window listener refuses to touch:
|
||||
* - typing surfaces (input/textarea/select/contenteditable) — the focused
|
||||
* surface owns its Enter (the dock field, the composer, settings);
|
||||
* - BUTTONS — Enter on a focused button is its click (keyboard button
|
||||
* semantics: the collapsed Problem bar must expand, not submit);
|
||||
* - modified Enter (⇧↵ is a newline, ⌘/Ctrl↵ belongs to the surface).
|
||||
*
|
||||
* BUTTONS — the pointer/keyboard split (owner, keyboard round 2026-10-10):
|
||||
* a mouse click on a transcript control (a thinking toggle, a copy button)
|
||||
* leaves focus ON that button; the learner's next Enter must SUBMIT, not
|
||||
* re-activate the toggle — the report that born this rule. But two button
|
||||
* populations keep native Enter-activates:
|
||||
* 1. KEYBOARD-focused buttons — :focus-visible means focus arrived by
|
||||
* keyboard (the arrow-walked menus; Tab is dead app-wide, so this
|
||||
* only happens inside overlays);
|
||||
* 2. buttons inside an OPEN OVERLAY — menus walk focus with arrows and
|
||||
* Enter activates the walked button natively (menu-shell's contract),
|
||||
* dialogs are their own surfaced layer (the ONE-ESC LAW's layers).
|
||||
* The pointer-clicked transcript button fails both tests → Enter is ours
|
||||
* (preventDefault also suppresses the browser's default activation, so
|
||||
* the toggle does not fire underneath the submit).
|
||||
*
|
||||
* The submit callbacks self-guard (empty thinking / canSubmit) — this hook
|
||||
* never checks answer validity, only focus honesty.
|
||||
*/
|
||||
@@ -24,6 +37,26 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { isTypingSurface } from "@/src/core";
|
||||
|
||||
/** Overlay roots whose buttons keep native Enter (hand-rolled menus mark
|
||||
* themselves data-lh-menu; Radix poppers/dialogs carry their own attrs). */
|
||||
const OVERLAY_ROOTS = '[data-lh-menu], [data-radix-popper-content-wrapper], [role="menu"], [role="dialog"]';
|
||||
|
||||
/** Pure decision, exported for pins: does a focused BUTTON keep native
|
||||
* Enter (true) or does Enter belong to submit (false)? */
|
||||
export function buttonKeepsNativeEnter(el: HTMLElement): boolean {
|
||||
// Keyboard-walked focus: the menu arrows landed here — Enter activates.
|
||||
// (Guarded: a selector engine without the pseudo — linkedom in the
|
||||
// test harness — degrades to "not keyboard focus", never throws on
|
||||
// every Enter.)
|
||||
try {
|
||||
if (el.matches(":focus-visible")) return true;
|
||||
} catch {
|
||||
// unsupported pseudo — treat as pointer focus
|
||||
}
|
||||
// Inside an open overlay: the overlay owns its keys.
|
||||
return !!el.closest(OVERLAY_ROOTS);
|
||||
}
|
||||
|
||||
/** Mount-scoped: while the dock is mounted (a problem/question is active),
|
||||
* plain Enter anywhere outside a typing surface or button submits it. */
|
||||
export function useEnterSubmits(submit: () => void): void {
|
||||
@@ -34,7 +67,8 @@ export function useEnterSubmits(submit: () => void): void {
|
||||
if (e.key !== "Enter") return;
|
||||
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
|
||||
if (isTypingSurface(e)) return;
|
||||
if ((e.target as HTMLElement | null)?.tagName === "BUTTON") return;
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target?.tagName === "BUTTON" && buttonKeepsNativeEnter(target)) return;
|
||||
e.preventDefault();
|
||||
submitRef.current();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user