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
+53 -2
View File
@@ -39,7 +39,9 @@ import {
INITIAL_DEMO_BRIDGE_STATE,
reduceDemoBridgeState,
} from "../components/artifacts/demo-card.tsx";
import { sanitizeSvg, VisualCard } from "../components/artifacts/visual-card.tsx";
import { sanitizeSvg, svgNaturalWidth, VisualCard, VisualOutputCard } from "../components/artifacts/visual-card.tsx";
import { slugFilename } from "../components/artifacts/download.ts";
import { chartSize } from "../components/session-overlay.tsx";
import { SubagentSteerCard } from "../components/artifacts/steer-card.tsx";
import { DispatchCard } from "../components/artifacts/dispatch-card.tsx";
import { JobCard, PdfJobCard } from "../components/artifacts/job-card.tsx";
@@ -378,7 +380,10 @@ describe("steer card rendering (the delivered artifact)", () => {
const html = renderToStaticMarkup(<SubagentSteerCard message={projectNative([steer])[0] as never} />);
assert.ok(html.includes("circle"), "svg content rendered inline");
assert.match(html, /Figure/);
assert.match(html, /svg\]:w-full/, "svg scales to the display area's full width");
// Natural sizing (owner, 2026-10-07): a DECLARED width (200) means the
// holder hugs + centers rather than stretching full-width.
assert.match(html, /w-fit/, "holder hugs the declared size");
assert.match(html, /svg\]:max-w-full/, "svg shrinks to fit only when oversized");
assert.ok(!html.includes("Looking at the render"), "model-facing notes never render");
});
@@ -475,6 +480,52 @@ describe("steer card rendering (the delivered artifact)", () => {
});
describe("artifact chrome — scrollbar, invert toggles, downloads (release polish 2026-10-07)", () => {
it("demo iframe: scrolling OFF (the bridge auto-sizes; a scrollable iframe traps the wheel) and the invert + download buttons render", () => {
const steer = buildSubagentSteer(
{ id: "jd3", kind: "demo", toolCallId: "cd3" },
{ title: "Wave interference", html: DEMO_HTML, tested: true },
);
const html = renderToStaticMarkup(<SubagentSteerCard message={projectNative([steer])[0] as never} />);
assert.match(html, /scrolling="no"/, "no internal scrollbar — the wheel chains to the session");
assert.match(html, /data-testid="demo-invert"/);
assert.match(html, /data-testid="demo-download"/);
assert.ok(!html.includes("hue-rotate"), "inversion is OFF by default — a manual toggle only");
});
it("figure card: invert + download buttons; NO fullscreen button without a chat page (the context is null there)", () => {
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><circle cx="50" cy="50" r="20"/></svg>';
const html = renderToStaticMarkup(<VisualOutputCard output={{ svg }} />);
assert.match(html, /data-testid="visual-invert"/);
assert.match(html, /data-testid="visual-download"/);
assert.ok(!html.includes('data-testid="visual-open"'), "fullscreen needs the session overlay context");
assert.ok(!html.includes("hue-rotate"), "inversion OFF by default");
});
it("svgNaturalWidth: declared px sizes parse; %, auto, and absent fall back to null", () => {
assert.equal(svgNaturalWidth('<svg width="200" height="100"></svg>'), 200);
assert.equal(svgNaturalWidth("<svg width='320'></svg>"), 320);
assert.equal(svgNaturalWidth('<svg width="100%"></svg>'), null, "% is no natural size");
assert.equal(svgNaturalWidth('<svg width="auto"></svg>'), null);
assert.equal(svgNaturalWidth('<svg viewBox="0 0 10 10"></svg>'), null, "viewBox-only → full-width law");
assert.equal(svgNaturalWidth('<svg width="0"></svg>'), null, "degenerate zero is no size");
});
it("slugFilename: titles slug to filename stems; symbol-only falls back", () => {
assert.equal(slugFilename("Damped oscillator"), "damped-oscillator");
assert.equal(slugFilename("Ünïcode — Tïtle!"), "unicode-title");
assert.equal(slugFilename("???"), "demo");
});
it("chartSize: width/height attrs are the fallback when no viewBox (figure fullscreen fit math)", () => {
assert.deepEqual(chartSize('<svg width="320" height="240"></svg>'), { w: 320, h: 240 });
assert.deepEqual(chartSize('<svg xmlns="x" width="50px" height="25px" viewBox="0 0 100 50"></svg>'), {
w: 100,
h: 50,
}, "viewBox always wins");
});
});
describe("sanitizeSvg", () => {
it("strips script blocks, event handlers, and javascript: urls, keeps geometry", () => {
const dirty = '<svg width="10" height="10" onload="alert(1)"><script>alert(2)</script><circle r="1" onclick="pwn()"/><a href="javascript:alert(3)"><rect width="2" height="2"/></a></svg>';
+6 -1
View File
@@ -160,7 +160,12 @@ describe("mermaid expand — the context seam", () => {
// owner is chat.tsx — geometry covered by the tests above.
const opened: string[] = [];
const html = renderToStaticMarkup(
<SessionMermaidContext.Provider value={{ openMermaid: (chart) => void opened.push(chart) }}>
<SessionMermaidContext.Provider
value={{
openMermaid: (chart) => void opened.push(chart),
openFigure: () => {},
}}
>
<div />
</SessionMermaidContext.Provider>,
);