diff --git a/components/artifacts/visual-card.tsx b/components/artifacts/visual-card.tsx index 4ec956a..efe7ab8 100644 --- a/components/artifacts/visual-card.tsx +++ b/components/artifacts/visual-card.tsx @@ -2,7 +2,7 @@ import { useContext, useState } from "react"; import { ChatActionsContext } from "@/components/chat"; -import { DownloadIcon, MaximizeIcon } from "lucide-react"; +import { ContrastIcon, DownloadIcon, MaximizeIcon } from "lucide-react"; import { Toggle } from "@/components/settings/controls"; import { useSessionMermaid } from "@/components/session-overlay"; import { downloadFile } from "@/components/artifacts/download"; @@ -99,11 +99,47 @@ export function VisualOutputCard({ ); } return ( -
-
+ /* NO CARD CHROME (owner round, 2026-10-07): the figure is document + material — no border, no padding box. Image first, then the CAPTION + ROW below: "Figure" left, the controls right, both edges following + the IMAGE's own width (the wrapper carries the natural-sizing law, + so the row hugs a centered narrow figure, not the column). */ +
+ {/* Natural sizing (owner, 2026-10-07): a declared-width svg sits at + ITS size; max-w-full still shrinks oversized ones to fit. No + declared size (viewBox-only, %-width) keeps the full-width law. + The invert filter rides the bg-white holder, so the backing flips + white→black together with the strokes; transparent areas ARE the + backing. */} +
+
Figure
- + {/* The invert affordance (owner round, 2026-10-07): the disc IS + the half-filled contrast circle; the tooltip names it. */} + } + />
- {/* Natural sizing (owner, 2026-10-07): a declared-width svg sits at - ITS size — the white figure area hugs it and centers (mx-auto), - max-w-full still shrinks oversized ones to fit the card. No - declared size (viewBox-only, %-width) keeps the old full-width - law. The invert filter rides the bg-white holder, so the backing - flips white→black together with the strokes; transparent areas - ARE the backing. */} -
); } diff --git a/components/settings/controls.tsx b/components/settings/controls.tsx index 13199fc..c7ee2f6 100644 --- a/components/settings/controls.tsx +++ b/components/settings/controls.tsx @@ -9,7 +9,7 @@ * control together) and the control itself locks — the row owns the dim. */ -import { useState } from "react"; +import { useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; /** A section block: pronounced mono heading + content. */ @@ -87,12 +87,20 @@ export function Toggle({ disabled, onChange, label, + discIcon, + title, }: { checked: boolean; disabled?: boolean; onChange: (next: boolean) => void; /** Accessible name for the switch role. */ label: string; + /** Optional icon disc (owner round, 2026-10-07): the figure cards' invert + * toggle renders the half-filled contrast circle AS the disc — the icon + * carries the meaning; plain toggles keep the plain disc. */ + discIcon?: ReactNode; + /** Optional native tooltip on the switch. */ + title?: string; }) { return ( ); } diff --git a/tests/demos.test.tsx b/tests/demos.test.tsx index 990814f..1f14c9e 100644 --- a/tests/demos.test.tsx +++ b/tests/demos.test.tsx @@ -505,6 +505,23 @@ describe("artifact chrome — scrollbar, invert toggles, downloads (release poli assert.ok(!html.includes("hue-rotate"), "inversion OFF by default"); }); + it("figure card: NO card chrome, image first, caption row BELOW (owner round, 2026-10-07)", () => { + const svg = ''; + const html = renderToStaticMarkup(); + // Document material: the outer box carries no card styling. + const rootAt = html.indexOf('data-testid="visual-card"'); + const root = html.slice(rootAt - 200, rootAt); + assert.ok(!root.includes("rounded-xl border"), "no card container"); + // Image BEFORE the caption row; the caption label and the controls follow it. + const svgAt = html.indexOf('data-testid="visual-svg"'); + const captionAt = html.indexOf(">Figure<"); + assert.ok(svgAt !== -1 && captionAt !== -1 && svgAt < captionAt, "the caption row sits below the image"); + assert.ok(captionAt < html.indexOf("visual-download"), "the controls ride the caption row"); + // The invert affordance: the contrast-circle disc + the tooltip. + assert.ok(html.includes("size-3"), "the icon disc"); + assert.ok(html.includes('title="Invert colors"'), "the tooltip names the toggle"); + }); + it("svgNaturalWidth: declared px sizes parse; %, auto, and absent fall back to null", () => { assert.equal(svgNaturalWidth(''), 200); assert.equal(svgNaturalWidth(""), 320);