ui: round four — thinner rail, tighter band, the accent step, early wrap

Owner round four on the user rows: (1) the rail thins - the gap inside
the 3ch inset grows 5px -> 8px, so the rail is calc(3ch-8px); total
inset still 3ch, chevron law intact. (2) The band padding drops again,
5px -> 4px (py-1). (3) The in-card thinking band steps to bg-accent -
the same ONE-STEP lift the chat band gets (background->card on the page,
card->accent on the card); card-color on card-color was invisible, the
accent gives the card rows a visible band with identical heights. (4)
pr-4 on the band rows - the expanded text wraps EARLY instead of hugging
the band's clear right edge.
This commit is contained in:
amosblomqvist
2026-10-08 18:38:07 +03:00
parent 497b29804c
commit d30ad42d9c
6 changed files with 47 additions and 29 deletions
+10 -5
View File
@@ -143,11 +143,16 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
{(answer.thinking ?? answer.note) && ( {(answer.thinking ?? answer.note) && (
/* The SAME unified row (owner round, 2026-10-05): the quiz answer's /* The SAME unified row (owner round, 2026-10-05): the quiz answer's
thinking is the identical collapsible ThinkingBlock — and since thinking is the identical collapsible ThinkingBlock — one
owner round three (2026-10-07) IDENTICALLY styled, band and all: geometry, and since owner round four (2026-10-07) the band steps
collapsed thinking matches in height everywhere, card or chat to accent on the card's surface (the same one-step lift the chat
(card-on-card is quiet by design; too light → revert). */ band gets). Collapsed thinking matches in height everywhere. */
<ThinkingBlock testId="your-thinking" label="YOUR THINKING" text={answer.thinking ?? answer.note ?? ""} /> <ThinkingBlock
testId="your-thinking"
label="YOUR THINKING"
text={answer.thinking ?? answer.note ?? ""}
inCard
/>
)} )}
{/* px-3→removed: card p-4 + box px-3 double-inset sat the "Why" label deeper {/* px-3→removed: card p-4 + box px-3 double-inset sat the "Why" label deeper
+1 -1
View File
@@ -1406,7 +1406,7 @@ export function Chat({
inset (the band is the authorship mark against the agent's inset (the band is the authorship mark against the agent's
transparent prose). The "You" label stays — one register transparent prose). The "You" label stays — one register
with the thinking labels. */ with the thinking labels. */
className="my-2 w-full border-l-[calc(3ch-5px)] border-muted-foreground/30 bg-card py-[5px] pl-[5px] pr-2 font-ui text-xs" className="my-2 w-full border-l-[calc(3ch-8px)] border-muted-foreground/30 bg-card py-1 pl-[8px] pr-4 font-ui text-xs"
> >
{/* The rail GROUPS (owner round 4, 2026-10-05): the YOU heading {/* The rail GROUPS (owner round 4, 2026-10-05): the YOU heading
sits INSIDE the rail, same mono-xs register and same 1ch sits INSIDE the rail, same mono-xs register and same 1ch
+20 -14
View File
@@ -23,13 +23,15 @@ import { cn } from "@/lib/utils";
* along the left edge. ONE geometry everywhere, in cards and out: the * along the left edge. ONE geometry everywhere, in cards and out: the
* band padding is a tight 5px (round three: halved — subtle, not tall), * band padding is a tight 5px (round three: halved — subtle, not tall),
* and the left gap MATCHES it, so the rail fills the rest of the 3ch * and the left gap MATCHES it, so the rail fills the rest of the 3ch
* inset — border-l-[calc(3ch-5px)] pl-[5px]. Total inset stays 3ch (the * inset — total inset stays 3ch (the chevron-alignment law with the
* chevron-alignment law with the agent's flush rows survives); the rail * agent's flush rows survives). Round four tunes the split: rail
* grows as the padding shrinks. In-card rows carry the identical * thinner (gap 8px, rail calc(3ch-8px)), band padding down to 4px, and
* styling (owner round three: same band, same heights — card-on-card is * pr-4 so the expanded text wraps EARLY — the band has a clear right
* quiet by design; if it reads too light we revert). `defaultOpen` * edge and text hugging it looks odd. In-card rows (on the card's own
* marks transcript rows, which arrive EXPANDED (clearly conveyed that * background) take bg-accent — the same ONE-STEP lift the chat band
* the thinking was sent). * gets (background→card); card→accent (`inCard`). `defaultOpen` marks
* transcript rows, which arrive EXPANDED (clearly conveyed that the
* thinking was sent).
* *
* ⌘T (toggle-user-thinking) sets EVERY row to one state via the module * ⌘T (toggle-user-thinking) sets EVERY row to one state via the module
* store below — one source of truth, no provider tree: rows subscribe on * store below — one source of truth, no provider tree: rows subscribe on
@@ -69,6 +71,7 @@ export function ThinkingBlock({
testId, testId,
dataCommit, dataCommit,
defaultOpen = false, defaultOpen = false,
inCard = false,
}: { }: {
label?: string; label?: string;
text: string; text: string;
@@ -77,6 +80,8 @@ export function ThinkingBlock({
dataCommit?: boolean; dataCommit?: boolean;
/** Transcript rows arrive expanded (owner, 2026-10-07). */ /** Transcript rows arrive expanded (owner, 2026-10-07). */
defaultOpen?: boolean; defaultOpen?: boolean;
/** On the card's own background: the band steps to accent (one lift). */
inCard?: boolean;
}) { }) {
const [open, setOpen] = useState(() => resolveUserThinkingOpen(defaultOpen)); const [open, setOpen] = useState(() => resolveUserThinkingOpen(defaultOpen));
// ⌘T: an explicit global set overrides every row (once set, rows re-sync). // ⌘T: an explicit global set overrides every row (once set, rows re-sync).
@@ -100,12 +105,13 @@ export function ThinkingBlock({
data-testid={testId} data-testid={testId}
data-commit={dataCommit === undefined ? undefined : dataCommit ? "true" : "false"} data-commit={dataCommit === undefined ? undefined : dataCommit ? "true" : "false"}
className={cn( className={cn(
// The rail geometry (owner round three, 2026-10-07): total inset // Round four (owner, 2026-10-07): total inset still 3ch (the
// STAYS 3ch (the chevron-alignment law with the agent's flush // chevron law); rail thinner (8px gap), padding 4px, and pr-4 —
// rows). The band padding is a tight 5px (halved from 10px) and // the text wraps EARLY, never hugging the band's right edge.
// the left gap MATCHES it — the rail fills the rest: // In-card rows step the band to accent: the same one-step lift
// calc(3ch-5px) wide. One geometry everywhere, cards included. // the chat band gets (background→card → card→accent).
"lh-card-enter my-2 w-full border-l-[calc(3ch-5px)] border-muted-foreground/30 bg-card py-[5px] pl-[5px] font-ui text-xs", "lh-card-enter my-2 w-full border-l-[calc(3ch-8px)] border-muted-foreground/30 py-1 pl-[8px] pr-4 font-ui text-xs",
inCard ? "bg-accent" : "bg-card",
)} )}
> >
<button <button
@@ -113,7 +119,7 @@ export function ThinkingBlock({
data-testid={testId ? `${testId}-trigger` : undefined} data-testid={testId ? `${testId}-trigger` : undefined}
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
aria-expanded={open} aria-expanded={open}
className="flex w-full items-center gap-2 pr-2 font-medium uppercase tracking-wide text-muted-foreground transition-colors hover:text-foreground" className="flex w-full items-center gap-2 font-medium uppercase tracking-wide text-muted-foreground transition-colors hover:text-foreground"
> >
{label} {label}
<ChevronDownIcon className={cn("size-4 shrink-0 transition-transform", open ? "rotate-180" : "rotate-0")} /> <ChevronDownIcon className={cn("size-4 shrink-0 transition-transform", open ? "rotate-180" : "rotate-0")} />
+5 -5
View File
@@ -295,14 +295,14 @@ describe("render smoke tests", () => {
// trigger row only — the thinking text is one click away, never loud. // trigger row only — the thinking text is one click away, never loud.
assert.ok(html.includes('data-testid="your-thinking-trigger"'), "the trigger row"); assert.ok(html.includes('data-testid="your-thinking-trigger"'), "the trigger row");
assert.ok(html.includes('aria-expanded="false"'), "arrives collapsed"); assert.ok(html.includes('aria-expanded="false"'), "arrives collapsed");
// IDENTICAL GEOMETRY (owner round three, 2026-10-07): the in-card row // ONE GEOMETRY + THE ACCENT STEP (owner round four, 2026-10-07): the
// carries the band too — collapsed thinking matches in height // in-card row carries the accent band — the same one-step lift the
// everywhere, card or chat. // chat band gets; collapsed heights match everywhere.
const yourRow = html.slice(html.indexOf('data-testid="your-thinking"')); const yourRow = html.slice(html.indexOf('data-testid="your-thinking"'));
assert.ok(yourRow.slice(0, 400).includes("bg-card"), "the band inside the card as well"); assert.ok(yourRow.slice(0, 400).includes("bg-accent"), "accent inside the card");
// THE RAIL GROUPS (owner correction): the label sits INSIDE the rail — // THE RAIL GROUPS (owner correction): the label sits INSIDE the rail —
// ownership readable from the rail alone, collapsed or expanded. // ownership readable from the rail alone, collapsed or expanded.
const railAt = html.indexOf("border-l-[calc(3ch-5px)]"); const railAt = html.indexOf("border-l-[calc(3ch-8px)]");
const trigAt = html.indexOf("your-thinking-trigger"); const trigAt = html.indexOf("your-thinking-trigger");
assert.ok(railAt !== -1 && railAt < trigAt, "the rail opens before the label (same quote)"); assert.ok(railAt !== -1 && railAt < trigAt, "the rail opens before the label (same quote)");
assert.ok(!html.includes("brightness isn&#x27;t distance"), "the text is hidden until expanded"); assert.ok(!html.includes("brightness isn&#x27;t distance"), "the text is hidden until expanded");
+10 -3
View File
@@ -243,16 +243,23 @@ describe("md-blocks — the restored plain blockquote (owner round, 2026-10-07)"
}); });
describe("ThinkingBlock — the document band + ⌘T sync (owner rounds, 2026-10-07)", () => { describe("ThinkingBlock — the document band + ⌘T sync (owner rounds, 2026-10-07)", () => {
it("the row is the document band — one geometry, cards included (owner round three)", () => { it("the row is the document band — round four geometry (owner, 2026-10-07)", () => {
const html = renderToStaticMarkup(<ThinkingBlock text="banded" defaultOpen />); const html = renderToStaticMarkup(<ThinkingBlock text="banded" defaultOpen />);
assert.ok(html.includes("bg-card"), "the card-color band"); assert.ok(html.includes("bg-card"), "the card-color band");
assert.ok(html.includes("border-l-[calc(3ch-5px)]"), "the rail fills the 3ch inset"); assert.ok(html.includes("border-l-[calc(3ch-8px)]"), "thinner rail — 8px gap inside the 3ch inset");
assert.ok(html.includes("py-[5px]") && html.includes("pl-[5px]"), "tight 5px padding, gap matches it"); assert.ok(html.includes("py-1") && html.includes("pl-[8px]"), "4px band padding");
assert.ok(html.includes("pr-4"), "the text wraps early — air at the band's right edge");
assert.ok(html.includes("w-full"), "full width — no chat side"); assert.ok(html.includes("w-full"), "full width — no chat side");
assert.match(html, /aria-expanded="true"/); assert.match(html, /aria-expanded="true"/);
assert.ok(html.includes("banded"), "text visible on arrival"); assert.ok(html.includes("banded"), "text visible on arrival");
}); });
it("in-card rows step the band to accent — the same one-step lift (owner round four)", () => {
const html = renderToStaticMarkup(<ThinkingBlock text="in card" inCard />);
assert.ok(html.includes("bg-accent"), "accent on the card's surface");
assert.ok(!html.includes("bg-card"), "card-color would vanish on the card");
});
it("⌘T sets every row — explicit sets stomp individual state, late mounts honor the last set", () => { it("⌘T sets every row — explicit sets stomp individual state, late mounts honor the last set", () => {
setAllUserThinkingOpen(false); setAllUserThinkingOpen(false);
const collapsed = renderToStaticMarkup(<ThinkingBlock text="synced" defaultOpen />); const collapsed = renderToStaticMarkup(<ThinkingBlock text="synced" defaultOpen />);
+1 -1
View File
@@ -185,7 +185,7 @@ describe("the practice transcript cards", () => {
assert.match(trace, /aria-expanded="true"/); assert.match(trace, /aria-expanded="true"/);
assert.ok(trace.includes("stuck at step two"), "the text is visible on arrival"); assert.ok(trace.includes("stuck at step two"), "the text is visible on arrival");
assert.ok(trace.includes("bg-card"), "the card-color band"); assert.ok(trace.includes("bg-card"), "the card-color band");
assert.ok(trace.includes("border-l-[calc(3ch-5px)]"), "the rail fills the 3ch inset"); assert.ok(trace.includes("border-l-[calc(3ch-8px)]"), "thinner rail inside the 3ch inset");
const legacy = renderToStaticMarkup(<ThinkingCard data={{ problemCallId: "c", commit: true, text: "It is 42." }} />); const legacy = renderToStaticMarkup(<ThinkingCard data={{ problemCallId: "c", commit: true, text: "It is 42." }} />);
assert.match(legacy, /YOUR THINKING/); assert.match(legacy, /YOUR THINKING/);
assert.doesNotMatch(legacy, /Your solution/); assert.doesNotMatch(legacy, /Your solution/);