artifacts: demo scrollbar death, figure natural sizing + fullscreen, downloads, invert toggles (release polish 7)

- demo iframe: scrolling=no (the bridge auto-sizes; a scrollable iframe
  trapped the wheel) + content-box so the bridge height is the viewport
- figures: svgNaturalWidth hug + center (w-fit max-w-full mx-auto),
  full-width fallback for viewBox-only svgs
- fullscreen: MermaidOverlay figure mode (raw svg, white stage, invert
  carried); openFigure context; figureFocus rides the mermaid Esc layer
- downloads: figure.svg + <slug>.html (raw pre-bridge), shared download.ts
- invert: manual ContrastIcon toggle on both cards — invert(1)
  hue-rotate(180deg) on the white-bearing element, default off, no
  detection, no theme wiring (owner rulings 2026-10-07)

1433 tests green; live smoke passed (hug/center measured, filters verified,
downloads fired, Esc closed; wheel-chaining structurally sound)
This commit is contained in:
amosblomqvist
2026-10-07 13:45:03 +03:00
parent a763b9fc5c
commit 4070cc7926
7 changed files with 289 additions and 28 deletions
+23 -2
View File
@@ -349,7 +349,7 @@ export function Chat({
const onAbort = () => {
const target = escAbortTarget({
abortAsk: abortAskRef.current !== null,
mermaidFocus: mermaidFocusRef.current !== null,
mermaidFocus: mermaidFocusRef.current !== null || figureFocusRef.current !== null,
planOpen: planOpenRef.current,
setupOpen: setupOpenRef.current,
pending: pendingRef.current !== null,
@@ -790,12 +790,21 @@ export function Chat({
// popup) mount INSIDE the session-area div — both are session-scoped, so
// the drag bar stays sharp and draggable while a focus owns the area.
const [mermaidFocus, setMermaidFocus] = useState<string | null>(null);
// Figure focus (2026-10-07): the figure card's fullscreen button — same
// overlay shell as mermaid, but carrying the raw svg + the card's invert
// state so the fullscreen view stays faithful to the toggled card.
const [figureFocus, setFigureFocus] = useState<{ svg: string; inverted: boolean } | null>(null);
// Overlay mirrors for the layered-Esc gate (owner 2026-10-04): the
// lh:kbd-abort-turn handler reads these at EVENT time — declared here
// beside their states, consumed below wherever the states live.
const mermaidFocusRef = useRef(mermaidFocus);
mermaidFocusRef.current = mermaidFocus;
// The figure focus rides the mermaid layer in the Esc gate — the same
// overlay class (session-scoped diagram focus, Esc claimed at the
// document level by the overlay itself); the window chain must stay out.
const figureFocusRef = useRef(figureFocus);
figureFocusRef.current = figureFocus;
const planOpenRef = useRef(planOpen);
planOpenRef.current = planOpen;
const setupOpenRef = useRef(setupOpen);
@@ -1137,7 +1146,12 @@ export function Chat({
openSetup: () => setSetupOpen(true),
}}
>
<SessionMermaidContext.Provider value={{ openMermaid: setMermaidFocus }}>
<SessionMermaidContext.Provider
value={{
openMermaid: setMermaidFocus,
openFigure: (svg, inverted) => setFigureFocus({ svg, inverted }),
}}
>
<JobsContext.Provider value={jobs}>
{/* The session surface: the transcript column (header + scroller) and
the plan-tree panel as full-height SIBLINGS — the panel spans top to
@@ -1601,6 +1615,13 @@ export function Chat({
{mermaidFocus !== null && (
<MermaidOverlay chart={mermaidFocus} onClose={() => setMermaidFocus(null)} />
)}
{figureFocus !== null && (
<MermaidOverlay
svg={figureFocus.svg}
inverted={figureFocus.inverted}
onClose={() => setFigureFocus(null)}
/>
)}
{planOpen && <PlanPopup messages={messages} onClose={() => setPlanOpen(false)} />}
<AbortTurnDialog mode={abortAsk} onClose={() => setAbortAsk(null)} onConfirm={confirmAbort} busy={busy} />
</div>