From 4cd7fb7c54be4d3aa2d6a9ae893cca86106afe14 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 15:29:28 -0700 Subject: [PATCH 01/26] =?UTF-8?q?docs:=20spec=20for=20docs=20visual=20desi?= =?UTF-8?q?gn=20=E2=80=94=20callouts,=20diagram=20kit,=20audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../2026-09-01-docs-visual-design-design.md | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-01-docs-visual-design-design.md diff --git a/docs/superpowers/specs/2026-09-01-docs-visual-design-design.md b/docs/superpowers/specs/2026-09-01-docs-visual-design-design.md new file mode 100644 index 000000000..5f32dd7be --- /dev/null +++ b/docs/superpowers/specs/2026-09-01-docs-visual-design-design.md @@ -0,0 +1,155 @@ +# Docs visual design: callouts, diagram kit, and the diagram audit + +**Date:** 2026-09-01 +**Status:** Approved for planning +**Scope:** apps/website — docs MDX components, docs content, and a marketing follow-up + +## Motivation + +Two visual problems on the docs side, one gap on the marketing side: + +1. The MDX callout is a left-border box with a solid mono-letter icon circle — it + does not speak the visual language the docs sidebar landed on (panels, + tinted chips, mono uppercase labels, hairline borders). +2. Architecture explanations ("How it fits") are ascii diagrams inside + ```` ```text ```` fences. They are load-bearing content rendered in the + least polished form on the page. +3. No marketing page uses a single architecture graphic; the two existing + diagram components are docs-only. + +Decisions below were made interactively (visual companion session, +`.superpowers/brainstorm/76640-1788300865`): callout direction **B — header +band**; diagram style **B — SVG schematic on dot grid**; production approach +**A — diagram kit**; docs scope **B — replace ascii + every intro gets a +diagram**; marketing scope **1+2 — homepage master diagram + adapter pages**. + +## 1. Callout redesign (header band) + +`components/docs/mdx/Callout.tsx` keeps its public API unchanged: +`type?: 'tip' | 'warning' | 'info' | 'danger'`, `title?: string`, `children`. + +Visual spec (all presentation in `src/styles/docs.css`): + +- **Card:** 1px solid `--color-border`, `--radius-md`, `--color-surface` + background, `overflow: hidden`. No left accent border. +- **Header band:** a full-width strip, tone-tinted background at ~6% opacity, + hairline bottom border, containing an outline SVG icon (stroke style, ~14px) + and the title, both in the tone's text color. Padding ~8px 14px. +- **Body:** plain surface below the band, Inter 15px / 1.6, + `--color-text-secondary`, padding ~12px 14px. +- **Title fallback:** when `title` is omitted, the band renders the kind name + ("Note", "Tip", "Warning", "Danger") so it is never empty. `info` renders as + "Note". +- **Icons:** outline SVGs replacing the solid letter circles — info: circled i, + tip: check-circle, warning: triangle-alert, danger: octagon/circle-x. Inline + `` in the component (stroke `currentColor`), sized by CSS. +- **Tones:** one CSS custom property pair per tone on the callout root + (`--callout-tone`, `--callout-tone-surface`), set by `[data-tone]` rules, + instead of today's per-tone hex repeated across icon/border rules. Values: + info `--color-accent`; tip `#1a7a40`; warning `#D4850F` with band text + darkened to `#B26D06` for contrast on the tint; danger `--color-angular-red`. + +The existing `data-mdx="callout"` / `data-tone` attribute contract stays, so +content and tests keep working. + +## 2. Diagram kit + +New directory `src/components/docs/diagrams/` containing SVG primitives and +per-diagram compositions. All styling via classes in `docs.css` — the +inline-style lint guard applies; geometry (x/y/w/h, path `d`) is SVG +attributes, which are fine. + +### Primitives + +- **`DiagramFrame`** — the outer `
` + responsive ``: + rounded dot-grid ground (`` of 1px `--color-border` dots), shared + arrowhead `` definition, `width: 100%` with `overflow-x: auto` on + the figure for narrow viewports, optional `caption` prop rendering a + `
`. Takes `viewBox` dims from props. +- **`DiagramNode`** — rounded rect (`rx` = radius token) with up to three text + lines: eyebrow (JetBrains Mono, 9px, uppercase, letter-spaced, + `--color-text-muted`), title (JetBrains Mono, ~13px, bold, + `--color-text-primary`), meta (Inter, ~10.5px, `--color-text-muted`). + Props: `x, y, w, h, eyebrow?, title, meta?, tone?: 'neutral' | 'accent' | + 'dim'`. Accent tone: `--color-accent-surface` fill, `--color-accent-border` + stroke, accent eyebrow. Dim tone: `--color-surface-dim` fill (backends). +- **`DiagramEdge`** — orthogonal `` (`d` supplied by the composition), + 1.2px `--color-text-muted` stroke, optional arrowhead via the shared marker. +- **`DiagramPill`** — rounded-full label chip placed on/along an edge: + JetBrains Mono ~10px, `--color-accent-surface` fill, `--color-accent-border` + stroke, `--color-accent` text. Used for contract/protocol labels + ("Agent contract · signals", "SSE"). + +Every fill/stroke/text color references `var(--color-*)` tokens so a future +dark theme needs no diagram changes. + +### Compositions + +One React component per diagram, hand-placed coordinates, registered in +`MdxRenderer.tsx`'s components map. Naming: `HowItFits` for intro +diagrams; concept diagrams named for their page. + +The existing HTML `AgUiArchDiagram` is replaced by its kit equivalent +(and removed) so the site has one schematic language. The animated +`ArchFlowDiagram` stays — it is a live event-flow demo, not a schematic. + +## 3. Docs audit + +### Replace (ascii diagram → kit diagram) + +| Page | Diagram | +| --- | --- | +| `ag-ui/getting-started/introduction.mdx` | chat → contract → two adapters → backends (the brainstorm mock) | +| `ag-ui/concepts/architecture.mdx` | vertical pipeline: component → contract → toAgent() → AbstractAgent → backend | +| `langgraph/concepts/agent-contract.mdx` | fan-in: LangGraph Platform / AG-UI backend / custom adapter → Agent → chat | +| `render/concepts/json-render-vs-a2ui.mdx` | three-package role split (render / a2ui / chat) | +| `chat/a2ui/overview.mdx` (first fence only) | A2UI content pipeline: classifier → parser → surface store → surface component | + +### Add ("How it fits" section + kit diagram to intros lacking one) + +`langgraph`, `chat`, `a2ui`, `render`, `middleware`, `telemetry`, `runtimes` +getting-started introductions. Each shows where that library sits relative to +the Agent contract and its neighbors, in the same schematic language. Section +heading: `## How it fits`, matching the ag-ui intro. + +### Keep as text fences (not diagrams) + +Log output, error output, JSONL payloads, bare URLs, and the A2UI message-type +list — everything else found in the fence audit stays untouched. + +## 4. Marketing follow-up (same arc, after docs land) + +- **Homepage:** a master stack diagram — Angular app → chat/render primitives + → Agent-contract seam → adapters → runtime fan-out (LangGraph Platform, + AG-UI runtimes) — as a new landing section component using the kit with a + marketing size variant (larger type/nodes via a `scale` or CSS variant + class, same primitives). +- **`/langgraph` and `/ag-ui`:** per-adapter schematic section, the + marketing-scaled sibling of that library's docs "How it fits" diagram. + +Placement within each page (which section slot, surrounding copy) is decided +at implementation time with the existing FeatureBlock rhythm; the spec +constraint is only: kit-rendered, token-styled, no new bespoke diagram +language. + +## 5. Testing + +- Callout spec: renders band title fallback per tone when `title` omitted; + `data-tone` maps to the right class hooks; body children render. +- Kit primitive specs: `DiagramFrame` renders caption and viewBox; + `DiagramNode` renders eyebrow/title/meta and tone attributes; + edge/pill render. +- Each registered MDX diagram component mounts without error (one smoke spec + iterating the registered diagram map). +- `nx test website`, `nx lint website` (inline-style guard), and a production + build stay green. + +## Out of scope + +- Concept-page diagrams beyond "How it fits" (event mapping, interrupt + lifecycle, checkpoint sync) — a later arc, same kit. +- `/pilot-to-prod` timeline, `/chat` anatomy, `/render` pipeline graphics — + new diagram species, later arcs. +- Any dark-mode work for the website docs (tokens are referenced so it is + free later). +- Auto-layout or spec-driven diagram generation. From e854b60279ec51d61e89cd09012fcd1871f56bc4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 15:36:42 -0700 Subject: [PATCH 02/26] docs: implementation plan for docs visual design Co-Authored-By: Claude Fable 5 --- .../plans/2026-09-01-docs-visual-design.md | 1557 +++++++++++++++++ 1 file changed, 1557 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-01-docs-visual-design.md diff --git a/docs/superpowers/plans/2026-09-01-docs-visual-design.md b/docs/superpowers/plans/2026-09-01-docs-visual-design.md new file mode 100644 index 000000000..7d91152a3 --- /dev/null +++ b/docs/superpowers/plans/2026-09-01-docs-visual-design.md @@ -0,0 +1,1557 @@ +# Docs Visual Design 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:** Replace the left-border docs callout with a header-band design, build an SVG diagram kit, replace all 5 ascii diagrams, add a "How it fits" diagram to every library intro, and add the master stack diagram to the homepage + adapter landing pages. + +**Architecture:** A four-primitive SVG kit (`DiagramFrame`/`DiagramNode`/`DiagramEdge`/`DiagramPill`) in `apps/website/src/components/docs/diagrams/`, styled entirely by classes in `docs.css` (token vars only — an ESLint guard bans new static inline `style` props). Hand-placed compositions per diagram; one parametrized `StackDiagram` covers the canonical chat→contract→adapters→backends picture with a `highlight` prop, reused by four docs pages, two blog posts, and three marketing pages. + +**Tech Stack:** Next.js (apps/website), MDX via next-mdx-remote (`MdxRenderer` components map), vitest + @testing-library/react, `npx nx test website` / `npx nx lint website`. + +**Spec:** `docs/superpowers/specs/2026-09-01-docs-visual-design-design.md` + +**Conventions for every task:** +- Working dir: repo root. Website content: `apps/website/content/docs/`, components: `apps/website/src/components/`, styles: `apps/website/src/styles/docs.css`. +- Specs are vitest + jsdom: start files with `// @vitest-environment jsdom` (see `apps/website/src/components/docs/LibraryMark.spec.tsx` for the idiom). +- Run tests with `npx nx test website` (full suite, fast, ~350 tests). Lint with `npx nx lint website`. +- NO inline `style` props anywhere (lint error). SVG geometry (x/y/width/d/viewBox) as attributes is fine; all colors/fonts go through CSS classes using `var(--color-*)` / `var(--font-*)`. + +--- + +### Task 1: Callout header-band redesign + +**Files:** +- Modify: `apps/website/src/components/docs/mdx/Callout.tsx` (full rewrite) +- Modify: `apps/website/src/styles/docs.css:372-439` (replace the whole `mdx — Callout` block) +- Create: `apps/website/src/components/docs/mdx/Callout.spec.tsx` + +- [ ] **Step 1: Write the 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 { Callout } from './Callout'; + +describe('Callout', () => { + it('renders a header band with the given title and tone', () => { + const { container } = render( + body text + ); + const root = container.querySelector('[data-mdx="callout"]'); + expect(root?.getAttribute('data-tone')).toBe('warning'); + const band = container.querySelector('.mdx-callout-band'); + expect(band?.textContent).toContain('Heads up'); + expect(band?.querySelector('svg')).not.toBeNull(); + }); + + it.each([ + ['info', 'Note'], + ['tip', 'Tip'], + ['warning', 'Warning'], + ['danger', 'Danger'], + ] as const)('falls back to the kind label for %s when title is omitted', (type, label) => { + const { container } = render(body); + expect(container.querySelector('.mdx-callout-title')?.textContent).toBe(label); + }); + + it('defaults to info and renders children in the body', () => { + const { container } = render(the body); + expect(container.querySelector('[data-mdx="callout"]')?.getAttribute('data-tone')).toBe('info'); + expect(container.querySelector('.mdx-callout-body')?.textContent).toBe('the body'); + }); +}); +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `npx nx test website` +Expected: the three new Callout tests FAIL (no `.mdx-callout-band` in the current markup); all pre-existing tests still pass. + +- [ ] **Step 3: Rewrite `Callout.tsx`** + +```tsx +import type { ReactNode } from 'react'; + +type CalloutType = 'tip' | 'warning' | 'info' | 'danger'; + +interface Props { + type?: CalloutType; + title?: string; + children: ReactNode; +} + +/** Band label when the author gives no title — the band never renders empty. */ +const KIND_LABEL: Record = { + tip: 'Tip', + warning: 'Warning', + info: 'Note', + danger: 'Danger', +}; + +const ICON_PATHS: Record = { + info: ( + <> + + + + ), + tip: ( + <> + + + + ), + warning: ( + <> + + + + ), + danger: ( + <> + + + + ), +}; + +export function Callout({ type = 'info', title, children }: Props) { + return ( +
+
+ + {title ?? KIND_LABEL[type]} +
+
{children}
+
+ ); +} +``` + +- [ ] **Step 4: Replace the callout CSS block** + +In `apps/website/src/styles/docs.css`, replace everything from `/* mdx — Callout */` (line 372) through the end of `.mdx-callout-body { ... }` (line 439) with: + +```css +/* mdx — Callout (header band; tone via custom properties) */ +[data-mdx="callout"] { + --callout-tone-text: var(--color-accent); + --callout-tone-surface: rgba(0, 64, 144, 0.06); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + overflow: hidden; + margin: 20px 0; +} +[data-mdx="callout"][data-tone="tip"] { + --callout-tone-text: #1a7a40; + --callout-tone-surface: rgba(26, 122, 64, 0.07); +} +[data-mdx="callout"][data-tone="warning"] { + /* Band text darkened from #D4850F for contrast on the tint. */ + --callout-tone-text: #b26d06; + --callout-tone-surface: rgba(212, 133, 15, 0.08); +} +[data-mdx="callout"][data-tone="danger"] { + --callout-tone-text: var(--color-angular-red); + --callout-tone-surface: rgba(221, 0, 49, 0.06); +} +.mdx-callout-band { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 14px; + background: var(--callout-tone-surface); + border-bottom: 1px solid var(--color-border); + color: var(--callout-tone-text); +} +.mdx-callout-icon { + width: 14px; + height: 14px; + flex-shrink: 0; +} +.mdx-callout-title { + font-family: Inter, system-ui, sans-serif; + font-size: 13px; + font-weight: 600; + color: var(--callout-tone-text); +} +.mdx-callout-body { + padding: 12px 14px; + font-family: var(--font-inter); + font-size: 15px; + line-height: 1.6; + color: var(--color-text-secondary); +} +.mdx-callout-body > :first-child { margin-top: 0; } +.mdx-callout-body > :last-child { margin-bottom: 0; } +``` + +Note: `.mdx-callout-header` / `.mdx-callout-header[data-has-title]` and the four `.mdx-callout-icon` background rules are gone. Grep for stale references: `grep -rn "mdx-callout-header" apps/website/src` must return nothing. + +- [ ] **Step 5: Run tests + lint** + +Run: `npx nx test website && npx nx lint website` +Expected: all tests PASS, lint clean. + +- [ ] **Step 6: Commit** + +```bash +git add apps/website/src/components/docs/mdx/Callout.tsx apps/website/src/components/docs/mdx/Callout.spec.tsx apps/website/src/styles/docs.css +git commit -m "feat(website): header-band docs callouts, retiring the left border" +``` + +--- + +### Task 2: Diagram kit primitives + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/DiagramFrame.tsx` +- Create: `apps/website/src/components/docs/diagrams/DiagramNode.tsx` +- Create: `apps/website/src/components/docs/diagrams/DiagramEdge.tsx` +- Create: `apps/website/src/components/docs/diagrams/DiagramPill.tsx` +- Create: `apps/website/src/components/docs/diagrams/primitives.spec.tsx` +- Modify: `apps/website/src/styles/docs.css` (append kit classes at end of the mdx components region, right before the `DocsSidebar` comment block at ~line 713) + +- [ ] **Step 1: Write the 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 { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +describe('diagram kit primitives', () => { + it('DiagramFrame renders a labeled svg with dot ground, arrow marker, and caption', () => { + const { container, getByText } = render( + + + + ); + const svg = container.querySelector('svg.tp-diagram-svg'); + expect(svg?.getAttribute('viewBox')).toBe('0 0 640 200'); + expect(svg?.getAttribute('aria-label')).toBe('test diagram'); + expect(container.querySelector('pattern#t-dots')).not.toBeNull(); + expect(container.querySelector('marker#t-arrow')).not.toBeNull(); + expect(container.querySelector('path.tp-diagram-edge')?.getAttribute('marker-end')).toBe('url(#t-arrow)'); + expect(getByText('a caption').tagName).toBe('FIGCAPTION'); + }); + + it('DiagramFrame passes the marketing scale through as a data attribute', () => { + const { container } = render( + + + + ); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('marketing'); + }); + + it('DiagramNode renders eyebrow, title, meta and tone', () => { + const { container } = render( + + + + ); + const g = container.querySelector('g.tp-diagram-node'); + expect(g?.getAttribute('data-tone')).toBe('accent'); + expect(g?.querySelector('.tp-diagram-eyebrow')?.textContent).toBe('ADAPTER'); + expect(g?.querySelector('.tp-diagram-title')?.textContent).toBe('@threadplane/ag-ui'); + expect(g?.querySelector('.tp-diagram-meta')?.textContent).toBe('toAgent()'); + }); + + it('DiagramNode centers a title-only node when align is middle', () => { + const { container } = render( + + + + ); + const title = container.querySelector('.tp-diagram-title'); + expect(title?.getAttribute('text-anchor')).toBe('middle'); + expect(container.querySelector('g.tp-diagram-node')?.getAttribute('data-title')).toBe('sans'); + }); + + it('DiagramPill renders a centered label', () => { + const { container } = render( + + + + ); + const text = container.querySelector('.tp-diagram-pill text'); + expect(text?.textContent).toBe('SSE'); + expect(text?.getAttribute('text-anchor')).toBe('middle'); + }); +}); +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `npx nx test website` +Expected: FAIL — modules not found. + +- [ ] **Step 3: Implement the four primitives** + +`DiagramFrame.tsx`: + +```tsx +import type { ReactNode } from 'react'; + +interface DiagramFrameProps { + /** Unique per rendered diagram; namespaces the SVG defs ids (`{slug}-dots`, `{slug}-arrow`). */ + slug: string; + viewWidth: number; + viewHeight: number; + /** Accessible one-sentence description of what the diagram shows. */ + label: string; + caption?: string; + /** Marketing pages render the same SVG larger. */ + scale?: 'docs' | 'marketing'; + children: ReactNode; +} + +export function DiagramFrame({ + slug, + viewWidth, + viewHeight, + label, + caption, + scale = 'docs', + children, +}: DiagramFrameProps) { + return ( +
+ + + + + + + + + + + {children} + + {caption ?
{caption}
: null} +
+ ); +} +``` + +`DiagramNode.tsx`: + +```tsx +interface DiagramNodeProps { + x: number; + y: number; + w: number; + h: number; + title: string; + eyebrow?: string; + meta?: string; + 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'; +} + +const PAD = 16; + +export function DiagramNode({ + x, + y, + w, + h, + title, + eyebrow, + meta, + tone = 'neutral', + align = 'start', + titleStyle = 'mono', +}: DiagramNodeProps) { + const tx = align === 'middle' ? x + w / 2 : x + PAD; + const anchor = align === 'middle' ? 'middle' : undefined; + // Baselines: with an eyebrow the stack is eyebrow/title/meta; without it the + // title floats up; a title-only node vertically centers. + const titleY = eyebrow ? y + 38 : meta ? y + 26 : y + h / 2 + 4; + const metaY = eyebrow ? y + 54 : y + 42; + return ( + + + {eyebrow ? ( + + {eyebrow.toUpperCase()} + + ) : null} + + {title} + + {meta ? ( + + {meta} + + ) : null} + + ); +} +``` + +`DiagramEdge.tsx`: + +```tsx +interface DiagramEdgeProps { + /** SVG path data; orthogonal segments (H/V) preferred. */ + d: string; + /** DiagramFrame slug — required when arrow is true, to reference `{slug}-arrow`. */ + slug?: string; + arrow?: boolean; +} + +export function DiagramEdge({ d, slug, arrow = false }: DiagramEdgeProps) { + return ( + + ); +} +``` + +`DiagramPill.tsx`: + +```tsx +interface DiagramPillProps { + /** Center of the pill. */ + cx: number; + cy: number; + w: number; + label: string; +} + +const PILL_H = 24; + +export function DiagramPill({ cx, cy, w, label }: DiagramPillProps) { + return ( + + + + {label} + + + ); +} +``` + +- [ ] **Step 4: Append kit CSS to `docs.css`** + +Insert immediately before the `/*\n * DocsSidebar` comment block: + +```css +/* mdx — diagram kit (components/docs/diagrams/*) */ +.tp-diagram-figure { + margin: 24px 0; + overflow-x: auto; +} +.tp-diagram-svg { + display: block; + width: 100%; + min-width: 480px; + max-width: 680px; + margin: 0 auto; +} +.tp-diagram-figure[data-scale="marketing"] .tp-diagram-svg { + max-width: 860px; +} +.tp-diagram-dot { fill: var(--color-border); } +.tp-diagram-caption { + font-family: var(--font-inter); + font-size: 13px; + color: var(--color-text-muted); + text-align: center; + margin-top: 10px; +} +.tp-diagram-node rect { + fill: var(--color-surface); + stroke: var(--color-border); +} +.tp-diagram-node[data-tone="accent"] rect { + fill: var(--color-accent-surface); + stroke: var(--color-accent-border); +} +.tp-diagram-node[data-tone="dim"] rect { + fill: var(--color-surface-dim); + stroke: var(--color-border); +} +.tp-diagram-eyebrow { + font-family: var(--font-mono); + font-size: 9px; + font-weight: 600; + letter-spacing: 0.1em; + fill: var(--color-text-muted); +} +.tp-diagram-node[data-tone="accent"] .tp-diagram-eyebrow { fill: var(--color-accent); } +.tp-diagram-title { + font-family: var(--font-mono); + font-size: 12.5px; + font-weight: 700; + fill: var(--color-text-primary); +} +.tp-diagram-node[data-title="sans"] .tp-diagram-title { + font-family: var(--font-inter); + font-size: 11px; + font-weight: 600; + fill: var(--color-text-secondary); +} +.tp-diagram-meta { + font-family: var(--font-inter); + font-size: 10.5px; + fill: var(--color-text-muted); +} +.tp-diagram-edge { + fill: none; + stroke: var(--color-text-muted); + stroke-width: 1.2; +} +.tp-diagram-arrowhead { + fill: none; + stroke: var(--color-text-muted); +} +.tp-diagram-pill rect { + fill: var(--color-accent-surface); + stroke: var(--color-accent-border); +} +.tp-diagram-pill text { + font-family: var(--font-mono); + font-size: 10px; + fill: var(--color-accent); + text-anchor: middle; +} +``` + +- [ ] **Step 5: Run tests + lint** + +Run: `npx nx test website && npx nx lint website` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add apps/website/src/components/docs/diagrams apps/website/src/styles/docs.css +git commit -m "feat(website): SVG diagram kit primitives on a dot grid" +``` + +--- + +### Task 3: StackDiagram composition + MDX registration + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/StackDiagram.tsx` +- Create: `apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx` +- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` + +- [ ] **Step 1: Write the 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 { StackDiagram } from './StackDiagram'; + +function toneOf(container: HTMLElement, title: string): string | null { + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')); + const t = titles.find((el) => el.textContent === title); + return t?.closest('g.tp-diagram-node')?.getAttribute('data-tone') ?? null; +} + +describe('StackDiagram', () => { + it('renders the canonical five-node stack', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/chat'); + expect(titles).toContain('@threadplane/langgraph'); + expect(titles).toContain('@threadplane/ag-ui'); + expect(container.querySelector('.tp-diagram-pill text')?.textContent).toBe('Agent contract · signals + events$'); + }); + + it.each([ + ['ag-ui', '@threadplane/ag-ui'], + ['langgraph', '@threadplane/langgraph'], + ['chat', '@threadplane/chat'], + ] as const)('highlight=%s accents that node', (highlight, title) => { + const { container } = render(); + expect(toneOf(container, title)).toBe('accent'); + }); + + it('highlight=runtimes accents the backend row', () => { + const { container } = render(); + expect(toneOf(container, 'LangGraph Platform')).toBe('accent'); + expect(toneOf(container, 'CrewAI · Mastra · MS Agent Fwk · Strands · …')).toBe('accent'); + }); + + it('renders a caption when given', () => { + const { getByText } = render(); + expect(getByText('the caption')).not.toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `npx nx test website` +Expected: FAIL — module not found. + +- [ ] **Step 3: Implement `StackDiagram.tsx`** + +```tsx +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +export type StackHighlight = 'none' | 'chat' | 'langgraph' | 'ag-ui' | 'runtimes' | 'contract'; + +interface StackDiagramProps { + highlight?: StackHighlight; + caption?: string; + scale?: 'docs' | 'marketing'; +} + +/** + * The canonical Threadplane stack: chat UI on top, the Agent contract as a + * labeled seam, the two runtime adapters, and their backends. `highlight` + * accents the node(s) a given page is about; `contract` accents both adapters. + */ +export function StackDiagram({ highlight = 'none', caption, scale = 'docs' }: StackDiagramProps) { + const slug = `stack-${highlight}`; + const adapters = highlight === 'contract'; + const backends = highlight === 'runtimes'; + return ( + + + + + + + + + + + + + + ); +} +``` + +- [ ] **Step 4: Register in `MdxRenderer.tsx`** + +Add import `import { StackDiagram } from './diagrams/StackDiagram';` and add `StackDiagram,` to the `mdxComponents` map (keep `AgUiArchDiagram` for now — it is removed in Task 4). + +- [ ] **Step 5: Run tests, then commit** + +Run: `npx nx test website && npx nx lint website` +Expected: PASS. + +```bash +git add apps/website/src/components/docs/diagrams/StackDiagram.tsx apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx apps/website/src/components/docs/MdxRenderer.tsx +git commit -m "feat(website): StackDiagram — the canonical stack schematic" +``` + +--- + +### Task 4: Replace ascii + retire AgUiArchDiagram + +**Files:** +- Modify: `apps/website/content/docs/ag-ui/getting-started/introduction.mdx:17-29` +- Modify: `apps/website/content/docs/langgraph/concepts/agent-contract.mdx` (the ```` ```text ```` fan-in fence) +- Modify: `apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx:62` +- Modify: `apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx:117` +- Delete: `apps/website/src/components/docs/AgUiArchDiagram.tsx` +- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (drop AgUiArchDiagram import + registration) +- Modify: `apps/website/src/styles/docs.css` (delete the `.ag-ui-arch-*` rule block, ~lines 195–260 — find with `grep -n "ag-ui-arch" apps/website/src/styles/docs.css`) + +- [ ] **Step 1: Replace the ag-ui intro ascii diagram** + +In `ag-ui/getting-started/introduction.mdx`, replace the whole ```` ```text ```` fence under `## How it fits` (the `@threadplane/chat … +--> @threadplane/ag-ui` tree) with: + +```mdx + +``` + +- [ ] **Step 2: Replace the agent-contract fan-in ascii** + +In `langgraph/concepts/agent-contract.mdx`, replace the ```` ```text ```` fence (the `LangGraph Platform -- @threadplane/langgraph --+ …` fan-in) with: + +```mdx + +``` + +- [ ] **Step 3: Swap the two blog usages** + +Replace `` with `` in both blog posts listed above. + +- [ ] **Step 4: Retire the old component** + +Delete `AgUiArchDiagram.tsx`; remove its import and map entry from `MdxRenderer.tsx`; delete the `.ag-ui-arch-*` CSS block (including its mobile `@media` override — grep to catch all of it). Verify: `grep -rn "AgUiArchDiagram\|ag-ui-arch" apps/website` returns nothing. + +- [ ] **Step 5: Run tests + lint, verify the pages render** + +Run: `npx nx test website && npx nx lint website` +Expected: PASS. +Then start the dev server and load `/docs/ag-ui/getting-started/introduction`, `/docs/langgraph/concepts/agent-contract`, and one of the two blog posts; the kit diagram must render with no console errors. + +- [ ] **Step 6: Commit** + +```bash +git add -A apps/website +git commit -m "refactor(website): replace ascii + HTML arch diagrams with StackDiagram" +``` + +--- + +### Task 5: "How it fits" for langgraph, chat, runtimes intros + +**Files:** +- Modify: `apps/website/content/docs/langgraph/getting-started/introduction.mdx` +- Modify: `apps/website/content/docs/chat/getting-started/introduction.mdx` +- Modify: `apps/website/content/docs/runtimes/getting-started/introduction.mdx` + +- [ ] **Step 1: Add the sections** + +In each file, insert a `## How it fits` section directly after the intro paragraph(s) and before the first existing `##` heading (langgraph: before `## What is \`injectAgent()\`?`; chat: before `## Two-Tier Architecture`; runtimes: before `## The runtimes`). Content per file: + +langgraph: + +```mdx +## How it fits + + +``` + +chat: + +```mdx +## How it fits + + +``` + +runtimes: + +```mdx +## How it fits + + +``` + +- [ ] **Step 2: Verify rendering, run tests, commit** + +Run: `npx nx test website` +Expected: PASS. Load each of the three intro pages on the dev server; diagrams render. + +```bash +git add apps/website/content/docs +git commit -m "docs(website): How-it-fits stack diagrams for langgraph, chat, runtimes intros" +``` + +--- + +### Task 6: AgUiArchitecturePipeline (ag-ui concepts/architecture) + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx` +- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (register) +- Modify: `apps/website/content/docs/ag-ui/concepts/architecture.mdx` (replace the ```` ```text ```` vertical pipeline fence) +- Modify: `apps/website/src/components/docs/diagrams/compositions.spec.tsx` (create; grows in later tasks) + +- [ ] **Step 1: Start the compositions spec (failing)** + +Create `compositions.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 { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; + +/** + * Compositions are hand-placed layouts; the spec guards that each mounts, + * is labeled for screen readers, and names its load-bearing packages. + * Later tasks append one describe block per composition. + */ +describe('AgUiArchitecturePipeline', () => { + it('mounts with an accessible label and the pipeline stages', () => { + const { container } = render(); + expect(container.querySelector('svg[role="img"]')?.getAttribute('aria-label')).toBeTruthy(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/ag-ui'); + expect(titles).toContain('AbstractAgent'); + }); +}); +``` + +Run: `npx nx test website` — expected FAIL (module not found). + +- [ ] **Step 2: Implement the composition** + +```tsx +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'agui-pipeline'; + +/** Vertical pipeline from an Angular component down to the AG-UI backend. */ +export function AgUiArchitecturePipeline() { + return ( + + + + + + + + + + + + ); +} +``` + +Register in `MdxRenderer.tsx` (import + map entry `AgUiArchitecturePipeline,`). + +- [ ] **Step 3: Replace the mdx fence** + +In `ag-ui/concepts/architecture.mdx`, replace the ```` ```text ```` fence (the `Angular component | v @threadplane/chat …` chain) with ``. + +- [ ] **Step 4: Test + verify + commit** + +Run: `npx nx test website && npx nx lint website` — PASS. Load `/docs/ag-ui/concepts/architecture` on the dev server. + +```bash +git add -A apps/website +git commit -m "docs(website): kit pipeline diagram for AG-UI architecture page" +``` + +--- + +### Task 7: A2uiMessageFlow (chat/a2ui overview + a2ui intro) + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx` +- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (register) +- Modify: `apps/website/content/docs/chat/a2ui/overview.mdx` (replace the FIRST ```` ```text ```` fence — the `assistant text starts with ---a2ui_JSON--- …` pipeline; the second fence, JSONL payload examples, stays) +- Modify: `apps/website/content/docs/a2ui/getting-started/introduction.mdx` (add `## How it fits` before `## What the package owns`, reusing the same diagram) +- Modify: `apps/website/src/components/docs/diagrams/compositions.spec.tsx` + +- [ ] **Step 1: Append the failing spec block** + +```tsx +import { A2uiMessageFlow } from './A2uiMessageFlow'; + +describe('A2uiMessageFlow', () => { + it('mounts and names the parser and surface store stages', () => { + const { container } = render(); + expect(container.querySelector('svg[role="img"]')).not.toBeNull(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('createA2uiMessageParser()'); + expect(titles).toContain('createA2uiSurfaceStore()'); + }); +}); +``` + +Run: `npx nx test website` — FAIL. + +- [ ] **Step 2: Implement** + +```tsx +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'a2ui-flow'; + +/** How an assistant message becomes a live A2UI surface. */ +export function A2uiMessageFlow() { + return ( + + + + + + + + + + + + ); +} +``` + +Register in `MdxRenderer.tsx`. + +- [ ] **Step 3: Wire into content** + +- `chat/a2ui/overview.mdx`: replace the first ```` ```text ```` fence with ``. +- `a2ui/getting-started/introduction.mdx`: insert before `## What the package owns`: + +```mdx +## How it fits + + +``` + +- [ ] **Step 4: Test + verify + commit** + +Run: `npx nx test website` — PASS. Load `/docs/chat/a2ui/overview` and `/docs/a2ui/getting-started/introduction`. + +```bash +git add -A apps/website +git commit -m "docs(website): A2UI message-flow diagram for overview and intro" +``` + +--- + +### Task 8: Render diagrams (intro + json-render-vs-a2ui) + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/RenderHowItFits.tsx` +- Create: `apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx` +- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (register both) +- Modify: `apps/website/content/docs/render/getting-started/introduction.mdx` (add `## How it fits` before `## Why @threadplane/render?`) +- Modify: `apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx` (replace the ```` ```text ```` three-package fence) +- Modify: `apps/website/src/components/docs/diagrams/compositions.spec.tsx` + +- [ ] **Step 1: Append failing spec blocks** + +```tsx +import { RenderHowItFits } from './RenderHowItFits'; +import { RenderVsA2ui } from './RenderVsA2ui'; + +describe('RenderHowItFits', () => { + it('mounts and shows the spec-to-components pipeline', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/render'); + }); +}); + +describe('RenderVsA2ui', () => { + it('mounts and shows both packages under chat', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/render'); + expect(titles).toContain('@threadplane/a2ui'); + expect(titles).toContain('@threadplane/chat'); + }); +}); +``` + +Run: `npx nx test website` — FAIL. + +- [ ] **Step 2: Implement `RenderHowItFits.tsx`** + +```tsx +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'render-fits'; + +/** JSON spec in, your Angular components out. */ +export function RenderHowItFits() { + return ( + + + + + + + + + + ); +} +``` + +Note the pill overlaps its edge midpoint deliberately — the pill rect masks the line beneath the label. + +- [ ] **Step 3: Implement `RenderVsA2ui.tsx`** + +```tsx +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'render-vs-a2ui'; + +/** The three-package split behind generative UI in chat. */ +export function RenderVsA2ui() { + return ( + + + + + + + + ); +} +``` + +Register both in `MdxRenderer.tsx`. + +- [ ] **Step 4: Wire into content** + +- `render/getting-started/introduction.mdx`: insert before `## Why @threadplane/render?`: + +```mdx +## How it fits + + +``` + +- `render/concepts/json-render-vs-a2ui.mdx`: replace the ```` ```text ```` fence with ``. + +- [ ] **Step 5: Test + verify + commit** + +Run: `npx nx test website` — PASS. Load both pages on the dev server. + +```bash +git add -A apps/website +git commit -m "docs(website): render pipeline + render-vs-a2ui kit diagrams" +``` + +--- + +### Task 9: MiddlewareHowItFits + TelemetryHowItFits + +**Files:** +- Create: `apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx` +- Create: `apps/website/src/components/docs/diagrams/TelemetryHowItFits.tsx` +- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (register both) +- Modify: `apps/website/content/docs/middleware/getting-started/introduction.mdx` (add `## How it fits` before `## What it does`) +- Modify: `apps/website/content/docs/telemetry/getting-started/introduction.mdx` (add `## How it fits` before `## Entry points`) +- Modify: `apps/website/src/components/docs/diagrams/compositions.spec.tsx` + +- [ ] **Step 1: Append failing spec blocks** + +```tsx +import { MiddlewareHowItFits } from './MiddlewareHowItFits'; +import { TelemetryHowItFits } from './TelemetryHowItFits'; + +describe('MiddlewareHowItFits', () => { + it('mounts and places the middleware between frontend and graph', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('threadplane-middleware'); + }); +}); + +describe('TelemetryHowItFits', () => { + it('mounts and shows both entry points feeding ingest', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/telemetry'); + }); +}); +``` + +Run: `npx nx test website` — FAIL. + +- [ ] **Step 2: Implement `MiddlewareHowItFits.tsx`** (horizontal: frontend → middleware → graph) + +```tsx +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'middleware-fits'; + +/** The Python middleware sits between the Angular frontend and your graph. */ +export function MiddlewareHowItFits() { + return ( + + + + + + + + + ); +} +``` + +- [ ] **Step 3: Implement `TelemetryHowItFits.tsx`** (fan-in: two entry points → package → ingest) + +```tsx +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'telemetry-fits'; + +/** Browser and Node entry points feed sampled events to the ingest endpoint. */ +export function TelemetryHowItFits() { + return ( + + + + + + + + + + + ); +} +``` + +Register both in `MdxRenderer.tsx`. + +Before wiring content, read both intros and adjust the eyebrow/meta strings to match what those pages actually claim (e.g. exact entry-point names in `telemetry/getting-started/introduction.mdx` under `## Entry points`) — the topology stays as coded. + +- [ ] **Step 4: Wire into content** + +Insert into each intro at the position listed in **Files**: + +```mdx +## How it fits + + +``` + +```mdx +## How it fits + + +``` + +- [ ] **Step 5: Test + verify + commit** + +Run: `npx nx test website && npx nx lint website` — PASS. Load both intro pages. + +```bash +git add -A apps/website +git commit -m "docs(website): middleware + telemetry How-it-fits diagrams" +``` + +--- + +### Task 10: Marketing — StackDiagramSection + homepage + +**Files:** +- Create: `apps/website/src/components/landing/StackDiagramSection.tsx` +- Create: `apps/website/src/components/landing/StackDiagramSection.spec.tsx` +- Modify: `apps/website/src/styles/landing.css` (append section classes) +- Modify: `apps/website/src/app/page.tsx` (insert after ``) + +- [ ] **Step 1: Write the 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 { StackDiagramSection } from './StackDiagramSection'; + +describe('StackDiagramSection', () => { + it('renders heading, body, and a marketing-scaled stack diagram', () => { + const { container, getByText } = render( + + ); + expect(getByText('One contract between your UI and any runtime').tagName).toBe('H2'); + expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('architecture-heading'); + expect(container.querySelector('figure.tp-diagram-figure')?.getAttribute('data-scale')).toBe('marketing'); + }); +}); +``` + +Run: `npx nx test website` — FAIL. + +- [ ] **Step 2: Implement the section component** + +```tsx +import type { ReactNode } from 'react'; +import { Section } from '../ui/Section'; +import { Container } from '../ui/Container'; +import { Eyebrow } from '../ui/Eyebrow'; +import { StackDiagram, type StackHighlight } from '../docs/diagrams/StackDiagram'; + +interface StackDiagramSectionProps { + id: string; + eyebrow: string; + headline: string; + body: ReactNode; + highlight?: StackHighlight; + caption?: string; +} + +export function StackDiagramSection({ + id, + eyebrow, + headline, + body, + highlight = 'none', + caption, +}: StackDiagramSectionProps) { + return ( +
+ +
+ {eyebrow} +

