From 1beb54d3d290a09ee23dc03db177dff3173f6691 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Thu, 8 Oct 2026 19:01:04 +0300 Subject: [PATCH] =?UTF-8?q?ui:=20figures=20join=20the=20document=20?= =?UTF-8?q?=E2=80=94=20no=20card=20chrome,=20caption=20row=20below?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner round: the figure card loses its container entirely (no border, no padding box) - the figure is document material. Image first, then the CAPTION ROW below it: 'Figure' label aligned to the image's left edge, the controls (invert toggle, download, fullscreen) on the same row aligned to its right edge. The wrapper carries the natural-sizing law, so for a declared-width svg the row hugs the centered figure's own edges, not the column's. The invert affordance: the house Toggle gains an optional icon disc - the figure's toggle renders lucide's Contrast (the half-filled circle) AS the sliding disc, plus a native 'Invert colors' tooltip; every plain toggle elsewhere is unchanged. Interactive demo cards keep their boxed stage (owner ruling: an interactive surface reads as its own stage). --- components/artifacts/visual-card.tsx | 61 ++++++++++++++++++---------- components/settings/controls.tsx | 40 ++++++++++++++---- tests/demos.test.tsx | 17 ++++++++ 3 files changed, 88 insertions(+), 30 deletions(-) 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);