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:
+53
-2
@@ -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>';
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user