From e00219c67dfddedea0463b712243b0a5fe62af7e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 22:07:44 -0700 Subject: [PATCH 01/19] docs: spec for customer-facing concept graphics Co-Authored-By: Claude Fable 5 --- ...-09-02-customer-concept-graphics-design.md | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-02-customer-concept-graphics-design.md diff --git a/docs/superpowers/specs/2026-09-02-customer-concept-graphics-design.md b/docs/superpowers/specs/2026-09-02-customer-concept-graphics-design.md new file mode 100644 index 000000000..55196520f --- /dev/null +++ b/docs/superpowers/specs/2026-09-02-customer-concept-graphics-design.md @@ -0,0 +1,139 @@ +# Customer-facing concept graphics: homepage grid, /render, /pilot-to-prod + +**Date:** 2026-09-02 +**Status:** Approved for planning +**Scope:** apps/website — diagram-kit extension, one new homepage section, two page graphics + +## Motivation + +The docs-visual-design arc (PR #950/#953) gave the site one schematic language +and put architecture diagrams everywhere a developer looks. Prospects are +still underserved: the homepage explains capabilities with video and code but +no at-a-glance concept; `/render` sells the hardest-to-grasp capability with +code only; `/pilot-to-prod` pitches a process with no picture of it. + +Decisions made interactively (visual companion session +`.superpowers/brainstorm/91468-1788325177`): customer-facing scope (homepage + +`/render` + `/pilot-to-prod`); **kit schematic register** (option A) over +product vignettes and icon cards; homepage placement as a **dedicated +section** (option C) with **four independent capability cards** (narrative +option 2, not the lifecycle strip); card diagrams built via a **compact kit +scale** (option A) rather than downscaled full-width compositions. + +## 1. Kit extension — `compact` scale + +- `DiagramFrame` gains `scale?: 'docs' | 'marketing' | 'compact'` (extends the + existing union; `data-scale` already flows to the figure). +- CSS (docs.css, kit block): `.tp-diagram-figure[data-scale="compact"] .tp-diagram-svg` + gets `min-width: 0` and `max-width: 100%` — a compact figure fills its card + and never scrolls. +- Authoring convention (documented in `DiagramFrame`'s JSDoc): compact + compositions use a ~320-wide viewBox and a larger type ramp so text renders + at or above designed size at card widths (~300–420px): eyebrow 10px, title + 13.5px, meta 11px, all in viewBox units. Implemented as CSS overrides under + the compact scale (`[data-scale="compact"] .tp-diagram-eyebrow` etc.), so + primitives stay unchanged. +- Style contract: pin the compact `min-width: 0` override so the mobile + 600px floor (PR #953) can never leak into cards. + +## 2. Homepage "How it works" section + +New landing section component `HomeConceptGrid` +(`apps/website/src/components/landing/HomeConceptGrid.tsx`), placed directly +after the Architecture (`StackDiagramSection`) section: architecture says +where things sit; this says what happens at runtime. + +- Shell: `Section surface="canvas"` (alternates with the tinted architecture + section above) + `SectionHeader variant="centered"` (eyebrow "How it works", + heading set at implementation against neighboring headlines — must not + rhyme with "Your UI talks to one contract…" above or the DemoShowcase + heading below). +- Body: a 2×2 grid (1-col on mobile) of four cards in the site card idiom. + Each card: compact diagram on top, capability title, one sentence, and a + "See it live" link to the matching existing anchor (`#stream`, `#render`, + `#ship`, `#approve`). +- The four compact compositions (`components/docs/diagrams/`, registered in + MDX only if a docs page later wants them — the grid imports directly): + - **`StreamConcept`** — user message node → `injectAgent()` pill → + signals node → UI node; the claim: tokens arrive as signals, the UI + updates itself. + - **`RenderConcept`** — spec node (mono JSON fragment) → registry pill → + "your component" node (accent); the claim: agent output renders as your + design system. + - **`ApproveConcept`** — agent node → `interrupt` pill → human node + (accent) → `resume` pill looping back; the claim: nothing irreversible + without a human (the register-A mock from the companion session). + - **`ShipConcept`** — a horizontal thread line crossing "reload" and + "deploy" tick pills and continuing to a "resumes" node; the claim: + threads survive everything between question and answer. +- Copy constraint: every card sentence must be verifiable against the docs + (same discipline as the last arc); implementation verifies each claim and + the reviewer re-verifies against pages/libs. + +## 3. `/render` marketing graphic + +- Generalize `StackDiagramSection` into a `DiagramSection` that accepts a + diagram child (keep `StackDiagramSection`'s existing prop surface by making + it a thin wrapper, or migrate its three call sites — implementer's choice, + no visual change to existing pages). +- New marketing-scale composition **`RenderTransform`** (640-wide, standard + scale): left node carries an abbreviated real spec fragment in mono + (2–3 lines, e.g. `{ "component": "Form", … }`), center accent node + `@threadplane/render` with meta `registry · state · handlers`, right node a + suggested rendered result ("Your form component — your styles, your + validation"). Edge pills: "JSON Spec" and "bindings + events" (labels + verified against the render docs, as in the last arc). +- Placed on `/render` after the hero in a `DiagramSection` (tinted surface — + verify the hero's surface at implementation and alternate correctly). + Headline/body written against the page's existing copy to avoid + duplication; body angle: "schema on the wire, your design system on + screen." + +## 4. `/pilot-to-prod` journey graphic + +- New composition species **`PilotJourney`** (640×~240, standard scale): a + horizontal baseline with three phase nodes — Pilot → Hardening → + Production — each with 2–3 meta deliverables, and gate pills on the line + between phases. Phase content MUST be lifted from the page's actual + copy at implementation time (the page defines what pilot includes); no + invented deliverables. +- Placed after that page's hero in a `DiagramSection`. + +## 5. What we are communicating (evaluation criteria) + +Each graphic exists to remove a specific reading burden. The implementation +and review MUST check each against this table — a graphic that fails its row +gets reworked, not shipped: + +| Graphic | The one-sentence claim | Complexity it removes | Overclaim risk to check | +| --- | --- | --- | --- | +| StreamConcept | Tokens arrive as signals; the UI updates itself | Reading the streaming guide to learn there's no manual subscription plumbing | Don't imply zero configuration; provider setup exists | +| RenderConcept | Agent output renders as your components | Reading the render intro to learn it's not an iframe/chat-widget | Don't show validation render doesn't do (last arc's finding) | +| ApproveConcept | Nothing irreversible without a human | Reading the interrupts guide to learn pauses are durable | "Durable" must match langgraph checkpoint behavior; AG-UI path differs — keep the card runtime-neutral | +| ShipConcept | Threads survive reloads and deploys | Reading persistence docs to learn state isn't in component memory | True for LangGraph Platform; AG-UI history is out of scope — phrase against the contract, not a runtime | +| RenderTransform | Schema on the wire, your design system on screen | Understanding generative UI without reading a line of code | The spec fragment must be a real, valid shape from the docs | +| PilotJourney | The engagement is three phases with concrete gates | Reading the whole page to learn what "pilot" includes | Deliverables must quote the page, not embellish it | + +Two systemic checks: (a) the homepage now has three visual systems in +sequence (stack diagram → concept grid → demo videos) — the section heading +and copy must differentiate their jobs (where / what happens / see it) so +they read as layers, not repetition; (b) no competitor names anywhere in +labels or copy. + +## 6. Testing + +- Vitest specs per composition (kit idioms: accessible label, load-bearing + titles, pill/edge counts) and for `HomeConceptGrid` / `DiagramSection` + (heading wiring, anchor links, compact data-scale present). +- Style-contract entries for the compact overrides. +- Browser verification at 375px and desktop: compact cards never scroll, + text ≥ designed size, page never scrolls horizontally; `/render` and + `/pilot-to-prod` sections alternate surfaces correctly. +- `nx test website`, `nx lint website` (0 errors), production build green. + +## Out of scope + +- `/chat` anatomy and `/solutions/*` graphics (inherit later). +- Docs concept-page diagrams beyond what exists (separate developer-facing + arc). +- Animation/interaction in the concept cards. From 2b3e54c2995228504d8c0284fdb396806fe6e318 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 22:20:25 -0700 Subject: [PATCH 02/19] docs: implementation plan for customer concept graphics Co-Authored-By: Claude Fable 5 --- .../2026-09-02-customer-concept-graphics.md | 848 ++++++++++++++++++ 1 file changed, 848 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-02-customer-concept-graphics.md diff --git a/docs/superpowers/plans/2026-09-02-customer-concept-graphics.md b/docs/superpowers/plans/2026-09-02-customer-concept-graphics.md new file mode 100644 index 000000000..a926c6c9d --- /dev/null +++ b/docs/superpowers/plans/2026-09-02-customer-concept-graphics.md @@ -0,0 +1,848 @@ +# Customer Concept Graphics Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a compact scale to the SVG diagram kit and use it for a homepage "How it works" concept grid, plus marketing graphics for /render (spec→components transform) and /pilot-to-prod (engagement journey). + +**Architecture:** Extends the existing diagram kit (`apps/website/src/components/docs/diagrams/`) with a `compact` scale (no min-width, larger type ramp via CSS under `[data-scale="compact"]`) and a `metaStyle` variant on `DiagramNode` for mono meta lines. A new generic `DiagramSection` landing component wraps any diagram child; `StackDiagramSection` becomes a thin wrapper over it. Four compact concept compositions feed a new `HomeConceptGrid` section; two standard-scale compositions (`RenderTransform`, `PilotJourney`) go on `/render` and `/pilot-to-prod`. + +**Tech Stack:** Next.js (apps/website), React Server Components, vitest + jsdom + @testing-library/react, token-styled CSS in `docs.css`/`landing.css` (inline-style lint guard applies). + +**Spec:** `docs/superpowers/specs/2026-09-02-customer-concept-graphics-design.md` — its §5 "What we are communicating" table is a SHIPPING GATE: every graphic's claim must be verified against the docs/libs during implementation and review. + +**Conventions (established by the prior arc — follow exactly):** +- Kit geometry: arrows stop 4px short of the target edge; edges NEVER run continuously under a pill (segment → pill → segment+arrow); 16px outer margins; slug unique per rendered instance. +- Standard-scale text-fit heuristics (640 viewBox): mono title 12.5px ≈ 7.5px/char, sans title 11px/600 ≈ 5.5px/char, meta 10.5px ≈ 5.2px/char, vs inner width w−32; keep ≥10px slack. +- Compact-scale heuristics (320 viewBox, ramp from Task 1): mono title 13.5px ≈ 8.1px/char, sans title 12px/600 ≈ 6px/char, meta 11px ≈ 5.5px/char, pill 10.5px ≈ 6.3px/char. +- Implementers verify every string against these heuristics AND against the target pages'/libs' actual wording (grep libs before attributing anything to a package); report deviations. Reviewers re-measure in a real browser. +- Tests: `npx nx test website`; lint: `npx nx lint website` (0 errors); spec files start with `// SPDX-License-Identifier: MIT` and `// @vitest-environment jsdom`. + +--- + +### Task 1: Kit compact scale + mono meta variant + +**Files:** +- Modify: `apps/website/src/components/docs/diagrams/DiagramFrame.tsx` (scale union) +- Modify: `apps/website/src/components/docs/diagrams/DiagramNode.tsx` (metaStyle prop) +- Modify: `apps/website/src/styles/docs.css` (compact overrides + mono meta, in the kit block) +- Modify: `apps/website/src/components/docs/diagrams/primitives.spec.tsx` +- Modify: `apps/website/src/styles/style-contracts.spec.ts` + +- [ ] **Step 1: Extend the failing spec** — append to `primitives.spec.tsx`: + +```tsx +it('DiagramFrame accepts the compact scale', () => { + const { container } = render( + + + + ); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); +}); + +it('DiagramNode renders a mono meta when metaStyle is mono', () => { + const { container } = render( + + + + ); + expect(container.querySelector('g.tp-diagram-node')?.getAttribute('data-meta')).toBe('mono'); +}); +``` + +- [ ] **Step 2: Run to verify failure** — `npx nx test website`. Expected: the two new tests FAIL (scale type error surfaces at runtime as attribute mismatch; `data-meta` absent). + +- [ ] **Step 3: Implement.** In `DiagramFrame.tsx` change the scale prop to: + +```tsx + /** Marketing pages render the same SVG larger; compact cards render it small with a bigger type ramp. */ + scale?: 'docs' | 'marketing' | 'compact'; +``` + +In `DiagramNode.tsx` add to the props interface and destructuring: + +```tsx + /** 'mono' for code-shaped meta lines (JSON fragments, API names); default Inter. */ + metaStyle?: 'sans' | 'mono'; +``` + +with `metaStyle = 'sans'` default, and add `data-meta={metaStyle}` to the `` attributes. Extend the component JSDoc: compact-scale compositions author at a ~320 viewBox with the compact type ramp (eyebrow 10 / mono title 13.5 / sans title 12 / meta 11 / pill 10.5, in viewBox units) — same baseline offsets apply. + +- [ ] **Step 4: CSS.** In `docs.css`, immediately after the existing `.tp-diagram-figure[data-scale="marketing"]` rule, add: + +```css +/* Compact scale: card-sized diagrams (~320 viewBox). No floor — a compact + * figure fills its card and never scrolls; legibility comes from the larger + * type ramp below, not from render width. */ +.tp-diagram-figure[data-scale="compact"] .tp-diagram-svg { + min-width: 0; + max-width: 100%; +} +.tp-diagram-figure[data-scale="compact"] .tp-diagram-eyebrow { font-size: 10px; } +.tp-diagram-figure[data-scale="compact"] .tp-diagram-title { font-size: 13.5px; } +.tp-diagram-figure[data-scale="compact"] .tp-diagram-node[data-title="sans"] .tp-diagram-title { font-size: 12px; } +.tp-diagram-figure[data-scale="compact"] .tp-diagram-meta { font-size: 11px; } +.tp-diagram-figure[data-scale="compact"] .tp-diagram-pill text { font-size: 10.5px; } +.tp-diagram-node[data-meta="mono"] .tp-diagram-meta { font-family: var(--font-mono); } +``` + +- [ ] **Step 5: Style contract.** In `style-contracts.spec.ts`, append after the `.tp-diagram-svg` entry: + +```ts + { + file: 'docs.css', + selector: '.tp-diagram-figure[data-scale="compact"] .tp-diagram-svg', + why: 'Compact card diagrams must never inherit the 600px phone floor: inside a grid card that floor would force an internal scroll where none is affordable. Losing this override silently reintroduces it.', + requires: { + 'min-width': /min-width:\s*0/, + }, + }, +``` + +- [ ] **Step 6: Run** `npx nx test website && npx nx lint website` — PASS, 0 errors. + +- [ ] **Step 7: Commit** + +```bash +git add apps/website/src/components/docs/diagrams apps/website/src/styles +git commit -m "feat(website): compact diagram scale + mono meta variant" +``` + +--- + +### Task 2: Generic DiagramSection; StackDiagramSection becomes a wrapper + +**Files:** +- Create: `apps/website/src/components/landing/DiagramSection.tsx` +- Create: `apps/website/src/components/landing/DiagramSection.spec.tsx` +- Modify: `apps/website/src/components/landing/StackDiagramSection.tsx` + +- [ ] **Step 1: Failing spec** (`DiagramSection.spec.tsx`): + +```tsx +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { DiagramSection } from './DiagramSection'; + +describe('DiagramSection', () => { + it('renders heading, body, and its diagram child', () => { + const { container, getByText } = render( + +
+ + ); + expect(getByText('The headline').tagName).toBe('H2'); + expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('j-heading'); + expect(container.querySelector('figure.tp-diagram-figure')).not.toBeNull(); + }); +}); +``` + +Run `npx nx test website` — FAIL (module not found). + +- [ ] **Step 2: Implement `DiagramSection.tsx`** (the current StackDiagramSection body with the diagram generalized to children): + +```tsx +// SPDX-License-Identifier: MIT +import type { ReactNode } from 'react'; +import { Section } from '../ui/Section'; +import { Container } from '../ui/Container'; +import { SectionHeader } from '../ui/SectionHeader'; + +interface DiagramSectionProps { + id: string; + eyebrow: string; + headline: string; + body: ReactNode; + children: ReactNode; +} + +/** + * A landing section framing any kit diagram with a centered header + body. + * The `.stack-diagram-*` classes predate the generalization and are shared. + */ +export function DiagramSection({ id, eyebrow, headline, body, children }: DiagramSectionProps) { + return ( +
+ +
+ +

