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..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 da7473909..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/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/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/content/docs/ag-ui/getting-started/introduction.mdx b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx index 1504eb95f..c4b4bc279 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 @@ -57,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 ``. 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/content/docs/chat/getting-started/introduction.mdx b/apps/website/content/docs/chat/getting-started/introduction.mdx index 283daa5fe..d0a51debf 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**. @@ -42,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 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/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/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/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/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/content/docs/runtimes/getting-started/introduction.mdx b/apps/website/content/docs/runtimes/getting-started/introduction.mdx index 91e98d18a..3a43f201a 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 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/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 }) => { diff --git a/apps/website/src/app/ag-ui/page.tsx b/apps/website/src/app/ag-ui/page.tsx index 43501bd5d..74aad1c1d 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,15 @@ export default async function AgUiPage() { + + + + + + {/* Interactive demo showcase */}
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 9a6d26bce..397ff687b 100644 --- a/apps/website/src/components/docs/MdxRenderer.tsx +++ b/apps/website/src/components/docs/MdxRenderer.tsx @@ -9,7 +9,15 @@ 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, + AgUiArchitecturePipeline, + A2uiMessageFlow, + RenderHowItFits, + RenderVsA2ui, + MiddlewareHowItFits, + TelemetryHowItFits, +} from './diagrams'; import rehypePrettyCode from 'rehype-pretty-code'; import rehypeSlug from 'rehype-slug'; import remarkGfm from 'remark-gfm'; @@ -38,7 +46,13 @@ const mdxComponents = { CardGroup, CodeGroup, ArchFlowDiagram, - AgUiArchDiagram, + StackDiagram, + AgUiArchitecturePipeline, + 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/A2uiMessageFlow.tsx b/apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx new file mode 100644 index 000000000..9f1034d94 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx @@ -0,0 +1,55 @@ +// 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 new file mode 100644 index 000000000..f499ba460 --- /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/DiagramEdge.tsx b/apps/website/src/components/docs/diagrams/DiagramEdge.tsx new file mode 100644 index 000000000..3757538e8 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/DiagramEdge.tsx @@ -0,0 +1,23 @@ +// 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; +} & ( + | { + /** 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 new file mode 100644 index 000000000..72d42bb7b --- /dev/null +++ b/apps/website/src/components/docs/diagrams/DiagramFrame.tsx @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +import type { ReactNode } from 'react'; + +interface DiagramFrameProps { + /** + * 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; + /** 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; +} + +/** + * 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, + 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..da0a66d6c --- /dev/null +++ b/apps/website/src/components/docs/diagrams/DiagramNode.tsx @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +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; + +/** + * 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({ + 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..2e9b7ae55 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/DiagramPill.tsx @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +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/MiddlewareHowItFits.tsx b/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx new file mode 100644 index 000000000..4dc5e78ea --- /dev/null +++ b/apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx @@ -0,0 +1,64 @@ +// 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 + * `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. + */ +export function MiddlewareHowItFits() { + return ( + + + + + + + + + + ); +} 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..dd27b87ad --- /dev/null +++ b/apps/website/src/components/docs/diagrams/RenderHowItFits.tsx @@ -0,0 +1,60 @@ +// 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..4daa0d936 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx @@ -0,0 +1,69 @@ +// 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/render` + * is usable directly from your own app (no chat required), while + * `@threadplane/chat` also detects assistant content and streams it into + * whichever surface applies — a json-render spec on one side, an A2UI + * surface on the other. + */ +export function RenderVsA2ui() { + return ( + + + + + + + + + + + ); +} 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..5ddd6e622 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx @@ -0,0 +1,60 @@ +// 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 · 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', () => { + 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..fb9b1a570 --- /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/TelemetryHowItFits.tsx b/apps/website/src/components/docs/diagrams/TelemetryHowItFits.tsx new file mode 100644 index 000000000..5325574fd --- /dev/null +++ b/apps/website/src/components/docs/diagrams/TelemetryHowItFits.tsx @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +import { DiagramFrame } from './DiagramFrame'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramPill } from './DiagramPill'; + +const SLUG = 'telemetry-how-it-fits'; + +/** + * `@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 ( + + + + + + + + + + + + + ); +} 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..259a37dff --- /dev/null +++ b/apps/website/src/components/docs/diagrams/compositions.spec.tsx @@ -0,0 +1,110 @@ +// 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'; +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, + * 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'); + }); +}); + +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()'); + }); + + 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()'); + }); +}); + +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'); + }); + + 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); + }); +}); + +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'); + }); + + 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).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/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 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); + }); +}); 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..a17101f51 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/index.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +export { DiagramFrame } from './DiagramFrame'; +export { DiagramNode } from './DiagramNode'; +export { DiagramEdge } from './DiagramEdge'; +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'; +export { MiddlewareHowItFits } from './MiddlewareHowItFits'; +export { TelemetryHowItFits } from './TelemetryHowItFits'; 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..64ec1e7b7 --- /dev/null +++ b/apps/website/src/components/docs/diagrams/primitives.spec.tsx @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { 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('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( + + + + ); + 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(title?.getAttribute('x')).toBe('100'); + 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/components/docs/mdx/Callout.spec.tsx b/apps/website/src/components/docs/mdx/Callout.spec.tsx new file mode 100644 index 000000000..43aba4ec1 --- /dev/null +++ b/apps/website/src/components/docs/mdx/Callout.spec.tsx @@ -0,0 +1,42 @@ +// 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(); + 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([ + ['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..ce93696b1 100644 --- a/apps/website/src/components/docs/mdx/Callout.tsx +++ b/apps/website/src/components/docs/mdx/Callout.tsx @@ -8,21 +8,61 @@ 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]}: : null} + {title ?? KIND_LABEL[type]} +
{children}
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..f817a9242 --- /dev/null +++ b/apps/website/src/components/landing/StackDiagramSection.spec.tsx @@ -0,0 +1,53 @@ +// 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'); + }); + + 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'); + }); + + 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'); + }); +}); diff --git a/apps/website/src/components/landing/StackDiagramSection.tsx b/apps/website/src/components/landing/StackDiagramSection.tsx new file mode 100644 index 000000000..d8ec4f98f --- /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 { SectionHeader } from '../ui/SectionHeader'; +import { StackDiagram, type StackHighlight } from '../docs/diagrams'; + +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 ( +
+ +
+ +

{body}

+ +
+
+
+ ); +} diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css index b9d171ac9..65e132962 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; @@ -425,74 +328,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 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"] { - 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-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 { @@ -765,6 +652,84 @@ 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); +} /* * DocsSidebar diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index a2e6a475b..86dbc2d4b 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1487,3 +1487,26 @@ 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-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; +} +/* 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%; +} 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', () => { 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.) 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.