ui: round three — the tight band, one geometry everywhere

Owner round three on the user rows: (1) the band padding halves,
py-2.5 -> py-[5px] — subtle, not tall; same for the messages. (2) The
in-card thinking joins the band: identical styling and heights for
collapsed thinking in the chat and inside question cards (card-on-card
is quiet by design; revert if it reads too light). (3) The left gap
MATCHES the new 5px padding, so the rail fills the rest of the 3ch
inset - border-l-[calc(3ch-5px)] pl-[5px]: a thick muted rail, total
inset still 3ch (the chevron-alignment law survives). The banded prop
dies - one geometry, cards included.
This commit is contained in:
amosblomqvist
2026-10-08 18:32:21 +03:00
parent 3691e07118
commit 497b29804c
6 changed files with 39 additions and 51 deletions
+5 -10
View File
@@ -143,16 +143,11 @@ 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 — it just thinking is the identical collapsible ThinkingBlock — and since
happens to sit INSIDE the card, so it takes the rail but no band owner round three (2026-10-07) IDENTICALLY styled, band and all:
(owner round two, 2026-10-07: card-color on card-color would be collapsed thinking matches in height everywhere, card or chat
invisible; geometry alone carries it there). */ (card-on-card is quiet by design; too light → revert). */
<ThinkingBlock <ThinkingBlock testId="your-thinking" label="YOUR THINKING" text={answer.thinking ?? answer.note ?? ""} />
testId="your-thinking"
label="YOUR THINKING"
text={answer.thinking ?? answer.note ?? ""}
banded={false}
/>
)} )}
{/* 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
+6 -5
View File
@@ -1399,13 +1399,14 @@ export function Chat({
Rendered WITHOUT the Message wrapper — that is the bubble. */ Rendered WITHOUT the Message wrapper — that is the bubble. */
<div <div
data-testid="user-message" data-testid="user-message"
/* THE DOCUMENT, ALL-IN (owner round two, 2026-10-07): no /* THE DOCUMENT, ALL-IN (owner rounds two+three, 2026-10-07):
chat side — the user message is a full-width card-color no chat side — the user message is a full-width card-color
BAND with the 6px rail, the same geometry as the thinking BAND, the IDENTICAL geometry as the thinking rows: 5px band
rows (the band is the authorship mark against the agent's padding, the left gap matches it, the rail fills the 3ch
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-[6px] border-muted-foreground/30 bg-card py-2.5 pl-[calc(3ch-6px)] pr-2 font-ui text-xs" 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"
> >
{/* 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
+18 -20
View File
@@ -17,17 +17,19 @@ import { cn } from "@/lib/utils";
* The glance-level law (owner): user thinking = rail + 50% italic; * The glance-level law (owner): user thinking = rail + 50% italic;
* agent thinking = no rail. * agent thinking = no rail.
* *
* THE DOCUMENT, ALL-IN (owner round two, 2026-10-07): no chat sides — * THE DOCUMENT, ALL-IN (owner rounds two+three, 2026-10-07): no chat
* transcript-level user rows (this block, the "You" message) are * sides — user rows are full-width CARD-COLOR BANDS (the band is the
* full-width CARD-COLOR BANDS: the band is the authorship mark against * authorship mark against the agent's transparent prose) with the rail
* the agent's transparent prose, and the rail runs along the band's left * along the left edge. ONE geometry everywhere, in cards and out: the
* edge. The rail keeps its muted color but is now 6px wide, eating the * band padding is a tight 5px (round three: halved — subtle, not tall),
* artificial air of the old 3ch inset — the total inset STAYS 3ch so the * and the left gap MATCHES it, so the rail fills the rest of the 3ch
* chevron-alignment law with the agent's flush rows survives; the gap * inset — border-l-[calc(3ch-5px)] pl-[5px]. Total inset stays 3ch (the
* between rail and text drops to natural (3ch - 6px). In-card rows * chevron-alignment law with the agent's flush rows survives); the rail
* (inside the answered card, on the card's own background) take the rail * grows as the padding shrinks. In-card rows carry the identical
* but NO band (`banded: false`). `defaultOpen` marks transcript rows, * styling (owner round three: same band, same heights — card-on-card is
* which arrive EXPANDED (clearly conveyed that the thinking was sent). * quiet by design; if it reads too light we revert). `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
@@ -66,7 +68,6 @@ export function ThinkingBlock({
text, text,
testId, testId,
dataCommit, dataCommit,
banded = true,
defaultOpen = false, defaultOpen = false,
}: { }: {
label?: string; label?: string;
@@ -74,8 +75,6 @@ export function ThinkingBlock({
testId?: string; testId?: string;
/** The practice marker's register flag (commit vs nudge). */ /** The practice marker's register flag (commit vs nudge). */
dataCommit?: boolean; dataCommit?: boolean;
/** Transcript rows carry the card-color band; in-card rows rail only. */
banded?: boolean;
/** Transcript rows arrive expanded (owner, 2026-10-07). */ /** Transcript rows arrive expanded (owner, 2026-10-07). */
defaultOpen?: boolean; defaultOpen?: boolean;
}) { }) {
@@ -101,13 +100,12 @@ 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 two, 2026-10-07): total inset // The rail geometry (owner round three, 2026-10-07): total inset
// STAYS 3ch (the chevron-alignment law with the agent's flush // STAYS 3ch (the chevron-alignment law with the agent's flush
// rows), re-split — 6px rail (was 2px) + a natural 3ch-6px gap // rows). The band padding is a tight 5px (halved from 10px) and
// (was the too-wide 3ch-2px). In-card rows keep the 12px my-3 // the left gap MATCHES it — the rail fills the rest:
// rhythm; banded rows sit my-2 apart (the band's own air). // calc(3ch-5px) wide. One geometry everywhere, cards included.
"lh-card-enter w-full border-l-[6px] border-muted-foreground/30 pl-[calc(3ch-6px)] font-ui text-xs", "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",
banded ? "my-2 bg-card py-2.5" : "my-3",
)} )}
> >
<button <button
+5 -4
View File
@@ -295,13 +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");
// IN-CARD = RAIL ONLY (owner round two, 2026-10-07): no band on the // IDENTICAL GEOMETRY (owner round three, 2026-10-07): the in-card row
// card's own background; geometry alone carries it there. // carries the band too — collapsed thinking matches in height
// everywhere, card or chat.
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"), "no band inside the card"); assert.ok(yourRow.slice(0, 400).includes("bg-card"), "the band inside the card as well");
// 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-[6px]"); const railAt = html.indexOf("border-l-[calc(3ch-5px)]");
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");
+4 -11
View File
@@ -242,24 +242,17 @@ describe("md-blocks — the restored plain blockquote (owner round, 2026-10-07)"
}); });
}); });
describe("ThinkingBlock — side-based authorship + ⌘T sync (owner round, 2026-10-07)", () => { describe("ThinkingBlock — the document band + ⌘T sync (owner rounds, 2026-10-07)", () => {
it("the banded row is the document band — full-width card color, 6px rail", () => { it("the row is the document band — one geometry, cards included (owner round three)", () => {
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-[6px]"), "the 6px rail"); assert.ok(html.includes("border-l-[calc(3ch-5px)]"), "the rail fills the 3ch inset");
assert.ok(html.includes("pl-[calc(3ch-6px)]"), "natural gap, 3ch total inset preserved"); assert.ok(html.includes("py-[5px]") && html.includes("pl-[5px]"), "tight 5px padding, gap matches it");
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 take the rail but no band", () => {
const html = renderToStaticMarkup(<ThinkingBlock text="in card" banded={false} />);
assert.ok(!html.includes("bg-card"), "no band on the card's own background");
assert.ok(html.includes("border-l-[6px]"), "the rail stays");
assert.ok(html.includes("my-3"), "the 12px in-card rhythm");
});
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-[6px]"), "the 6px rail, left side"); assert.ok(trace.includes("border-l-[calc(3ch-5px)]"), "the rail fills 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/);