{body}

+ {children} +
+
+
+ ); +} +``` + +- [ ] **Step 3: Rewrite `StackDiagramSection.tsx` as a thin wrapper** (same public API, no visual change; its existing spec must keep passing untouched): + +```tsx +// SPDX-License-Identifier: MIT +import type { ReactNode } from 'react'; +import { DiagramSection } from './DiagramSection'; +import { StackDiagram, type StackHighlight } from '../docs/diagrams'; + +interface StackDiagramSectionProps { + id: string; + eyebrow: string; + headline: string; + body: ReactNode; + highlight?: StackHighlight; + caption?: string; +} + +/** The canonical stack diagram in a DiagramSection frame (homepage + adapter pages). */ +export function StackDiagramSection({ id, eyebrow, headline, body, highlight = 'none', caption }: StackDiagramSectionProps) { + return ( + + + + ); +} +``` + +- [ ] **Step 4: Run** `npx nx test website && npx nx lint website` — PASS (including the untouched `StackDiagramSection.spec.tsx`), 0 errors. + +- [ ] **Step 5: Commit** + +```bash +git add apps/website/src/components/landing +git commit -m "refactor(website): generic DiagramSection; StackDiagramSection wraps it" +``` + +--- + +### Task 3: Compact compositions — StreamConcept + RenderConcept + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/StreamConcept.tsx` +- Create: `apps/website/src/components/docs/diagrams/RenderConcept.tsx` +- Modify: `apps/website/src/components/docs/diagrams/index.ts` (export both) +- Create: `apps/website/src/components/docs/diagrams/concepts.spec.tsx` + +Before coding: read `/docs/langgraph/guides/streaming` and `/docs/render/getting-started/introduction` content files so every label matches the docs' own vocabulary; grep `libs/langgraph` for `injectAgent` and `libs/render` for the registry naming. Report label sources. + +- [ ] **Step 1: Failing spec** (`concepts.spec.tsx`; later tasks append): + +```tsx +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { StreamConcept } from './StreamConcept'; +import { RenderConcept } from './RenderConcept'; + +/** Compact homepage concept cards: each must mount labeled, at compact scale, + * and carry its load-bearing API/package names. */ +describe('StreamConcept', () => { + it('mounts compact with the signals claim', () => { + const { container } = render(); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('injectAgent()'); + }); +}); + +describe('RenderConcept', () => { + it('mounts compact and accents the your-components claim', () => { + const { container } = render(); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); + expect(container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]').length).toBeGreaterThan(0); + }); +}); +``` + +Run `npx nx test website` — FAIL. + +- [ ] **Step 2: Implement `StreamConcept.tsx`** (draft geometry — verify fit with the compact heuristics and adjust honestly, keeping the 4px-arrow and margin conventions; report changes): + +```tsx +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'concept-stream'; + +/** Homepage concept card: tokens arrive as signals; the UI updates itself. */ +export function StreamConcept() { + return ( + + + + + + + + ); +} +``` + +- [ ] **Step 3: Implement `RenderConcept.tsx`** (the spec fragment must be a REAL shape from the render docs — copy an abbreviated valid fragment from `/docs/render` content, mono meta via Task 1's `metaStyle`): + +```tsx +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'concept-render'; + +/** Homepage concept card: a JSON spec renders as your components. */ +export function RenderConcept() { + return ( + + + + + + + + ); +} +``` + +- [ ] **Step 4: Export both from `index.ts`; run** `npx nx test website && npx nx lint website` — PASS, 0 errors. + +- [ ] **Step 5: Commit** + +```bash +git add apps/website/src/components/docs/diagrams +git commit -m "feat(website): Stream + Render compact concept diagrams" +``` + +--- + +### Task 4: Compact compositions — ApproveConcept + ShipConcept + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/ApproveConcept.tsx` +- Create: `apps/website/src/components/docs/diagrams/ShipConcept.tsx` +- Modify: `apps/website/src/components/docs/diagrams/index.ts`, `concepts.spec.tsx` + +Before coding: read the interrupts guide and persistence guide content. Per the spec's §5 table: ApproveConcept stays runtime-neutral on durability; ShipConcept phrases against the contract, not a runtime. + +- [ ] **Step 1: Append failing spec blocks** (imports at top): + +```tsx +import { ApproveConcept } from './ApproveConcept'; +import { ShipConcept } from './ShipConcept'; + +describe('ApproveConcept', () => { + it('mounts compact with the interrupt/resume loop', () => { + const { container } = render(); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); + const pills = Array.from(container.querySelectorAll('.tp-diagram-pill text')).map((t) => t.textContent); + expect(pills).toContain('interrupt'); + expect(pills).toContain('resume'); + }); +}); + +describe('ShipConcept', () => { + it('mounts compact with the thread crossing reload and deploy', () => { + const { container } = render(); + const pills = Array.from(container.querySelectorAll('.tp-diagram-pill text')).map((t) => t.textContent); + expect(pills).toContain('reload'); + expect(pills).toContain('deploy'); + }); +}); +``` + +Run — FAIL. + +- [ ] **Step 2: Implement `ApproveConcept.tsx`** (the register-mock topology at compact scale; pills sit in edge breaks): + +```tsx +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'concept-approve'; + +/** Homepage concept card: nothing irreversible without a human. */ +export function ApproveConcept() { + return ( + + + + + + + + + + + + ); +} +``` + +Note the resume loop runs right-to-left then down; verify pill breaks leave no line under either pill and the final arrow lands 4px above the bottom node. Adjust coordinates honestly and report. + +- [ ] **Step 3: Implement `ShipConcept.tsx`** (a thread line crossing survival events): + +```tsx +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'concept-ship'; + +/** Homepage concept card: the thread survives everything between question and answer. */ +export function ShipConcept() { + return ( + + + + + + + + + + ); +} +``` + +- [ ] **Step 4: Export from `index.ts`; run tests + lint** — PASS, 0 errors. + +- [ ] **Step 5: Commit** + +```bash +git add apps/website/src/components/docs/diagrams +git commit -m "feat(website): Approve + Ship compact concept diagrams" +``` + +--- + +### Task 5: HomeConceptGrid section + homepage insertion + +**Files:** +- Create: `apps/website/src/components/landing/HomeConceptGrid.tsx` +- Create: `apps/website/src/components/landing/HomeConceptGrid.spec.tsx` +- Modify: `apps/website/src/styles/landing.css` (grid + card classes) +- Modify: `apps/website/src/app/page.tsx` (insert after the Architecture section) + +Before coding: re-read `page.tsx` and the FeatureBlock headings so no card sentence duplicates a neighboring headline; confirm the anchors `#stream`, `#render`, `#ship`, `#approve` exist as FeatureBlock ids. + +- [ ] **Step 1: Failing spec:** + +```tsx +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { HomeConceptGrid } from './HomeConceptGrid'; + +describe('HomeConceptGrid', () => { + it('renders four compact concept cards with anchor links', () => { + const { container } = render(); + expect(container.querySelectorAll('.home-concept-card')).toHaveLength(4); + expect(container.querySelectorAll('figure[data-scale="compact"]')).toHaveLength(4); + const hrefs = Array.from(container.querySelectorAll('a.home-concept-link')).map((a) => a.getAttribute('href')); + expect(hrefs).toEqual(['#stream', '#render', '#approve', '#ship']); + }); + + it('is a labeled section', () => { + const { container } = render(); + expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('how-it-works-heading'); + }); +}); +``` + +Run — FAIL. + +- [ ] **Step 2: Implement `HomeConceptGrid.tsx`:** + +```tsx +// SPDX-License-Identifier: MIT +import type { ReactNode } from 'react'; +import { Section } from '../ui/Section'; +import { Container } from '../ui/Container'; +import { SectionHeader } from '../ui/SectionHeader'; +import { StreamConcept, RenderConcept, ApproveConcept, ShipConcept } from '../docs/diagrams'; + +interface ConceptCard { + anchor: string; + title: string; + sentence: string; + diagram: ReactNode; +} + +/** Card order mirrors the FeatureBlock order below (stream, render, approve, ship). */ +const CARDS: ConceptCard[] = [ + { + anchor: '#stream', + title: 'Stream', + sentence: 'Tokens arrive as signals — the UI updates itself, no subscription plumbing.', + diagram: , + }, + { + anchor: '#render', + title: 'Render', + sentence: 'Agent output arrives as a JSON spec and renders as your components.', + diagram: , + }, + { + anchor: '#approve', + title: 'Approve', + sentence: 'Interrupts pause the thread for a human decision, then resume exactly there.', + diagram: , + }, + { + anchor: '#ship', + title: 'Ship', + sentence: 'Threads live behind the Agent contract — they outlast reloads and deploys.', + diagram: , + }, +]; + +export function HomeConceptGrid() { + return ( +
+ +
+ +
+ {CARDS.map((card) => ( +
+ {card.diagram} +

{card.title}

+

{card.sentence}

+ + See it live + +
+ ))} +
+
+
+
+ ); +} +``` + +Card ORDER in `CARDS` must match the spec test (`stream, render, approve, ship`). The heading must not rhyme with the Architecture headline ("Your UI talks to one contract, never to a runtime") or the DemoShowcase heading ("One chat UI. Two runtimes. Same code.") — the draft above satisfies that; improve it only if you find something better while reading the page, and report. + +- [ ] **Step 3: CSS** — append to `landing.css`: + +```css +/* HomeConceptGrid */ +.home-concept { + display: flex; + flex-direction: column; + gap: 28px; +} +.home-concept-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; +} +@media (max-width: 767px) { + .home-concept-grid { + grid-template-columns: 1fr; + } +} +.home-concept-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 20px 20px 18px; + display: flex; + flex-direction: column; + gap: 6px; +} +.home-concept-card .tp-diagram-figure { + margin: 0 0 10px; +} +.home-concept-title { + font-family: var(--font-inter); + font-size: 1rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0; +} +.home-concept-sentence { + font-family: var(--font-inter); + font-size: 0.9rem; + line-height: 1.55; + color: var(--color-text-secondary); + margin: 0; +} +.home-concept-link { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 600; + color: var(--color-accent); + text-decoration: none; + margin-top: 4px; +} +.home-concept-link:hover { + text-decoration: underline; +} +``` + +- [ ] **Step 4: Homepage insertion** — in `page.tsx`, import `HomeConceptGrid` and insert directly AFTER the `` block and BEFORE the DemoShowcase `
`. The architecture section is tinted and DemoShowcase is canvas; this section is canvas — verify the resulting surface sequence alternates (tinted → canvas → canvas is acceptable only if the card grid visually separates; if it reads as one blob, switch DemoShowcase's neighbor handling is NOT in scope — instead give this section `surface="tinted"`? NO: two adjacent tinted sections (architecture + this) would blob. Keep `canvas` and verify in the browser; report what you see). + +- [ ] **Step 5: Run tests + lint; then dev-server check** — load `/`, confirm the grid renders 2×2 desktop / 1-col at 375px, cards never scroll internally, anchors jump to the FeatureBlocks. + +- [ ] **Step 6: Commit** + +```bash +git add -A apps/website +git commit -m "feat(website): homepage How-it-works concept grid" +``` + +--- + +### Task 6: RenderTransform + /render section + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/RenderTransform.tsx` +- Modify: `apps/website/src/components/docs/diagrams/index.ts`, `compositions.spec.tsx` +- Modify: `apps/website/src/app/render/page.tsx` (DiagramSection after the hero) + +Before coding: read `/render`'s page.tsx fully (hero is `surface="canvas"`, first FeatureBlock id="schemas") and the render docs for a REAL abbreviated spec fragment and honest pill labels (the prior arc established: no "validated spec" — validation is the app's job). + +- [ ] **Step 1: Append failing spec block** to `compositions.spec.tsx`: + +```tsx +import { RenderTransform } from './RenderTransform'; + +describe('RenderTransform', () => { + it('mounts at standard scale with spec, render, and result stages', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/render'); + expect(container.querySelectorAll('.tp-diagram-pill')).toHaveLength(2); + }); +}); +``` + +Run — FAIL. + +- [ ] **Step 2: Implement `RenderTransform.tsx`** (640 viewBox, horizontal, mono-meta spec fragment; draft — verify fit + honest labels): + +```tsx +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'render-transform'; + +/** /render marketing graphic: schema on the wire, your design system on screen. */ +export function RenderTransform() { + return ( + + + + + + + + + + ); +} +``` + +(That draft has only one pill; the spec test requires 2 — add a second pill labeled `bindings + events` in an edge break between renderer and result, widening the gap accordingly, or change the geometry honestly and update the test to the final pill count. Either way test and diagram must agree AND labels must be verified against the render docs.) + +- [ ] **Step 3: Insert on `/render`** — in `render/page.tsx`, import `DiagramSection` and `RenderTransform`, insert after the hero `
`: + +```tsx + + + +``` + +Check the page's existing headings for copy collisions (esp. the `#schemas` FeatureBlock) and adjust minimally; report. + +- [ ] **Step 4: Tests + lint + dev-server check of `/render`; commit** + +```bash +git add -A apps/website +git commit -m "feat(website): render transform graphic on /render" +``` + +--- + +### Task 7: PilotJourney + /pilot-to-prod section + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/PilotJourney.tsx` +- Modify: `apps/website/src/components/docs/diagrams/index.ts`, `compositions.spec.tsx` +- Modify: `apps/website/src/app/pilot-to-prod/page.tsx` + +MANDATORY first step: read `pilot-to-prod/page.tsx` in full. The page's phases are its FeatureBlocks (`id="discover"`, `id="build"`, `id="harden"`) plus an outcomes section — the diagram's phase titles and meta deliverables MUST be lifted from that copy verbatim-or-abbreviated, not invented. The draft below uses the block ids as titles; replace metas with the page's actual row copy and report the mapping. + +- [ ] **Step 1: Append failing spec block:** + +```tsx +import { PilotJourney } from './PilotJourney'; + +describe('PilotJourney', () => { + it('mounts with three phase nodes on the journey line', () => { + const { container } = render(); + expect(container.querySelectorAll('g.tp-diagram-node')).toHaveLength(3); + expect(container.querySelector('svg[role="img"]')?.getAttribute('aria-label')).toBeTruthy(); + }); +}); +``` + +Run — FAIL. + +- [ ] **Step 2: Implement `PilotJourney.tsx`** (draft; metas to be replaced from the page): + +```tsx +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'pilot-journey'; + +/** /pilot-to-prod: the engagement as three phases with concrete gates. */ +export function PilotJourney() { + return ( + + + + + + + + + + ); +} +``` + +The two literal strings `"replace from page copy"` and `"gate label"` are DELIBERATE red flags: the implementer MUST substitute the page's real deliverables/gates (abbreviated to fit the standard-scale meta heuristic) before committing, and the reviewer must diff them against the page. A commit containing those literals is a task failure. Only one of the two inter-phase gaps has a gate pill in the draft; add the second gate pill (with an edge break) if the page copy provides a natural second gate, else remove the first for symmetry — report the choice. + +- [ ] **Step 3: Insert on the page** — after the hero `` in `pilot-to-prod/page.tsx`: + +```tsx + + + +``` + +Verify heading/body against the page's hero + outcomes copy for collisions; adjust minimally and report. + +- [ ] **Step 4: Tests + lint + dev-server check; commit** + +```bash +git add -A apps/website +git commit -m "feat(website): pilot journey graphic on /pilot-to-prod" +``` + +--- + +### Task 8: Communication audit (spec §5 gate) + +**Files:** none expected (fixes only if the audit fails a row) + +- [ ] **Step 1:** For each of the six graphics, verify its §5 table row against the actual docs/libs on disk: + - StreamConcept: does any label imply zero setup? (`provideAgent` is required — the card must not deny it.) + - RenderConcept/RenderTransform: no validation implied; spec fragments match a real documented shape (`grep` the render docs for the fragment's keys). + - ApproveConcept: labels runtime-neutral (no LangGraph-only durability claim). Check the interrupts guide + the ag-ui event mapping for what both runtimes support. + - ShipConcept: phrased against the Agent contract; confirm the langgraph persistence docs support "outlast reloads and deploys" and that nothing claims AG-UI history (out of scope per its own intro). + - PilotJourney: every meta string traceable to `pilot-to-prod/page.tsx` copy. +- [ ] **Step 2:** Check the homepage reads as three distinct layers (architecture → how-it-works → demo): load `/`, read the three headings in sequence; no rhyme, no repeated claim. Fix copy if needed. +- [ ] **Step 3:** `grep -rn "CopilotKit" apps/website/src apps/website/content` → must return nothing new (competitor-mention rule). +- [ ] **Step 4:** Write the audit findings (row-by-row pass/fail + any fixes made) into the task report. Commit any fixes: + +```bash +git add -A apps/website +git commit -m "fix(website): communication-audit fixes for concept graphics" +``` + +(Skip the commit if nothing needed fixing.) + +--- + +### Task 9: Full verification pass + +- [ ] **Step 1:** `rm -rf apps/website/.next && npx nx test website --skip-nx-cache && npx nx lint website` — PASS, 0 lint errors. +- [ ] **Step 2:** `npx nx build website --configuration=production` — green. (If Turbopack panics with "leaves the filesystem root", `rm -rf apps/website/.next` first — stale dev artifacts.) +- [ ] **Step 3:** Playwright docs suite still green: `cd apps/website && npx playwright test --grep "Docs slug page"` (kit CSS changed; the docs pages must be unaffected). +- [ ] **Step 4:** Browser sweep at 375px and desktop: `/` (grid 2×2 → 1-col, compact cards never scroll, anchors work), `/render`, `/pilot-to-prod` (sections alternate surfaces, diagrams scroll internally only at standard scale), plus one docs page spot-check (`/docs/ag-ui/getting-started/introduction`) to confirm compact CSS leaked nothing. +- [ ] **Step 5:** Commit any sweep fixes: + +```bash +git add -A apps/website +git commit -m "fix(website): verification-sweep fixes for concept graphics" +``` From f4221384b448dcc09ddc7cdd4f7016c00816383a Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 22:26:42 -0700 Subject: [PATCH 03/19] feat(website): compact diagram scale + mono meta variant Co-Authored-By: Claude Fable 5 --- .../components/docs/diagrams/DiagramFrame.tsx | 4 ++-- .../components/docs/diagrams/DiagramNode.tsx | 9 ++++++++- .../docs/diagrams/primitives.spec.tsx | 18 ++++++++++++++++++ apps/website/src/styles/docs.css | 13 +++++++++++++ .../website/src/styles/style-contracts.spec.ts | 8 ++++++++ 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx index 72d42bb7b..a20517670 100644 --- a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx @@ -13,8 +13,8 @@ interface DiagramFrameProps { /** Accessible one-sentence description of what the diagram shows. */ label: string; caption?: string; - /** Marketing pages render the same SVG larger. */ - scale?: 'docs' | 'marketing'; + /** Marketing pages render the same SVG larger; compact cards render it small with a bigger type ramp. */ + scale?: 'docs' | 'marketing' | 'compact'; children: ReactNode; } diff --git a/apps/website/src/components/docs/diagrams/DiagramNode.tsx b/apps/website/src/components/docs/diagrams/DiagramNode.tsx index da0a66d6c..ad0f5bde5 100644 --- a/apps/website/src/components/docs/diagrams/DiagramNode.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramNode.tsx @@ -12,6 +12,8 @@ interface DiagramNodeProps { align?: 'start' | 'middle'; /** 'sans' for prose-y titles (backend lists); default mono for package names. */ titleStyle?: 'mono' | 'sans'; + /** 'mono' for code-shaped meta lines (JSON fragments, API names); default Inter. */ + metaStyle?: 'sans' | 'mono'; } const PAD = 16; @@ -21,6 +23,10 @@ const PAD = 16; * Minimum heights: `h >= 64` with eyebrow+meta, `h >= 52` with meta only, * `h >= 52` with eyebrow and no meta (eyebrow at y+20, title at y+38), * any `h` for title-only (vertically centered). + * + * Compact-scale compositions author at a ~320 viewBox with the compact type + * ramp (eyebrow 10 / mono title 13.5 / sans title 12 / meta 11 / pill 10.5, + * viewBox units); the same baseline offsets above apply unchanged. */ export function DiagramNode({ x, @@ -33,6 +39,7 @@ export function DiagramNode({ tone = 'neutral', align = 'start', titleStyle = 'mono', + metaStyle = 'sans', }: DiagramNodeProps) { const tx = align === 'middle' ? x + w / 2 : x + PAD; const anchor = align === 'middle' ? 'middle' : undefined; @@ -41,7 +48,7 @@ export function DiagramNode({ const titleY = eyebrow ? y + 38 : meta ? y + 26 : y + h / 2 + 4; const metaY = eyebrow ? y + 54 : y + 42; return ( - + {eyebrow ? ( diff --git a/apps/website/src/components/docs/diagrams/primitives.spec.tsx b/apps/website/src/components/docs/diagrams/primitives.spec.tsx index 64ec1e7b7..0476bc7a6 100644 --- a/apps/website/src/components/docs/diagrams/primitives.spec.tsx +++ b/apps/website/src/components/docs/diagrams/primitives.spec.tsx @@ -76,6 +76,24 @@ describe('diagram kit primitives', () => { expect(container.querySelector('g.tp-diagram-node')?.getAttribute('data-title')).toBe('sans'); }); + it('DiagramFrame accepts the compact scale', () => { + const { container } = render( + + + + ); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); + }); + + it('DiagramNode renders a mono meta when metaStyle is mono', () => { + const { container } = render( + + + + ); + expect(container.querySelector('g.tp-diagram-node')?.getAttribute('data-meta')).toBe('mono'); + }); + it('DiagramPill renders a centered label', () => { const { container } = render( diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index 697fc9eac..a10d0241c 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -686,6 +686,19 @@ body:has([data-website-workspace-host]) { .tp-diagram-figure[data-scale="marketing"] .tp-diagram-svg { max-width: 860px; } +/* Compact scale: card-sized diagrams (~320 viewBox). No floor — a compact + * figure fills its card and never scrolls; legibility comes from the larger + * type ramp below, not from render width. */ +.tp-diagram-figure[data-scale="compact"] .tp-diagram-svg { + min-width: 0; + max-width: 100%; +} +.tp-diagram-figure[data-scale="compact"] .tp-diagram-eyebrow { font-size: 10px; } +.tp-diagram-figure[data-scale="compact"] .tp-diagram-title { font-size: 13.5px; } +.tp-diagram-figure[data-scale="compact"] .tp-diagram-node[data-title="sans"] .tp-diagram-title { font-size: 12px; } +.tp-diagram-figure[data-scale="compact"] .tp-diagram-meta { font-size: 11px; } +.tp-diagram-figure[data-scale="compact"] .tp-diagram-pill text { font-size: 10.5px; } +.tp-diagram-node[data-meta="mono"] .tp-diagram-meta { font-family: var(--font-mono); } .tp-diagram-dot { fill: var(--color-border); } .tp-diagram-caption { font-family: var(--font-inter); diff --git a/apps/website/src/styles/style-contracts.spec.ts b/apps/website/src/styles/style-contracts.spec.ts index fe111f669..d82db283e 100644 --- a/apps/website/src/styles/style-contracts.spec.ts +++ b/apps/website/src/styles/style-contracts.spec.ts @@ -137,6 +137,14 @@ const CONTRACTS: StyleContract[] = [ 'min-width': /min-width:\s*600px/, }, }, + { + file: 'docs.css', + selector: '.tp-diagram-figure[data-scale="compact"] .tp-diagram-svg', + why: 'Compact card diagrams must never inherit the 600px phone floor: inside a grid card that floor would force an internal scroll where none is affordable. Losing this override silently reintroduces it.', + requires: { + 'min-width': /min-width:\s*0/, + }, + }, ]; describe('style contracts', () => { From c64b6cfaf6b5aadc890f4f7ebe92ea61dece0846 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 22:35:26 -0700 Subject: [PATCH 04/19] =?UTF-8?q?fix(website):=20compact=20scale=20?= =?UTF-8?q?=E2=80=94=20no=20scroll=20chrome,=20420px=20cap,=20cascade=20no?= =?UTF-8?q?tes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../src/components/docs/diagrams/DiagramFrame.tsx | 7 ++++++- .../components/docs/diagrams/primitives.spec.tsx | 5 +++++ apps/website/src/styles/docs.css | 13 ++++++++++++- apps/website/src/styles/style-contracts.spec.ts | 3 ++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx index a20517670..300d02b4d 100644 --- a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx @@ -13,7 +13,12 @@ interface DiagramFrameProps { /** Accessible one-sentence description of what the diagram shows. */ label: string; caption?: string; - /** Marketing pages render the same SVG larger; compact cards render it small with a bigger type ramp. */ + /** + * Marketing pages render the same SVG larger; compact cards render it small with a bigger type + * ramp. Compact compositions author at a ~320 viewBox with the compact type ramp (eyebrow 10 / + * mono title 13.5 / sans title 12 / meta 11 / pill 10.5, viewBox units) — see DiagramNode's note + * for the shared baseline offsets; rendered width is capped at 420px regardless of card size. + */ scale?: 'docs' | 'marketing' | 'compact'; children: ReactNode; } diff --git a/apps/website/src/components/docs/diagrams/primitives.spec.tsx b/apps/website/src/components/docs/diagrams/primitives.spec.tsx index 0476bc7a6..7a694cc3e 100644 --- a/apps/website/src/components/docs/diagrams/primitives.spec.tsx +++ b/apps/website/src/components/docs/diagrams/primitives.spec.tsx @@ -76,6 +76,11 @@ describe('diagram kit primitives', () => { expect(container.querySelector('g.tp-diagram-node')?.getAttribute('data-title')).toBe('sans'); }); + // This runtime assertion is trivially green — 'compact' is a literal in the + // scale union, so any string reaching here has already type-checked. The + // real guard is tsc during `nx build` (spec .tsx files are type-checked + // because tsconfig excludes only `*.spec.ts`, not `*.spec.tsx`) — don't + // "fix" that tsconfig exclude without replacing this guard. it('DiagramFrame accepts the compact scale', () => { const { container } = render( diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index a10d0241c..f553b9c9c 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -689,12 +689,23 @@ body:has([data-website-workspace-host]) { /* Compact scale: card-sized diagrams (~320 viewBox). No floor — a compact * figure fills its card and never scrolls; legibility comes from the larger * type ramp below, not from render width. */ +.tp-diagram-figure[data-scale="compact"] { + /* Compact figures cannot overflow, live inside cards that own their own + * spacing, and sit on arbitrary card backgrounds — no scroll affordance, + * no margins. */ + margin: 0; + overflow-x: visible; + background: none; +} .tp-diagram-figure[data-scale="compact"] .tp-diagram-svg { min-width: 0; - max-width: 100%; + max-width: 420px; } .tp-diagram-figure[data-scale="compact"] .tp-diagram-eyebrow { font-size: 10px; } .tp-diagram-figure[data-scale="compact"] .tp-diagram-title { font-size: 13.5px; } +/* Ties with the base .tp-diagram-node[data-title="sans"] rule below in file + * order — this higher-specificity duplicate must stay or compact sans + * titles lose the cascade. */ .tp-diagram-figure[data-scale="compact"] .tp-diagram-node[data-title="sans"] .tp-diagram-title { font-size: 12px; } .tp-diagram-figure[data-scale="compact"] .tp-diagram-meta { font-size: 11px; } .tp-diagram-figure[data-scale="compact"] .tp-diagram-pill text { font-size: 10.5px; } diff --git a/apps/website/src/styles/style-contracts.spec.ts b/apps/website/src/styles/style-contracts.spec.ts index d82db283e..52b4e3875 100644 --- a/apps/website/src/styles/style-contracts.spec.ts +++ b/apps/website/src/styles/style-contracts.spec.ts @@ -140,9 +140,10 @@ const CONTRACTS: StyleContract[] = [ { file: 'docs.css', selector: '.tp-diagram-figure[data-scale="compact"] .tp-diagram-svg', - why: 'Compact card diagrams must never inherit the 600px phone floor: inside a grid card that floor would force an internal scroll where none is affordable. Losing this override silently reintroduces it.', + why: 'Compact card diagrams must never inherit the 600px phone floor: inside a grid card that floor would force an internal scroll where none is affordable. Losing the min-width override silently reintroduces that 600px floor. Losing the max-width cap lets a wide grid card scale the diagram past its tuned compact type ramp, ballooning the text.', requires: { 'min-width': /min-width:\s*0/, + 'max-width': /max-width:\s*420px/, }, }, ]; From 9ebd9f3dd4650e970e8eb24a3c58b5c4f015bd32 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 22:37:48 -0700 Subject: [PATCH 05/19] refactor(website): generic DiagramSection; StackDiagramSection wraps it Co-Authored-By: Claude Fable 5 --- .../landing/DiagramSection.spec.tsx | 19 ++++++++++ .../src/components/landing/DiagramSection.tsx | 36 +++++++++++++++++++ .../landing/StackDiagramSection.tsx | 21 +++-------- 3 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 apps/website/src/components/landing/DiagramSection.spec.tsx create mode 100644 apps/website/src/components/landing/DiagramSection.tsx diff --git a/apps/website/src/components/landing/DiagramSection.spec.tsx b/apps/website/src/components/landing/DiagramSection.spec.tsx new file mode 100644 index 000000000..e94adccdb --- /dev/null +++ b/apps/website/src/components/landing/DiagramSection.spec.tsx @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { DiagramSection } from './DiagramSection'; + +describe('DiagramSection', () => { + it('renders heading, body, and its diagram child', () => { + const { container, getByText } = render( + +
+
+ ); + expect(getByText('The headline').tagName).toBe('H2'); + expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('j-heading'); + expect(container.querySelector('figure.tp-diagram-figure')).not.toBeNull(); + }); +}); diff --git a/apps/website/src/components/landing/DiagramSection.tsx b/apps/website/src/components/landing/DiagramSection.tsx new file mode 100644 index 000000000..eacc0ecc8 --- /dev/null +++ b/apps/website/src/components/landing/DiagramSection.tsx @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +import type { ReactNode } from 'react'; +import { Section } from '../ui/Section'; +import { Container } from '../ui/Container'; +import { SectionHeader } from '../ui/SectionHeader'; + +interface DiagramSectionProps { + id: string; + eyebrow: string; + headline: string; + body: ReactNode; + children: ReactNode; +} + +/** + * A landing section framing any kit diagram with a centered header + body. + * The `.stack-diagram-*` classes predate the generalization and are shared. + */ +export function DiagramSection({ id, eyebrow, headline, body, children }: DiagramSectionProps) { + return ( +
+ +
+ +

{body}

+ {children} +
+ + + ); +} diff --git a/apps/website/src/components/landing/StackDiagramSection.tsx b/apps/website/src/components/landing/StackDiagramSection.tsx index d8ec4f98f..5cd0344d5 100644 --- a/apps/website/src/components/landing/StackDiagramSection.tsx +++ b/apps/website/src/components/landing/StackDiagramSection.tsx @@ -1,8 +1,6 @@ // SPDX-License-Identifier: MIT import type { ReactNode } from 'react'; -import { Section } from '../ui/Section'; -import { Container } from '../ui/Container'; -import { SectionHeader } from '../ui/SectionHeader'; +import { DiagramSection } from './DiagramSection'; import { StackDiagram, type StackHighlight } from '../docs/diagrams'; interface StackDiagramSectionProps { @@ -27,19 +25,8 @@ export function StackDiagramSection({ caption, }: StackDiagramSectionProps) { return ( -
- -
- -

