ui: the document, all-in — card-color bands and the 6px rail
Owner round two on user-vs-agent clarity (the chat-comeback was tried and set aside): we go all in on documents. No chat sides. User messages and every transcript user-thinking row are full-width CARD-COLOR BANDS - the band is the authorship mark against the agent's transparent prose. No rounded corners, no borders besides the rail. The rail keeps its muted color but is now 6px wide (was 2px): the artificially wide 3ch inset (the chevron-alignment law with the agent's flush rows) is re-split - rail eats the air, the rail-to-text gap drops from 3ch-2px to a natural 3ch-6px, and the total inset stays 3ch so the chevrons still align. The 'You' label stays on messages - one register with the thinking labels. In-card thinking (inside answered cards) takes the rail but NO band: card-color on card-color would be invisible; geometry alone carries it. Transcript rows arrive expanded (clearly sent) - the reading lift now opens on arrival too; the in-card row stays collapsed. Cmd+T still rules all rows. Also this round: the answered-card last-child rhythm law ([&>*:last-child]:mb-0) - on ask cards the thinking row's my-3 stacked with the card's p-4 into a 28px void below (12px above); the card's own padding is the bottom rhythm when a row ends the card.
This commit is contained in:
@@ -41,9 +41,16 @@ export function AnsweredCard({ answer }: { answer: QuestionAnswerData }) {
|
|||||||
ThinkingBlock, instead of an inset inside the answer. Same muted
|
ThinkingBlock, instead of an inset inside the answer. Same muted
|
||||||
rail as before; the label is now plain "Your thinking". */}
|
rail as before; the label is now plain "Your thinking". */}
|
||||||
{answer.readingThinking && (
|
{answer.readingThinking && (
|
||||||
<ThinkingBlock testId="reading-thinking-block" label="YOUR THINKING" text={answer.readingThinking} />
|
<ThinkingBlock testId="reading-thinking-block" label="YOUR THINKING" text={answer.readingThinking} defaultOpen />
|
||||||
)}
|
)}
|
||||||
<div className="rounded-xl border bg-card p-4 text-card-foreground" data-testid="answered-card">
|
{/* [&>*:last-child]:mb-0 (owner round, 2026-10-07): on ask cards the
|
||||||
|
thinking row is the last child — its my-3 stacked with the card's
|
||||||
|
p-4 into a 28px void below (12px above); the card's own padding is
|
||||||
|
the bottom rhythm when a row ends the card. */}
|
||||||
|
<div
|
||||||
|
className="rounded-xl border bg-card p-4 text-card-foreground [&>*:last-child]:mb-0"
|
||||||
|
data-testid="answered-card"
|
||||||
|
>
|
||||||
<div className="mb-1 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
<div className="mb-1 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
{isQuiz ? (answer.questionType === "predict" ? "Prediction" : "Quiz") : isExplain ? "Explain" : "Question"}
|
{isQuiz ? (answer.questionType === "predict" ? "Prediction" : "Quiz") : isExplain ? "Explain" : "Question"}
|
||||||
{isQuiz &&
|
{isQuiz &&
|
||||||
@@ -137,9 +144,15 @@ 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 — it just
|
||||||
happens to sit inside the quiz container. Same placement, same
|
happens to sit INSIDE the card, so it takes the rail but no band
|
||||||
trigger, same 50%-italic quote block on expand. */
|
(owner round two, 2026-10-07: card-color on card-color would be
|
||||||
<ThinkingBlock testId="your-thinking" label="YOUR THINKING" text={answer.thinking ?? answer.note ?? ""} />
|
invisible; geometry alone carries it there). */
|
||||||
|
<ThinkingBlock
|
||||||
|
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
|
||||||
|
|||||||
+7
-6
@@ -1399,12 +1399,13 @@ 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"
|
||||||
/* SIDE-BASED AUTHORSHIP (owner round, 2026-10-07): the
|
/* THE DOCUMENT, ALL-IN (owner round two, 2026-10-07): no
|
||||||
document shape blurred user into agent — three left-rail
|
chat side — the user message is a full-width card-color
|
||||||
idioms (user text, user thinking, quoted prose) collided.
|
BAND with the 6px rail, the same geometry as the thinking
|
||||||
User rows go RIGHT: mirrored rail, ~85% width, text still
|
rows (the band is the authorship mark against the agent's
|
||||||
reading left-to-right (the chat convention). */
|
transparent prose). The "You" label stays — one register
|
||||||
className="my-1 ml-auto w-[85%] border-r-2 border-muted-foreground/30 pr-[calc(3ch-2px)] font-ui text-xs"
|
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"
|
||||||
>
|
>
|
||||||
{/* 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
|
||||||
|
|||||||
@@ -293,8 +293,6 @@ export function ThinkingCard({ data }: { data: PracticeThinkingData }) {
|
|||||||
// The unified row (owner rounds, 2026-10-05): one style, one label —
|
// The unified row (owner rounds, 2026-10-05): one style, one label —
|
||||||
// 50%-italic quote block on expand. Legacy records' commit flag is
|
// 50%-italic quote block on expand. Legacy records' commit flag is
|
||||||
// deliberately unread (one lane now). A TRANSCRIPT row (owner round,
|
// deliberately unread (one lane now). A TRANSCRIPT row (owner round,
|
||||||
// 2026-10-07): mirrored to the right side, arriving expanded.
|
// 2026-10-07): full-width band + rail, arriving expanded.
|
||||||
return (
|
return <ThinkingBlock testId="thinking-card" label="YOUR THINKING" text={data.text} defaultOpen />;
|
||||||
<ThinkingBlock testId="thinking-card" label="YOUR THINKING" text={data.text} mirror defaultOpen />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,17 @@ 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.
|
||||||
*
|
*
|
||||||
* SIDE-BASED AUTHORSHIP (owner round, 2026-10-07): transcript-level rows
|
* THE DOCUMENT, ALL-IN (owner round two, 2026-10-07): no chat sides —
|
||||||
* go RIGHT — mirrored rail (border-right), block hugging the right edge
|
* transcript-level user rows (this block, the "You" message) are
|
||||||
* at ~85% width, text still reading left-to-right. In-card rows (inside
|
* full-width CARD-COLOR BANDS: the band is the authorship mark against
|
||||||
* the answered card, the agent's context) stay left. `mirror` selects
|
* the agent's transparent prose, and the rail runs along the band's left
|
||||||
* the side; `defaultOpen` marks the transcript rows, which arrive
|
* edge. The rail keeps its muted color but is now 6px wide, eating the
|
||||||
* EXPANDED (owner: clearly conveyed that the thinking was sent).
|
* artificial air of the old 3ch inset — the total inset STAYS 3ch so the
|
||||||
|
* chevron-alignment law with the agent's flush rows survives; the gap
|
||||||
|
* between rail and text drops to natural (3ch - 6px). In-card rows
|
||||||
|
* (inside the answered card, on the card's own background) take the rail
|
||||||
|
* but NO band (`banded: false`). `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
|
||||||
@@ -61,7 +66,7 @@ export function ThinkingBlock({
|
|||||||
text,
|
text,
|
||||||
testId,
|
testId,
|
||||||
dataCommit,
|
dataCommit,
|
||||||
mirror = false,
|
banded = true,
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
}: {
|
}: {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -69,8 +74,8 @@ 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;
|
||||||
/** Right-side placement (transcript rows): mirrored rail, right edge. */
|
/** Transcript rows carry the card-color band; in-card rows rail only. */
|
||||||
mirror?: boolean;
|
banded?: boolean;
|
||||||
/** Transcript rows arrive expanded (owner, 2026-10-07). */
|
/** Transcript rows arrive expanded (owner, 2026-10-07). */
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
}) {
|
}) {
|
||||||
@@ -95,15 +100,14 @@ export function ThinkingBlock({
|
|||||||
<div
|
<div
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
data-commit={dataCommit === undefined ? undefined : dataCommit ? "true" : "false"}
|
data-commit={dataCommit === undefined ? undefined : dataCommit ? "true" : "false"}
|
||||||
data-side={mirror ? "right" : "left"}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
// my-3 (owner, 2026-10-07): the row's own rhythm — 4px hugged the
|
// The rail geometry (owner round two, 2026-10-07): total inset
|
||||||
// options on non-graded answered cards; 12px matches the card's
|
// STAYS 3ch (the chevron-alignment law with the agent's flush
|
||||||
// mt-2/mt-3 spacing grammar.
|
// rows), re-split — 6px rail (was 2px) + a natural 3ch-6px gap
|
||||||
"lh-card-enter my-3 font-ui text-xs",
|
// (was the too-wide 3ch-2px). In-card rows keep the 12px my-3
|
||||||
mirror
|
// rhythm; banded rows sit my-2 apart (the band's own air).
|
||||||
? "ml-auto w-[85%] border-r-2 border-muted-foreground/30 pr-[calc(3ch-2px)]"
|
"lh-card-enter w-full border-l-[6px] border-muted-foreground/30 pl-[calc(3ch-6px)] font-ui text-xs",
|
||||||
: "w-full border-l-2 border-muted-foreground/30 pl-[calc(3ch-2px)]",
|
banded ? "my-2 bg-card py-2.5" : "my-3",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -111,7 +115,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 font-medium uppercase tracking-wide text-muted-foreground transition-colors hover:text-foreground"
|
className="flex w-full items-center gap-2 pr-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")} />
|
||||||
|
|||||||
@@ -295,9 +295,13 @@ 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
|
||||||
|
// card's own background; geometry alone carries it there.
|
||||||
|
const yourRow = html.slice(html.indexOf('data-testid="your-thinking"'));
|
||||||
|
assert.ok(!yourRow.slice(0, 400).includes("bg-card"), "no band 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-2");
|
const railAt = html.indexOf("border-l-[6px]");
|
||||||
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't distance"), "the text is hidden until expanded");
|
assert.ok(!html.includes("brightness isn't distance"), "the text is hidden until expanded");
|
||||||
@@ -591,9 +595,9 @@ describe("predict vs check rendering", () => {
|
|||||||
readingThinking: "was unsure mid-paragraph",
|
readingThinking: "was unsure mid-paragraph",
|
||||||
});
|
});
|
||||||
const html = renderToStaticMarkup(<AnsweredCard answer={withReading} />);
|
const html = renderToStaticMarkup(<AnsweredCard answer={withReading} />);
|
||||||
// The unified ThinkingBlock (owner round, 2026-10-05): the reading
|
// The unified ThinkingBlock (owner round, 2026-10-05; round two
|
||||||
// thoughts render BEFORE the card as the collapsed user-thinking row
|
// 2026-10-07): the reading thoughts render BEFORE the card as the
|
||||||
// — same trigger, same 50%-italic quote block on expand.
|
// banded user-thinking row, arriving EXPANDED (clearly sent).
|
||||||
const blockAt = html.indexOf("reading-thinking-block");
|
const blockAt = html.indexOf("reading-thinking-block");
|
||||||
const cardAt = html.indexOf("answered-card");
|
const cardAt = html.indexOf("answered-card");
|
||||||
assert.ok(blockAt !== -1, "the lifted block renders");
|
assert.ok(blockAt !== -1, "the lifted block renders");
|
||||||
@@ -601,9 +605,11 @@ describe("predict vs check rendering", () => {
|
|||||||
assert.ok(html.includes("YOUR THINKING"), "the plain label");
|
assert.ok(html.includes("YOUR THINKING"), "the plain label");
|
||||||
assert.ok(!html.includes("Your thinking while reading"), "the old verbose label is gone");
|
assert.ok(!html.includes("Your thinking while reading"), "the old verbose label is gone");
|
||||||
assert.ok(!html.includes("your-reading-thinking"), "the in-card inset is gone");
|
assert.ok(!html.includes("your-reading-thinking"), "the in-card inset is gone");
|
||||||
// COLLAPSED ARRIVAL: SSR shows the triggers, not the texts.
|
// EXPANDED ARRIVAL (owner round two, 2026-10-07): transcript rows
|
||||||
|
// render their text; the in-card answer thinking stays collapsed.
|
||||||
assert.ok(html.includes('data-testid="your-thinking-trigger"'), "the answer thinking stays inside the card");
|
assert.ok(html.includes('data-testid="your-thinking-trigger"'), "the answer thinking stays inside the card");
|
||||||
assert.ok(!html.includes("answering thought") && !html.includes("was unsure mid-paragraph"), "texts hidden until expanded");
|
assert.ok(html.includes("was unsure mid-paragraph"), "the lifted row arrives expanded");
|
||||||
|
assert.ok(!html.includes("answering thought"), "the in-card row stays collapsed");
|
||||||
const legacy = { ...buildAnswer(quizQD(), { values: ["Mercury"] }), note: "a 2026-09 answer" };
|
const legacy = { ...buildAnswer(quizQD(), { values: ["Mercury"] }), note: "a 2026-09 answer" };
|
||||||
const legacyHtml = renderToStaticMarkup(<AnsweredCard answer={legacy} />);
|
const legacyHtml = renderToStaticMarkup(<AnsweredCard answer={legacy} />);
|
||||||
assert.ok(legacyHtml.includes("a 2026-09 answer") || legacyHtml.includes('data-testid="your-thinking-trigger"'), "records from before the rename still render");
|
assert.ok(legacyHtml.includes("a 2026-09 answer") || legacyHtml.includes('data-testid="your-thinking-trigger"'), "records from before the rename still render");
|
||||||
|
|||||||
@@ -243,19 +243,21 @@ 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 — side-based authorship + ⌘T sync (owner round, 2026-10-07)", () => {
|
||||||
it("the default row stays left with the my-3 rhythm", () => {
|
it("the banded row is the document band — full-width card color, 6px rail", () => {
|
||||||
const html = renderToStaticMarkup(<ThinkingBlock text="left side" />);
|
const html = renderToStaticMarkup(<ThinkingBlock text="banded" defaultOpen />);
|
||||||
assert.ok(html.includes("border-l-2"), "left rail");
|
assert.ok(html.includes("bg-card"), "the card-color band");
|
||||||
assert.ok(html.includes("my-3"), "the 12px rhythm (was my-1)");
|
assert.ok(html.includes("border-l-[6px]"), "the 6px rail");
|
||||||
|
assert.ok(html.includes("pl-[calc(3ch-6px)]"), "natural gap, 3ch total inset preserved");
|
||||||
|
assert.ok(html.includes("w-full"), "full width — no chat side");
|
||||||
|
assert.match(html, /aria-expanded="true"/);
|
||||||
|
assert.ok(html.includes("banded"), "text visible on arrival");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mirrored rows hug the right edge, rail flipped, arriving open", () => {
|
it("in-card rows take the rail but no band", () => {
|
||||||
const html = renderToStaticMarkup(<ThinkingBlock text="right side" mirror defaultOpen />);
|
const html = renderToStaticMarkup(<ThinkingBlock text="in card" banded={false} />);
|
||||||
assert.ok(html.includes('data-side="right"'));
|
assert.ok(!html.includes("bg-card"), "no band on the card's own background");
|
||||||
assert.ok(html.includes("ml-auto"), "pushed to the right edge");
|
assert.ok(html.includes("border-l-[6px]"), "the rail stays");
|
||||||
assert.ok(html.includes("border-r-2") && !html.includes("border-l-2"), "rail on the right");
|
assert.ok(html.includes("my-3"), "the 12px in-card rhythm");
|
||||||
assert.match(html, /aria-expanded="true"/);
|
|
||||||
assert.ok(html.includes("right side"), "text visible on arrival");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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", () => {
|
||||||
|
|||||||
@@ -179,12 +179,13 @@ describe("the practice transcript cards", () => {
|
|||||||
it("ThinkingCard: one label, right side, expanded arrival (owner round, 2026-10-07)", () => {
|
it("ThinkingCard: one label, right side, expanded arrival (owner round, 2026-10-07)", () => {
|
||||||
const trace = renderToStaticMarkup(<ThinkingCard data={{ problemCallId: "c", commit: false, text: "stuck at step two" }} />);
|
const trace = renderToStaticMarkup(<ThinkingCard data={{ problemCallId: "c", commit: false, text: "stuck at step two" }} />);
|
||||||
assert.match(trace, /YOUR THINKING/);
|
assert.match(trace, /YOUR THINKING/);
|
||||||
// Transcript rows arrive EXPANDED (clearly conveyed as sent) and
|
// Transcript rows arrive EXPANDED (clearly conveyed as sent) as
|
||||||
// MIRRORED right (side-based authorship).
|
// full-width card-color bands with the 6px rail (the document,
|
||||||
|
// all-in — owner round two, 2026-10-07).
|
||||||
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('data-side="right"'), "mirrored to the right side");
|
assert.ok(trace.includes("bg-card"), "the card-color band");
|
||||||
assert.ok(trace.includes("border-r-2"), "the rail is on the right");
|
assert.ok(trace.includes("border-l-[6px]"), "the 6px rail, left side");
|
||||||
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/);
|
||||||
|
|||||||
Reference in New Issue
Block a user