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/app/pilot-to-prod/page.tsx b/apps/website/src/app/pilot-to-prod/page.tsx index 3972089ab..0fa5d9537 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/DiagramFrame.tsx b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx index 72d42bb7b..2c03045f5 100644 --- a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx @@ -13,8 +13,18 @@ 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. 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. 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 da0a66d6c..a6f81404a 100644 --- a/apps/website/src/components/docs/diagrams/DiagramNode.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramNode.tsx @@ -7,11 +7,16 @@ 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'; /** '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 +26,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 +42,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 +51,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/DiagramPill.tsx b/apps/website/src/components/docs/diagrams/DiagramPill.tsx index 2e9b7ae55..2eef73eb5 100644 --- a/apps/website/src/components/docs/diagrams/DiagramPill.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramPill.tsx @@ -5,13 +5,19 @@ 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. 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'; } 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/PilotJourney.tsx b/apps/website/src/components/docs/diagrams/PilotJourney.tsx new file mode 100644 index 000000000..3c78119a7 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/PilotJourney.tsx @@ -0,0 +1,106 @@ +// 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 marketing graphic: the three FeatureBlock phases of the + * engagement (`id="discover"`, `id="build"`, `id="harden"` on the page) laid + * 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) or its + * own body copy (Harden's is literally "on-call runbook") rather than + * inventing new copy. + * + * 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 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 + * 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 "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/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 + * gap (a real named artifact, "working agent") gets the same pill treatment + * 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 ( + + + + + + + + + + + + + + ); +} 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..810393155 --- /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 UI 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/RenderTransform.tsx b/apps/website/src/components/docs/diagrams/RenderTransform.tsx new file mode 100644 index 000000000..574f5ddb5 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/RenderTransform.tsx @@ -0,0 +1,80 @@ +// 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: a horizontal transform story. A UI spec — + * abbreviated from the `@threadplane/render` intro's own example + * (`type: 'Text'`, `props: { label: { $state: '/message' } }`) — resolves + * 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 ( + + + + + + + + + + + + ); +} 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..943564f2a --- /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/StreamConcept.tsx b/apps/website/src/components/docs/diagrams/StreamConcept.tsx new file mode 100644 index 000000000..4e6aee304 --- /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/compositions.spec.tsx b/apps/website/src/components/docs/diagrams/compositions.spec.tsx index 259a37dff..09ea14845 100644 --- a/apps/website/src/components/docs/diagrams/compositions.spec.tsx +++ b/apps/website/src/components/docs/diagrams/compositions.spec.tsx @@ -7,8 +7,10 @@ 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'; +import { PilotJourney } from './PilotJourney'; /** * Compositions are hand-placed layouts; the spec guards that each mounts, @@ -108,3 +110,53 @@ 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'); + }); +}); + +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', 'handoff']); + const neutralPills = container.querySelectorAll('.tp-diagram-pill[data-tone="neutral"]'); + expect(neutralPills).toHaveLength(3); + }); + + 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/concepts.spec.tsx b/apps/website/src/components/docs/diagrams/concepts.spec.tsx new file mode 100644 index 000000000..76ba7966e --- /dev/null +++ b/apps/website/src/components/docs/diagrams/concepts.spec.tsx @@ -0,0 +1,89 @@ +// 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'; +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. */ +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()'); + }); + + 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', () => { + it('accents the your-components payoff node', () => { + 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("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'); + }); +}); + +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 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'); + }); + }); +}); + +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'); + 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/index.ts b/apps/website/src/components/docs/diagrams/index.ts index a17101f51..6a8888303 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -8,5 +8,11 @@ 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'; +export { RenderConcept } from './RenderConcept'; +export { ApproveConcept } from './ApproveConcept'; +export { ShipConcept } from './ShipConcept'; +export { PilotJourney } from './PilotJourney'; diff --git a/apps/website/src/components/docs/diagrams/primitives.spec.tsx b/apps/website/src/components/docs/diagrams/primitives.spec.tsx index 64ec1e7b7..e2a097ce4 100644 --- a/apps/website/src/components/docs/diagrams/primitives.spec.tsx +++ b/apps/website/src/components/docs/diagrams/primitives.spec.tsx @@ -76,6 +76,29 @@ 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( + + + + ); + 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( @@ -86,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/components/landing/DiagramSection.spec.tsx b/apps/website/src/components/landing/DiagramSection.spec.tsx new file mode 100644 index 000000000..1d6fc27ac --- /dev/null +++ b/apps/website/src/components/landing/DiagramSection.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 { 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(); + 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 new file mode 100644 index 000000000..ac9084d0b --- /dev/null +++ b/apps/website/src/components/landing/DiagramSection.tsx @@ -0,0 +1,39 @@ +// 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. + * 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 ( +
+ +
+ +

{body}

+ {children} +
+ + + ); +} 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..9763be9ee --- /dev/null +++ b/apps/website/src/components/landing/HomeConceptGrid.spec.tsx @@ -0,0 +1,29 @@ +// 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'); + }); + + 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 new file mode 100644 index 000000000..bfd930e8e --- /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: 'A JSON spec resolves through your registry into components you already own.', + diagram: , + }, + { + anchor: '#ship', + title: 'Ship', + sentence: 'Threads live behind the contract — a persistent backend carries them across 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/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}

- -
-
-
+ + + ); } diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index 697fc9eac..529a26583 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -686,6 +686,30 @@ 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"] { + /* 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: 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; } +.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); @@ -749,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 diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index 40cd5d6d1..71698aa6c 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; @@ -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; +} diff --git a/apps/website/src/styles/style-contracts.spec.ts b/apps/website/src/styles/style-contracts.spec.ts index fe111f669..52b4e3875 100644 --- a/apps/website/src/styles/style-contracts.spec.ts +++ b/apps/website/src/styles/style-contracts.spec.ts @@ -137,6 +137,15 @@ 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 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/, + }, + }, ]; describe('style contracts', () => { 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" +``` 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.