{body}

- -
-
-
+ + + ); } From 5f01811ddbf2d52fb9783af6da8345005904faf2 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 22:44:41 -0700 Subject: [PATCH 06/19] feat(website): Stream + Render compact concept diagrams Adds compact-scale StreamConcept and RenderConcept homepage cards, plus Task 2 carry-overs: a stronger DiagramSection test, a corrected component-name code comment in landing.css, and a JSDoc note on why DiagramSection is always tinted. Co-Authored-By: Claude Fable 5 --- .../docs/diagrams/RenderConcept.tsx | 56 +++++++++++++++++++ .../docs/diagrams/StreamConcept.tsx | 25 +++++++++ .../docs/diagrams/concepts.spec.tsx | 26 +++++++++ .../src/components/docs/diagrams/index.ts | 2 + .../landing/DiagramSection.spec.tsx | 2 + .../src/components/landing/DiagramSection.tsx | 3 + apps/website/src/styles/landing.css | 2 +- 7 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 apps/website/src/components/docs/diagrams/RenderConcept.tsx create mode 100644 apps/website/src/components/docs/diagrams/StreamConcept.tsx create mode 100644 apps/website/src/components/docs/diagrams/concepts.spec.tsx diff --git a/apps/website/src/components/docs/diagrams/RenderConcept.tsx b/apps/website/src/components/docs/diagrams/RenderConcept.tsx new file mode 100644 index 000000000..ffd476456 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/RenderConcept.tsx @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'concept-render'; + +/** + * Homepage concept card: a JSON spec — abbreviated from the `@threadplane/render` + * intro's own example (`type: 'Text'`, `props: { … }`) — resolves through + * `defineAngularRegistry()` into your own Angular components. + */ +export function RenderConcept() { + return ( + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/StreamConcept.tsx b/apps/website/src/components/docs/diagrams/StreamConcept.tsx new file mode 100644 index 000000000..f076f5bf3 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/StreamConcept.tsx @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'concept-stream'; + +/** Homepage concept card: tokens arrive as signals; the UI updates itself. */ +export function StreamConcept() { + return ( + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/concepts.spec.tsx b/apps/website/src/components/docs/diagrams/concepts.spec.tsx new file mode 100644 index 000000000..33327430b --- /dev/null +++ b/apps/website/src/components/docs/diagrams/concepts.spec.tsx @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { StreamConcept } from './StreamConcept'; +import { RenderConcept } from './RenderConcept'; + +/** Compact homepage concept cards: each must mount labeled, at compact scale, + * and carry its load-bearing API/package names. */ +describe('StreamConcept', () => { + it('mounts compact with the signals claim', () => { + const { container } = render(); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('injectAgent()'); + }); +}); + +describe('RenderConcept', () => { + it('mounts compact and accents the your-components claim', () => { + const { container } = render(); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); + expect(container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]').length).toBeGreaterThan(0); + }); +}); diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index a17101f51..251f6b13c 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -10,3 +10,5 @@ export { RenderHowItFits } from './RenderHowItFits'; export { RenderVsA2ui } from './RenderVsA2ui'; export { MiddlewareHowItFits } from './MiddlewareHowItFits'; export { TelemetryHowItFits } from './TelemetryHowItFits'; +export { StreamConcept } from './StreamConcept'; +export { RenderConcept } from './RenderConcept'; diff --git a/apps/website/src/components/landing/DiagramSection.spec.tsx b/apps/website/src/components/landing/DiagramSection.spec.tsx index e94adccdb..1d6fc27ac 100644 --- a/apps/website/src/components/landing/DiagramSection.spec.tsx +++ b/apps/website/src/components/landing/DiagramSection.spec.tsx @@ -15,5 +15,7 @@ describe('DiagramSection', () => { expect(getByText('The headline').tagName).toBe('H2'); expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('j-heading'); expect(container.querySelector('figure.tp-diagram-figure')).not.toBeNull(); + expect(getByText('The body.').className).toContain('stack-diagram-body'); + expect(getByText('Journey')).toBeTruthy(); }); }); diff --git a/apps/website/src/components/landing/DiagramSection.tsx b/apps/website/src/components/landing/DiagramSection.tsx index eacc0ecc8..ac9084d0b 100644 --- a/apps/website/src/components/landing/DiagramSection.tsx +++ b/apps/website/src/components/landing/DiagramSection.tsx @@ -15,6 +15,9 @@ interface DiagramSectionProps { /** * A landing section framing any kit diagram with a centered header + body. * The `.stack-diagram-*` classes predate the generalization and are shared. + * Always rendered on a tinted surface — landing.css pins the diagram + * scroll-shadow cover to `--color-surface-tinted`, so surface is deliberately + * not parameterized here. */ export function DiagramSection({ id, eyebrow, headline, body, children }: DiagramSectionProps) { return ( diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index 40cd5d6d1..5a25a5eea 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1488,7 +1488,7 @@ background: var(--color-border); } -/* StackDiagramSection — components/landing/StackDiagramSection.tsx */ +/* DiagramSection — components/landing/DiagramSection.tsx (wrapped by StackDiagramSection) */ .stack-diagram-section { display: flex; flex-direction: column; From 1137d10e163a01fda97c76c3ccdc577a2e7d8d25 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 22:49:58 -0700 Subject: [PATCH 07/19] =?UTF-8?q?fix(website):=20RenderConcept=20bottom=20?= =?UTF-8?q?margin=20=E2=80=94=20viewHeight=20240?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Row 3 content bottom is 224 (y180+h44); with the kit's 16px bottom margin viewHeight must be 240, matching the 16px top margin. Was 232, leaving only an 8px bottom margin. Co-Authored-By: Claude Fable 5 --- apps/website/src/components/docs/diagrams/RenderConcept.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/src/components/docs/diagrams/RenderConcept.tsx b/apps/website/src/components/docs/diagrams/RenderConcept.tsx index ffd476456..eb150ff38 100644 --- a/apps/website/src/components/docs/diagrams/RenderConcept.tsx +++ b/apps/website/src/components/docs/diagrams/RenderConcept.tsx @@ -15,7 +15,7 @@ export function RenderConcept() { From 1b5d1ba36d32b70d27a6708d3da5ef37718dc864 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 23:07:07 -0700 Subject: [PATCH 08/19] =?UTF-8?q?fix(website):=20concept=20cards=20?= =?UTF-8?q?=E2=80=94=20payoff=20accent,=20uniform=20240=20height,=20honest?= =?UTF-8?q?=20slack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../src/components/docs/diagrams/DiagramFrame.tsx | 4 ++++ .../src/components/docs/diagrams/RenderConcept.tsx | 6 +++--- .../src/components/docs/diagrams/StreamConcept.tsx | 14 +++++++------- .../src/components/docs/diagrams/concepts.spec.tsx | 11 +++++++++-- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx index 300d02b4d..4b43ecfe6 100644 --- a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx @@ -18,6 +18,10 @@ interface DiagramFrameProps { * ramp. Compact compositions author at a ~320 viewBox with the compact type ramp (eyebrow 10 / * mono title 13.5 / sans title 12 / meta 11 / pill 10.5, viewBox units) — see DiagramNode's note * for the shared baseline offsets; rendered width is capped at 420px regardless of card size. + * Compact rhythm: 18px inter-node gaps, edges stop 4px short of the node they arrive at + * (arrowhead fills the rest), 16px outer margins, and a uniform viewHeight of 240 across the + * homepage concept-card set — so every card in that set is the same height regardless of + * viewWidth or node count. */ scale?: 'docs' | 'marketing' | 'compact'; children: ReactNode; diff --git a/apps/website/src/components/docs/diagrams/RenderConcept.tsx b/apps/website/src/components/docs/diagrams/RenderConcept.tsx index eb150ff38..810393155 100644 --- a/apps/website/src/components/docs/diagrams/RenderConcept.tsx +++ b/apps/website/src/components/docs/diagrams/RenderConcept.tsx @@ -6,7 +6,7 @@ import { DiagramEdge } from './DiagramEdge'; const SLUG = 'concept-render'; /** - * Homepage concept card: a JSON spec — abbreviated from the `@threadplane/render` + * Homepage concept card: a UI spec — abbreviated from the `@threadplane/render` * intro's own example (`type: 'Text'`, `props: { … }`) — resolves through * `defineAngularRegistry()` into your own Angular components. */ @@ -17,7 +17,7 @@ export function RenderConcept() { viewWidth={320} viewHeight={240} scale="compact" - label="Generative UI concept: a JSON spec resolves through your Angular registry into your own components." + label="Generative UI concept: a UI spec resolves through your Angular registry into your own components." > ); diff --git a/apps/website/src/components/docs/diagrams/StreamConcept.tsx b/apps/website/src/components/docs/diagrams/StreamConcept.tsx index f076f5bf3..0650abdf0 100644 --- a/apps/website/src/components/docs/diagrams/StreamConcept.tsx +++ b/apps/website/src/components/docs/diagrams/StreamConcept.tsx @@ -11,15 +11,15 @@ export function StreamConcept() { - - - - - + + + + + ); } diff --git a/apps/website/src/components/docs/diagrams/concepts.spec.tsx b/apps/website/src/components/docs/diagrams/concepts.spec.tsx index 33327430b..ae439ac45 100644 --- a/apps/website/src/components/docs/diagrams/concepts.spec.tsx +++ b/apps/website/src/components/docs/diagrams/concepts.spec.tsx @@ -11,16 +11,23 @@ import { RenderConcept } from './RenderConcept'; describe('StreamConcept', () => { it('mounts compact with the signals claim', () => { const { container } = render(); + const svg = container.querySelector('svg'); expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); + expect(svg?.getAttribute('aria-label')).toBeTruthy(); const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); expect(titles).toContain('injectAgent()'); }); }); describe('RenderConcept', () => { - it('mounts compact and accents the your-components claim', () => { + it('accents the your-components payoff node', () => { const { container } = render(); expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); - expect(container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]').length).toBeGreaterThan(0); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain("type: 'Text'"); + expect(titles).toContain('defineAngularRegistry()'); + const accentNodes = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); + expect(accentNodes.length).toBe(1); + expect(accentNodes[0].querySelector('.tp-diagram-title')?.textContent).toBe('Your own Angular components'); }); }); From 2abb6ef2b4c846365fe054d3bf51e765d5fd4778 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 23:14:15 -0700 Subject: [PATCH 09/19] feat(website): Approve + Ship compact concept diagrams Co-Authored-By: Claude Fable 5 --- .../docs/diagrams/ApproveConcept.tsx | 59 +++++++++++++++++++ .../components/docs/diagrams/ShipConcept.tsx | 49 +++++++++++++++ .../docs/diagrams/concepts.spec.tsx | 39 ++++++++++++ .../src/components/docs/diagrams/index.ts | 2 + 4 files changed, 149 insertions(+) create mode 100644 apps/website/src/components/docs/diagrams/ApproveConcept.tsx create mode 100644 apps/website/src/components/docs/diagrams/ShipConcept.tsx diff --git a/apps/website/src/components/docs/diagrams/ApproveConcept.tsx b/apps/website/src/components/docs/diagrams/ApproveConcept.tsx new file mode 100644 index 000000000..41ed0838e --- /dev/null +++ b/apps/website/src/components/docs/diagrams/ApproveConcept.tsx @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'concept-approve'; + +/** + * Homepage concept card: nothing irreversible without a human — the agent + * proposes an action, an `interrupt` pauses for a human decision, and + * `resume` continues exactly there (interrupts.mdx lifecycle: Agent Plans → + * Interrupt Fires → UI Shows Dialog → User Decides → Agent Resumes). + * + * Kept runtime-neutral per the design spec's CLAIMS table: both the LangGraph + * and AG-UI interrupts guides use the same "agent pauses, hands control to a + * human, resumes with the decision" language, so the card names neither + * runtime and never claims durability (that's LangGraph-checkpoint-specific; + * the AG-UI path differs). + * + * Geometry (viewHeight 240, centered): row 1 is a 52px-tall pair of nodes + * broken by the `interrupt` pill (Agent → interrupt → Human, human accented + * as the payoff — the buyer's control point). A loop drops from the human + * node, jogs to center, and breaks around the `resume` pill into a 44px + * outcome node. Content height 52 + 48 (12 segment + 24 pill + 8 segment + 4 + * arrow-fill) + 44 = 144; top/bottom margins split the remaining 96 evenly + * (48 each), so the stack sits dead-center in the 240 frame. + */ +export function ApproveConcept() { + return ( + + + + + + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/ShipConcept.tsx b/apps/website/src/components/docs/diagrams/ShipConcept.tsx new file mode 100644 index 000000000..227e69387 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/ShipConcept.tsx @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'concept-ship'; + +/** + * Homepage concept card: the thread survives everything between question and + * answer — phrased against the `@threadplane/langgraph` persistence guide + * ("keeps conversations alive across page refreshes, browser restarts, and + * server deployments" / "users resume exactly where they left off"), not a + * runtime-neutral claim (the design spec's CLAIMS table flags AG-UI history + * as out of scope, so this card stays scoped to the LangGraph contract via + * the reload/deploy pills rather than promising the same for every adapter). + * + * Geometry (viewHeight 240, centered): a single 52px-tall row — Thread + * "Starts" (dim) breaks around the `reload` and `deploy` pills (12px + * segments, 48px pills) into Thread "Resumes" (accent, the payoff). Content + * height is just the 52px row; the remaining 188 splits into 94px top/bottom + * margins. That is a wide empty band above and below a single row, but it is + * accepted for grid uniformity with the other three cards (the dot grid + * fills it) — a second row was considered (e.g. a "history intact" caption) + * but dropped: the persistence guide's own "Adapter-defined behavior" callout + * says thread-history restore is LangGraph-specific, and this card already + * carries a LangGraph-scoped claim via reload/deploy, so adding another + * unverifiable-across-runtimes line risked stacking the same overclaim twice + * rather than earning its space. + */ +export function ShipConcept() { + return ( + + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/concepts.spec.tsx b/apps/website/src/components/docs/diagrams/concepts.spec.tsx index ae439ac45..2b58780e9 100644 --- a/apps/website/src/components/docs/diagrams/concepts.spec.tsx +++ b/apps/website/src/components/docs/diagrams/concepts.spec.tsx @@ -5,6 +5,8 @@ import { describe, expect, it } from 'vitest'; import { render } from '@testing-library/react'; import { StreamConcept } from './StreamConcept'; import { RenderConcept } from './RenderConcept'; +import { ApproveConcept } from './ApproveConcept'; +import { ShipConcept } from './ShipConcept'; /** Compact homepage concept cards: each must mount labeled, at compact scale, * and carry its load-bearing API/package names. */ @@ -31,3 +33,40 @@ describe('RenderConcept', () => { expect(accentNodes[0].querySelector('.tp-diagram-title')?.textContent).toBe('Your own Angular components'); }); }); + +describe('ApproveConcept', () => { + it('mounts compact with the interrupt/resume loop', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); + expect(svg?.getAttribute('aria-label')).toBeTruthy(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('Agent'); + expect(titles).toContain('Human'); + expect(titles).toContain('Resumes exactly there'); + const pills = Array.from(container.querySelectorAll('.tp-diagram-pill text')).map((t) => t.textContent); + expect(pills).toContain('interrupt'); + expect(pills).toContain('resume'); + const accentNodes = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); + expect(accentNodes.length).toBe(1); + expect(accentNodes[0].querySelector('.tp-diagram-title')?.textContent).toBe('Human'); + }); +}); + +describe('ShipConcept', () => { + it('mounts compact with the thread crossing reload and deploy', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('compact'); + expect(svg?.getAttribute('aria-label')).toBeTruthy(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('Starts'); + expect(titles).toContain('Resumes'); + const pills = Array.from(container.querySelectorAll('.tp-diagram-pill text')).map((t) => t.textContent); + expect(pills).toContain('reload'); + expect(pills).toContain('deploy'); + const accentNodes = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); + expect(accentNodes.length).toBe(1); + expect(accentNodes[0].querySelector('.tp-diagram-title')?.textContent).toBe('Resumes'); + }); +}); diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index 251f6b13c..be4cb7930 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -12,3 +12,5 @@ export { MiddlewareHowItFits } from './MiddlewareHowItFits'; export { TelemetryHowItFits } from './TelemetryHowItFits'; export { StreamConcept } from './StreamConcept'; export { RenderConcept } from './RenderConcept'; +export { ApproveConcept } from './ApproveConcept'; +export { ShipConcept } from './ShipConcept'; From a7532c9466af71e9b1c6102f54a9af5525dfeddf Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 23:35:33 -0700 Subject: [PATCH 10/19] =?UTF-8?q?fix(website):=20concept-card=20family=20?= =?UTF-8?q?=E2=80=94=20payoff=20accents,=20neutral=20pills,=20loop=20routi?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes review findings on the four compact concept diagrams: the ApproveConcept resume loop butt-ended into the pill's border instead of dropping into its face, row 1 wasn't right-aligned with the outcome node, and "resumes exactly there" soft-claimed checkpoint durability the runtime-neutral card doesn't earn. ShipConcept's end nodes never named their subject (missing eyebrow). StreamConcept accented the signals source instead of the payoff. Pills are event labels, not payoffs, so DiagramPill gains a neutral tone (default accent unchanged) and every pill in the concept cards now renders neutral, leaving exactly one accented node per card. Co-Authored-By: Claude Fable 5 --- .../docs/diagrams/ApproveConcept.tsx | 28 +++++++++++-------- .../components/docs/diagrams/DiagramPill.tsx | 7 +++-- .../components/docs/diagrams/ShipConcept.tsx | 10 +++---- .../docs/diagrams/StreamConcept.tsx | 4 +-- .../docs/diagrams/concepts.spec.tsx | 19 ++++++++++++- .../docs/diagrams/primitives.spec.tsx | 16 +++++++++++ apps/website/src/styles/docs.css | 7 +++++ 7 files changed, 69 insertions(+), 22 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/ApproveConcept.tsx b/apps/website/src/components/docs/diagrams/ApproveConcept.tsx index 41ed0838e..4f4311180 100644 --- a/apps/website/src/components/docs/diagrams/ApproveConcept.tsx +++ b/apps/website/src/components/docs/diagrams/ApproveConcept.tsx @@ -9,8 +9,9 @@ const SLUG = 'concept-approve'; /** * Homepage concept card: nothing irreversible without a human — the agent * proposes an action, an `interrupt` pauses for a human decision, and - * `resume` continues exactly there (interrupts.mdx lifecycle: Agent Plans → - * Interrupt Fires → UI Shows Dialog → User Decides → Agent Resumes). + * `resume` sends the agent on with the decision (interrupts.mdx lifecycle: + * Agent Plans → Interrupt Fires → UI Shows Dialog → User Decides → Agent + * Resumes). * * Kept runtime-neutral per the design spec's CLAIMS table: both the LangGraph * and AG-UI interrupts guides use the same "agent pauses, hands control to a @@ -20,9 +21,12 @@ const SLUG = 'concept-approve'; * * Geometry (viewHeight 240, centered): row 1 is a 52px-tall pair of nodes * broken by the `interrupt` pill (Agent → interrupt → Human, human accented - * as the payoff — the buyer's control point). A loop drops from the human - * node, jogs to center, and breaks around the `resume` pill into a 44px - * outcome node. Content height 52 + 48 (12 segment + 24 pill + 8 segment + 4 + * as the payoff — the buyer's control point). Human's right edge (x=214, + * w=90 → 304) aligns with the outcome node's right edge (x=16, w=288 → 304). + * A loop drops from the human node's bottom-mid (x=259), jogs 6px above the + * `resume` pill's top face before dropping into it (avoids butting into the + * pill along its border), and breaks around the pill into a 44px outcome + * node. Content height 52 + 48 (12 segment + 24 pill + 8 segment + 4 * arrow-fill) + 44 = 144; top/bottom margins split the remaining 96 evenly * (48 each), so the stack sits dead-center in the 240 frame. */ @@ -33,16 +37,16 @@ export function ApproveConcept() { viewWidth={320} viewHeight={240} scale="compact" - label="Approval concept: the agent plans an action, an interrupt pauses for a human decision, and resume continues exactly there." + label="Approval concept: the agent plans an action, an interrupt pauses for a human decision, and the agent resumes with the decision." > - - - + + + - - + + diff --git a/apps/website/src/components/docs/diagrams/DiagramPill.tsx b/apps/website/src/components/docs/diagrams/DiagramPill.tsx index 2e9b7ae55..ffb724684 100644 --- a/apps/website/src/components/docs/diagrams/DiagramPill.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramPill.tsx @@ -5,13 +5,16 @@ interface DiagramPillProps { cy: number; w: number; label: string; + /** 'accent' (default) for a payoff-adjacent pill; 'neutral' for an event + * label that should not compete with the card's one accented node. */ + tone?: 'accent' | 'neutral'; } const PILL_H = 24; -export function DiagramPill({ cx, cy, w, label }: DiagramPillProps) { +export function DiagramPill({ cx, cy, w, label, tone = 'accent' }: DiagramPillProps) { return ( - + {label} diff --git a/apps/website/src/components/docs/diagrams/ShipConcept.tsx b/apps/website/src/components/docs/diagrams/ShipConcept.tsx index 227e69387..943564f2a 100644 --- a/apps/website/src/components/docs/diagrams/ShipConcept.tsx +++ b/apps/website/src/components/docs/diagrams/ShipConcept.tsx @@ -37,13 +37,13 @@ export function ShipConcept() { scale="compact" label="Durability concept: a thread starts, survives a page reload and a deploy, and resumes." > - + - + - - - + + + ); } diff --git a/apps/website/src/components/docs/diagrams/StreamConcept.tsx b/apps/website/src/components/docs/diagrams/StreamConcept.tsx index 0650abdf0..4e6aee304 100644 --- a/apps/website/src/components/docs/diagrams/StreamConcept.tsx +++ b/apps/website/src/components/docs/diagrams/StreamConcept.tsx @@ -17,9 +17,9 @@ export function StreamConcept() { > - + - + ); } diff --git a/apps/website/src/components/docs/diagrams/concepts.spec.tsx b/apps/website/src/components/docs/diagrams/concepts.spec.tsx index 2b58780e9..76ba7966e 100644 --- a/apps/website/src/components/docs/diagrams/concepts.spec.tsx +++ b/apps/website/src/components/docs/diagrams/concepts.spec.tsx @@ -19,6 +19,13 @@ describe('StreamConcept', () => { const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); expect(titles).toContain('injectAgent()'); }); + + it('accents the UI-updates-itself payoff node, not the signals source', () => { + const { container } = render(); + const accentNodes = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); + expect(accentNodes.length).toBe(1); + expect(accentNodes[0].querySelector('.tp-diagram-title')?.textContent).toBe('UI updates itself'); + }); }); describe('RenderConcept', () => { @@ -43,13 +50,18 @@ describe('ApproveConcept', () => { const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); expect(titles).toContain('Agent'); expect(titles).toContain('Human'); - expect(titles).toContain('Resumes exactly there'); + expect(titles).toContain('Resumes with the decision'); const pills = Array.from(container.querySelectorAll('.tp-diagram-pill text')).map((t) => t.textContent); expect(pills).toContain('interrupt'); expect(pills).toContain('resume'); const accentNodes = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); expect(accentNodes.length).toBe(1); expect(accentNodes[0].querySelector('.tp-diagram-title')?.textContent).toBe('Human'); + const pillGroups = container.querySelectorAll('.tp-diagram-pill'); + expect(pillGroups.length).toBeGreaterThan(0); + pillGroups.forEach((pill) => { + expect(pill.getAttribute('data-tone')).toBe('neutral'); + }); }); }); @@ -68,5 +80,10 @@ describe('ShipConcept', () => { const accentNodes = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); expect(accentNodes.length).toBe(1); expect(accentNodes[0].querySelector('.tp-diagram-title')?.textContent).toBe('Resumes'); + const pillGroups = container.querySelectorAll('.tp-diagram-pill'); + expect(pillGroups.length).toBeGreaterThan(0); + pillGroups.forEach((pill) => { + expect(pill.getAttribute('data-tone')).toBe('neutral'); + }); }); }); diff --git a/apps/website/src/components/docs/diagrams/primitives.spec.tsx b/apps/website/src/components/docs/diagrams/primitives.spec.tsx index 7a694cc3e..e2a097ce4 100644 --- a/apps/website/src/components/docs/diagrams/primitives.spec.tsx +++ b/apps/website/src/components/docs/diagrams/primitives.spec.tsx @@ -109,4 +109,20 @@ describe('diagram kit primitives', () => { expect(text?.textContent).toBe('SSE'); expect(text?.getAttribute('text-anchor')).toBe('middle'); }); + + it('DiagramPill defaults to accent tone and accepts neutral', () => { + const { container: defaultContainer } = render( + + + + ); + expect(defaultContainer.querySelector('.tp-diagram-pill')?.getAttribute('data-tone')).toBe('accent'); + + const { container: neutralContainer } = render( + + + + ); + expect(neutralContainer.querySelector('.tp-diagram-pill')?.getAttribute('data-tone')).toBe('neutral'); + }); }); diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index f553b9c9c..529a26583 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -773,6 +773,13 @@ body:has([data-website-workspace-host]) { font-size: 10px; fill: var(--color-accent); } +.tp-diagram-pill[data-tone="neutral"] rect { + fill: var(--color-surface); + stroke: var(--color-border-strong); +} +.tp-diagram-pill[data-tone="neutral"] text { + fill: var(--color-text-muted); +} /* * DocsSidebar From d1a05b2f438642980433409d85688732af1b386f Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 23:39:56 -0700 Subject: [PATCH 11/19] feat(website): homepage How-it-works concept grid Co-Authored-By: Claude Fable 5 --- apps/website/src/app/page.tsx | 3 + .../landing/HomeConceptGrid.spec.tsx | 21 ++++++ .../components/landing/HomeConceptGrid.tsx | 70 +++++++++++++++++++ apps/website/src/styles/landing.css | 57 +++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 apps/website/src/components/landing/HomeConceptGrid.spec.tsx create mode 100644 apps/website/src/components/landing/HomeConceptGrid.tsx diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index cdfdd8a26..4766d0f67 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -3,6 +3,7 @@ import { LogoRibbon } from '../components/landing/LogoRibbon'; import { YesWall } from '../components/landing/YesWall'; import { FeatureBlock } from '../components/landing/FeatureBlock'; import { StackDiagramSection } from '../components/landing/StackDiagramSection'; +import { HomeConceptGrid } from '../components/landing/HomeConceptGrid'; import { DemoShowcase } from '../components/landing/DemoShowcase'; import { MediumSwitcher } from '../components/landing/MediumSwitcher'; import { SECTION_MEDIA } from '../lib/section-media'; @@ -46,6 +47,8 @@ export default async function HomePage() { caption="The chat surface never imports a runtime SDK — only the contract." /> + + {/* Interactive demo showcase */}
diff --git a/apps/website/src/components/landing/HomeConceptGrid.spec.tsx b/apps/website/src/components/landing/HomeConceptGrid.spec.tsx new file mode 100644 index 000000000..ab4ce9739 --- /dev/null +++ b/apps/website/src/components/landing/HomeConceptGrid.spec.tsx @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { HomeConceptGrid } from './HomeConceptGrid'; + +describe('HomeConceptGrid', () => { + it('renders four compact concept cards with anchor links in page order', () => { + const { container } = render(); + expect(container.querySelectorAll('.home-concept-card')).toHaveLength(4); + expect(container.querySelectorAll('figure[data-scale="compact"]')).toHaveLength(4); + const hrefs = Array.from(container.querySelectorAll('a.home-concept-link')).map((a) => a.getAttribute('href')); + expect(hrefs).toEqual(['#stream', '#render', '#ship', '#approve']); + }); + + it('is a labeled section', () => { + const { container } = render(); + expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('how-it-works-heading'); + }); +}); diff --git a/apps/website/src/components/landing/HomeConceptGrid.tsx b/apps/website/src/components/landing/HomeConceptGrid.tsx new file mode 100644 index 000000000..bf304097a --- /dev/null +++ b/apps/website/src/components/landing/HomeConceptGrid.tsx @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +import type { ReactNode } from 'react'; +import { Section } from '../ui/Section'; +import { Container } from '../ui/Container'; +import { SectionHeader } from '../ui/SectionHeader'; +import { StreamConcept, RenderConcept, ApproveConcept, ShipConcept } from '../docs/diagrams'; + +interface ConceptCard { + anchor: string; + title: string; + sentence: string; + diagram: ReactNode; +} + +/** Card order mirrors the FeatureBlock order below (stream, render, ship, approve). */ +const CARDS: ConceptCard[] = [ + { + anchor: '#stream', + title: 'Stream', + sentence: 'Tokens arrive as signals — the UI updates itself, no subscription plumbing.', + diagram: , + }, + { + anchor: '#render', + title: 'Render', + sentence: 'Agent output arrives as a spec and renders as your own components.', + diagram: , + }, + { + anchor: '#ship', + title: 'Ship', + sentence: 'Threads live behind the Agent contract — they outlast reloads and deploys.', + diagram: , + }, + { + anchor: '#approve', + title: 'Approve', + sentence: 'Interrupts pause for a human decision, then the agent resumes with it.', + diagram: , + }, +]; + +export function HomeConceptGrid() { + return ( +
+ +
+ +
+ {CARDS.map((card) => ( +
+ {card.diagram} +

{card.title}

+

{card.sentence}

+ + See it live + +
+ ))} +
+
+
+
+ ); +} diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index 5a25a5eea..71698aa6c 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1513,3 +1513,60 @@ must match it or they read as white bars at the figure edges. */ --diagram-scroll-bg: var(--color-surface-tinted); } + +/* HomeConceptGrid — components/landing/HomeConceptGrid.tsx */ +.home-concept { + display: flex; + flex-direction: column; + gap: 28px; +} +.home-concept-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; +} +@media (max-width: 767px) { + .home-concept-grid { + grid-template-columns: 1fr; + } +} +.home-concept-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 20px 20px 18px; + display: flex; + flex-direction: column; + gap: 6px; +} +.home-concept-card .tp-diagram-figure { + margin: 0 auto 10px; + width: 100%; + max-width: 420px; +} +.home-concept-title { + font-family: var(--font-inter); + font-size: 1rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0; +} +.home-concept-sentence { + font-family: var(--font-inter); + font-size: 0.9rem; + line-height: 1.55; + color: var(--color-text-secondary); + margin: 0; +} +.home-concept-link { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 600; + color: var(--color-accent); + text-decoration: none; + margin-top: auto; + padding-top: 6px; +} +.home-concept-link:hover { + text-decoration: underline; +} From be4c8ff35132dee83aac7b3456ec54b838b57a65 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 23:43:31 -0700 Subject: [PATCH 12/19] =?UTF-8?q?fix(website):=20honest=20Ship=20card=20se?= =?UTF-8?q?ntence=20=E2=80=94=20durability=20is=20the=20backend's=20job?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- apps/website/src/components/landing/HomeConceptGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/src/components/landing/HomeConceptGrid.tsx b/apps/website/src/components/landing/HomeConceptGrid.tsx index bf304097a..59ec5f5ca 100644 --- a/apps/website/src/components/landing/HomeConceptGrid.tsx +++ b/apps/website/src/components/landing/HomeConceptGrid.tsx @@ -29,7 +29,7 @@ const CARDS: ConceptCard[] = [ { anchor: '#ship', title: 'Ship', - sentence: 'Threads live behind the Agent contract — they outlast reloads and deploys.', + sentence: 'Threads live behind the contract, not in components — a persistent backend carries them across reloads and deploys.', diagram: , }, { From 6febe1f8744d9fcf927dbd0c7fd5bd20ae9cc1b8 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 23:51:27 -0700 Subject: [PATCH 13/19] =?UTF-8?q?fix(website):=20concept-grid=20copy=20?= =?UTF-8?q?=E2=80=94=20distinct=20Render=20sentence,=20tighter=20Ship=20cl?= =?UTF-8?q?aim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../src/components/landing/HomeConceptGrid.spec.tsx | 8 ++++++++ apps/website/src/components/landing/HomeConceptGrid.tsx | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/website/src/components/landing/HomeConceptGrid.spec.tsx b/apps/website/src/components/landing/HomeConceptGrid.spec.tsx index ab4ce9739..9763be9ee 100644 --- a/apps/website/src/components/landing/HomeConceptGrid.spec.tsx +++ b/apps/website/src/components/landing/HomeConceptGrid.spec.tsx @@ -18,4 +18,12 @@ describe('HomeConceptGrid', () => { const { container } = render(); expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('how-it-works-heading'); }); + + it("phrases thread durability as the backend's job (spec §5 gate)", () => { + const { container } = render(); + const sentences = Array.from(container.querySelectorAll('.home-concept-sentence')).map((p) => p.textContent ?? ''); + const ship = sentences.find((s) => s.includes('Threads')); + expect(ship).toContain('persistent backend'); + expect(ship).not.toMatch(/they outlast/); + }); }); diff --git a/apps/website/src/components/landing/HomeConceptGrid.tsx b/apps/website/src/components/landing/HomeConceptGrid.tsx index 59ec5f5ca..bfd930e8e 100644 --- a/apps/website/src/components/landing/HomeConceptGrid.tsx +++ b/apps/website/src/components/landing/HomeConceptGrid.tsx @@ -23,13 +23,13 @@ const CARDS: ConceptCard[] = [ { anchor: '#render', title: 'Render', - sentence: 'Agent output arrives as a spec and renders as your own components.', + sentence: 'A JSON spec resolves through your registry into components you already own.', diagram: , }, { anchor: '#ship', title: 'Ship', - sentence: 'Threads live behind the contract, not in components — a persistent backend carries them across reloads and deploys.', + sentence: 'Threads live behind the contract — a persistent backend carries them across reloads and deploys.', diagram: , }, { From 3701b0120a1de036973fa257448f5d13a04071d1 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 23:59:41 -0700 Subject: [PATCH 14/19] feat(website): render transform graphic on /render Add RenderTransform, a horizontal spec-to-components diagram (UI spec -> @threadplane/render -> your components) at custom 760x96 geometry so both real edge-break pill labels fit at docs-scale text metrics. Wire it into a new DiagramSection on /render, distinct in copy from the existing #schemas FeatureBlock right below it. Co-Authored-By: Claude Fable 5 --- apps/website/src/app/render/page.tsx | 11 ++++ .../docs/diagrams/RenderTransform.tsx | 65 +++++++++++++++++++ .../docs/diagrams/compositions.spec.tsx | 26 ++++++++ .../src/components/docs/diagrams/index.ts | 1 + 4 files changed, 103 insertions(+) create mode 100644 apps/website/src/components/docs/diagrams/RenderTransform.tsx diff --git a/apps/website/src/app/render/page.tsx b/apps/website/src/app/render/page.tsx index 6b551b41f..a20a0b2de 100644 --- a/apps/website/src/app/render/page.tsx +++ b/apps/website/src/app/render/page.tsx @@ -7,6 +7,8 @@ import { FeatureBlock } from '../../components/landing/FeatureBlock'; import { WhitePaperBlock } from '../../components/landing/WhitePaperBlock'; import { FinalCTA } from '../../components/landing/FinalCTA'; import { MediumSwitcher } from '../../components/landing/MediumSwitcher'; +import { DiagramSection } from '../../components/landing/DiagramSection'; +import { RenderTransform } from '../../components/docs/diagrams'; import { RenderCodeShowcase } from '../../components/landing/render/RenderCodeShowcase'; import { createPageMetadata } from '../../lib/site-metadata'; import { SECTION_MEDIA } from '../../lib/section-media'; @@ -51,6 +53,15 @@ export default async function RenderPage() {
+ + + + + + + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/compositions.spec.tsx b/apps/website/src/components/docs/diagrams/compositions.spec.tsx index 259a37dff..f8433f9ca 100644 --- a/apps/website/src/components/docs/diagrams/compositions.spec.tsx +++ b/apps/website/src/components/docs/diagrams/compositions.spec.tsx @@ -7,6 +7,7 @@ import { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; import { A2uiMessageFlow } from './A2uiMessageFlow'; import { RenderHowItFits } from './RenderHowItFits'; import { RenderVsA2ui } from './RenderVsA2ui'; +import { RenderTransform } from './RenderTransform'; import { MiddlewareHowItFits } from './MiddlewareHowItFits'; import { TelemetryHowItFits } from './TelemetryHowItFits'; @@ -108,3 +109,28 @@ describe('TelemetryHowItFits', () => { expect(container.querySelectorAll('path.tp-diagram-edge')).toHaveLength(5); }); }); + +describe('RenderTransform', () => { + it('mounts at standard scale with spec, render, and result stages', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/render'); + expect(container.querySelectorAll('.tp-diagram-pill')).toHaveLength(2); + }); + + it('names the spec fragment, the transport pills, and the payoff node', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain("type: 'Text'"); + expect(titles).toContain('your components'); + const pills = Array.from(container.querySelectorAll('.tp-diagram-pill text')).map((t) => t.textContent); + expect(pills).toEqual(['UI spec', 'bindings + events']); + }); + + it('accents only the payoff node', () => { + const { container } = render(); + const accented = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); + expect(accented).toHaveLength(1); + expect(accented[0]?.querySelector('.tp-diagram-title')?.textContent).toBe('your components'); + }); +}); diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index be4cb7930..4cfcc8b7f 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -8,6 +8,7 @@ export { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; export { A2uiMessageFlow } from './A2uiMessageFlow'; export { RenderHowItFits } from './RenderHowItFits'; export { RenderVsA2ui } from './RenderVsA2ui'; +export { RenderTransform } from './RenderTransform'; export { MiddlewareHowItFits } from './MiddlewareHowItFits'; export { TelemetryHowItFits } from './TelemetryHowItFits'; export { StreamConcept } from './StreamConcept'; From d836f3cdbb6fbae39ff98ffbf61f0ba0c7c14dd0 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 2 Sep 2026 00:18:55 -0700 Subject: [PATCH 15/19] fix(website): RenderTransform at kit-standard 640; concrete headline The 760 canvas made this the smallest-typed diagram on the site and dropped phone rendering below the 600px legibility floor. Re-tiled at viewWidth 640 using real getBBox() glyph-width reads (not character estimates) so every node keeps genuine slack at this width. Also swaps the vague headline/body for concrete mechanism copy and gives the payoff node's meta a descriptive line instead of a negation. Co-Authored-By: Claude Fable 5 --- apps/website/src/app/render/page.tsx | 4 +- .../docs/diagrams/RenderTransform.tsx | 41 +++++++++++++------ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/website/src/app/render/page.tsx b/apps/website/src/app/render/page.tsx index a20a0b2de..a276ce52e 100644 --- a/apps/website/src/app/render/page.tsx +++ b/apps/website/src/app/render/page.tsx @@ -56,8 +56,8 @@ export default async function RenderPage() { diff --git a/apps/website/src/components/docs/diagrams/RenderTransform.tsx b/apps/website/src/components/docs/diagrams/RenderTransform.tsx index c3961e933..574f5ddb5 100644 --- a/apps/website/src/components/docs/diagrams/RenderTransform.tsx +++ b/apps/website/src/components/docs/diagrams/RenderTransform.tsx @@ -13,12 +13,27 @@ const SLUG = 'render-transform'; * through `@threadplane/render`'s registry/state/handlers into components * you already own. The right-hand node carries the payoff accent; the * renderer and both pills stay neutral so the accent reads as a single beat. + * + * Tiled at the kit-standard 640 viewWidth (16px outer margins) against + * measured glyph widths (JetBrains Mono / Inter, actual `getBBox()` reads — + * not a character-count estimate) so every node keeps real slack between its + * widest line and its right edge at this width, not just at the larger + * marketing scale that stretches the SVG in CSS: + * node "type: 'Text'" text 90.0 → w=113 (slack 7.0) + * node "@threadplane/render" text 142.5 → w=166 (slack 7.5) + * node "your components" / + * "your styles · your rules" text 112.8 → w=136 (slack 7.2) + * pill "UI spec" text 42.0 → w=52 (pad 5.0/side) + * pill "bindings + events" text 102.0 → w=112 (pad 5.0/side) + * Inter-node gaps are a plain 5px segment, the pill, a 7px arrow-bearing + * segment, and a 2px arrowhead stop-short — tight but exact, verified + * against the live render rather than assumed. */ export function RenderTransform() { return ( - - - + + + - - - + + + From 7aa84f97dbb0472a4cdede3a4c1458656ec7cf77 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 2 Sep 2026 00:26:55 -0700 Subject: [PATCH 16/19] feat(website): pilot journey graphic on /pilot-to-prod Co-Authored-By: Claude Fable 5 --- apps/website/src/app/pilot-to-prod/page.tsx | 11 +++ .../components/docs/diagrams/PilotJourney.tsx | 92 +++++++++++++++++++ .../docs/diagrams/compositions.spec.tsx | 26 ++++++ .../src/components/docs/diagrams/index.ts | 1 + 4 files changed, 130 insertions(+) create mode 100644 apps/website/src/components/docs/diagrams/PilotJourney.tsx diff --git a/apps/website/src/app/pilot-to-prod/page.tsx b/apps/website/src/app/pilot-to-prod/page.tsx index 3972089ab..ef95c0fa6 100644 --- a/apps/website/src/app/pilot-to-prod/page.tsx +++ b/apps/website/src/app/pilot-to-prod/page.tsx @@ -8,6 +8,8 @@ import { BrowserFrame } from '../../components/ui/BrowserFrame'; import { WhitePaperBlock } from '../../components/landing/WhitePaperBlock'; import { Promises } from '../../components/landing/Promises'; import { FinalCTA } from '../../components/landing/FinalCTA'; +import { DiagramSection } from '../../components/landing/DiagramSection'; +import { PilotJourney } from '../../components/docs/diagrams'; import { createPageMetadata } from '../../lib/site-metadata'; export const metadata = createPageMetadata({ @@ -45,6 +47,15 @@ export default function PilotToProdPage() { + + + + {/* Discover */} + + + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/compositions.spec.tsx b/apps/website/src/components/docs/diagrams/compositions.spec.tsx index f8433f9ca..32ca5097a 100644 --- a/apps/website/src/components/docs/diagrams/compositions.spec.tsx +++ b/apps/website/src/components/docs/diagrams/compositions.spec.tsx @@ -10,6 +10,7 @@ import { RenderVsA2ui } from './RenderVsA2ui'; import { RenderTransform } from './RenderTransform'; import { MiddlewareHowItFits } from './MiddlewareHowItFits'; import { TelemetryHowItFits } from './TelemetryHowItFits'; +import { PilotJourney } from './PilotJourney'; /** * Compositions are hand-placed layouts; the spec guards that each mounts, @@ -134,3 +135,28 @@ describe('RenderTransform', () => { expect(accented[0]?.querySelector('.tp-diagram-title')?.textContent).toBe('your components'); }); }); + +describe('PilotJourney', () => { + it('mounts with three phase nodes on the journey line', () => { + const { container } = render(); + expect(container.querySelectorAll('g.tp-diagram-node')).toHaveLength(3); + expect(container.querySelector('svg[role="img"]')?.getAttribute('aria-label')).toBeTruthy(); + }); + + it('names the three phases and their gate pills', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toEqual(['Discover', 'Build', 'Harden']); + const pills = Array.from(container.querySelectorAll('.tp-diagram-pill text')).map((t) => t.textContent); + expect(pills).toEqual(['roadmap', 'working agent']); + const neutralPills = container.querySelectorAll('.tp-diagram-pill[data-tone="neutral"]'); + expect(neutralPills).toHaveLength(2); + }); + + it('accents only the Harden node — the production-ready system the customer keeps', () => { + const { container } = render(); + const accented = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); + expect(accented).toHaveLength(1); + expect(accented[0]?.querySelector('.tp-diagram-title')?.textContent).toBe('Harden'); + }); +}); diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index 4cfcc8b7f..6a8888303 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -15,3 +15,4 @@ export { StreamConcept } from './StreamConcept'; export { RenderConcept } from './RenderConcept'; export { ApproveConcept } from './ApproveConcept'; export { ShipConcept } from './ShipConcept'; +export { PilotJourney } from './PilotJourney'; From 40309329b1de528f6772a539b0be80b855b0fb62 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 2 Sep 2026 00:37:06 -0700 Subject: [PATCH 17/19] =?UTF-8?q?fix(website):=20PilotJourney=20=E2=80=94?= =?UTF-8?q?=20symmetric=20margins,=20handoff=20gate,=20page-faithful=20cop?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebalance margins from 16/36 to 16/14 by shortening Harden's meta to the page's own "on-call runbook" phrase, freeing enough room for a terminal "handoff" gate pill (grounded in the page's week-8 roadmap item, "Train your team · handoff") so all three phase transitions get a named gate, matching the section headline. Replace the synthesized "a production-ready system" in the section body with the page's own "an on-call runbook". Co-Authored-By: Claude Fable 5 --- apps/website/src/app/pilot-to-prod/page.tsx | 2 +- .../components/docs/diagrams/PilotJourney.tsx | 47 ++++++++++++------- .../docs/diagrams/compositions.spec.tsx | 4 +- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/apps/website/src/app/pilot-to-prod/page.tsx b/apps/website/src/app/pilot-to-prod/page.tsx index ef95c0fa6..0fa5d9537 100644 --- a/apps/website/src/app/pilot-to-prod/page.tsx +++ b/apps/website/src/app/pilot-to-prod/page.tsx @@ -51,7 +51,7 @@ export default function PilotToProdPage() { id="pilot-journey" eyebrow="The engagement" headline="Three phases, each with a gate you can point at" - body="No open-ended consulting arc: each phase ends with a concrete deliverable — a roadmap, a working agent, a production-ready system — not a status update." + body="No open-ended consulting arc: each phase ends with a concrete deliverable — a roadmap, a working agent, an on-call runbook — not a status update." > diff --git a/apps/website/src/components/docs/diagrams/PilotJourney.tsx b/apps/website/src/components/docs/diagrams/PilotJourney.tsx index 4a61fa5bc..77f3f7d92 100644 --- a/apps/website/src/components/docs/diagrams/PilotJourney.tsx +++ b/apps/website/src/components/docs/diagrams/PilotJourney.tsx @@ -12,33 +12,44 @@ const SLUG = 'pilot-journey'; * out as a horizontal journey. Eyebrows and titles are lifted verbatim from * the page's own phase framing ("Week 1–2 · Discover", "Week 3–5 · Build", * "Week 6–7 · Harden"); each node's meta abbreviates that phase's actual - * `rows` claims (stack audit + roadmap; real data + weekly demos; - * observability + runbook) rather than inventing new copy. + * `rows` claims (stack audit + roadmap; real data + weekly demos) or its + * own body copy (Harden's is literally "on-call runbook") rather than + * inventing new copy. * - * Gate pills mark ONLY the two transitions the page's copy actually names an + * Gate pills mark every transition the page's copy actually names an * artifact for: Discover's own row api is literally "roadmap" (carried into - * Build), and Build's headline is literally "Ship a working agent on your - * real data" (the thing Harden then hardens). Both pills stay neutral so - * they read as hand-offs, not payoffs. The Harden node carries the sole - * accent: it is what the customer is left holding at the end of the - * engagement (a production-ready, on-call-ready system), matching the - * outcomes section below it on the page ("A working agent. A trained team. - * A runbook."). + * Build), Build's headline is literally "Ship a working agent on your real + * data" (the thing Harden then hardens), and Harden's own roadmap row is + * literally "Train your team · handoff" (the page's own week-8 close, not a + * fourth phase — Harden is still the last FeatureBlock). All three pills + * stay neutral so they read as hand-offs, not payoffs; the terminal one + * after Harden has no outbound arrow since there is no fourth node. The + * Harden node carries the sole accent: it is what the customer is left + * holding at the end of the engagement (a working agent, a trained team, an + * on-call runbook), matching the outcomes section below it on the page ("A + * working agent. A trained team. A runbook."). * * Tiled at the kit-standard 640 viewWidth (16px outer margins) against * measured glyph widths (JetBrains Mono / Inter, actual `getBBox()` reads via * a live render of this exact composition — not a character-count estimate): * node "Discover" meta "stack audit · roadmap" text 107.1 → w=132 (slack 8.9) * node "Build" meta "real data · weekly demos" text 123.4 → w=146 (slack 6.6) - * node "Harden" meta "observability · runbook" text 113.9 → w=136 (slack 6.1) + * node "Harden" meta "on-call runbook" text 78.5 → w=101 (slack 6.5) * pill "roadmap" text 42.0 → w=52 (pad 5.0/side) * pill "working agent" text 78.0 → w=94 (pad 8.0/side) + * pill "handoff" text 42.0 → w=52 (pad 5.0/side) * (meta is the widest line in every node; eyebrow and title both measure - * narrower at 132/146/136.) + * narrower at 132/146/101.) * Inter-node rhythm mirrors RenderTransform: a 5px segment into each pill, a - * 7px arrow-bearing segment out, a 2px arrowhead stop-short; the Build→Harden + * 7px arrow-bearing segment out, a 2px arrowhead stop-short. The Build→Harden * gap (a real named artifact, "working agent") gets the same pill treatment - * rather than a bare arrow, since the page names it too. + * rather than a bare arrow, since the page names it too. The terminal + * Harden→handoff gap is a bare 5px segment into the pill with no exit + * segment or arrowhead — it is the end of the line, not a hand-off to a + * fourth node. Margins land at 16px left / 14px right (was 16/36 — the + * 20px right-margin surplus, plus shortening Harden's meta from + * "observability · runbook" to the page's own "on-call runbook", made room + * for the terminal pill without touching the earlier two nodes/pills). */ export function PilotJourney() { return ( @@ -47,7 +58,7 @@ export function PilotJourney() { viewWidth={640} viewHeight={96} scale="marketing" - label="The Pilot-to-Prod journey: Discover produces a roadmap, Build ships a working agent on your real data, and Harden turns it into the production-ready system you keep." + label="The Pilot-to-Prod journey: Discover produces a roadmap, Build ships a working agent on your real data, Harden delivers an on-call runbook, and the engagement ends in handoff." > + + ); } diff --git a/apps/website/src/components/docs/diagrams/compositions.spec.tsx b/apps/website/src/components/docs/diagrams/compositions.spec.tsx index 32ca5097a..09ea14845 100644 --- a/apps/website/src/components/docs/diagrams/compositions.spec.tsx +++ b/apps/website/src/components/docs/diagrams/compositions.spec.tsx @@ -148,9 +148,9 @@ describe('PilotJourney', () => { const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); expect(titles).toEqual(['Discover', 'Build', 'Harden']); const pills = Array.from(container.querySelectorAll('.tp-diagram-pill text')).map((t) => t.textContent); - expect(pills).toEqual(['roadmap', 'working agent']); + expect(pills).toEqual(['roadmap', 'working agent', 'handoff']); const neutralPills = container.querySelectorAll('.tp-diagram-pill[data-tone="neutral"]'); - expect(neutralPills).toHaveLength(2); + expect(neutralPills).toHaveLength(3); }); it('accents only the Harden node — the production-ready system the customer keeps', () => { From 062298a28478e9ac89508aace2b5822059a07906 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 2 Sep 2026 00:45:03 -0700 Subject: [PATCH 18/19] fix(website): communication-audit fixes for concept graphics Correct PilotJourney's traceability JSDoc: the "handoff" pill quotes the W8 row of the Discover block's "Roadmap draft" panel, not a Harden row. Co-Authored-By: Claude Fable 5 --- apps/website/src/components/docs/diagrams/PilotJourney.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/PilotJourney.tsx b/apps/website/src/components/docs/diagrams/PilotJourney.tsx index 77f3f7d92..3c78119a7 100644 --- a/apps/website/src/components/docs/diagrams/PilotJourney.tsx +++ b/apps/website/src/components/docs/diagrams/PilotJourney.tsx @@ -19,9 +19,10 @@ const SLUG = 'pilot-journey'; * Gate pills mark every transition the page's copy actually names an * artifact for: Discover's own row api is literally "roadmap" (carried into * Build), Build's headline is literally "Ship a working agent on your real - * data" (the thing Harden then hardens), and Harden's own roadmap row is - * literally "Train your team · handoff" (the page's own week-8 close, not a - * fourth phase — Harden is still the last FeatureBlock). All three pills + * data" (the thing Harden then hardens), and the "Roadmap draft" panel in + * the Discover block ends on a row that is literally "W8 · Train your team · + * handoff" (the page's own week-8 close, not a fourth phase — Harden is + * still the last FeatureBlock). All three pills * stay neutral so they read as hand-offs, not payoffs; the terminal one * after Harden has no outbound arrow since there is no fourth node. The * Harden node carries the sole accent: it is what the customer is left From 4e336618e845a6761587bebf094fa04e199da493 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 2 Sep 2026 00:57:48 -0700 Subject: [PATCH 19/19] docs(website): diagram-kit tone semantics + compact pill rhythm Co-Authored-By: Claude Fable 5 --- apps/website/src/components/docs/diagrams/DiagramFrame.tsx | 3 ++- apps/website/src/components/docs/diagrams/DiagramNode.tsx | 3 +++ apps/website/src/components/docs/diagrams/DiagramPill.tsx | 5 ++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx index 4b43ecfe6..2c03045f5 100644 --- a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx @@ -21,7 +21,8 @@ interface DiagramFrameProps { * Compact rhythm: 18px inter-node gaps, edges stop 4px short of the node they arrive at * (arrowhead fills the rest), 16px outer margins, and a uniform viewHeight of 240 across the * homepage concept-card set — so every card in that set is the same height regardless of - * viewWidth or node count. + * viewWidth or node count. Second rhythm, for node↔pill gaps: 12px (segment lengths flex to + * fit; pills abut their segments). */ scale?: 'docs' | 'marketing' | 'compact'; children: ReactNode; diff --git a/apps/website/src/components/docs/diagrams/DiagramNode.tsx b/apps/website/src/components/docs/diagrams/DiagramNode.tsx index ad0f5bde5..a6f81404a 100644 --- a/apps/website/src/components/docs/diagrams/DiagramNode.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramNode.tsx @@ -7,6 +7,9 @@ interface DiagramNodeProps { title: string; eyebrow?: string; meta?: string; + /** 'accent' has two conventions: concept/marketing compositions accent the single + * PAYOFF node (what the customer gets); StackDiagram uses it as a subject + * highlight ("the node this page is about"). 'dim' marks inputs/externals. */ tone?: 'neutral' | 'accent' | 'dim'; /** 'middle' centers text horizontally (title-only summary nodes). */ align?: 'start' | 'middle'; diff --git a/apps/website/src/components/docs/diagrams/DiagramPill.tsx b/apps/website/src/components/docs/diagrams/DiagramPill.tsx index ffb724684..2eef73eb5 100644 --- a/apps/website/src/components/docs/diagrams/DiagramPill.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramPill.tsx @@ -6,7 +6,10 @@ interface DiagramPillProps { w: number; label: string; /** 'accent' (default) for a payoff-adjacent pill; 'neutral' for an event - * label that should not compete with the card's one accented node. */ + * label that should not compete with the card's one accented node. The + * default preserves the older docs diagrams, but NEW compositions should + * pass 'neutral' — pills label events/gates, and an accent pill competes + * with the payoff node. */ tone?: 'accent' | 'neutral'; }