+ {headline} +

+

{body}

+ +
+
+
+ ); +} +``` + +Append to `landing.css` (match the file's existing class style — check its heading classes for the exact Garamond pattern before writing): + +```css +/* StackDiagramSection */ +.stack-diagram-section { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 12px; +} +.stack-diagram-headline { + font-family: var(--font-garamond); + font-size: clamp(1.75rem, 3.5vw, 2.5rem); + color: var(--color-text-primary); + margin: 0; +} +.stack-diagram-body { + font-family: var(--font-inter); + font-size: 1.05rem; + line-height: 1.6; + color: var(--color-text-secondary); + max-width: 620px; + margin: 0 0 12px; +} +``` + +- [ ] **Step 3: Insert on the homepage** + +In `apps/website/src/app/page.tsx`, import `StackDiagramSection` and insert between `` and the DemoShowcase `
`: + +```tsx + +``` + +- [ ] **Step 4: Test + verify + commit** + +Run: `npx nx test website && npx nx lint website` — PASS. Load `/` on the dev server; check the section renders between the Yes wall and the demo showcase, at both desktop and 375px widths (the figure scrolls horizontally, the page must not). + +```bash +git add -A apps/website +git commit -m "feat(website): homepage architecture section with the master stack diagram" +``` + +--- + +### Task 11: Marketing — adapter pages (/langgraph, /ag-ui) + +**Files:** +- Modify: `apps/website/src/app/langgraph/page.tsx` +- Modify: `apps/website/src/app/ag-ui/page.tsx` + +- [ ] **Step 1: Insert the sections** + +In each page, import `StackDiagramSection` from `../../components/landing/StackDiagramSection` and insert directly after the hero `
` (the first Section in the returned JSX): + +`/langgraph`: + +```tsx + +``` + +`/ag-ui`: + +```tsx + +``` + +- [ ] **Step 2: Test + verify + commit** + +Run: `npx nx test website` — PASS (both pages have `page.spec.tsx` files; if a spec asserts on section order/count, update it to include the new section). Load `/langgraph` and `/ag-ui` on the dev server. + +```bash +git add apps/website/src/app/langgraph/page.tsx apps/website/src/app/ag-ui/page.tsx +git commit -m "feat(website): stack diagram sections on the adapter landing pages" +``` + +--- + +### Task 12: Full verification pass + +- [ ] **Step 1: Suite + lint** + +Run: `npx nx test website && npx nx lint website` +Expected: PASS, no lint errors (warnings tolerated; errors not — pipe through `sed -e 's/\x1b\[[0-9;]*m//g'` before grepping if counting). + +- [ ] **Step 2: Production build** + +Run: `npx nx build website --configuration=production` +Expected: builds clean. + +- [ ] **Step 3: Visual sweep** + +On the dev server, load and eyeball every touched page: the 4 docs pages with replaced diagrams, the 7 intros, one blog post, `/`, `/langgraph`, `/ag-ui`. Check: no horizontal page scroll at 375px (diagrams scroll inside their figure), callouts render the band on pages using `` (e.g. `/docs/ag-ui/getting-started/introduction` has two), no console errors. + +- [ ] **Step 4: Commit any fixes** + +```bash +git add -A apps/website +git commit -m "fix(website): visual-sweep fixes for callouts and diagrams" +``` + +(Skip the commit if the sweep found nothing.) From 434308743868fd79ba8ec820c220f79bb4ae8f4d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 15:40:23 -0700 Subject: [PATCH 03/26] feat(website): header-band docs callouts, retiring the left border --- .../src/components/docs/mdx/Callout.spec.tsx | 35 ++++++++++ .../src/components/docs/mdx/Callout.tsx | 57 ++++++++++++--- apps/website/src/styles/docs.css | 70 +++++++------------ 3 files changed, 109 insertions(+), 53 deletions(-) create mode 100644 apps/website/src/components/docs/mdx/Callout.spec.tsx diff --git a/apps/website/src/components/docs/mdx/Callout.spec.tsx b/apps/website/src/components/docs/mdx/Callout.spec.tsx new file mode 100644 index 000000000..af956ca59 --- /dev/null +++ b/apps/website/src/components/docs/mdx/Callout.spec.tsx @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { Callout } from './Callout'; + +describe('Callout', () => { + it('renders a header band with the given title and tone', () => { + const { container } = render( + body text + ); + const root = container.querySelector('[data-mdx="callout"]'); + expect(root?.getAttribute('data-tone')).toBe('warning'); + const band = container.querySelector('.mdx-callout-band'); + expect(band?.textContent).toContain('Heads up'); + expect(band?.querySelector('svg')).not.toBeNull(); + }); + + it.each([ + ['info', 'Note'], + ['tip', 'Tip'], + ['warning', 'Warning'], + ['danger', 'Danger'], + ] as const)('falls back to the kind label for %s when title is omitted', (type, label) => { + const { container } = render(body); + expect(container.querySelector('.mdx-callout-title')?.textContent).toBe(label); + }); + + it('defaults to info and renders children in the body', () => { + const { container } = render(the body); + expect(container.querySelector('[data-mdx="callout"]')?.getAttribute('data-tone')).toBe('info'); + expect(container.querySelector('.mdx-callout-body')?.textContent).toBe('the body'); + }); +}); diff --git a/apps/website/src/components/docs/mdx/Callout.tsx b/apps/website/src/components/docs/mdx/Callout.tsx index 24d100d26..edfb1c004 100644 --- a/apps/website/src/components/docs/mdx/Callout.tsx +++ b/apps/website/src/components/docs/mdx/Callout.tsx @@ -8,21 +8,58 @@ interface Props { children: ReactNode; } -const ICON: Record = { - tip: '✓', - warning: '!', - info: 'i', - danger: '✕', +/** Band label when the author gives no title — the band never renders empty. */ +const KIND_LABEL: Record = { + tip: 'Tip', + warning: 'Warning', + info: 'Note', + danger: 'Danger', +}; + +const ICON_PATHS: Record = { + info: ( + <> + + + + ), + tip: ( + <> + + + + ), + warning: ( + <> + + + + ), + danger: ( + <> + + + + ), }; export function Callout({ type = 'info', title, children }: Props) { return (
-
- - {title ? {title} : null} +
+ + {title ?? KIND_LABEL[type]}
{children}
diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index b9d171ac9..b0d956969 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -425,74 +425,58 @@ body:has([data-website-workspace-host]) { * Fonts: unified onto the next/font vars — see the font note atop ui.css. */ -/* mdx — Callout */ +/* mdx — Callout (header band; tone via custom properties) */ [data-mdx="callout"] { - position: relative; - background: var(--color-surface-tinted); + --callout-tone-text: var(--color-accent); + --callout-tone-surface: rgba(0, 64, 144, 0.06); + background: var(--color-surface); border: 1px solid var(--color-border); - border-left-width: 4px; border-radius: var(--radius-md); - padding: 14px 18px 14px 22px; + overflow: hidden; margin: 20px 0; } [data-mdx="callout"][data-tone="tip"] { - border-left-color: #1a7a40; + --callout-tone-text: #1a7a40; + --callout-tone-surface: rgba(26, 122, 64, 0.07); } [data-mdx="callout"][data-tone="warning"] { - border-left-color: #D4850F; -} -[data-mdx="callout"][data-tone="info"] { - border-left-color: var(--color-accent); + /* Band text darkened from #D4850F for contrast on the tint. */ + --callout-tone-text: #b26d06; + --callout-tone-surface: rgba(212, 133, 15, 0.08); } [data-mdx="callout"][data-tone="danger"] { - border-left-color: var(--color-angular-red); + --callout-tone-text: var(--color-angular-red); + --callout-tone-surface: rgba(221, 0, 49, 0.06); } -.mdx-callout-header { +.mdx-callout-band { display: flex; - align-items: baseline; - gap: 10px; - margin-bottom: 0; -} -.mdx-callout-header[data-has-title] { - margin-bottom: 6px; + align-items: center; + gap: 8px; + padding: 8px 14px; + background: var(--callout-tone-surface); + border-bottom: 1px solid var(--color-border); + color: var(--callout-tone-text); } .mdx-callout-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: var(--radius-full); - color: var(--color-text-inverted); - font-family: var(--font-mono); - font-size: 11px; - font-weight: 700; + width: 14px; + height: 14px; flex-shrink: 0; } -[data-mdx="callout"][data-tone="tip"] .mdx-callout-icon { - background: #1a7a40; -} -[data-mdx="callout"][data-tone="warning"] .mdx-callout-icon { - background: #D4850F; -} -[data-mdx="callout"][data-tone="info"] .mdx-callout-icon { - background: var(--color-accent); -} -[data-mdx="callout"][data-tone="danger"] .mdx-callout-icon { - background: var(--color-angular-red); -} .mdx-callout-title { - font-family: var(--font-inter); - font-size: 15px; - color: var(--color-text-primary); + font-family: Inter, system-ui, sans-serif; + font-size: 13px; font-weight: 600; + color: var(--callout-tone-text); } .mdx-callout-body { + padding: 12px 14px; font-family: var(--font-inter); font-size: 15px; line-height: 1.6; color: var(--color-text-secondary); } +.mdx-callout-body > :first-child { margin-top: 0; } +.mdx-callout-body > :last-child { margin-bottom: 0; } /* mdx — Steps / Step */ .mdx-steps-list { From ad6a5932a710f008f36a809ce7c59bf490ff0e59 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 15:49:49 -0700 Subject: [PATCH 04/26] =?UTF-8?q?fix(website):=20callout=20band=20a11y=20?= =?UTF-8?q?=E2=80=94=20AA=20warning=20contrast=20+=20sr-only=20kind=20pref?= =?UTF-8?q?ix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/website/src/components/docs/mdx/Callout.spec.tsx | 7 +++++++ apps/website/src/components/docs/mdx/Callout.tsx | 5 ++++- apps/website/src/styles/docs.css | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/website/src/components/docs/mdx/Callout.spec.tsx b/apps/website/src/components/docs/mdx/Callout.spec.tsx index af956ca59..43aba4ec1 100644 --- a/apps/website/src/components/docs/mdx/Callout.spec.tsx +++ b/apps/website/src/components/docs/mdx/Callout.spec.tsx @@ -15,6 +15,13 @@ describe('Callout', () => { const band = container.querySelector('.mdx-callout-band'); expect(band?.textContent).toContain('Heads up'); expect(band?.querySelector('svg')).not.toBeNull(); + expect(band?.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true'); + }); + + it('prefixes a screen-reader kind label when a title is given', () => { + const { container } = render(body); + const sr = container.querySelector('.mdx-callout-title .sr-only'); + expect(sr?.textContent).toBe('Warning: '); }); it.each([ diff --git a/apps/website/src/components/docs/mdx/Callout.tsx b/apps/website/src/components/docs/mdx/Callout.tsx index edfb1c004..ce93696b1 100644 --- a/apps/website/src/components/docs/mdx/Callout.tsx +++ b/apps/website/src/components/docs/mdx/Callout.tsx @@ -59,7 +59,10 @@ export function Callout({ type = 'info', title, children }: Props) { > {ICON_PATHS[type]} - {title ?? KIND_LABEL[type]} + + {title ? {KIND_LABEL[type]}: : null} + {title ?? KIND_LABEL[type]} +
{children}
diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index b0d956969..25d9b4261 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -440,8 +440,8 @@ body:has([data-website-workspace-host]) { --callout-tone-surface: rgba(26, 122, 64, 0.07); } [data-mdx="callout"][data-tone="warning"] { - /* Band text darkened from #D4850F for contrast on the tint. */ - --callout-tone-text: #b26d06; + /* Band text darkened from #D4850F for WCAG AA contrast on the tint. */ + --callout-tone-text: #8a5a00; --callout-tone-surface: rgba(212, 133, 15, 0.08); } [data-mdx="callout"][data-tone="danger"] { From 732b0178075d31e2b3798fa3d54645c2c25bd4a5 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 15:56:00 -0700 Subject: [PATCH 05/26] feat(website): SVG diagram kit primitives on a dot grid --- .../components/docs/diagrams/DiagramEdge.tsx | 17 ++++ .../components/docs/diagrams/DiagramFrame.tsx | 61 ++++++++++++++ .../components/docs/diagrams/DiagramNode.tsx | 54 +++++++++++++ .../components/docs/diagrams/DiagramPill.tsx | 20 +++++ .../docs/diagrams/primitives.spec.tsx | 70 ++++++++++++++++ apps/website/src/styles/docs.css | 79 +++++++++++++++++++ 6 files changed, 301 insertions(+) create mode 100644 apps/website/src/components/docs/diagrams/DiagramEdge.tsx create mode 100644 apps/website/src/components/docs/diagrams/DiagramFrame.tsx create mode 100644 apps/website/src/components/docs/diagrams/DiagramNode.tsx create mode 100644 apps/website/src/components/docs/diagrams/DiagramPill.tsx create mode 100644 apps/website/src/components/docs/diagrams/primitives.spec.tsx diff --git a/apps/website/src/components/docs/diagrams/DiagramEdge.tsx b/apps/website/src/components/docs/diagrams/DiagramEdge.tsx new file mode 100644 index 000000000..ed7e0f68c --- /dev/null +++ b/apps/website/src/components/docs/diagrams/DiagramEdge.tsx @@ -0,0 +1,17 @@ +interface DiagramEdgeProps { + /** SVG path data; orthogonal segments (H/V) preferred. */ + d: string; + /** DiagramFrame slug — required when arrow is true, to reference `{slug}-arrow`. */ + slug?: string; + arrow?: boolean; +} + +export function DiagramEdge({ d, slug, arrow = false }: DiagramEdgeProps) { + return ( + + ); +} diff --git a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx new file mode 100644 index 000000000..43487878e --- /dev/null +++ b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx @@ -0,0 +1,61 @@ +import type { ReactNode } from 'react'; + +interface DiagramFrameProps { + /** Unique per rendered diagram; namespaces the SVG defs ids (`{slug}-dots`, `{slug}-arrow`). */ + slug: string; + viewWidth: number; + viewHeight: number; + /** Accessible one-sentence description of what the diagram shows. */ + label: string; + caption?: string; + /** Marketing pages render the same SVG larger. */ + scale?: 'docs' | 'marketing'; + children: ReactNode; +} + +export function DiagramFrame({ + slug, + viewWidth, + viewHeight, + label, + caption, + scale = 'docs', + children, +}: DiagramFrameProps) { + return ( +
+ + + + + + + + + + + {children} + + {caption ?
{caption}
: null} +
+ ); +} diff --git a/apps/website/src/components/docs/diagrams/DiagramNode.tsx b/apps/website/src/components/docs/diagrams/DiagramNode.tsx new file mode 100644 index 000000000..21906cb10 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/DiagramNode.tsx @@ -0,0 +1,54 @@ +interface DiagramNodeProps { + x: number; + y: number; + w: number; + h: number; + title: string; + eyebrow?: string; + meta?: string; + 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'; +} + +const PAD = 16; + +export function DiagramNode({ + x, + y, + w, + h, + title, + eyebrow, + meta, + tone = 'neutral', + align = 'start', + titleStyle = 'mono', +}: DiagramNodeProps) { + const tx = align === 'middle' ? x + w / 2 : x + PAD; + const anchor = align === 'middle' ? 'middle' : undefined; + // Baselines: with an eyebrow the stack is eyebrow/title/meta; without it the + // title floats up; a title-only node vertically centers. + const titleY = eyebrow ? y + 38 : meta ? y + 26 : y + h / 2 + 4; + const metaY = eyebrow ? y + 54 : y + 42; + return ( + + + {eyebrow ? ( + + {eyebrow.toUpperCase()} + + ) : null} + + {title} + + {meta ? ( + + {meta} + + ) : null} + + ); +} diff --git a/apps/website/src/components/docs/diagrams/DiagramPill.tsx b/apps/website/src/components/docs/diagrams/DiagramPill.tsx new file mode 100644 index 000000000..ef242e992 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/DiagramPill.tsx @@ -0,0 +1,20 @@ +interface DiagramPillProps { + /** Center of the pill. */ + cx: number; + cy: number; + w: number; + label: string; +} + +const PILL_H = 24; + +export function DiagramPill({ cx, cy, w, label }: DiagramPillProps) { + return ( + + + + {label} + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/primitives.spec.tsx b/apps/website/src/components/docs/diagrams/primitives.spec.tsx new file mode 100644 index 000000000..6f1c04b21 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/primitives.spec.tsx @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +describe('diagram kit primitives', () => { + it('DiagramFrame renders a labeled svg with dot ground, arrow marker, and caption', () => { + const { container, getByText } = render( + + + + ); + const svg = container.querySelector('svg.tp-diagram-svg'); + expect(svg?.getAttribute('viewBox')).toBe('0 0 640 200'); + expect(svg?.getAttribute('aria-label')).toBe('test diagram'); + expect(container.querySelector('pattern#t-dots')).not.toBeNull(); + expect(container.querySelector('marker#t-arrow')).not.toBeNull(); + expect(container.querySelector('path.tp-diagram-edge')?.getAttribute('marker-end')).toBe('url(#t-arrow)'); + expect(getByText('a caption').tagName).toBe('FIGCAPTION'); + }); + + it('DiagramFrame passes the marketing scale through as a data attribute', () => { + const { container } = render( + + + + ); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('marketing'); + }); + + it('DiagramNode renders eyebrow, title, meta and tone', () => { + const { container } = render( + + + + ); + const g = container.querySelector('g.tp-diagram-node'); + expect(g?.getAttribute('data-tone')).toBe('accent'); + expect(g?.querySelector('.tp-diagram-eyebrow')?.textContent).toBe('ADAPTER'); + expect(g?.querySelector('.tp-diagram-title')?.textContent).toBe('@threadplane/ag-ui'); + expect(g?.querySelector('.tp-diagram-meta')?.textContent).toBe('toAgent()'); + }); + + it('DiagramNode centers a title-only node when align is middle', () => { + const { container } = render( + + + + ); + const title = container.querySelector('.tp-diagram-title'); + expect(title?.getAttribute('text-anchor')).toBe('middle'); + expect(container.querySelector('g.tp-diagram-node')?.getAttribute('data-title')).toBe('sans'); + }); + + it('DiagramPill renders a centered label', () => { + const { container } = render( + + + + ); + const text = container.querySelector('.tp-diagram-pill text'); + expect(text?.textContent).toBe('SSE'); + expect(text?.getAttribute('text-anchor')).toBe('middle'); + }); +}); diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index 25d9b4261..33d2ce385 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -749,6 +749,85 @@ body:has([data-website-workspace-host]) { font-size: 10px; color: var(--color-accent); } +/* mdx — diagram kit (components/docs/diagrams/*) */ +.tp-diagram-figure { + margin: 24px 0; + overflow-x: auto; +} +.tp-diagram-svg { + display: block; + width: 100%; + min-width: 480px; + max-width: 680px; + margin: 0 auto; +} +.tp-diagram-figure[data-scale="marketing"] .tp-diagram-svg { + max-width: 860px; +} +.tp-diagram-dot { fill: var(--color-border); } +.tp-diagram-caption { + font-family: var(--font-inter); + font-size: 13px; + color: var(--color-text-muted); + text-align: center; + margin-top: 10px; +} +.tp-diagram-node rect { + fill: var(--color-surface); + stroke: var(--color-border); +} +.tp-diagram-node[data-tone="accent"] rect { + fill: var(--color-accent-surface); + stroke: var(--color-accent-border); +} +.tp-diagram-node[data-tone="dim"] rect { + fill: var(--color-surface-dim); + stroke: var(--color-border); +} +.tp-diagram-eyebrow { + font-family: var(--font-mono); + font-size: 9px; + font-weight: 600; + letter-spacing: 0.1em; + fill: var(--color-text-muted); +} +.tp-diagram-node[data-tone="accent"] .tp-diagram-eyebrow { fill: var(--color-accent); } +.tp-diagram-title { + font-family: var(--font-mono); + font-size: 12.5px; + font-weight: 700; + fill: var(--color-text-primary); +} +.tp-diagram-node[data-title="sans"] .tp-diagram-title { + font-family: var(--font-inter); + font-size: 11px; + font-weight: 600; + fill: var(--color-text-secondary); +} +.tp-diagram-meta { + font-family: var(--font-inter); + font-size: 10.5px; + fill: var(--color-text-muted); +} +.tp-diagram-edge { + fill: none; + stroke: var(--color-text-muted); + stroke-width: 1.2; +} +.tp-diagram-arrowhead { + fill: none; + stroke: var(--color-text-muted); +} +.tp-diagram-pill rect { + fill: var(--color-accent-surface); + stroke: var(--color-accent-border); +} +.tp-diagram-pill text { + font-family: var(--font-mono); + font-size: 10px; + fill: var(--color-accent); + text-anchor: middle; +} /* * DocsSidebar From 94fe735bd527695c62e8584516e20c883d274c7e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 16:12:09 -0700 Subject: [PATCH 06/26] =?UTF-8?q?refactor(website):=20harden=20diagram-kit?= =?UTF-8?q?=20API=20=E2=80=94=20typed=20arrows,=20contracts,=20barrel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/docs/diagrams/DiagramEdge.tsx | 17 +++++++++++------ .../components/docs/diagrams/DiagramFrame.tsx | 11 ++++++++++- .../components/docs/diagrams/DiagramNode.tsx | 5 +++++ .../src/components/docs/diagrams/index.ts | 4 ++++ .../docs/diagrams/primitives.spec.tsx | 19 +++++++++++++++++++ apps/website/src/styles/docs.css | 1 - .../src/styles/style-contracts.spec.ts | 16 ++++++++++++++++ 7 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 apps/website/src/components/docs/diagrams/index.ts diff --git a/apps/website/src/components/docs/diagrams/DiagramEdge.tsx b/apps/website/src/components/docs/diagrams/DiagramEdge.tsx index ed7e0f68c..c16a2565a 100644 --- a/apps/website/src/components/docs/diagrams/DiagramEdge.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramEdge.tsx @@ -1,10 +1,15 @@ -interface DiagramEdgeProps { - /** SVG path data; orthogonal segments (H/V) preferred. */ +type DiagramEdgeProps = { + /** SVG path data; orthogonal segments (H/V) preferred. Arrowheads point along path direction — reverse `d` for a reversed arrow. */ d: string; - /** DiagramFrame slug — required when arrow is true, to reference `{slug}-arrow`. */ - slug?: string; - arrow?: boolean; -} +} & ( + | { + /** Draw an arrowhead at the path end. */ + arrow: true; + /** DiagramFrame slug, to reference its `{slug}-arrow` marker. */ + slug: string; + } + | { arrow?: false; slug?: string } +); export function DiagramEdge({ d, slug, arrow = false }: DiagramEdgeProps) { return ( diff --git a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx index 43487878e..26a832fd5 100644 --- a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx @@ -1,7 +1,11 @@ import type { ReactNode } from 'react'; interface DiagramFrameProps { - /** Unique per rendered diagram; namespaces the SVG defs ids (`{slug}-dots`, `{slug}-arrow`). */ + /** + * Unique per rendered diagram; namespaces the SVG defs ids (`{slug}-dots`, `{slug}-arrow`). + * Must be unique per rendered diagram INSTANCE on a page — a duplicate slug cross-wires + * `url(#…)` defs document-wide (first match wins). + */ slug: string; viewWidth: number; viewHeight: number; @@ -13,6 +17,11 @@ interface DiagramFrameProps { children: ReactNode; } +/** + * These are React Server Components by design — no hooks/context, which is + * why `slug` is threaded explicitly through every primitive instead of being + * derived implicitly. + */ export function DiagramFrame({ slug, viewWidth, diff --git a/apps/website/src/components/docs/diagrams/DiagramNode.tsx b/apps/website/src/components/docs/diagrams/DiagramNode.tsx index 21906cb10..3c627e3ec 100644 --- a/apps/website/src/components/docs/diagrams/DiagramNode.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramNode.tsx @@ -15,6 +15,11 @@ interface DiagramNodeProps { const PAD = 16; +/** + * SVG text neither wraps nor clips — the caller owns fitting text to `w`. + * Minimum heights: `h >= 64` with eyebrow+meta, `h >= 52` with meta only, + * any `h` for title-only (vertically centered). + */ export function DiagramNode({ x, y, diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts new file mode 100644 index 000000000..8b421755b --- /dev/null +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -0,0 +1,4 @@ +export { DiagramFrame } from './DiagramFrame'; +export { DiagramNode } from './DiagramNode'; +export { DiagramEdge } from './DiagramEdge'; +export { DiagramPill } from './DiagramPill'; diff --git a/apps/website/src/components/docs/diagrams/primitives.spec.tsx b/apps/website/src/components/docs/diagrams/primitives.spec.tsx index 6f1c04b21..64ec1e7b7 100644 --- a/apps/website/src/components/docs/diagrams/primitives.spec.tsx +++ b/apps/website/src/components/docs/diagrams/primitives.spec.tsx @@ -33,6 +33,24 @@ describe('diagram kit primitives', () => { expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('marketing'); }); + it('DiagramFrame defaults data-scale to docs', () => { + const { container } = render( + + + + ); + expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('docs'); + }); + + it('DiagramEdge omits marker-end when arrow is not set', () => { + const { container } = render( + + + + ); + expect(container.querySelector('path.tp-diagram-edge')?.getAttribute('marker-end')).toBeNull(); + }); + it('DiagramNode renders eyebrow, title, meta and tone', () => { const { container } = render( @@ -54,6 +72,7 @@ describe('diagram kit primitives', () => { ); const title = container.querySelector('.tp-diagram-title'); expect(title?.getAttribute('text-anchor')).toBe('middle'); + expect(title?.getAttribute('x')).toBe('100'); expect(container.querySelector('g.tp-diagram-node')?.getAttribute('data-title')).toBe('sans'); }); diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index 33d2ce385..e74088a29 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -826,7 +826,6 @@ body:has([data-website-workspace-host]) { font-family: var(--font-mono); font-size: 10px; fill: var(--color-accent); - text-anchor: middle; } /* diff --git a/apps/website/src/styles/style-contracts.spec.ts b/apps/website/src/styles/style-contracts.spec.ts index afaac58ce..4d4269cec 100644 --- a/apps/website/src/styles/style-contracts.spec.ts +++ b/apps/website/src/styles/style-contracts.spec.ts @@ -120,6 +120,22 @@ const CONTRACTS: StyleContract[] = [ 'overscroll-behavior': /overscroll-behavior:\s*none/, }, }, + { + file: 'docs.css', + selector: '.tp-diagram-figure', + why: 'Diagram SVGs are wider than the article column on narrow viewports. Without horizontal scrolling the figure either overflows the page or gets silently clipped.', + requires: { + 'overflow-x': /overflow-x:\s*auto/, + }, + }, + { + file: 'docs.css', + selector: '.tp-diagram-svg', + why: 'The docs-scale cap keeps diagrams from ballooning past a readable width in the article column; losing it lets the SVG stretch to the full (scrollable) figure width instead.', + requires: { + 'max-width': /max-width:\s*680px/, + }, + }, ]; describe('style contracts', () => { From 5c58a4c1d3a84cbd06a6a7312f79ef65026df1c7 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 16:16:32 -0700 Subject: [PATCH 07/26] =?UTF-8?q?feat(website):=20StackDiagram=20=E2=80=94?= =?UTF-8?q?=20the=20canonical=20stack=20schematic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/docs/MdxRenderer.tsx | 2 + .../docs/diagrams/StackDiagram.spec.tsx | 43 +++++++++ .../components/docs/diagrams/StackDiagram.tsx | 89 +++++++++++++++++++ .../src/components/docs/diagrams/index.ts | 1 + 4 files changed, 135 insertions(+) create mode 100644 apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx create mode 100644 apps/website/src/components/docs/diagrams/StackDiagram.tsx diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index 9a6d26bce..3c99446eb 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -10,6 +10,7 @@ import { FeatureChips } from './mdx/FeatureChips'; import { mdxHeadingComponents } from './mdx/headings'; import { ArchFlowDiagram } from './ArchFlowDiagram'; import { AgUiArchDiagram } from './AgUiArchDiagram'; +import { StackDiagram } from './diagrams/StackDiagram'; import rehypePrettyCode from 'rehype-pretty-code'; import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; @@ -39,6 +40,7 @@ const mdxComponents = { CodeGroup, ArchFlowDiagram, AgUiArchDiagram, + StackDiagram, FeatureChips, pre: Pre, // Explicit width/height let the browser reserve the box before the file diff --git a/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx b/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx new file mode 100644 index 000000000..2d889baf6 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { StackDiagram } from './StackDiagram'; + +function toneOf(container: HTMLElement, title: string): string | null { + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')); + const t = titles.find((el) => el.textContent === title); + return t?.closest('g.tp-diagram-node')?.getAttribute('data-tone') ?? null; +} + +describe('StackDiagram', () => { + it('renders the canonical five-node stack', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/chat'); + expect(titles).toContain('@threadplane/langgraph'); + expect(titles).toContain('@threadplane/ag-ui'); + expect(container.querySelector('.tp-diagram-pill text')?.textContent).toBe('Agent contract · signals + events$'); + }); + + it.each([ + ['ag-ui', '@threadplane/ag-ui'], + ['langgraph', '@threadplane/langgraph'], + ['chat', '@threadplane/chat'], + ] as const)('highlight=%s accents that node', (highlight, title) => { + const { container } = render(); + expect(toneOf(container, title)).toBe('accent'); + }); + + it('highlight=runtimes accents the backend row', () => { + const { container } = render(); + expect(toneOf(container, 'LangGraph Platform')).toBe('accent'); + expect(toneOf(container, 'CrewAI · Mastra · MS Agent Fwk · Strands · …')).toBe('accent'); + }); + + it('renders a caption when given', () => { + const { getByText } = render(); + expect(getByText('the caption')).not.toBeNull(); + }); +}); diff --git a/apps/website/src/components/docs/diagrams/StackDiagram.tsx b/apps/website/src/components/docs/diagrams/StackDiagram.tsx new file mode 100644 index 000000000..fe06bc7cd --- /dev/null +++ b/apps/website/src/components/docs/diagrams/StackDiagram.tsx @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +export type StackHighlight = 'none' | 'chat' | 'langgraph' | 'ag-ui' | 'runtimes' | 'contract'; + +interface StackDiagramProps { + highlight?: StackHighlight; + caption?: string; + scale?: 'docs' | 'marketing'; +} + +/** + * The canonical Threadplane stack: chat UI on top, the Agent contract as a + * labeled seam, the two runtime adapters, and their backends. `highlight` + * accents the node(s) a given page is about; `contract` accents both adapters. + */ +export function StackDiagram({ highlight = 'none', caption, scale = 'docs' }: StackDiagramProps) { + const slug = `stack-${highlight}`; + const adapters = highlight === 'contract'; + const backends = highlight === 'runtimes'; + return ( + + + + + + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index 8b421755b..d068410b2 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -2,3 +2,4 @@ export { DiagramFrame } from './DiagramFrame'; export { DiagramNode } from './DiagramNode'; export { DiagramEdge } from './DiagramEdge'; export { DiagramPill } from './DiagramPill'; +export { StackDiagram, type StackHighlight } from './StackDiagram'; From a01ae110c56d67c2bdfdc21e40fbef46627fa194 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 16:26:31 -0700 Subject: [PATCH 08/26] fix(website): StackDiagram text fit + arrow gap; harden highlight tests --- .../components/docs/diagrams/DiagramNode.tsx | 1 + .../docs/diagrams/StackDiagram.spec.tsx | 19 ++++++++++++++++++- .../components/docs/diagrams/StackDiagram.tsx | 4 ++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/DiagramNode.tsx b/apps/website/src/components/docs/diagrams/DiagramNode.tsx index 3c627e3ec..7e3ce52a6 100644 --- a/apps/website/src/components/docs/diagrams/DiagramNode.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramNode.tsx @@ -18,6 +18,7 @@ const PAD = 16; /** * SVG text neither wraps nor clips — the caller owns fitting text to `w`. * 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). */ export function DiagramNode({ diff --git a/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx b/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx index 2d889baf6..aeaa17a56 100644 --- a/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx +++ b/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx @@ -33,7 +33,24 @@ describe('StackDiagram', () => { it('highlight=runtimes accents the backend row', () => { const { container } = render(); expect(toneOf(container, 'LangGraph Platform')).toBe('accent'); - expect(toneOf(container, 'CrewAI · Mastra · MS Agent Fwk · Strands · …')).toBe('accent'); + expect(toneOf(container, 'CrewAI · Mastra · MS Agent Fwk · Strands')).toBe('accent'); + }); + + it('highlight=contract accents both adapters', () => { + const { container } = render(); + expect(toneOf(container, '@threadplane/langgraph')).toBe('accent'); + expect(toneOf(container, '@threadplane/ag-ui')).toBe('accent'); + }); + + it('highlight=chat leaves both adapters neutral', () => { + const { container } = render(); + expect(toneOf(container, '@threadplane/langgraph')).toBe('neutral'); + expect(toneOf(container, '@threadplane/ag-ui')).toBe('neutral'); + }); + + it('highlight default (none) accents nothing', () => { + const { container } = render(); + expect(container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]').length).toBe(0); }); it('renders a caption when given', () => { diff --git a/apps/website/src/components/docs/diagrams/StackDiagram.tsx b/apps/website/src/components/docs/diagrams/StackDiagram.tsx index fe06bc7cd..c5cb50905 100644 --- a/apps/website/src/components/docs/diagrams/StackDiagram.tsx +++ b/apps/website/src/components/docs/diagrams/StackDiagram.tsx @@ -40,7 +40,7 @@ export function StackDiagram({ highlight = 'none', caption, scale = 'docs' }: St meta=" · · " tone={highlight === 'chat' ? 'accent' : 'neutral'} /> - + @@ -79,7 +79,7 @@ export function StackDiagram({ highlight = 'none', caption, scale = 'docs' }: St y={284} w={240} h={40} - title="CrewAI · Mastra · MS Agent Fwk · Strands · …" + title="CrewAI · Mastra · MS Agent Fwk · Strands" align="middle" titleStyle="sans" tone={backends ? 'accent' : 'dim'} From d1992b5443c3680df14856beaeaca7ca15926f70 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 16:28:48 -0700 Subject: [PATCH 09/26] refactor(website): replace ascii + HTML arch diagrams with StackDiagram --- ...stack-agentic-angular-apps-using-ag-ui.mdx | 2 +- ...-what-changes-when-the-runtime-changes.mdx | 2 +- .../ag-ui/getting-started/introduction.mdx | 17 +--- .../langgraph/concepts/agent-contract.mdx | 11 +-- .../src/components/docs/AgUiArchDiagram.tsx | 81 --------------- .../src/components/docs/MdxRenderer.tsx | 2 - apps/website/src/styles/docs.css | 99 +------------------ 7 files changed, 11 insertions(+), 203 deletions(-) delete mode 100644 apps/website/src/components/docs/AgUiArchDiagram.tsx diff --git a/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx b/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx index c7956f4a7..604ffc79d 100644 --- a/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx +++ b/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx @@ -59,7 +59,7 @@ The whole protocol is seventeen events; you can hold it in your head. The seams come first, before any code. - + Three boxes. Two seams. diff --git a/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx b/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx index da7473909..7b84cc099 100644 --- a/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx +++ b/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx @@ -114,7 +114,7 @@ They describe different topologies. LangGraph addresses a platform API and names a graph on it. AG-UI addresses one HTTP endpoint that streams Server-Sent Events, and the endpoint is the whole address space. - + One SSE route to allowlist, proxy, and monitor is a smaller operational surface than a platform API with a run lifecycle behind it. Whether that simplicity is a feature depends entirely on the next section. diff --git a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx index 1504eb95f..8956bc763 100644 --- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx @@ -14,19 +14,10 @@ The [AG-UI demo](https://ag-ui.threadplane.ai) runs this exact chat surface agai ## How it fits -```text -@threadplane/chat - , , - @threadplane/chat/debug -> - | - | Agent contract (signals + events$) - | - +--> @threadplane/langgraph - | LangGraphAgent -> LangGraph Platform - | - +--> @threadplane/ag-ui - toAgent(AbstractAgent) -> AG-UI backend -``` + ## What you get diff --git a/apps/website/content/docs/langgraph/concepts/agent-contract.mdx b/apps/website/content/docs/langgraph/concepts/agent-contract.mdx index 787ba9c10..a7f1c6781 100644 --- a/apps/website/content/docs/langgraph/concepts/agent-contract.mdx +++ b/apps/website/content/docs/langgraph/concepts/agent-contract.mdx @@ -6,13 +6,10 @@ The `Agent` contract is the spine between runtime adapters and chat UI. The UI shouldn't care whether a response came from LangGraph Platform, AG-UI, a local mock, or a custom HTTP service. That's the point. The boundary is explicit: adapters translate runtime events into Angular signals and a small action surface. -```text -LangGraph Platform -- @threadplane/langgraph --+ - +-- Agent -- @threadplane/chat -AG-UI backend ------ @threadplane/ag-ui -------+ - -custom backend ------ your adapter -----+ -``` + ## The Contract Surface diff --git a/apps/website/src/components/docs/AgUiArchDiagram.tsx b/apps/website/src/components/docs/AgUiArchDiagram.tsx deleted file mode 100644 index 2b8c1fdf2..000000000 --- a/apps/website/src/components/docs/AgUiArchDiagram.tsx +++ /dev/null @@ -1,81 +0,0 @@ -interface BoxProps { - eyebrow: string; - title: string; - meta: string; - tone?: 'neutral' | 'accent'; -} - -function Box({ eyebrow, title, meta, tone = 'neutral' }: BoxProps) { - return ( -
- - {eyebrow} - - - {title} - - - {meta} - -
- ); -} - -function ArrowLabel({ label, sub }: { label: string; sub: string }) { - return ( -
- - {label} - - - - - - {sub} - -
- ); -} - -export function AgUiArchDiagram() { - return ( -
-
- - - - - -
-
- Backend speaks AG-UI over SSE → adapter exposes a signal-shaped Agent contract → chat UI renders. -
-
- ); -} diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index 3c99446eb..f5682875e 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -9,7 +9,6 @@ import { Pre } from './mdx/CodeBlock'; import { FeatureChips } from './mdx/FeatureChips'; import { mdxHeadingComponents } from './mdx/headings'; import { ArchFlowDiagram } from './ArchFlowDiagram'; -import { AgUiArchDiagram } from './AgUiArchDiagram'; import { StackDiagram } from './diagrams/StackDiagram'; import rehypePrettyCode from 'rehype-pretty-code'; import rehypeSlug from 'rehype-slug'; @@ -39,7 +38,6 @@ const mdxComponents = { CardGroup, CodeGroup, ArchFlowDiagram, - AgUiArchDiagram, StackDiagram, FeatureChips, pre: Pre, diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index e74088a29..7e20e1328 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -190,7 +190,7 @@ body:has([data-website-workspace-host]) { * rather than an inline asset. Tinted backdrop + soft shadow + clean caption. * Scoped to image figures via :has(> img) so it doesn't collide with the * code-block figures from rehype-pretty-code or component figures (e.g. - * AgUiArchDiagram). */ + * StackDiagram). */ .docs-prose figure:has(> img) { margin: 2.5rem 0; padding: 0.75rem 0.75rem 0; @@ -262,103 +262,6 @@ body:has([data-website-workspace-host]) { .docs-prose td { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--docs-accent-tint-soft); color: var(--color-text-secondary); } .docs-prose td code { font-size: 0.8em; } -/* AG-UI architecture diagram */ -.ag-ui-arch-grid { - display: grid; - grid-template-columns: 1fr auto 1fr auto 1fr; - align-items: stretch; - gap: 0; -} -/* Reunited with the .ag-ui-arch-arrow mobile override below — this base - * rule was migrated from AgUiArchDiagram.tsx's inline style, the media - * query pre-existed it. */ -.ag-ui-arch-arrow { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 4px; - color: var(--color-text-muted); - padding: 0 4px; -} -@media (max-width: 720px) { - .ag-ui-arch-grid { - grid-template-columns: 1fr; - } - .ag-ui-arch-arrow { - transform: rotate(90deg); - padding: 12px 0; - margin: 4px auto; - } -} -.ag-ui-arch-box { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - padding: 20px 22px; - display: flex; - flex-direction: column; - gap: 6px; - min-height: 116px; -} -.ag-ui-arch-box[data-tone="accent"] { - background: var(--color-accent-surface); - border-color: #00409033; -} -.ag-ui-arch-box-eyebrow { - font-family: var(--font-mono); - font-size: 0.7rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--color-text-muted); -} -.ag-ui-arch-box[data-tone="accent"] .ag-ui-arch-box-eyebrow { - color: var(--color-accent); -} -.ag-ui-arch-box-title { - font-family: var(--font-mono); - font-size: 0.95rem; - font-weight: 600; - color: var(--color-text-primary); -} -.ag-ui-arch-box-meta { - font-size: 0.85rem; - line-height: 1.5; - color: var(--color-text-secondary); -} -.ag-ui-arch-arrow-label { - font-family: var(--font-mono); - font-size: 0.65rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--color-accent); - white-space: nowrap; -} -.ag-ui-arch-arrow-svg { - display: block; -} -.ag-ui-arch-arrow-sub { - font-size: 0.72rem; - color: var(--color-text-muted); - white-space: nowrap; -} -.ag-ui-arch-figure { - margin: 2rem 0; - padding: 28px 24px; - background: var(--color-canvas); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); -} -.ag-ui-arch-caption { - margin-top: 16px; - font-size: 0.8rem; - color: var(--color-text-muted); - text-align: center; - font-style: italic; -} - /* Docs — readable column max-width */ .docs-prose { max-width: 70ch; From 7b104b50114a6bbdf10d34c5fc37f4b7e8f22d4d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 16:34:47 -0700 Subject: [PATCH 10/26] fix(website): align prose and captions with the swapped-in StackDiagram --- ...05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx | 4 ++-- .../blog/2026-08-31-what-changes-when-the-runtime-changes.mdx | 2 +- .../content/docs/ag-ui/getting-started/introduction.mdx | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx b/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx index 604ffc79d..193863efb 100644 --- a/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx +++ b/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx @@ -59,9 +59,9 @@ The whole protocol is seventeen events; you can hold it in your head. The seams come first, before any code. - + -Three boxes. Two seams. +Two seams: the wire, and the contract. **The backend.** Whatever you want, as long as it can emit AG-UI events. LangGraph, CrewAI, Mastra, Microsoft Agent Framework, Pydantic AI, AG2, AWS Strands, Agno. They all have first-party or partnership adapters. diff --git a/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx b/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx index 7b84cc099..2189bc5f6 100644 --- a/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx +++ b/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx @@ -114,7 +114,7 @@ They describe different topologies. LangGraph addresses a platform API and names a graph on it. AG-UI addresses one HTTP endpoint that streams Server-Sent Events, and the endpoint is the whole address space. - + One SSE route to allowlist, proxy, and monitor is a smaller operational surface than a platform API with a run lifecycle behind it. Whether that simplicity is a feature depends entirely on the next section. diff --git a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx index 8956bc763..c4b4bc279 100644 --- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx +++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx @@ -48,3 +48,4 @@ Out of scope for now (use `@threadplane/langgraph` if you need LangGraph Platfor - [Fake Agent](/docs/ag-ui/guides/fake-agent) - build UI without a backend. - [Citations](/docs/ag-ui/guides/citations) - attach sources through `state.citations`. - [Troubleshooting](/docs/ag-ui/guides/troubleshooting) - debug provider, stream, state, and citation issues. +- [Chat Debug](/docs/chat/components/chat-debug) - inspect the raw event stream with ``. From e308e32652286954a94fc4189acd16b77c8dbb97 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 16:36:22 -0700 Subject: [PATCH 11/26] docs(website): How-it-fits stack diagrams for langgraph, chat, runtimes intros --- .../content/docs/chat/getting-started/introduction.mdx | 7 +++++++ .../docs/langgraph/getting-started/introduction.mdx | 7 +++++++ .../content/docs/runtimes/getting-started/introduction.mdx | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/apps/website/content/docs/chat/getting-started/introduction.mdx b/apps/website/content/docs/chat/getting-started/introduction.mdx index 283daa5fe..2244aebaa 100644 --- a/apps/website/content/docs/chat/getting-started/introduction.mdx +++ b/apps/website/content/docs/chat/getting-started/introduction.mdx @@ -6,6 +6,13 @@ This guide explains the library's two-tier architecture, how it relates to the rest of the Threadplane stack, and when to reach for the all-in-one composition versus assembling primitives yourself.
+## How it fits + + + ## Two-Tier Architecture The library is organized into two layers: **primitives** and **compositions**. diff --git a/apps/website/content/docs/langgraph/getting-started/introduction.mdx b/apps/website/content/docs/langgraph/getting-started/introduction.mdx index 767c72292..19a5b7e61 100644 --- a/apps/website/content/docs/langgraph/getting-started/introduction.mdx +++ b/apps/website/content/docs/langgraph/getting-started/introduction.mdx @@ -8,6 +8,13 @@ This guide walks you through the complete workflow: build a LangGraph agent in Python, run it locally, connect it to an Angular app with `injectAgent()`, and deploy to production. +## How it fits + + + ## What is `injectAgent()`? `injectAgent()` is an Angular function that creates a reactive, streaming connection to a LangGraph agent. Configured globally via `provideAgent({...})` at bootstrap, it returns an object whose properties are Angular Signals — meaning your templates update automatically as the agent streams responses, token by token. diff --git a/apps/website/content/docs/runtimes/getting-started/introduction.mdx b/apps/website/content/docs/runtimes/getting-started/introduction.mdx index 91e98d18a..7dbbde640 100644 --- a/apps/website/content/docs/runtimes/getting-started/introduction.mdx +++ b/apps/website/content/docs/runtimes/getting-started/introduction.mdx @@ -13,6 +13,13 @@ This section documents what happened when it was tested. On 2026-08-31 the adapt These pages document runtimes as *backends measured against the adapter*. They are not a substitute for each vendor's own documentation, and Threadplane does not maintain any of the upstream AG-UI bridges described here. +## How it fits + + + ## The runtimes From ff5cdd88a496ce077e85c1192325d6a84adcff14 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 16:40:30 -0700 Subject: [PATCH 12/26] fix(website): honest runtimes caption; retire the chat intro's stale stack ascii --- .../docs/chat/getting-started/introduction.mdx | 18 ++---------------- .../runtimes/getting-started/introduction.mdx | 2 +- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/apps/website/content/docs/chat/getting-started/introduction.mdx b/apps/website/content/docs/chat/getting-started/introduction.mdx index 2244aebaa..a2fa15d16 100644 --- a/apps/website/content/docs/chat/getting-started/introduction.mdx +++ b/apps/website/content/docs/chat/getting-started/introduction.mdx @@ -10,7 +10,7 @@ This guide explains the library's two-tier architecture, how it relates to the r ## Two-Tier Architecture @@ -49,21 +49,7 @@ Compositions are opinionated, styled components that combine primitives into rea ## How the Stack Fits Together -`@threadplane/chat` sits between your application and two other Threadplane libraries: - -``` -Your App - | - v -@threadplane/chat <-- UI components (this library) - | | - v v -@threadplane/langgraph @threadplane/render - (streaming state) (generative UI) - | - v -LangGraph Platform -``` +`@threadplane/chat` sits between your application and two other Threadplane libraries, on either side of the seam shown in the diagram above. - **`@threadplane/langgraph`** provides `provideAgent()` and `injectAgent()` returning a `LangGraphAgent`, which satisfies the `Agent` contract consumed by `@threadplane/chat`. The base `Agent` contract exposes reactive Signals for `messages()`, `status()`, `isLoading()`, `error()`, `toolCalls()`, and `state()`, plus optional `interrupt()` and `subagents()`. Adapters can layer on more: LangGraph implements `AgentWithHistory`, adding `history()` for checkpoint time-travel. diff --git a/apps/website/content/docs/runtimes/getting-started/introduction.mdx b/apps/website/content/docs/runtimes/getting-started/introduction.mdx index 7dbbde640..3a43f201a 100644 --- a/apps/website/content/docs/runtimes/getting-started/introduction.mdx +++ b/apps/website/content/docs/runtimes/getting-started/introduction.mdx @@ -17,7 +17,7 @@ These pages document runtimes as *backends measured against the adapter*. They a ## The runtimes From e0d7f55001645ddce122d20424324fc9a9c65855 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 16:43:16 -0700 Subject: [PATCH 13/26] docs(website): kit pipeline diagram for AG-UI architecture page --- .../docs/ag-ui/concepts/architecture.mdx | 18 +------ .../src/components/docs/MdxRenderer.tsx | 2 + .../diagrams/AgUiArchitecturePipeline.tsx | 48 +++++++++++++++++++ .../docs/diagrams/compositions.spec.tsx | 21 ++++++++ .../src/components/docs/diagrams/index.ts | 1 + 5 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx create mode 100644 apps/website/src/components/docs/diagrams/compositions.spec.tsx diff --git a/apps/website/content/docs/ag-ui/concepts/architecture.mdx b/apps/website/content/docs/ag-ui/concepts/architecture.mdx index 25b961f85..b4778c981 100644 --- a/apps/website/content/docs/ag-ui/concepts/architecture.mdx +++ b/apps/website/content/docs/ag-ui/concepts/architecture.mdx @@ -4,23 +4,7 @@ The package takes an AG-UI `AbstractAgent`, listens to its protocol events, and exposes the runtime-neutral `Agent` contract that the chat components already understand. -```text -Angular component - | - v -@threadplane/chat Agent contract - | messages, status, isLoading, error, toolCalls, state, events$ - | - v -@threadplane/ag-ui toAgent() - | reduces AG-UI events into Angular signals - | - v -@ag-ui/client AbstractAgent - | - v -AG-UI backend or in-process fake agent -``` + ## The boundary diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index f5682875e..25633adc4 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -10,6 +10,7 @@ import { FeatureChips } from './mdx/FeatureChips'; import { mdxHeadingComponents } from './mdx/headings'; import { ArchFlowDiagram } from './ArchFlowDiagram'; import { StackDiagram } from './diagrams/StackDiagram'; +import { AgUiArchitecturePipeline } from './diagrams/AgUiArchitecturePipeline'; import rehypePrettyCode from 'rehype-pretty-code'; import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; @@ -39,6 +40,7 @@ const mdxComponents = { CodeGroup, ArchFlowDiagram, StackDiagram, + AgUiArchitecturePipeline, FeatureChips, pre: Pre, // Explicit width/height let the browser reserve the box before the file diff --git a/apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx b/apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx new file mode 100644 index 000000000..01aefd3f0 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'agui-pipeline'; + +/** + * Vertical pipeline from an Angular component down to the AG-UI backend: + * the runtime-neutral Agent contract, the `toAgent()` adapter, the AG-UI + * `AbstractAgent` protocol client, then the backend (or a fake agent). + */ +export function AgUiArchitecturePipeline() { + return ( + + + + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/compositions.spec.tsx b/apps/website/src/components/docs/diagrams/compositions.spec.tsx new file mode 100644 index 000000000..924a2515e --- /dev/null +++ b/apps/website/src/components/docs/diagrams/compositions.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 { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; + +/** + * Compositions are hand-placed layouts; the spec guards that each mounts, + * is labeled for screen readers, and names its load-bearing packages. + * Later tasks append one describe block per composition. + */ +describe('AgUiArchitecturePipeline', () => { + it('mounts with an accessible label and the pipeline stages', () => { + const { container } = render(); + expect(container.querySelector('svg[role="img"]')?.getAttribute('aria-label')).toBeTruthy(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/ag-ui'); + expect(titles).toContain('AbstractAgent'); + }); +}); diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index d068410b2..71668ae6e 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -3,3 +3,4 @@ export { DiagramNode } from './DiagramNode'; export { DiagramEdge } from './DiagramEdge'; export { DiagramPill } from './DiagramPill'; export { StackDiagram, type StackHighlight } from './StackDiagram'; +export { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; From cf1dc9dfe7f06779e846d4df195e67e7562fecb9 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 16:56:00 -0700 Subject: [PATCH 14/26] docs(website): A2UI message-flow diagram for overview and intro --- .../a2ui/getting-started/introduction.mdx | 4 ++ .../content/docs/chat/a2ui/overview.mdx | 9 +-- .../src/components/docs/MdxRenderer.tsx | 2 + .../docs/diagrams/A2uiMessageFlow.tsx | 56 +++++++++++++++++++ .../diagrams/AgUiArchitecturePipeline.tsx | 2 +- .../docs/diagrams/compositions.spec.tsx | 11 ++++ .../src/components/docs/diagrams/index.ts | 1 + 7 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx diff --git a/apps/website/content/docs/a2ui/getting-started/introduction.mdx b/apps/website/content/docs/a2ui/getting-started/introduction.mdx index 0071a07a9..4f03d1fee 100644 --- a/apps/website/content/docs/a2ui/getting-started/introduction.mdx +++ b/apps/website/content/docs/a2ui/getting-started/introduction.mdx @@ -4,6 +4,10 @@ It does not render Angular components. It does not register handler functions. It does not decide how an agent should respond to a button click. Those jobs sit in `@threadplane/chat` and `@threadplane/render`. +## How it fits + + + ## What the package owns The public entry point exports four groups of tools: diff --git a/apps/website/content/docs/chat/a2ui/overview.mdx b/apps/website/content/docs/chat/a2ui/overview.mdx index e45df50b8..674518865 100644 --- a/apps/website/content/docs/chat/a2ui/overview.mdx +++ b/apps/website/content/docs/chat/a2ui/overview.mdx @@ -8,14 +8,7 @@ When an assistant message starts with `---a2ui_JSON---`, the chat streaming pipe ## Runtime Flow -```text -assistant text starts with ---a2ui_JSON--- - -> content classifier switches to A2UI mode - -> createA2uiMessageParser() parses JSONL messages - -> createA2uiSurfaceStore() applies those messages by surface id - -> ChatComponent passes surface + state into A2uiSurfaceComponent - -> A2uiSurfaceComponent renders progressive state through your catalog -``` + This is why A2UI sits between chat and render. Chat owns message streaming. A2UI owns the protocol shapes. `A2uiSurfaceComponent` turns the accumulated surface state into a render spec and delegates to `@threadplane/render`, so handlers, render events, and json-render state bindings use the same path for both the preferred `state` input and the legacy `surface` input. diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index 25633adc4..22771ee4d 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -11,6 +11,7 @@ import { mdxHeadingComponents } from './mdx/headings'; import { ArchFlowDiagram } from './ArchFlowDiagram'; import { StackDiagram } from './diagrams/StackDiagram'; import { AgUiArchitecturePipeline } from './diagrams/AgUiArchitecturePipeline'; +import { A2uiMessageFlow } from './diagrams/A2uiMessageFlow'; import rehypePrettyCode from 'rehype-pretty-code'; import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; @@ -41,6 +42,7 @@ const mdxComponents = { ArchFlowDiagram, StackDiagram, AgUiArchitecturePipeline, + A2uiMessageFlow, FeatureChips, pre: Pre, // Explicit width/height let the browser reserve the box before the file diff --git a/apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx b/apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx new file mode 100644 index 000000000..c568bbb91 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'a2ui-flow'; + +/** + * Vertical pipeline from a raw assistant message to a rendered surface: the + * `---a2ui_JSON---` sentinel, the content classifier, the streaming + * `@threadplane/a2ui` parser and surface store, then the `` + * render component. + */ +export function A2uiMessageFlow() { + return ( + + + + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx b/apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx index 01aefd3f0..f499ba460 100644 --- a/apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx +++ b/apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx @@ -29,7 +29,7 @@ export function AgUiArchitecturePipeline() { diff --git a/apps/website/src/components/docs/diagrams/compositions.spec.tsx b/apps/website/src/components/docs/diagrams/compositions.spec.tsx index 924a2515e..4b5fe3561 100644 --- a/apps/website/src/components/docs/diagrams/compositions.spec.tsx +++ b/apps/website/src/components/docs/diagrams/compositions.spec.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { describe, expect, it } from 'vitest'; import { render } from '@testing-library/react'; import { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; +import { A2uiMessageFlow } from './A2uiMessageFlow'; /** * Compositions are hand-placed layouts; the spec guards that each mounts, @@ -19,3 +20,13 @@ describe('AgUiArchitecturePipeline', () => { expect(titles).toContain('AbstractAgent'); }); }); + +describe('A2uiMessageFlow', () => { + it('mounts and names the parser and surface store stages', () => { + const { container } = render(); + expect(container.querySelector('svg[role="img"]')).not.toBeNull(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('createA2uiMessageParser()'); + expect(titles).toContain('createA2uiSurfaceStore()'); + }); +}); diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index 71668ae6e..3c0b49d69 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -4,3 +4,4 @@ export { DiagramEdge } from './DiagramEdge'; export { DiagramPill } from './DiagramPill'; export { StackDiagram, type StackHighlight } from './StackDiagram'; export { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; +export { A2uiMessageFlow } from './A2uiMessageFlow'; From fd44de7d8b04d92be9c412daae254fd99cbe05f1 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 17:06:37 -0700 Subject: [PATCH 15/26] fix(website): surface store belongs to chat, not a2ui, in A2uiMessageFlow --- .../src/components/docs/diagrams/A2uiMessageFlow.tsx | 3 +-- .../src/components/docs/diagrams/compositions.spec.tsx | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx b/apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx index c568bbb91..9f1034d94 100644 --- a/apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx +++ b/apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx @@ -41,9 +41,8 @@ export function A2uiMessageFlow() { { expect(titles).toContain('createA2uiMessageParser()'); expect(titles).toContain('createA2uiSurfaceStore()'); }); + + it('accents only the a2ui-owned parser stage', () => { + const { container } = render(); + const accented = Array.from(container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]')); + expect(accented).toHaveLength(1); + expect(accented[0]?.querySelector('.tp-diagram-eyebrow')?.textContent).toBe('@THREADPLANE/A2UI'); + expect(accented[0]?.querySelector('.tp-diagram-title')?.textContent).toBe('createA2uiMessageParser()'); + }); }); From db5a70628fb9259e15d4f88125ebd32fdf72526d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 17:10:31 -0700 Subject: [PATCH 16/26] docs(website): render pipeline + render-vs-a2ui kit diagrams Adds RenderHowItFits (agent output -> @threadplane/render -> Angular components pipeline, with pill-labeled edges) and RenderVsA2ui (chat fanning out to json-render vs A2UI) to the docs diagram kit, and wires both into the render getting-started intro and the json-render-vs-a2ui concept page in place of the old plain-text layer diagram. --- .../render/concepts/json-render-vs-a2ui.mdx | 11 +--- .../render/getting-started/introduction.mdx | 4 ++ .../src/components/docs/MdxRenderer.tsx | 4 ++ .../docs/diagrams/RenderHowItFits.tsx | 58 +++++++++++++++++++ .../components/docs/diagrams/RenderVsA2ui.tsx | 56 ++++++++++++++++++ .../docs/diagrams/compositions.spec.tsx | 20 +++++++ .../src/components/docs/diagrams/index.ts | 2 + 7 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 apps/website/src/components/docs/diagrams/RenderHowItFits.tsx create mode 100644 apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx diff --git a/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx b/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx index 23d4f2a4c..74789b659 100644 --- a/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx +++ b/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx @@ -14,16 +14,7 @@ This matters because the tradeoff is not "which renderer is better." The tradeof ## The Layers -```text -@threadplane/render - renders a Spec using an Angular registry, state store, functions, and handlers - -@threadplane/a2ui - defines A2UI v0.9 message and component types - -@threadplane/chat - detects assistant content, manages streaming state, and mounts render or A2UI surfaces -``` + Use `@threadplane/render` directly when your application already has a spec. diff --git a/apps/website/content/docs/render/getting-started/introduction.mdx b/apps/website/content/docs/render/getting-started/introduction.mdx index 6b723fcd5..2255d1d23 100644 --- a/apps/website/content/docs/render/getting-started/introduction.mdx +++ b/apps/website/content/docs/render/getting-started/introduction.mdx @@ -2,6 +2,10 @@ `@threadplane/render` is the Angular rendering engine for [json-render](https://github.com/nicholasgriffintn/json-render) specs. It takes a declarative JSON specification and renders it into a live Angular component tree -- with reactive state, event handling, and conditional rendering built in. +## How it fits + + + ## Why @threadplane/render? Building dynamic UIs from server-driven specs shows up everywhere -- AI apps, form builders, CMS-powered frontends. `@threadplane/render` bridges `@json-render/core` (a framework-agnostic spec evaluation engine) and Angular's component model. diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index 22771ee4d..133fa4dc3 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -12,6 +12,8 @@ import { ArchFlowDiagram } from './ArchFlowDiagram'; import { StackDiagram } from './diagrams/StackDiagram'; import { AgUiArchitecturePipeline } from './diagrams/AgUiArchitecturePipeline'; import { A2uiMessageFlow } from './diagrams/A2uiMessageFlow'; +import { RenderHowItFits } from './diagrams/RenderHowItFits'; +import { RenderVsA2ui } from './diagrams/RenderVsA2ui'; import rehypePrettyCode from 'rehype-pretty-code'; import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; @@ -43,6 +45,8 @@ const mdxComponents = { StackDiagram, AgUiArchitecturePipeline, A2uiMessageFlow, + RenderHowItFits, + RenderVsA2ui, FeatureChips, pre: Pre, // Explicit width/height let the browser reserve the box before the file diff --git a/apps/website/src/components/docs/diagrams/RenderHowItFits.tsx b/apps/website/src/components/docs/diagrams/RenderHowItFits.tsx new file mode 100644 index 000000000..4c511cca6 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/RenderHowItFits.tsx @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'render-how-it-fits'; + +/** + * The three-step pipeline from `@threadplane/render`'s intro: an agent (or + * your own code) produces a JSON spec, `@threadplane/render` validates and + * renders it against a registry/state/functions/handlers, and the result is + * a live tree of your own Angular components. + */ +export function RenderHowItFits() { + return ( + + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx b/apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx new file mode 100644 index 000000000..13acbe319 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; + +const SLUG = 'render-vs-a2ui'; + +/** + * The layering from the json-render-vs-A2UI concept page: `@threadplane/chat` + * detects assistant content and streams it into whichever surface applies, + * fanning out to a fixed json-render spec on one side and an A2UI surface on + * the other. + */ +export function RenderVsA2ui() { + return ( + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/compositions.spec.tsx b/apps/website/src/components/docs/diagrams/compositions.spec.tsx index bb7bf3b92..670f3028d 100644 --- a/apps/website/src/components/docs/diagrams/compositions.spec.tsx +++ b/apps/website/src/components/docs/diagrams/compositions.spec.tsx @@ -5,6 +5,8 @@ import { describe, expect, it } from 'vitest'; import { render } from '@testing-library/react'; import { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; import { A2uiMessageFlow } from './A2uiMessageFlow'; +import { RenderHowItFits } from './RenderHowItFits'; +import { RenderVsA2ui } from './RenderVsA2ui'; /** * Compositions are hand-placed layouts; the spec guards that each mounts, @@ -38,3 +40,21 @@ describe('A2uiMessageFlow', () => { expect(accented[0]?.querySelector('.tp-diagram-title')?.textContent).toBe('createA2uiMessageParser()'); }); }); + +describe('RenderHowItFits', () => { + it('mounts and shows the spec-to-components pipeline', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/render'); + }); +}); + +describe('RenderVsA2ui', () => { + it('mounts and shows both packages under chat', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/render'); + expect(titles).toContain('@threadplane/a2ui'); + expect(titles).toContain('@threadplane/chat'); + }); +}); diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index 3c0b49d69..231c62793 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -5,3 +5,5 @@ export { DiagramPill } from './DiagramPill'; export { StackDiagram, type StackHighlight } from './StackDiagram'; export { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; export { A2uiMessageFlow } from './A2uiMessageFlow'; +export { RenderHowItFits } from './RenderHowItFits'; +export { RenderVsA2ui } from './RenderVsA2ui'; From c5f4104adae5c43c78a0106bd999af0ef1ccc3fb Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 17:25:28 -0700 Subject: [PATCH 17/26] =?UTF-8?q?fix(website):=20render=20diagrams=20?= =?UTF-8?q?=E2=80=94=20break=20edges=20under=20pills,=20honest=20entry=20p?= =?UTF-8?q?aths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/diagrams/RenderHowItFits.tsx | 10 +++-- .../components/docs/diagrams/RenderVsA2ui.tsx | 39 ++++++++++++------- .../docs/diagrams/compositions.spec.tsx | 13 +++++++ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/RenderHowItFits.tsx b/apps/website/src/components/docs/diagrams/RenderHowItFits.tsx index 4c511cca6..dd27b87ad 100644 --- a/apps/website/src/components/docs/diagrams/RenderHowItFits.tsx +++ b/apps/website/src/components/docs/diagrams/RenderHowItFits.tsx @@ -25,13 +25,14 @@ export function RenderHowItFits() { y={16} w={300} h={44} - title="Agent output — a JSON Spec" + title="Agent or app output — a JSON Spec" align="middle" titleStyle="sans" tone="dim" /> - - + + + - + + + + - - + + + diff --git a/apps/website/src/components/docs/diagrams/compositions.spec.tsx b/apps/website/src/components/docs/diagrams/compositions.spec.tsx index 670f3028d..95862f0fc 100644 --- a/apps/website/src/components/docs/diagrams/compositions.spec.tsx +++ b/apps/website/src/components/docs/diagrams/compositions.spec.tsx @@ -47,6 +47,11 @@ describe('RenderHowItFits', () => { const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); expect(titles).toContain('@threadplane/render'); }); + + it('breaks each edge around its pill so the line does not show through the label', () => { + const { container } = render(); + expect(container.querySelectorAll('.tp-diagram-pill')).toHaveLength(2); + }); }); describe('RenderVsA2ui', () => { @@ -57,4 +62,12 @@ describe('RenderVsA2ui', () => { expect(titles).toContain('@threadplane/a2ui'); expect(titles).toContain('@threadplane/chat'); }); + + it('accents exactly the two surface nodes and wires at least three edges', () => { + const { container } = render(); + const accented = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); + expect(accented).toHaveLength(2); + const edges = container.querySelectorAll('path.tp-diagram-edge'); + expect(edges.length).toBeGreaterThanOrEqual(3); + }); }); From a924099fae2d0af4f9f700403d0fcc0fd16cf5ed Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 17:37:17 -0700 Subject: [PATCH 18/26] docs(website): middleware + telemetry How-it-fits diagrams --- .../getting-started/introduction.mdx | 4 + .../getting-started/introduction.mdx | 4 + .../src/components/docs/MdxRenderer.tsx | 4 + .../docs/diagrams/MiddlewareHowItFits.tsx | 62 +++++++++++++++ .../components/docs/diagrams/RenderVsA2ui.tsx | 2 +- .../docs/diagrams/TelemetryHowItFits.tsx | 79 +++++++++++++++++++ .../docs/diagrams/compositions.spec.tsx | 25 +++++- .../src/components/docs/diagrams/index.ts | 2 + 8 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx create mode 100644 apps/website/src/components/docs/diagrams/TelemetryHowItFits.tsx diff --git a/apps/website/content/docs/middleware/getting-started/introduction.mdx b/apps/website/content/docs/middleware/getting-started/introduction.mdx index 80b601060..a93519c09 100644 --- a/apps/website/content/docs/middleware/getting-started/introduction.mdx +++ b/apps/website/content/docs/middleware/getting-started/introduction.mdx @@ -21,6 +21,10 @@ import { There is no root `@threadplane/middleware` JavaScript entry point. Import from `@threadplane/middleware/langgraph`. +## How it fits + + + ## What it does The LangGraph entry points read a client tool catalog from graph state, convert it into OpenAI function-tool objects, bind those tool stubs onto your chat model, and route client-tool calls to `END` so the browser can execute them. diff --git a/apps/website/content/docs/telemetry/getting-started/introduction.mdx b/apps/website/content/docs/telemetry/getting-started/introduction.mdx index 1f2803f2d..55c357c8c 100644 --- a/apps/website/content/docs/telemetry/getting-started/introduction.mdx +++ b/apps/website/content/docs/telemetry/getting-started/introduction.mdx @@ -10,6 +10,10 @@ The important boundary is simple: Don't treat the browser and Node entry points as interchangeable. They have different runtime assumptions and different privacy defaults. +## How it fits + + + ## Entry points ```ts diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index 133fa4dc3..1f220a461 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -14,6 +14,8 @@ import { AgUiArchitecturePipeline } from './diagrams/AgUiArchitecturePipeline'; import { A2uiMessageFlow } from './diagrams/A2uiMessageFlow'; import { RenderHowItFits } from './diagrams/RenderHowItFits'; import { RenderVsA2ui } from './diagrams/RenderVsA2ui'; +import { MiddlewareHowItFits } from './diagrams/MiddlewareHowItFits'; +import { TelemetryHowItFits } from './diagrams/TelemetryHowItFits'; import rehypePrettyCode from 'rehype-pretty-code'; import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; @@ -47,6 +49,8 @@ const mdxComponents = { A2uiMessageFlow, RenderHowItFits, RenderVsA2ui, + MiddlewareHowItFits, + TelemetryHowItFits, FeatureChips, pre: Pre, // Explicit width/height let the browser reserve the box before the file diff --git a/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx b/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx new file mode 100644 index 000000000..f93f1173e --- /dev/null +++ b/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'middleware-how-it-fits'; + +/** + * The middleware intro's runtime flow, condensed to three stages: the + * browser declares client tools through `@threadplane/chat`, the Python + * `threadplane-middleware` package binds those tool stubs onto the model via + * `bind_client_tools()` (verified against + * `threadplane.middleware.langgraph.__init__`), and the bound model runs + * inside your own LangGraph graph. The `client_tools` pill sits above the + * first edge rather than on it — a horizontal edge with nothing else sharing + * its line doesn't need the pill-break treatment vertical stacks do. + */ +export function MiddlewareHowItFits() { + return ( + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx b/apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx index 27ba36381..4daa0d936 100644 --- a/apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx +++ b/apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx @@ -18,7 +18,7 @@ export function RenderVsA2ui() { slug={SLUG} viewWidth={640} viewHeight={204} - label="Chat detects assistant content and mounts either a json-render spec or an A2UI surface." + label="json-render versus A2UI: your Angular app renders json-render specs directly through @threadplane/render; @threadplane/chat also detects assistant content and mounts either a json-render spec or an A2UI surface. @threadplane/a2ui defines the A2UI v0.9 message and component types." > + + + + + + + + + + + + + + ); +} diff --git a/apps/website/src/components/docs/diagrams/compositions.spec.tsx b/apps/website/src/components/docs/diagrams/compositions.spec.tsx index 95862f0fc..45e5dcca8 100644 --- a/apps/website/src/components/docs/diagrams/compositions.spec.tsx +++ b/apps/website/src/components/docs/diagrams/compositions.spec.tsx @@ -7,6 +7,8 @@ import { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; import { A2uiMessageFlow } from './A2uiMessageFlow'; import { RenderHowItFits } from './RenderHowItFits'; import { RenderVsA2ui } from './RenderVsA2ui'; +import { MiddlewareHowItFits } from './MiddlewareHowItFits'; +import { TelemetryHowItFits } from './TelemetryHowItFits'; /** * Compositions are hand-placed layouts; the spec guards that each mounts, @@ -51,6 +53,7 @@ describe('RenderHowItFits', () => { it('breaks each edge around its pill so the line does not show through the label', () => { const { container } = render(); expect(container.querySelectorAll('.tp-diagram-pill')).toHaveLength(2); + expect(container.querySelectorAll('path.tp-diagram-edge')).toHaveLength(4); }); }); @@ -63,11 +66,29 @@ describe('RenderVsA2ui', () => { expect(titles).toContain('@threadplane/chat'); }); - it('accents exactly the two surface nodes and wires at least three edges', () => { + it('accents exactly the two surface nodes and wires exactly four edges', () => { const { container } = render(); const accented = container.querySelectorAll('g.tp-diagram-node[data-tone="accent"]'); expect(accented).toHaveLength(2); const edges = container.querySelectorAll('path.tp-diagram-edge'); - expect(edges.length).toBeGreaterThanOrEqual(3); + expect(edges).toHaveLength(4); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('Your Angular app'); + }); +}); + +describe('MiddlewareHowItFits', () => { + it('mounts and places the middleware between frontend and graph', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('threadplane-middleware'); + }); +}); + +describe('TelemetryHowItFits', () => { + it('mounts and shows both entry points feeding ingest', () => { + const { container } = render(); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/telemetry'); }); }); diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index 231c62793..9f0cab387 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -7,3 +7,5 @@ export { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline'; export { A2uiMessageFlow } from './A2uiMessageFlow'; export { RenderHowItFits } from './RenderHowItFits'; export { RenderVsA2ui } from './RenderVsA2ui'; +export { MiddlewareHowItFits } from './MiddlewareHowItFits'; +export { TelemetryHowItFits } from './TelemetryHowItFits'; From 0d3f6c4a3480da0edfb7a4eafd4e8ea7b3469dad Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 17:42:00 -0700 Subject: [PATCH 19/26] =?UTF-8?q?fix(website):=20vertical=20middleware=20d?= =?UTF-8?q?iagram=20=E2=80=94=20pill=20no=20longer=20overlaps=20nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/diagrams/MiddlewareHowItFits.tsx | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx b/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx index f93f1173e..ca202c6fe 100644 --- a/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx +++ b/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx @@ -12,46 +12,47 @@ const SLUG = 'middleware-how-it-fits'; * `threadplane-middleware` package binds those tool stubs onto the model via * `bind_client_tools()` (verified against * `threadplane.middleware.langgraph.__init__`), and the bound model runs - * inside your own LangGraph graph. The `client_tools` pill sits above the - * first edge rather than on it — a horizontal edge with nothing else sharing - * its line doesn't need the pill-break treatment vertical stacks do. + * inside your own LangGraph graph. Stacked vertically (matching the kit's + * other how-it-fits diagrams) so the `client_tools` pill has its own 48px + * gap between nodes instead of overlapping either flanking node. */ export function MiddlewareHowItFits() { return ( - - + + + - + Date: Tue, 1 Sep 2026 17:52:12 -0700 Subject: [PATCH 20/26] fix(website): honest telemetry destinations; runtime-neutral middleware node --- .../docs/diagrams/MiddlewareHowItFits.tsx | 13 +-- .../docs/diagrams/TelemetryHowItFits.tsx | 88 +++++++++---------- .../docs/diagrams/compositions.spec.tsx | 18 +++- 3 files changed, 66 insertions(+), 53 deletions(-) diff --git a/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx b/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx index ca202c6fe..4dc5e78ea 100644 --- a/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx +++ b/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx @@ -8,10 +8,11 @@ const SLUG = 'middleware-how-it-fits'; /** * The middleware intro's runtime flow, condensed to three stages: the - * browser declares client tools through `@threadplane/chat`, the Python - * `threadplane-middleware` package binds those tool stubs onto the model via - * `bind_client_tools()` (verified against - * `threadplane.middleware.langgraph.__init__`), and the bound model runs + * browser declares client tools through `@threadplane/chat`, the + * `threadplane-middleware` package binds those tool stubs onto the model — + * runtime-neutral, since it ships both a Python `bind_client_tools()` + * (`packages/threadplane-middleware`) and a TypeScript `bindClientTools()` + * (`libs/middleware/src/langgraph/middleware.ts`) — and the bound model runs * inside your own LangGraph graph. Stacked vertically (matching the kit's * other how-it-fits diagrams) so the `client_tools` pill has its own 48px * gap between nodes instead of overlapping either flanking node. @@ -42,9 +43,9 @@ export function MiddlewareHowItFits() { y={128} w={300} h={64} - eyebrow="Python" + eyebrow="Middleware" title="threadplane-middleware" - meta="bind_client_tools()" + meta="bind_client_tools() · bindClientTools()" tone="accent" /> diff --git a/apps/website/src/components/docs/diagrams/TelemetryHowItFits.tsx b/apps/website/src/components/docs/diagrams/TelemetryHowItFits.tsx index b4af08060..5325574fd 100644 --- a/apps/website/src/components/docs/diagrams/TelemetryHowItFits.tsx +++ b/apps/website/src/components/docs/diagrams/TelemetryHowItFits.tsx @@ -7,69 +7,65 @@ import { DiagramPill } from './DiagramPill'; const SLUG = 'telemetry-how-it-fits'; /** - * A diamond: the shared `@threadplane/telemetry` entry point (environment - * and event helpers, verified against `libs/telemetry/package.json` - * `exports["."]`) underpins both the browser and Node surfaces. Both - * surfaces are genuine entry points a caller imports directly — the intro's - * "Entry points" section lists all three import paths — and, when each is - * enabled or explicitly invoked, both fan back in to the same ingest - * endpoint (`DEFAULT_INGEST` in `libs/telemetry/src/node/client.ts`, and the - * `endpoint`/`sink` delivery path documented for the browser surface). The - * fan-in trunk breaks around its "sampled events" pill rather than drawing a - * continuous line under it. + * `@threadplane/telemetry` has two peer entry points (`./browser`, + * `./node` — verified against `libs/telemetry/package.json` `exports`); the + * browser entry deliberately cannot import the package root (ng-packagr's + * rootDir rule forces it to inline shared helpers — see the comment atop + * `libs/telemetry/src/browser/service.ts`), so there is no root-fans-out-to + * hierarchy to draw. What the two entries share is a destination story: + * - Browser (`libs/telemetry/src/browser/service.ts`) never has a default + * network target. It delivers only through an app-supplied `sink`, + * `endpoint`, or `posthogKey` — otherwise `capture()` is a no-op. + * - Node (`libs/telemetry/src/node/client.ts`) defaults to + * `DEFAULT_INGEST` (`https://threadplane.ai/api/ingest`), overridable via + * `TPLANE_TELEMETRY_INGEST_URL`. + * Both entries sample (browser via `config.sampleRate`, node via + * `TPLANE_TELEMETRY_SAMPLE_RATE`), so sampling is not a node-only trait — + * the branch pills are labeled by destination, not by sampling. Each branch + * breaks around its pill (segment, pill, segment+arrow) rather than drawing + * a continuous line under the label. */ export function TelemetryHowItFits() { return ( - - - - + + + + + + + - - - - - { it('mounts and places the middleware between frontend and graph', () => { const { container } = render(); const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); + expect(titles).toContain('@threadplane/chat'); expect(titles).toContain('threadplane-middleware'); + expect(titles).toContain('Your LangGraph graph'); + }); + + it('breaks the single edge around its pill and wires exactly three edges', () => { + const { container } = render(); + expect(container.querySelectorAll('.tp-diagram-pill')).toHaveLength(1); + expect(container.querySelectorAll('path.tp-diagram-edge')).toHaveLength(3); }); }); describe('TelemetryHowItFits', () => { - it('mounts and shows both entry points feeding ingest', () => { + it('mounts and names the package and both honest destinations', () => { const { container } = render(); const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent); expect(titles).toContain('@threadplane/telemetry'); + expect(titles).toContain('Your sink or endpoint — app-owned'); + expect(titles).toContain('threadplane.ai/api/ingest — default'); + }); + + it('breaks each branch around its pill and wires exactly five edges', () => { + const { container } = render(); + expect(container.querySelectorAll('.tp-diagram-pill')).toHaveLength(2); + expect(container.querySelectorAll('path.tp-diagram-edge')).toHaveLength(5); }); }); From 138cd2098d5c1de5b1ce56ff5082520ee7f5441b Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 17:54:13 -0700 Subject: [PATCH 21/26] feat(website): homepage architecture section with the master stack diagram --- apps/website/src/app/page.tsx | 9 ++++ .../landing/StackDiagramSection.spec.tsx | 23 ++++++++++ .../landing/StackDiagramSection.tsx | 45 +++++++++++++++++++ apps/website/src/styles/landing.css | 32 +++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 apps/website/src/components/landing/StackDiagramSection.spec.tsx create mode 100644 apps/website/src/components/landing/StackDiagramSection.tsx diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index 548c61f88..8a4293c33 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -2,6 +2,7 @@ import { Hero } from '../components/landing/Hero'; 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 { DemoShowcase } from '../components/landing/DemoShowcase'; import { MediumSwitcher } from '../components/landing/MediumSwitcher'; import { SECTION_MEDIA } from '../lib/section-media'; @@ -37,6 +38,14 @@ export default async function HomePage() { + + {/* Interactive demo showcase */}
diff --git a/apps/website/src/components/landing/StackDiagramSection.spec.tsx b/apps/website/src/components/landing/StackDiagramSection.spec.tsx new file mode 100644 index 000000000..88fbcc9ab --- /dev/null +++ b/apps/website/src/components/landing/StackDiagramSection.spec.tsx @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { StackDiagramSection } from './StackDiagramSection'; + +describe('StackDiagramSection', () => { + it('renders heading, body, and a marketing-scaled stack diagram', () => { + const { container, getByText } = render( + + ); + expect(getByText('One contract between your UI and any runtime').tagName).toBe('H2'); + expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('architecture-heading'); + expect(container.querySelector('figure.tp-diagram-figure')?.getAttribute('data-scale')).toBe('marketing'); + }); +}); diff --git a/apps/website/src/components/landing/StackDiagramSection.tsx b/apps/website/src/components/landing/StackDiagramSection.tsx new file mode 100644 index 000000000..88fbbadaa --- /dev/null +++ b/apps/website/src/components/landing/StackDiagramSection.tsx @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +import type { ReactNode } from 'react'; +import { Section } from '../ui/Section'; +import { Container } from '../ui/Container'; +import { Eyebrow } from '../ui/Eyebrow'; +import { StackDiagram, type StackHighlight } from '../docs/diagrams/StackDiagram'; + +interface StackDiagramSectionProps { + id: string; + eyebrow: string; + headline: string; + body: ReactNode; + highlight?: StackHighlight; + caption?: string; +} + +/** + * Homepage architecture section: the canonical stack diagram, widened to the + * marketing scale, framed by a headline + body in the site's landing idiom. + */ +export function StackDiagramSection({ + id, + eyebrow, + headline, + body, + highlight = 'none', + caption, +}: StackDiagramSectionProps) { + return ( +
+ +
+ + {eyebrow} + +

+ {headline} +

+

{body}

+ +
+
+
+ ); +} diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index a2e6a475b..bc56aa609 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1487,3 +1487,35 @@ height: 1px; background: var(--color-border); } + +/* StackDiagramSection — components/landing/StackDiagramSection.tsx */ +.stack-diagram-section { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 12px; +} +.stack-diagram-eyebrow { + margin-bottom: 4px; +} +.stack-diagram-headline { + font-family: var(--font-garamond); + font-size: var(--text-h2); + line-height: var(--text-h2--line-height); + font-weight: 700; + color: var(--color-text-primary); + margin: 0; + letter-spacing: -0.015em; +} +.stack-diagram-body { + font-family: var(--font-inter); + font-size: var(--text-body-lg); + line-height: var(--text-body-lg--line-height); + color: var(--color-text-secondary); + max-width: 620px; + margin: 0 0 12px; +} +.stack-diagram-section .tp-diagram-figure { + width: 100%; +} From 739201dfdfb2bf72c0088e01345d358aef3226cc Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 18:04:32 -0700 Subject: [PATCH 22/26] fix(website): StackDiagramSection reuses SectionHeader; distinct headline --- apps/website/src/app/page.tsx | 2 +- .../landing/StackDiagramSection.spec.tsx | 15 +++++++++++++++ .../components/landing/StackDiagramSection.tsx | 14 +++++++------- apps/website/src/styles/landing.css | 15 +++------------ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index 8a4293c33..cdfdd8a26 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -41,7 +41,7 @@ export default async function HomePage() { diff --git a/apps/website/src/components/landing/StackDiagramSection.spec.tsx b/apps/website/src/components/landing/StackDiagramSection.spec.tsx index 88fbcc9ab..076adfd64 100644 --- a/apps/website/src/components/landing/StackDiagramSection.spec.tsx +++ b/apps/website/src/components/landing/StackDiagramSection.spec.tsx @@ -20,4 +20,19 @@ describe('StackDiagramSection', () => { expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('architecture-heading'); expect(container.querySelector('figure.tp-diagram-figure')?.getAttribute('data-scale')).toBe('marketing'); }); + + it('accents the LangGraph node when highlight="langgraph"', () => { + const { container } = render( + + ); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')); + const langgraphTitle = titles.find((el) => el.textContent === '@threadplane/langgraph'); + expect(langgraphTitle?.closest('g.tp-diagram-node')?.getAttribute('data-tone')).toBe('accent'); + }); }); diff --git a/apps/website/src/components/landing/StackDiagramSection.tsx b/apps/website/src/components/landing/StackDiagramSection.tsx index 88fbbadaa..90f20f4b7 100644 --- a/apps/website/src/components/landing/StackDiagramSection.tsx +++ b/apps/website/src/components/landing/StackDiagramSection.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react'; import { Section } from '../ui/Section'; import { Container } from '../ui/Container'; -import { Eyebrow } from '../ui/Eyebrow'; +import { SectionHeader } from '../ui/SectionHeader'; import { StackDiagram, type StackHighlight } from '../docs/diagrams/StackDiagram'; interface StackDiagramSectionProps { @@ -30,12 +30,12 @@ export function StackDiagramSection({
- - {eyebrow} - -

- {headline} -

+

{body}

diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index bc56aa609..86dbc2d4b 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1496,18 +1496,6 @@ text-align: center; gap: 12px; } -.stack-diagram-eyebrow { - margin-bottom: 4px; -} -.stack-diagram-headline { - font-family: var(--font-garamond); - font-size: var(--text-h2); - line-height: var(--text-h2--line-height); - font-weight: 700; - color: var(--color-text-primary); - margin: 0; - letter-spacing: -0.015em; -} .stack-diagram-body { font-family: var(--font-inter); font-size: var(--text-body-lg); @@ -1516,6 +1504,9 @@ max-width: 620px; margin: 0 0 12px; } +/* Guards against flex-item shrink-to-content: without an explicit width the + figure would collapse to its intrinsic SVG size inside the centered + flex column, instead of filling the container like the surrounding text. */ .stack-diagram-section .tp-diagram-figure { width: 100%; } From a99931c4bb4ca8ad9f6bff8e0b19775008dd3ae5 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 18:06:01 -0700 Subject: [PATCH 23/26] feat(website): stack diagram sections on the adapter landing pages --- apps/website/src/app/ag-ui/page.tsx | 9 +++++++++ apps/website/src/app/langgraph/page.tsx | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/apps/website/src/app/ag-ui/page.tsx b/apps/website/src/app/ag-ui/page.tsx index 43501bd5d..ad76d3ab9 100644 --- a/apps/website/src/app/ag-ui/page.tsx +++ b/apps/website/src/app/ag-ui/page.tsx @@ -10,6 +10,7 @@ import { WhitePaperBlock } from '../../components/landing/WhitePaperBlock'; import { FinalCTA } from '../../components/landing/FinalCTA'; import { MediumSwitcher } from '../../components/landing/MediumSwitcher'; import { BackendsGrid } from '../../components/landing/ag-ui/BackendsGrid'; +import { StackDiagramSection } from '../../components/landing/StackDiagramSection'; import { createPageMetadata, SHORT_POSITIONING_DESCRIPTION } from '../../lib/site-metadata'; import { SECTION_MEDIA } from '../../lib/section-media'; import { buildPanes } from '../../lib/build-panes'; @@ -63,6 +64,14 @@ export default async function AgUiPage() {
+ +
+ + Date: Tue, 1 Sep 2026 18:13:52 -0700 Subject: [PATCH 24/26] =?UTF-8?q?fix(website):=20ag-ui=20section=20copy=20?= =?UTF-8?q?=E2=80=94=20distinct=20angle,=20seven-runtime=20caption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/website/src/app/ag-ui/page.tsx | 7 ++++--- apps/website/src/app/langgraph/page.tsx | 2 +- .../landing/StackDiagramSection.spec.tsx | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/website/src/app/ag-ui/page.tsx b/apps/website/src/app/ag-ui/page.tsx index ad76d3ab9..74aad1c1d 100644 --- a/apps/website/src/app/ag-ui/page.tsx +++ b/apps/website/src/app/ag-ui/page.tsx @@ -66,10 +66,11 @@ export default async function AgUiPage() { { const langgraphTitle = titles.find((el) => el.textContent === '@threadplane/langgraph'); expect(langgraphTitle?.closest('g.tp-diagram-node')?.getAttribute('data-tone')).toBe('accent'); }); + + it('accents the AG-UI node when highlight="ag-ui"', () => { + const { container } = render( + + ); + const titles = Array.from(container.querySelectorAll('.tp-diagram-title')); + const agUiTitle = titles.find((el) => el.textContent === '@threadplane/ag-ui'); + expect(agUiTitle?.closest('g.tp-diagram-node')?.getAttribute('data-tone')).toBe('accent'); + }); }); From 19dec6e45c0bdce6a7bc6bbaeba655769021acb4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 18:22:01 -0700 Subject: [PATCH 25/26] =?UTF-8?q?chore(website):=20final=20polish=20?= =?UTF-8?q?=E2=80=94=20token=20font,=20barrel=20wiring,=20honest=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/chat/getting-started/introduction.mdx | 2 +- apps/website/src/components/docs/MdxRenderer.tsx | 16 +++++++++------- .../src/components/docs/diagrams/DiagramEdge.tsx | 1 + .../components/docs/diagrams/DiagramFrame.tsx | 1 + .../src/components/docs/diagrams/DiagramNode.tsx | 1 + .../src/components/docs/diagrams/DiagramPill.tsx | 1 + .../docs/diagrams/StackDiagram.spec.tsx | 2 +- .../components/docs/diagrams/StackDiagram.tsx | 2 +- .../src/components/docs/diagrams/index.ts | 1 + .../components/landing/StackDiagramSection.tsx | 2 +- apps/website/src/styles/docs.css | 2 +- 11 files changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/website/content/docs/chat/getting-started/introduction.mdx b/apps/website/content/docs/chat/getting-started/introduction.mdx index a2fa15d16..d0a51debf 100644 --- a/apps/website/content/docs/chat/getting-started/introduction.mdx +++ b/apps/website/content/docs/chat/getting-started/introduction.mdx @@ -49,7 +49,7 @@ Compositions are opinionated, styled components that combine primitives into rea ## How the Stack Fits Together -`@threadplane/chat` sits between your application and two other Threadplane libraries, on either side of the seam shown in the diagram above. +`@threadplane/chat` sits between your application and the adapters below the Agent-contract seam shown above; alongside it, `@threadplane/render` turns specs in assistant messages into your components. - **`@threadplane/langgraph`** provides `provideAgent()` and `injectAgent()` returning a `LangGraphAgent`, which satisfies the `Agent` contract consumed by `@threadplane/chat`. The base `Agent` contract exposes reactive Signals for `messages()`, `status()`, `isLoading()`, `error()`, `toolCalls()`, and `state()`, plus optional `interrupt()` and `subagents()`. Adapters can layer on more: LangGraph implements `AgentWithHistory`, adding `history()` for checkpoint time-travel. diff --git a/apps/website/src/components/docs/MdxRenderer.tsx b/apps/website/src/components/docs/MdxRenderer.tsx index 1f220a461..397ff687b 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -9,13 +9,15 @@ import { Pre } from './mdx/CodeBlock'; import { FeatureChips } from './mdx/FeatureChips'; import { mdxHeadingComponents } from './mdx/headings'; import { ArchFlowDiagram } from './ArchFlowDiagram'; -import { StackDiagram } from './diagrams/StackDiagram'; -import { AgUiArchitecturePipeline } from './diagrams/AgUiArchitecturePipeline'; -import { A2uiMessageFlow } from './diagrams/A2uiMessageFlow'; -import { RenderHowItFits } from './diagrams/RenderHowItFits'; -import { RenderVsA2ui } from './diagrams/RenderVsA2ui'; -import { MiddlewareHowItFits } from './diagrams/MiddlewareHowItFits'; -import { TelemetryHowItFits } from './diagrams/TelemetryHowItFits'; +import { + StackDiagram, + AgUiArchitecturePipeline, + A2uiMessageFlow, + RenderHowItFits, + RenderVsA2ui, + MiddlewareHowItFits, + TelemetryHowItFits, +} from './diagrams'; import rehypePrettyCode from 'rehype-pretty-code'; import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; diff --git a/apps/website/src/components/docs/diagrams/DiagramEdge.tsx b/apps/website/src/components/docs/diagrams/DiagramEdge.tsx index c16a2565a..3757538e8 100644 --- a/apps/website/src/components/docs/diagrams/DiagramEdge.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramEdge.tsx @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT type DiagramEdgeProps = { /** SVG path data; orthogonal segments (H/V) preferred. Arrowheads point along path direction — reverse `d` for a reversed arrow. */ d: string; diff --git a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx index 26a832fd5..72d42bb7b 100644 --- a/apps/website/src/components/docs/diagrams/DiagramFrame.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT import type { ReactNode } from 'react'; interface DiagramFrameProps { diff --git a/apps/website/src/components/docs/diagrams/DiagramNode.tsx b/apps/website/src/components/docs/diagrams/DiagramNode.tsx index 7e3ce52a6..da0a66d6c 100644 --- a/apps/website/src/components/docs/diagrams/DiagramNode.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramNode.tsx @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT interface DiagramNodeProps { x: number; y: number; diff --git a/apps/website/src/components/docs/diagrams/DiagramPill.tsx b/apps/website/src/components/docs/diagrams/DiagramPill.tsx index ef242e992..2e9b7ae55 100644 --- a/apps/website/src/components/docs/diagrams/DiagramPill.tsx +++ b/apps/website/src/components/docs/diagrams/DiagramPill.tsx @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT interface DiagramPillProps { /** Center of the pill. */ cx: number; diff --git a/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx b/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx index aeaa17a56..5ddd6e622 100644 --- a/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx +++ b/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx @@ -33,7 +33,7 @@ describe('StackDiagram', () => { it('highlight=runtimes accents the backend row', () => { const { container } = render(); expect(toneOf(container, 'LangGraph Platform')).toBe('accent'); - expect(toneOf(container, 'CrewAI · Mastra · MS Agent Fwk · Strands')).toBe('accent'); + expect(toneOf(container, 'CrewAI · Mastra · Agent Fwk · Strands · …')).toBe('accent'); }); it('highlight=contract accents both adapters', () => { diff --git a/apps/website/src/components/docs/diagrams/StackDiagram.tsx b/apps/website/src/components/docs/diagrams/StackDiagram.tsx index c5cb50905..fb9b1a570 100644 --- a/apps/website/src/components/docs/diagrams/StackDiagram.tsx +++ b/apps/website/src/components/docs/diagrams/StackDiagram.tsx @@ -79,7 +79,7 @@ export function StackDiagram({ highlight = 'none', caption, scale = 'docs' }: St y={284} w={240} h={40} - title="CrewAI · Mastra · MS Agent Fwk · Strands" + title="CrewAI · Mastra · Agent Fwk · Strands · …" align="middle" titleStyle="sans" tone={backends ? 'accent' : 'dim'} diff --git a/apps/website/src/components/docs/diagrams/index.ts b/apps/website/src/components/docs/diagrams/index.ts index 9f0cab387..a17101f51 100644 --- a/apps/website/src/components/docs/diagrams/index.ts +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT export { DiagramFrame } from './DiagramFrame'; export { DiagramNode } from './DiagramNode'; export { DiagramEdge } from './DiagramEdge'; diff --git a/apps/website/src/components/landing/StackDiagramSection.tsx b/apps/website/src/components/landing/StackDiagramSection.tsx index 90f20f4b7..d8ec4f98f 100644 --- a/apps/website/src/components/landing/StackDiagramSection.tsx +++ b/apps/website/src/components/landing/StackDiagramSection.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from 'react'; import { Section } from '../ui/Section'; import { Container } from '../ui/Container'; import { SectionHeader } from '../ui/SectionHeader'; -import { StackDiagram, type StackHighlight } from '../docs/diagrams/StackDiagram'; +import { StackDiagram, type StackHighlight } from '../docs/diagrams'; interface StackDiagramSectionProps { id: string; diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index 7e20e1328..65e132962 100644 --- a/apps/website/src/styles/docs.css +++ b/apps/website/src/styles/docs.css @@ -366,7 +366,7 @@ body:has([data-website-workspace-host]) { flex-shrink: 0; } .mdx-callout-title { - font-family: Inter, system-ui, sans-serif; + font-family: var(--font-inter); font-size: 13px; font-weight: 600; color: var(--callout-tone-text); From e67f1ba51863a918757463e9d042c8f8412a2117 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 1 Sep 2026 20:12:00 -0700 Subject: [PATCH 26/26] test(website): gate the permalink-glyph e2e on workspace hydration The #949 workspace shell re-renders the docs article on hydration; on a cold CI dev server the computed ::before content reads as empty before that settles. Same data-hydrated gate #949 added to the control-plane tests, plus a poll on the computed glyph. Red on main at b7b582e8. Co-Authored-By: Claude Fable 5 --- apps/website/e2e/docs.spec.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/website/e2e/docs.spec.ts b/apps/website/e2e/docs.spec.ts index 594edc15c..03801fd28 100644 --- a/apps/website/e2e/docs.spec.ts +++ b/apps/website/e2e/docs.spec.ts @@ -117,6 +117,15 @@ test.describe('Docs slug page', () => { test('heading permalinks carry no glyph in the text, only a CSS ::before', async ({ page }) => { await page.goto(route); + + // The workspace shell re-renders the article on hydration; before that, + // computed styles on its subtree can read as empty on a cold CI server. + // Same gate the control-plane tests above use. + await expect(page.locator('[data-workspace-shell]')).toHaveAttribute( + 'data-hydrated', + 'true', + ); + const h2 = page.locator('article h2').first(); await expect(h2).toBeVisible(); @@ -127,10 +136,14 @@ test.describe('Docs slug page', () => { // ...which means the visible affordance hangs entirely on one CSS rule // (`.docs-prose h2 .heading-anchor::before` in global.css). jsdom cannot // resolve pseudo-element content, so this is the only place it is guarded. + // Poll: stylesheet application can trail hydration on a cold dev server. const anchor = h2.locator('a.heading-anchor'); await expect(anchor).toHaveCount(1); - const glyph = await anchor.evaluate((el) => getComputedStyle(el, '::before').content); - expect(glyph).toBe('"#"'); + await expect + .poll(() => + anchor.evaluate((el) => getComputedStyle(el, '::before').content) + ) + .toBe('"#"'); }); test('breadcrumb renders exactly once', async ({ page }) => {