From de6d2bc25c0e9f88ba3e9b54018dbf2242c7506b Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 7 Sep 2026 08:07:14 -0700 Subject: [PATCH 1/8] =?UTF-8?q?docs:=20spec=20and=20plan=20=E2=80=94=20ent?= =?UTF-8?q?erprise=20architecture=20diagram=20on=20the=20homepage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- ...6-09-07-enterprise-architecture-diagram.md | 35 ++++++++++++ ...-enterprise-architecture-diagram-design.md | 57 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-07-enterprise-architecture-diagram.md create mode 100644 docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md diff --git a/docs/superpowers/plans/2026-09-07-enterprise-architecture-diagram.md b/docs/superpowers/plans/2026-09-07-enterprise-architecture-diagram.md new file mode 100644 index 000000000..263aa58e3 --- /dev/null +++ b/docs/superpowers/plans/2026-09-07-enterprise-architecture-diagram.md @@ -0,0 +1,35 @@ +# Enterprise Architecture Diagram 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 homepage's "Where Threadplane fits" scope table with the researched enterprise architecture diagram, pixel-aligned on an 8px grid, every card linking to its docs page, verified by a geometry unit test and a text-overflow e2e, and shown to the user before merge. + +**Architecture:** Geometry and copy live in a typed data module so the component, the unit test, and the e2e read the same numbers. The component is a server-rendered SVG inside the kit's `DiagramFrame`. The mockup generator in this session's scratchpad is the reference; the data module ports its coordinates verbatim. + +**Tech Stack:** Next.js server components, the docs diagram kit (`DiagramFrame`, `DiagramSection`), Vitest, Playwright. + +**Spec:** `docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md`. + +--- + +### Task 1: The data module +**Files:** create `apps/website/src/lib/architecture-diagram.ts`, `architecture-diagram.spec.ts`. +- [ ] Types: `Zone { id, label, owner, y, height, fill, stroke, mark? }`, `Card { id, zoneId, x, y, width, height, title, icon?, tint?, href, tag?, rows: Row[] }`, `Row` = `{ kind: 'chips', y, chips: { mark?: string; label: string; href?: string }[] } | { kind: 'text', y, text } | { kind: 'mono', y, text } | { kind: 'caps', y, caps: { icon, label, href }[] }`, `Arrow { x, y1, y2, caption }`. Constants `VIEW = { width: 1280, height: 1088 }`, `GRID = 8`, `MAJOR = 40`, `ZONE_PAD = 32`, `CARD_GAP = 40`. +- [ ] Data: port every coordinate from the approved mockup (three zones at y 40/360/624, heights 280/224/424; cards per spec §3 with the mockup's x/y/width/height; arrows at x 444 between zones with their captions). +- [ ] Spec: every zone and card coordinate divisible by 8; cards inside their zone with ≥ 24px inset; no two cards in a zone overlap; gaps between horizontally adjacent cards equal `CARD_GAP`; every href starts with `/docs/` and the corresponding `apps/website/content/docs/.mdx` exists (or `/render` → `src/app/render/page.tsx`); every `mark` names a file under `apps/website/public/logos/`. +- [ ] Commit `feat(website): architecture diagram geometry and copy as data`. + +### Task 2: The component and the section +**Files:** create `apps/website/src/components/landing/EnterpriseArchitecture.tsx`, `EnterpriseArchitecture.spec.tsx`; modify `apps/website/src/app/page.tsx`, `apps/website/src/lib/positioning.ts` (+spec), `apps/website/src/styles/landing.css`; delete `ScopeTable.tsx` + spec and `FINAL_MILE_*`. +- [ ] Component: `DiagramSection id="architecture" eyebrow="Architecture" headline=… body=…` wrapping `DiagramFrame slug="enterprise-architecture" viewWidth=1280 viewHeight=1088 scale="marketing" label=…`; draws zones, cards, rows, arrows from the data module; icons as inline paths keyed by name; logos as ``; each card an `` with a "docs ↗" text at top-right (none on the Threadplane card); `data-grid` attribute on the figure toggles a hidden 8/40px grid `` (rendered only when the attribute is present, for review). +- [ ] CSS: `.arch-*` text classes mirroring the mockup's type ramp (title 15/600, body 12.5, chip 11.5/500, mono 11.5, zone label 11.5 tracked, owner 11, tag 10.5 tracked blue, docs 10.5 blue), fills via tokens where they exist (`--color-text-primary`, `--color-text-secondary`, border tokens) and literal gradient stops otherwise; `.tp-diagram-figure` minimum width 1024px inside the landing scroll frame. +- [ ] Spec: renders 3 zones, 10 cards, 12+ links with the data module's hrefs; the `data-grid` group is absent by default and present with the attribute; the heading id is `architecture-heading`. +- [ ] `page.tsx`: `` where `` was; remove `ScopeTable`, `FINAL_MILE_*`, the `.scope-table*` CSS; update `positioning.spec.ts` and `e2e/website.spec.ts` (`why-heading` → `architecture-heading`). +- [ ] Commit `feat(website): enterprise architecture diagram replaces the scope table`. + +### Task 3: e2e and visual proof +**Files:** create `apps/website/e2e/home-architecture.spec.ts`. +- [ ] At 1440×900: for every `text` inside `[data-diagram="enterprise-architecture"]`, `getBBox()` lies inside the nearest ancestor card rect (from `data-card` on the group) with ≥ 6px margin; every chip rect inside its card; every `image` has a non-zero bbox; the section heading text and the 12 card hrefs match. At 390×844: the figure's scroll width ≥ 1024 and `scrollWidth > clientWidth`. +- [ ] Render PNGs at 1440 and 390 (Playwright) for the user, plus one with the alignment grid on. +- [ ] Run `npx nx test website`, lint, `npx nx build website`, `npx nx e2e website -- --grep "homepage architecture|landing page|homepage stage"`. +- [ ] Commit `test(website): architecture diagram geometry and overflow guards`; push; open the PR WITHOUT auto-merge; show the user the frames; merge only on their word. diff --git a/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md b/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md new file mode 100644 index 000000000..9d0f2f4f8 --- /dev/null +++ b/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md @@ -0,0 +1,57 @@ +# Enterprise architecture diagram on the homepage + +**Date:** 2026-09-07 +**Status:** Design approved in brainstorming (iterated through six mockups with the user); implementation gated on the user's visual sign-off before merge. +**Surface:** `apps/website` only. +**Replaces:** the "Where Threadplane fits" section (`ScopeTable`, section id `why`) on the homepage. `ScopeTable` and the `FINAL_MILE_*` copy leave with it. + +## 1. Why + +The homepage tells a developer what Threadplane does (the stage) but not where it sits in the estate they already run. Architects need one picture: three zones they recognise, one highlighted box inside their own application, and every named product in the place it actually occupies. The old scope table answered "what do we add" as prose; the diagram answers "where does it go" spatially, which is the final-mile argument made visible. + +## 2. Decisions + +| Decision | Choice | +|---|---| +| Framing | Layers and boundaries (framing A): three horizontal zones, your Angular application, your platform edge, the agent platform, with Threadplane the only highlighted box, inside your application zone. | +| Source of truth for contents | The docs. Every box links to the page that backs its wording; nothing on the diagram claims what a docs page does not say. Named third-party products appear as examples of a role, never as integrations Threadplane claims. | +| Marks | Only marks the site already holds under `apps/website/public/logos/` (README documents their sources). Generic enterprise boxes get a consistent line icon in a tinted badge, never a fake logo. LangSmith is a text chip until its mark is sourced under the same rules. | +| Precision | Coordinates on an 8px grid with 40px majors; a hidden alignment grid is part of the component (`data-grid` attribute toggles it) for review; a unit test asserts every rect lands on the grid; an e2e measures every text run and asserts it is inside its box. | +| Colour | Tinted gradient zones (blue-violet application, neutral edge, green-to-blue platform), white cards with a faint vertical gradient, a light blue tint with a blue hairline for Threadplane, the site's dot grid behind. | +| Interaction | Every card is a link to its docs page with a "docs ↗" affordance at the top-right; no hover states beyond the link. No animation. | +| Placement | Where `ScopeTable` was: between Reliability and the stage. Eyebrow "Architecture", headline "Where Threadplane fits in your agent platform.", body: "One highlighted box inside your Angular application. Everything else is yours or your runtime's, and the docs say what crosses each line." | +| Responsive | The SVG scales with its container down to 1024px; below that it scrolls horizontally inside the diagram frame at a 1024px minimum width (the kit's scroll-shadow treatment), so text never drops below legibility. | + +## 3. Contents (verbatim, with the source page each links to) + +**Zone 1, YOUR ANGULAR APPLICATION (Angular mark), "you own this zone".** +- **Threadplane** card, tag THE FINAL MILE; capabilities as icon badges, each a link: Chat (`/docs/chat/components/chat`), Interrupts (`/docs/langgraph/guides/interrupts`), Threads (`/docs/langgraph/guides/persistence`), Generative UI (`/docs/chat/guides/generative-ui`), Client tools (`/docs/chat/guides/client-tools`); mono line `@threadplane/chat · render · langgraph · ag-ui · middleware`; chips A2UI v0.9 (Google mark) and json-render (Vercel mark); caption "generative UI on two open standards · one Agent contract for every adapter". Card links to `/docs/chat/getting-started/introduction`. +- **Your components** (layers icon): "design system · tool views · client-tool handlers", "pages · routing · state · your APM on status() and error()", "unchanged by Threadplane". Links `/docs/chat/guides/client-tools`. +- Arrow to zone 2, caption "relative apiUrl · POST + SSE via the LangGraph SDK" (langgraph-basics, angular-signals, deployment). + +**Zone 2, YOUR PLATFORM EDGE, "you own this zone".** +- **Same-origin proxy or API gateway** (gateway icon): chips Azure API Management, Amazon API Gateway, Apigee; line "adds the deployment credentials · forwards user identity · keys never reach the browser". Links `/docs/langgraph/guides/deployment`. +- **Identity & session** (key icon): chip Microsoft Entra ID; line "or Okta · session as HTTP-only cookies". Links `/docs/langgraph/guides/deployment`. +- Arrow to zone 3, caption "credentials added server-side · CORS on the runtime". + +**Zone 3, AGENT PLATFORM, "your runtime owns this zone".** +- **Agent runtime** (cpu icon): chips LangGraph Platform, AG-UI; Strands (AWS mark), Agent Framework (Microsoft mark), Mastra. Links `/docs/langgraph/getting-started/introduction`. +- **Models** (sparkle icon): chips Azure OpenAI, Amazon Bedrock, Vertex AI; OpenAI and Anthropic marks as badges. Links `/docs/runtimes/getting-started/introduction`. +- **Tools · MCP · data** (plug icon): "server tools run here, against your systems", "client tools round-trip to the browser". Links `/docs/middleware/getting-started/introduction`. +- **Observability** (trace icon): text chip LangSmith; "traces every run · evals · token cost". Links `/docs/langgraph/guides/deployment`. +- **Durable state** (database icon): "checkpoints at every super-step, keyed by thread · platform-managed, or Postgres / SQLite when you embed the graph", "exposed by Threadplane, never faked". Links `/docs/langgraph/guides/persistence`. + +## 4. Component + +`apps/website/src/components/landing/EnterpriseArchitecture.tsx`, a server component rendering one `` inside the kit's `DiagramFrame` at `scale="marketing"`, framed by `DiagramSection`. Logos are `` referencing the public files. The diagram's geometry lives in a data module, `apps/website/src/lib/architecture-diagram.ts`, as typed zone/card/chip records so the tests can read the same numbers the component draws. The line icons are inline paths in the component (five to seven, matching the mockup). + +## 5. Verification + +- **Unit:** every zone and card rect has `x, y, width, height` divisible by 8, zone padding is 32 and card gaps 40, no two cards overlap, and every card sits inside its zone (from the data module). The component renders 12 links whose hrefs each resolve to an existing docs page or route (the spec reads `apps/website/content/docs` and `src/app`). Public-copy scan stays green. +- **e2e (`home-architecture.spec.ts`):** at 1440×900 every `` in the diagram has a `getBBox()` inside its parent card's rect with 8px of margin, every chip rect is inside its card, every `` loaded (`naturalWidth`/complete via the parent's `getBBox` non-zero), and the section's links have the expected hrefs. At 390px the frame scrolls horizontally and the SVG is at least 1024px wide. +- **Visual sign-off:** rendered frames at 1440 and 390 shown to the user before the PR merges; the PR is opened without auto-merge. +- **Spine test:** the homepage heading order replaces `why-heading` with `architecture-heading`. + +## 6. Out of scope + +Hover interactions, a mobile-specific stacked variant, sourcing the LangSmith mark, changes to the docs pages linked. From d9b271ed4007af56ad8bb781457962fa96e00c6d Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 7 Sep 2026 08:17:16 -0700 Subject: [PATCH 2/8] feat(website): enterprise architecture diagram replaces the scope table Three zones (your Angular application, your platform edge, the agent platform) with Threadplane the one highlighted card inside your own zone. Every card links to the docs page that backs its wording; third-party products appear as examples of a role, never as claimed integrations. Geometry and copy are one data module read by the component, a unit spec that pins every rectangle to the 8px grid inside its zone, and an e2e that measures every rendered text run against its card. A hidden 8/40px alignment grid can be rendered for review. Cards whose rows carry their own links (the Threadplane card's capabilities) are groups with a title link, since anchors do not nest. Co-Authored-By: Claude Fable 5.1 --- apps/website/e2e/home-architecture.spec.ts | 103 ++++ apps/website/e2e/website.spec.ts | 2 +- apps/website/src/app/page.tsx | 4 +- .../landing/EnterpriseArchitecture.spec.tsx | 64 +++ .../landing/EnterpriseArchitecture.tsx | 443 +++++++++++++++++ .../components/landing/ScopeTable.spec.tsx | 18 - .../src/components/landing/ScopeTable.tsx | 63 --- .../src/lib/architecture-diagram.spec.ts | 142 ++++++ apps/website/src/lib/architecture-diagram.ts | 462 ++++++++++++++++++ apps/website/src/lib/positioning.spec.ts | 6 - apps/website/src/lib/positioning.ts | 3 - apps/website/src/styles/landing.css | 155 ++++-- 12 files changed, 1331 insertions(+), 134 deletions(-) create mode 100644 apps/website/e2e/home-architecture.spec.ts create mode 100644 apps/website/src/components/landing/EnterpriseArchitecture.spec.tsx create mode 100644 apps/website/src/components/landing/EnterpriseArchitecture.tsx delete mode 100644 apps/website/src/components/landing/ScopeTable.spec.tsx delete mode 100644 apps/website/src/components/landing/ScopeTable.tsx create mode 100644 apps/website/src/lib/architecture-diagram.spec.ts create mode 100644 apps/website/src/lib/architecture-diagram.ts diff --git a/apps/website/e2e/home-architecture.spec.ts b/apps/website/e2e/home-architecture.spec.ts new file mode 100644 index 000000000..8a2f1a312 --- /dev/null +++ b/apps/website/e2e/home-architecture.spec.ts @@ -0,0 +1,103 @@ +import { test, expect, type Page } from '@playwright/test'; +import { CARDS, diagramHrefs } from '../src/lib/architecture-diagram'; + +const DIAGRAM = '[data-diagram="enterprise-architecture"]'; + +/** + * Measures every rendered text run and chip in the diagram against the card + * that owns it, in viewBox units (getBBox reports user space, so the numbers + * compare directly with lib/architecture-diagram.ts). The unit spec proves + * the boxes are on the grid; this proves the type set inside them fits. + */ +async function overflowReport(page: Page) { + return page.evaluate((sel) => { + const issues: string[] = []; + for (const a of document.querySelectorAll( + `${sel} [data-card]` + )) { + const g = a.querySelector('[data-card-rect]'); + if (!g) continue; + const cx = +g.dataset['x']!; + const cy = +g.dataset['y']!; + const cw = +g.dataset['w']!; + const ch = +g.dataset['h']!; + for (const t of a.querySelectorAll('text')) { + const b = t.getBBox(); + if ( + b.x < cx + 6 || + b.x + b.width > cx + cw - 6 || + b.y < cy + 4 || + b.y + b.height > cy + ch - 4 + ) { + issues.push( + `${a.dataset['card']}: "${t.textContent?.slice( + 0, + 40 + )}" ${Math.round(b.x)}..${Math.round(b.x + b.width)} vs ${cx}..${ + cx + cw + }` + ); + } + } + for (const r of a.querySelectorAll('[data-chip] rect')) { + const b = r.getBBox(); + if (b.x + b.width > cx + cw - 6 || b.y + b.height > cy + ch - 8) { + issues.push(`${a.dataset['card']}: chip past the card edge`); + } + } + } + const images = [ + ...document.querySelectorAll(`${sel} image`), + ].map((i) => i.getBBox().width); + return { issues, images }; + }, DIAGRAM); +} + +test.describe('homepage architecture', () => { + test('replaces the scope table, links every card to its docs page, and keeps every text run inside its card', async ({ + page, + }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto('/'); + await expect(page.locator('#architecture-heading')).toHaveText( + 'Where Threadplane fits in your agent platform.' + ); + await expect(page.locator('#why-heading')).toHaveCount(0); + const cards = page.locator(`${DIAGRAM} [data-card]`); + await expect(cards).toHaveCount(CARDS.length); + for (const c of CARDS) { + const card = page.locator(`${DIAGRAM} [data-card="${c.id}"]`); + const link = + (await card.evaluate((el) => el.tagName.toLowerCase())) === 'a' + ? card + : card.locator('a.arch-title-link'); + await expect(link).toHaveAttribute('href', c.href); + } + const hrefs = await page + .locator(`${DIAGRAM} a[href]`) + .evaluateAll((els) => els.map((e) => e.getAttribute('href'))); + for (const href of diagramHrefs()) expect(hrefs).toContain(href); + + // Fonts must be loaded before measuring, or a fallback face lies about widths. + await page.evaluate(() => document.fonts.ready); + const report = await overflowReport(page); + expect(report.issues, report.issues.join('\n')).toEqual([]); + expect(report.images.length).toBeGreaterThanOrEqual(15); + for (const w of report.images) expect(w).toBeGreaterThan(0); + }); + + test('scrolls horizontally on a phone instead of shrinking the type', async ({ + page, + }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto('/'); + const fig = page.locator(`${DIAGRAM} .tp-diagram-figure`); + await fig.scrollIntoViewIfNeeded(); + const size = await fig.evaluate((el) => ({ + scroll: el.scrollWidth, + client: el.clientWidth, + })); + expect(size.scroll).toBeGreaterThanOrEqual(1024); + expect(size.scroll).toBeGreaterThan(size.client); + }); +}); diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index 728694006..2580117c2 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -50,7 +50,7 @@ test('landing page renders the spine in order (live-stage spec §3)', async ({ p const ids = [ 'hero-heading', 'proof-heading', - 'why-heading', + 'architecture-heading', 'stage-heading', 'final-cta-heading', 'pilot-heading', diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx index d85b5e4c1..5d6c39df1 100644 --- a/apps/website/src/app/page.tsx +++ b/apps/website/src/app/page.tsx @@ -1,6 +1,6 @@ import { Hero } from '../components/landing/Hero'; import { Reliability } from '../components/landing/Reliability'; -import { ScopeTable } from '../components/landing/ScopeTable'; +import { EnterpriseArchitecture } from '../components/landing/EnterpriseArchitecture'; import { Stage } from '../components/landing/Stage'; import { TeamsBlock } from '../components/landing/TeamsBlock'; import { HomeFAQ } from '../components/landing/HomeFAQ'; @@ -34,7 +34,7 @@ export default function HomePage() { <> - + {/* The four capability beats (stream, persist, approve, render): stills by default, the pinned live act on wide, motion-tolerant viewports diff --git a/apps/website/src/components/landing/EnterpriseArchitecture.spec.tsx b/apps/website/src/components/landing/EnterpriseArchitecture.spec.tsx new file mode 100644 index 000000000..3c1524784 --- /dev/null +++ b/apps/website/src/components/landing/EnterpriseArchitecture.spec.tsx @@ -0,0 +1,64 @@ +// @vitest-environment jsdom +import React from 'react'; +import { render } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { + EnterpriseArchitecture, + ARCHITECTURE_HEADLINE, +} from './EnterpriseArchitecture'; +import { CARDS, ZONES, diagramHrefs } from '../../lib/architecture-diagram'; + +describe('EnterpriseArchitecture', () => { + it('renders the section with its heading id, every zone and card, and a link per card', () => { + render(); + const section = document.querySelector('section#architecture'); + expect(section).not.toBeNull(); + expect(document.querySelector('#architecture-heading')?.textContent).toBe( + ARCHITECTURE_HEADLINE + ); + expect(document.querySelectorAll('[data-zone]')).toHaveLength(ZONES.length); + expect(document.querySelectorAll('[data-card]')).toHaveLength(CARDS.length); + const hrefs = new Set( + [...document.querySelectorAll('a[href]')].map((a) => + a.getAttribute('href') + ) + ); + for (const href of diagramHrefs()) expect(hrefs.has(href), href).toBe(true); + }); + + it('marks the Threadplane card as the highlight and gives every other card a docs affordance', () => { + render(); + expect( + document.querySelector('[data-card="threadplane"] .arch-card--tp') + ).not.toBeNull(); + const docs = [...document.querySelectorAll('.arch-docs')]; + expect(docs).toHaveLength(CARDS.filter((c) => c.docsLabel).length); + expect( + document.querySelector('[data-card="threadplane"] .arch-docs') + ).toBeNull(); + }); + + it('draws every card rect at the data module coordinates', () => { + render(); + for (const c of CARDS) { + const rect = document.querySelector( + `[data-card="${c.id}"] rect.arch-card` + ); + expect(rect?.getAttribute('x')).toBe(String(c.x)); + expect(rect?.getAttribute('y')).toBe(String(c.y)); + expect(rect?.getAttribute('width')).toBe(String(c.width)); + expect(rect?.getAttribute('height')).toBe(String(c.height)); + } + }); + + it('shows the alignment grid only when asked', () => { + render(); + expect(document.querySelector('[data-alignment-grid]')).toBeNull(); + document.body.innerHTML = ''; + render(); + expect(document.querySelector('[data-alignment-grid]')).not.toBeNull(); + expect( + document.querySelector('[data-diagram]')?.getAttribute('data-grid') + ).toBe('true'); + }); +}); diff --git a/apps/website/src/components/landing/EnterpriseArchitecture.tsx b/apps/website/src/components/landing/EnterpriseArchitecture.tsx new file mode 100644 index 000000000..d5ee17bb9 --- /dev/null +++ b/apps/website/src/components/landing/EnterpriseArchitecture.tsx @@ -0,0 +1,443 @@ +import type { ReactNode } from 'react'; +import { DiagramSection } from './DiagramSection'; +import { DiagramFrame } from '../docs/diagrams'; +import { + ARROWS, + CARDS, + CARD_PAD, + CHIP_GAP, + CHIP_H, + GRID, + LOGOS, + MAJOR, + TITLE_DY, + VIEW, + ZONES, + chipWidth, + type Card, + type IconKey, + type Row, +} from '../../lib/architecture-diagram'; + +export const ARCHITECTURE_EYEBROW = 'Architecture'; +export const ARCHITECTURE_HEADLINE = + 'Where Threadplane fits in your agent platform.'; +export const ARCHITECTURE_BODY = + 'One highlighted box inside your Angular application. Everything else is yours or your runtime’s, and the docs say what crosses each line.'; +export const ARCHITECTURE_LABEL = + 'Where Threadplane fits in an enterprise agent architecture: your Angular application with Threadplane as its only vendor box, your platform edge with a same-origin proxy and identity, and the agent platform beneath with the runtime, models, tools, observability and durable state.'; + +const SLUG = 'enterprise-architecture'; + +/** Line icons (24-unit paths) for the roles that have no mark. */ +const ICONS: Readonly> = { + key: 'M21 2l-2 2m-7.6 7.6a5 5 0 1 1-7.1 7.1 5 5 0 0 1 7.1-7.1zm0 0L19 3m-3 3l2 2', + gateway: 'M3 4h18v6H3zM3 14h18v6H3zM7 7h.01M7 17h.01', + trace: 'M3 12h4l3-8 4 16 3-8h4', + plug: 'M9 2v6m6-6v6M5 8h14l-1 5a6 6 0 0 1-12 0zM12 19v3', + db: 'M4 5a8 3 0 1 0 16 0a8 3 0 1 0-16 0M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3', + layers: 'M12 2l10 5-10 5L2 7zM2 12l10 5 10-5M2 17l10 5 10-5', + cpu: 'M5 5h14v14H5zM9 9h6v6H9zM9 2v3m6-3v3M9 19v3m6-3v3M2 9h3m-3 6h3m14-6h3m-3 6h3', + chat: 'M21 12a8 8 0 0 1-8 8H8l-5 3 1-5A8 8 0 1 1 21 12z', + pause: 'M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18M10 9v6m4-6v6', + branch: + 'M6 2a2 2 0 1 0 0 4a2 2 0 1 0 0-4M6 18a2 2 0 1 0 0 4a2 2 0 1 0 0-4M18 7a2 2 0 1 0 0 4a2 2 0 1 0 0-4M6 6v12M18 11a6 6 0 0 1-6 6h-1', + sparkles: 'M12 3l2 5 5 2-5 2-2 5-2-5-5-2 5-2z', + wrench: 'M14.7 6.3a4 4 0 0 0 5 5L13 18l-2 2-4-4 2-2 6.7-7.7zM3 21l4-4', + sparkle: + 'M12 3l2 5 5 2-5 2-2 5-2-5-5-2 5-2zM19 16l1 2 2 1-2 1-1 2-1-2-2-1 2-1z', +}; + +function Icon({ + name, + x, + y, + size = 32, + bg, + fg, +}: { + name: IconKey; + x: number; + y: number; + size?: number; + bg: string; + fg: string; +}) { + const s = (size - 8) / 24; + return ( + + + + + ); +} + +function Mark({ + mark, + x, + y, + size, +}: { + mark: keyof typeof LOGOS; + x: number; + y: number; + size: number; +}) { + return ( + + ); +} + +function ChipRow({ + card, + row, +}: { + card: Card; + row: Extract; +}) { + let x = card.x + CARD_PAD; + const out: ReactNode[] = []; + for (const chip of row.chips) { + const w = chipWidth(chip.label, !!chip.mark); + out.push( + + + {chip.mark ? ( + + ) : null} + + {chip.label} + + + ); + x += w + CHIP_GAP; + } + if (row.trailing) { + out.push( + + {row.trailing} + + ); + } + return <>{out}; +} + +function CardRows({ card }: { card: Card }) { + return ( + <> + {card.rows.map((row, i) => { + switch (row.kind) { + case 'text': + return ( + + {row.text} + + ); + case 'mono': + return ( + + {row.text} + + ); + case 'chips': + return ; + case 'marks': + return ( + + {row.marks.map((m, j) => ( + + + + + ))} + + ); + case 'caps': { + let x = card.x + CARD_PAD; + return ( + + {row.caps.map((cap) => { + const cx = x; + x += 42 + Math.round(cap.label.length * 7.2) + 28; + return ( + + + + {cap.label} + + + ); + })} + + ); + } + } + })} + + ); +} + +function CardView({ card }: { card: Card }) { + const titleY = card.y + TITLE_DY; + const hasInnerLinks = card.rows.some((r) => r.kind === 'caps'); + const body = ( + + + {card.icon ? ( + + ) : null} + {hasInnerLinks ? ( + // A card whose rows carry their own links cannot itself be a link + // (anchors do not nest), so its title is the link instead. + + + {card.highlight ? '\u{1F6E9}\uFE0F ' : ''} + {card.title} + + + ) : ( + + {card.highlight ? '\u{1F6E9}\uFE0F ' : ''} + {card.title} + + )} + {card.tag ? ( + + {card.tag} + + ) : null} + {card.docsLabel ? ( + + docs ↗ + + ) : null} + + + ); + return hasInnerLinks ? ( + {body} + ) : ( + + {body} + + ); +} + +function AlignmentGrid() { + const lines: ReactNode[] = []; + for (let x = 0; x <= VIEW.width; x += GRID) { + lines.push( + + ); + } + for (let y = 0; y <= VIEW.height; y += GRID) { + lines.push( + + ); + } + return ( + + {lines} + + ); +} + +interface Props { + /** Review aid: overlays the 8px / 40px alignment grid the geometry is authored on. */ + grid?: boolean; +} + +/** + * The homepage architecture section (spec 2026-09-07): three zones, one + * highlighted box, every card a link to the docs page that backs its wording. + * Geometry comes from `lib/architecture-diagram.ts`, which the unit spec and + * the e2e read too. + */ +export function EnterpriseArchitecture({ grid = false }: Props) { + return ( + +
+ + + + + + + + + + + + + + + + + + + + + + + + {ZONES.map((z) => ( + + + {z.mark ? ( + + ) : null} + + {z.label} + + + {z.owner} + + + ))} + {CARDS.map((c) => ( + + ))} + {ARROWS.map((a) => ( + + + + {a.caption} + + + ))} + {grid ? : null} + +
+ + ); +} diff --git a/apps/website/src/components/landing/ScopeTable.spec.tsx b/apps/website/src/components/landing/ScopeTable.spec.tsx deleted file mode 100644 index b84de63b8..000000000 --- a/apps/website/src/components/landing/ScopeTable.spec.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// apps/website/src/components/landing/ScopeTable.spec.tsx -// @vitest-environment jsdom -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; -import { ScopeTable } from './ScopeTable'; -import { FINAL_MILE_ASIDE, FINAL_MILE_EYEBROW, FINAL_MILE_HEADING } from '../../lib/positioning'; - -describe('ScopeTable as the final-mile section', () => { - it('leads with the last-mile line and keeps the table and its anchor', () => { - const { container } = render(); - expect(screen.getByRole('heading', { name: FINAL_MILE_HEADING }).id).toBe('why-heading'); - expect(screen.getByText(FINAL_MILE_ASIDE)).toBeTruthy(); - expect(screen.getByText(FINAL_MILE_EYEBROW)).toBeTruthy(); - expect(container.querySelector('[data-ui="section"]')?.getAttribute('id')).toBe('why'); - expect(screen.getAllByRole('row')).toHaveLength(5); - }); -}); diff --git a/apps/website/src/components/landing/ScopeTable.tsx b/apps/website/src/components/landing/ScopeTable.tsx deleted file mode 100644 index dacc0780f..000000000 --- a/apps/website/src/components/landing/ScopeTable.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Container } from '../ui/Container'; -import { Section } from '../ui/Section'; -import { SectionHeader } from '../ui/SectionHeader'; -import { FINAL_MILE_ASIDE, FINAL_MILE_EYEBROW, FINAL_MILE_HEADING } from '../../lib/positioning'; - -const ROWS = [ - { - start: 'Raw SSE or stream SDK', - gives: 'Transport and events', - adds: 'Angular state model, chat UX, threads, approvals, generated UI, recovery, tests', - }, - { - start: 'Backend agent framework', - gives: 'Agent runtime and orchestration', - adds: 'The production Angular application and interaction layer', - }, - { - start: 'Generative-UI renderer', - gives: 'Structured UI rendering', - adds: 'Full agent UI, adapters, thread UX, interrupts, testing, and render support', - }, - { - start: 'React-first agent UI', - gives: 'Mature React patterns', - adds: 'Native Angular Signals, DI, templates, components, and testing', - }, -]; - -export function ScopeTable() { - return ( -
- - -
- - - - - - - - - - {ROWS.map((row) => ( - - - - - - ))} - -
Starting pointWhat it gives youWhat Threadplane adds
{row.start}{row.gives}{row.adds}
-
-
-
- ); -} diff --git a/apps/website/src/lib/architecture-diagram.spec.ts b/apps/website/src/lib/architecture-diagram.spec.ts new file mode 100644 index 000000000..c5b548714 --- /dev/null +++ b/apps/website/src/lib/architecture-diagram.spec.ts @@ -0,0 +1,142 @@ +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { + ARROWS, + CARDS, + CARD_GAP, + GRID, + LOGOS, + VIEW, + ZONES, + ZONE_HEAD, + ZONE_INSET, + chipWidth, + diagramHrefs, + CHIP_GAP, + CARD_PAD, +} from './architecture-diagram'; + +const WEBSITE = resolve(__dirname, '../..'); +const onGrid = (n: number) => n % GRID === 0; + +describe('architecture diagram geometry', () => { + it('puts every zone and card on the 8px grid', () => { + for (const z of ZONES) { + expect(onGrid(z.y), `${z.id}.y`).toBe(true); + expect(onGrid(z.height), `${z.id}.height`).toBe(true); + } + for (const c of CARDS) { + for (const [k, v] of Object.entries({ + x: c.x, + y: c.y, + width: c.width, + height: c.height, + })) { + expect(onGrid(v), `${c.id}.${k} = ${v}`).toBe(true); + } + } + expect(onGrid(VIEW.width) && onGrid(VIEW.height)).toBe(true); + }); + + it('lays the zones end to end with one 40px gap and a 40px margin', () => { + expect(ZONES[0].y).toBe(40); + ZONES.slice(1).forEach((z, i) => + expect(z.y).toBe(ZONES[i].y + ZONES[i].height + CARD_GAP) + ); + const last = ZONES[ZONES.length - 1]; + expect(last.y + last.height + 40).toBe(VIEW.height); + }); + + it('keeps every card inside its zone with the zone inset, below the zone head', () => { + for (const c of CARDS) { + const z = ZONES.find((x) => x.id === c.zone)!; + expect(c.x, `${c.id} left`).toBeGreaterThanOrEqual(40 + ZONE_INSET); + expect(c.x + c.width, `${c.id} right`).toBeLessThanOrEqual( + 40 + 1200 - ZONE_INSET + ); + expect(c.y, `${c.id} top`).toBeGreaterThanOrEqual(z.y + ZONE_HEAD); + expect(c.y + c.height, `${c.id} bottom`).toBeLessThanOrEqual( + z.y + z.height - 24 + ); + } + }); + + it('never overlaps two cards, and separates horizontal neighbours by exactly the card gap', () => { + const overlap = (a: (typeof CARDS)[number], b: (typeof CARDS)[number]) => + a.x < b.x + b.width && + b.x < a.x + a.width && + a.y < b.y + b.height && + b.y < a.y + a.height; + for (const a of CARDS) + for (const b of CARDS) + if (a !== b) expect(overlap(a, b), `${a.id} vs ${b.id}`).toBe(false); + for (const a of CARDS) { + const right = CARDS.filter((b) => b.y === a.y && b.x > a.x).sort( + (p, q) => p.x - q.x + )[0]; + if (right) + expect(right.x - (a.x + a.width), `${a.id} → ${right.id}`).toBe( + CARD_GAP + ); + } + }); + + it('draws each arrow in the gap between two zones, at a grid x', () => { + for (const a of ARROWS) { + expect(onGrid(a.x)).toBe(true); + const from = ZONES.find( + (z) => z.y + z.height <= a.y1 + 40 && z.y < a.y1 + )!; + const to = ZONES.find((z) => z.y === a.y2)!; + expect(from, `arrow at ${a.y1} leaves a zone`).toBeDefined(); + expect(to, `arrow at ${a.y2} enters a zone`).toBeDefined(); + } + }); + + it('keeps every chip row inside its card', () => { + for (const c of CARDS) { + for (const r of c.rows) { + if (r.kind !== 'chips') continue; + let x = c.x + CARD_PAD; + for (const chip of r.chips) + x += chipWidth(chip.label, !!chip.mark) + CHIP_GAP; + expect(x - CHIP_GAP, `${c.id} chip row at ${r.y}`).toBeLessThanOrEqual( + c.x + c.width - CARD_PAD + ); + expect(r.y + 28, `${c.id} chip row bottom`).toBeLessThanOrEqual( + c.y + c.height - 16 + ); + } + } + }); +}); + +describe('architecture diagram links and marks', () => { + it('links every card and capability to a docs page that exists', () => { + for (const href of diagramHrefs()) { + const mdx = resolve( + WEBSITE, + `content/docs${href.replace(/^\/docs/, '')}.mdx` + ); + const page = resolve(WEBSITE, `src/app${href}/page.tsx`); + expect(existsSync(mdx) || existsSync(page), `${href}`).toBe(true); + } + expect(diagramHrefs().length).toBeGreaterThanOrEqual(10); + }); + + it('uses only marks that exist under /logos', () => { + for (const [key, path] of Object.entries(LOGOS)) { + expect(existsSync(resolve(WEBSITE, `public${path}`)), key).toBe(true); + } + for (const c of CARDS) { + for (const r of c.rows) { + if (r.kind === 'chips') + for (const chip of r.chips) + if (chip.mark) expect(LOGOS[chip.mark]).toBeDefined(); + if (r.kind === 'marks') + for (const m of r.marks) expect(LOGOS[m]).toBeDefined(); + } + } + }); +}); diff --git a/apps/website/src/lib/architecture-diagram.ts b/apps/website/src/lib/architecture-diagram.ts new file mode 100644 index 000000000..c8821793d --- /dev/null +++ b/apps/website/src/lib/architecture-diagram.ts @@ -0,0 +1,462 @@ +/** + * Geometry and copy for the homepage enterprise architecture diagram + * (spec: docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md). + * + * One table, three readers: the component draws it, the unit spec checks that + * every rectangle lands on the 8px grid inside its zone, and the e2e measures + * the rendered text against these same boxes. Coordinates are viewBox units; + * the SVG scales with its container. + * + * Every card links to the docs page that backs its wording. Third-party + * products are examples of the role a card describes, not integrations the + * library claims; the spec §3 lists the source page for each line. + */ + +export const VIEW = { width: 1280, height: 1088 } as const; +export const GRID = 8; +export const MAJOR = 40; +/** Distance from a zone's edge to its cards' outer edges (left/right/bottom). */ +export const ZONE_INSET = 32; +/** Vertical room a zone keeps above its first row of cards for its label. */ +export const ZONE_HEAD = 72; +export const CARD_GAP = 40; + +export type ZoneId = 'app' | 'edge' | 'platform'; + +export interface Zone { + readonly id: ZoneId; + readonly label: string; + readonly owner: string; + readonly y: number; + readonly height: number; + /** Gradient id in the component's defs. */ + readonly fill: 'app' | 'edge' | 'plat'; + readonly stroke: string; + /** A logo file under /logos to show beside the label. */ + readonly mark?: LogoKey; +} + +export type LogoKey = + | 'angular' + | 'vercel' + | 'google' + | 'langgraph' + | 'agui' + | 'bedrock' + | 'azure' + | 'microsoft' + | 'mastra' + | 'openai' + | 'anthropic'; + +/** Public paths of the marks the diagram is allowed to use (README under /logos). */ +export const LOGOS: Readonly> = { + angular: '/logos/surface/angular.svg', + vercel: '/logos/surface/vercel.svg', + google: '/logos/providers/google.svg', + langgraph: '/logos/langgraph.svg', + agui: '/logos/ag-ui.svg', + bedrock: '/logos/providers/bedrock.svg', + azure: '/logos/providers/azure.svg', + microsoft: '/logos/runtimes/microsoft.svg', + mastra: '/logos/runtimes/mastra.svg', + openai: '/logos/providers/openai.svg', + anthropic: '/logos/providers/anthropic.svg', +}; + +export type IconKey = + | 'key' + | 'gateway' + | 'trace' + | 'plug' + | 'db' + | 'layers' + | 'cpu' + | 'chat' + | 'pause' + | 'branch' + | 'sparkles' + | 'wrench' + | 'sparkle'; + +export interface Chip { + readonly mark?: LogoKey; + readonly label: string; +} + +export interface Capability { + readonly icon: IconKey; + readonly label: string; + readonly href: string; +} + +export type Row = + /** A row of pill chips, each with an optional mark. `trailing` is a sentence set after the last chip. */ + | { + readonly kind: 'chips'; + readonly y: number; + readonly chips: readonly Chip[]; + readonly trailing?: string; + readonly tone?: 'light' | 'tp'; + } + /** A row of square mark badges, no label. */ + | { + readonly kind: 'marks'; + readonly y: number; + readonly x: number; + readonly marks: readonly LogoKey[]; + } + | { readonly kind: 'text'; readonly y: number; readonly text: string } + | { readonly kind: 'mono'; readonly y: number; readonly text: string } + /** The Threadplane card's capability badges, each its own link. */ + | { + readonly kind: 'caps'; + readonly y: number; + readonly caps: readonly Capability[]; + }; + +export interface Card { + readonly id: string; + readonly zone: ZoneId; + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; + readonly title: string; + readonly href: string; + /** Icon badge beside the title; absent on the Threadplane card, which carries the brand mark. */ + readonly icon?: { + readonly name: IconKey; + readonly bg: string; + readonly fg: string; + }; + /** Right-aligned tag on the title line (only the Threadplane card). */ + readonly tag?: string; + /** Whether to render the "docs ↗" affordance at the top-right. */ + readonly docsLabel: boolean; + readonly highlight?: boolean; + readonly rows: readonly Row[]; +} + +export interface Arrow { + readonly x: number; + readonly y1: number; + readonly y2: number; + readonly caption: string; +} + +export const ZONES: readonly Zone[] = [ + { + id: 'app', + label: 'YOUR ANGULAR APPLICATION', + owner: 'you own this zone', + y: 40, + height: 280, + fill: 'app', + stroke: '#d6deee', + mark: 'angular', + }, + { + id: 'edge', + label: 'YOUR PLATFORM EDGE', + owner: 'you own this zone', + y: 360, + height: 224, + fill: 'edge', + stroke: '#d9dee6', + }, + { + id: 'platform', + label: 'AGENT PLATFORM', + owner: 'your runtime owns this zone', + y: 624, + height: 424, + fill: 'plat', + stroke: '#d3e3d9', + }, +]; + +export const CARDS: readonly Card[] = [ + { + id: 'threadplane', + zone: 'app', + x: 72, + y: 112, + width: 744, + height: 176, + title: 'Threadplane', + href: '/docs/chat/getting-started/introduction', + tag: 'THE FINAL MILE', + docsLabel: false, + highlight: true, + rows: [ + { + kind: 'caps', + y: 168, + caps: [ + { icon: 'chat', label: 'Chat', href: '/docs/chat/components/chat' }, + { + icon: 'pause', + label: 'Interrupts', + href: '/docs/langgraph/guides/interrupts', + }, + { + icon: 'branch', + label: 'Threads', + href: '/docs/langgraph/guides/persistence', + }, + { + icon: 'sparkles', + label: 'Generative UI', + href: '/docs/chat/guides/generative-ui', + }, + { + icon: 'wrench', + label: 'Client tools', + href: '/docs/chat/guides/client-tools', + }, + ], + }, + { + kind: 'mono', + y: 228, + text: '@threadplane/chat · render · langgraph · ag-ui · middleware', + }, + { + kind: 'chips', + y: 244, + tone: 'tp', + chips: [ + { mark: 'google', label: 'A2UI v0.9' }, + { mark: 'vercel', label: 'json-render' }, + ], + trailing: + 'generative UI on two open standards · one Agent contract for every adapter', + }, + ], + }, + { + id: 'components', + zone: 'app', + x: 856, + y: 112, + width: 352, + height: 176, + title: 'Your components', + href: '/docs/chat/guides/client-tools', + icon: { name: 'layers', bg: '#ecfdf5', fg: '#047857' }, + docsLabel: true, + rows: [ + { + kind: 'text', + y: 192, + text: 'design system · tool views · client-tool handlers', + }, + { + kind: 'text', + y: 218, + text: 'pages · routing · state · APM on status() and error()', + }, + { kind: 'text', y: 244, text: 'unchanged by Threadplane' }, + ], + }, + { + id: 'gateway', + zone: 'edge', + x: 72, + y: 432, + width: 744, + height: 128, + title: 'Same-origin proxy or API gateway', + href: '/docs/langgraph/guides/deployment', + icon: { name: 'gateway', bg: '#eef2f7', fg: '#4a5568' }, + docsLabel: true, + rows: [ + { + kind: 'chips', + y: 494, + chips: [ + { mark: 'azure', label: 'Azure API Management' }, + { mark: 'bedrock', label: 'Amazon API Gateway' }, + { mark: 'google', label: 'Apigee' }, + ], + }, + { + kind: 'text', + y: 544, + text: 'adds the deployment credentials · forwards user identity · keys never reach the browser', + }, + ], + }, + { + id: 'identity', + zone: 'edge', + x: 856, + y: 432, + width: 352, + height: 128, + title: 'Identity & session', + href: '/docs/langgraph/guides/deployment', + icon: { name: 'key', bg: '#fff3e0', fg: '#c2410c' }, + docsLabel: true, + rows: [ + { + kind: 'chips', + y: 494, + chips: [{ mark: 'microsoft', label: 'Microsoft Entra ID' }], + }, + { kind: 'text', y: 544, text: 'or Okta · session as HTTP-only cookies' }, + ], + }, + { + id: 'runtime', + zone: 'platform', + x: 72, + y: 696, + width: 408, + height: 152, + title: 'Agent runtime', + href: '/docs/langgraph/getting-started/introduction', + icon: { name: 'cpu', bg: '#e0f2fe', fg: '#0369a1' }, + docsLabel: true, + rows: [ + { + kind: 'chips', + y: 762, + chips: [ + { mark: 'langgraph', label: 'LangGraph Platform' }, + { mark: 'agui', label: 'AG-UI' }, + ], + }, + { + kind: 'chips', + y: 798, + chips: [ + { mark: 'bedrock', label: 'Strands' }, + { mark: 'microsoft', label: 'Agent Framework' }, + { mark: 'mastra', label: 'Mastra' }, + ], + }, + ], + }, + { + id: 'models', + zone: 'platform', + x: 520, + y: 696, + width: 336, + height: 152, + title: 'Models', + href: '/docs/runtimes/getting-started/introduction', + icon: { name: 'sparkle', bg: '#f5f3ff', fg: '#6d28d9' }, + docsLabel: true, + rows: [ + { + kind: 'chips', + y: 762, + chips: [ + { mark: 'azure', label: 'Azure OpenAI' }, + { mark: 'bedrock', label: 'Amazon Bedrock' }, + ], + }, + { + kind: 'chips', + y: 798, + chips: [{ mark: 'google', label: 'Vertex AI' }], + }, + { kind: 'marks', y: 796, x: 664, marks: ['openai', 'anthropic'] }, + ], + }, + { + id: 'tools', + zone: 'platform', + x: 896, + y: 696, + width: 312, + height: 152, + title: 'Tools · MCP · data', + href: '/docs/middleware/getting-started/introduction', + icon: { name: 'plug', bg: '#fff7ed', fg: '#c2410c' }, + docsLabel: true, + rows: [ + { kind: 'text', y: 780, text: 'server tools run here, on your systems' }, + { kind: 'text', y: 808, text: 'client tools round-trip to the browser' }, + ], + }, + { + id: 'observability', + zone: 'platform', + x: 72, + y: 888, + width: 352, + height: 128, + title: 'Observability', + href: '/docs/langgraph/guides/deployment', + icon: { name: 'trace', bg: '#fdf2f8', fg: '#be185d' }, + docsLabel: true, + rows: [ + { + kind: 'chips', + y: 952, + chips: [{ label: 'LangSmith' }], + trailing: 'traces every run · evals · token cost', + }, + ], + }, + { + id: 'state', + zone: 'platform', + x: 464, + y: 888, + width: 744, + height: 128, + title: 'Durable state', + href: '/docs/langgraph/guides/persistence', + icon: { name: 'db', bg: '#f1f5f9', fg: '#334155' }, + docsLabel: true, + rows: [ + { + kind: 'text', + y: 968, + text: 'checkpoints at every super-step, keyed by thread · platform-managed, or Postgres / SQLite when you embed the graph', + }, + { kind: 'text', y: 994, text: 'exposed by Threadplane, never faked' }, + ], + }, +]; + +export const ARROWS: readonly Arrow[] = [ + { + x: 440, + y1: 288, + y2: 360, + caption: 'relative apiUrl · POST + SSE via the LangGraph SDK', + }, + { + x: 440, + y1: 560, + y2: 624, + caption: 'credentials added server-side · CORS on the runtime', + }, +]; + +/** Every docs href the diagram links to, deduplicated, for the link-resolution spec. */ +export function diagramHrefs(): readonly string[] { + const out = new Set(); + for (const c of CARDS) { + out.add(c.href); + for (const r of c.rows) + if (r.kind === 'caps') for (const cap of r.caps) out.add(cap.href); + } + return [...out]; +} + +/** Baseline of a card's title row: 38 below the card's top. */ +export const TITLE_DY = 38; +/** Left inset of card content. */ +export const CARD_PAD = 24; +/** Chip geometry: height and the width formula the component and the e2e share. */ +export const CHIP_H = 28; +export const chipWidth = (label: string, withMark: boolean): number => + Math.round(label.length * 7.2) + (withMark ? 44 : 24); +export const CHIP_GAP = 12; diff --git a/apps/website/src/lib/positioning.spec.ts b/apps/website/src/lib/positioning.spec.ts index 8ad5cdbc1..ff6daad4b 100644 --- a/apps/website/src/lib/positioning.spec.ts +++ b/apps/website/src/lib/positioning.spec.ts @@ -155,12 +155,6 @@ describe('positioning: coding-agent prompt', () => { }); describe('homepage restructure copy (live-stage spec §3)', () => { - it('pins the final-mile eyebrow, heading and aside', async () => { - const { FINAL_MILE_EYEBROW, FINAL_MILE_HEADING, FINAL_MILE_ASIDE } = await import('./positioning'); - expect(FINAL_MILE_EYEBROW).toBe('Where Threadplane fits'); - expect(FINAL_MILE_HEADING).toBe('Angular teams are building agents. The last mile is still messy.'); - expect(FINAL_MILE_ASIDE).toBe('What you start with, and what Threadplane adds.'); - }); it('carries three reliability receipts, each linking a human-readable page', async () => { const { RELIABILITY_RECEIPTS } = await import('./positioning'); diff --git a/apps/website/src/lib/positioning.ts b/apps/website/src/lib/positioning.ts index 0e2535dae..be5b788c0 100644 --- a/apps/website/src/lib/positioning.ts +++ b/apps/website/src/lib/positioning.ts @@ -51,9 +51,6 @@ export function formatAngularRange(majors: readonly number[]): string { export const HERO_TRUST_LINE = `MIT · ${formatAngularRange(WEBSITE_SUPPORTED_ANGULAR_MAJORS)} · no account, no cloud`; // ── The final mile (live-stage spec §3, block 3) ───────────────────────────── -export const FINAL_MILE_EYEBROW = 'Where Threadplane fits'; -export const FINAL_MILE_HEADING = 'Angular teams are building agents. The last mile is still messy.'; -export const FINAL_MILE_ASIDE = 'What you start with, and what Threadplane adds.'; // ── Reliability receipts (spec §3, block 2). Each links a page a human can read; // the sourced numbers stay in Reliability.tsx beside them. ─────────────────── diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index 3d730b2d8..0b3f65114 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1676,6 +1676,120 @@ --diagram-scroll-bg: var(--color-surface-tinted); } +/* EnterpriseArchitecture — components/landing/EnterpriseArchitecture.tsx + * The homepage architecture diagram. Geometry lives in lib/architecture-diagram.ts; + * these rules are only the type ramp and the fills. Text sizes are viewBox + * units (the SVG is 1280 wide and scales with the figure). */ +.stack-diagram-section .arch-figure { + width: 100%; +} +.arch-figure .tp-diagram-svg { + min-width: 1024px; +} +.arch-figure text { + font-family: var(--font-inter); + font-size: 12.5px; + fill: var(--color-text-primary); +} +.arch-figure .arch-title { + font-size: 15px; + font-weight: 600; +} +.arch-figure .arch-body { + font-size: 12.5px; + fill: var(--color-text-secondary); +} +.arch-figure .arch-caption { + font-size: 12px; + font-style: italic; + fill: var(--color-text-secondary); +} +.arch-figure .arch-mono { + font-family: var(--font-mono); + font-size: 11.5px; + fill: #2a3a5c; +} +.arch-figure .arch-zone-label { + font-size: 11.5px; + font-weight: 700; + letter-spacing: 0.14em; + fill: var(--color-text-secondary); +} +.arch-figure .arch-zone-owner { + font-size: 11px; + text-anchor: end; + fill: var(--color-text-muted, #8a8f98); +} +.arch-figure .arch-tag { + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.12em; + text-anchor: end; + fill: #2f5fa8; +} +.arch-figure .arch-docs { + font-size: 10.5px; + text-anchor: end; + fill: #2f5fa8; + opacity: 0.75; +} +.arch-figure .arch-card-link:hover .arch-docs { + opacity: 1; +} +.arch-figure .arch-card-link:hover .arch-card { + stroke: #9db6e4; +} +.arch-figure .arch-card { + fill: url(#enterprise-architecture-card); + stroke: #dfe3ea; + stroke-width: 1; +} +.arch-figure .arch-card--tp { + fill: url(#enterprise-architecture-tp); + stroke: #9db6e4; + stroke-width: 1.2; +} +.arch-figure .arch-chip rect { + fill: #fff; + stroke: #e2e6ec; +} +.arch-figure .arch-chip[data-tone='tp'] rect { + fill: rgba(255, 255, 255, 0.7); + stroke: #c9d6ee; +} +.arch-figure .arch-chip text { + font-size: 11.5px; + font-weight: 500; + fill: #2a2f3a; +} +.arch-figure .arch-badge rect { + fill: #fff; + stroke: #e2e6ec; +} +.arch-figure .arch-cap text { + font-size: 13px; + font-weight: 500; + fill: var(--color-text-primary); +} +.arch-figure .arch-cap:hover text { + fill: #2f5fa8; +} +.arch-figure .arch-arrow path { + stroke: #7d8492; + stroke-width: 1.5; + fill: none; +} +.arch-figure .arch-grid-major { + stroke: #7fb0ff; + stroke-width: 1; + opacity: 0.6; +} +.arch-figure .arch-grid-minor { + stroke: #dbe7fb; + stroke-width: 0.5; + opacity: 0.6; +} + /* HomeConceptGrid — components/landing/HomeConceptGrid.tsx */ .home-concept { display: flex; @@ -1902,44 +2016,3 @@ font-weight: 600; text-decoration: none; } - -/* ── Scope table (Task 13) ───────────────────────────────────────────────── */ -.scope-table-wrap { - overflow-x: auto; - margin-top: 20px; -} - -.scope-table { - width: 100%; - min-width: 640px; - border-collapse: collapse; - font-size: 14px; -} - -.scope-table th, -.scope-table td { - text-align: left; - padding: 12px 14px; - border-bottom: 1px solid var(--color-border); - vertical-align: top; -} - -.scope-table thead th { - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--color-text-muted); -} - -/* The stills form of the beat block and the ledger — StageStills.tsx. The - * block mirrors `.stage-rail-beat` without the grid placement; the ending - * reuses `.stage-rail-close` (its `grid-area` is inert outside the rail grid) - * and only needs room after the last still. */ -.stage-still-text { - display: flex; - gap: 14px; - align-items: flex-start; -} -.stage-stills-close { - margin-top: 64px; -} From e51762364434e9b761daf249ccc590f964fb4db1 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 7 Sep 2026 13:29:23 -0700 Subject: [PATCH 3/8] feat(website): the architecture diagram is the UI layer between your users and your agents Scaled back from the three-zone enterprise map at the user's direction: four columns (your users, your Angular application with Threadplane as its UI layer, the two adapters, your agents) and a model-provider strip, on the kit's dot grid. The LangGraph SDK lane is highlighted as first-class with the capabilities the docs reserve for a checkpoint-aware runtime; LangSmith takes the platform card under the LangChain mark, documented in the logo README. Geometry stays one data module read by the component, the grid spec, and the overflow e2e. Co-Authored-By: Claude Fable 5.1 --- apps/website/e2e/home-architecture.spec.ts | 2 +- apps/website/public/logos/README.md | 1 + apps/website/public/logos/langchain.svg | 1 + .../landing/EnterpriseArchitecture.spec.tsx | 41 +- .../landing/EnterpriseArchitecture.tsx | 352 +++++++------ .../src/lib/architecture-diagram.spec.ts | 141 +++--- apps/website/src/lib/architecture-diagram.ts | 470 ++++++------------ apps/website/src/styles/landing.css | 17 +- ...-enterprise-architecture-diagram-design.md | 55 +- 9 files changed, 509 insertions(+), 571 deletions(-) create mode 100644 apps/website/public/logos/langchain.svg diff --git a/apps/website/e2e/home-architecture.spec.ts b/apps/website/e2e/home-architecture.spec.ts index 8a2f1a312..2342b67ff 100644 --- a/apps/website/e2e/home-architecture.spec.ts +++ b/apps/website/e2e/home-architecture.spec.ts @@ -60,7 +60,7 @@ test.describe('homepage architecture', () => { await page.setViewportSize({ width: 1440, height: 900 }); await page.goto('/'); await expect(page.locator('#architecture-heading')).toHaveText( - 'Where Threadplane fits in your agent platform.' + 'The UI layer between your users and your agents.' ); await expect(page.locator('#why-heading')).toHaveCount(0); const cards = page.locator(`${DIAGRAM} [data-card]`); diff --git a/apps/website/public/logos/README.md b/apps/website/public/logos/README.md index d7febc6dc..1090d9c5d 100644 --- a/apps/website/public/logos/README.md +++ b/apps/website/public/logos/README.md @@ -28,6 +28,7 @@ These were downloaded from `https://cdn.simpleicons.org//111827` for a com - `runtimes/crewai.svg` from slug `crewai`. - `runtimes/pydantic.svg` from slug `pydantic`. +- `langchain.svg` from slug `langchain`, used for the LangSmith card on the homepage architecture diagram (Simple Icons has no LangSmith slug; LangSmith is a LangChain product in the same mark family). - `surface/angular.svg` from slug `angular`. - `surface/reactivex.svg` from slug `reactivex`. - `surface/vercel.svg` from slug `vercel`. diff --git a/apps/website/public/logos/langchain.svg b/apps/website/public/logos/langchain.svg new file mode 100644 index 000000000..df09c90d7 --- /dev/null +++ b/apps/website/public/logos/langchain.svg @@ -0,0 +1 @@ +LangChain \ No newline at end of file diff --git a/apps/website/src/components/landing/EnterpriseArchitecture.spec.tsx b/apps/website/src/components/landing/EnterpriseArchitecture.spec.tsx index 3c1524784..25c1b580d 100644 --- a/apps/website/src/components/landing/EnterpriseArchitecture.spec.tsx +++ b/apps/website/src/components/landing/EnterpriseArchitecture.spec.tsx @@ -6,17 +6,23 @@ import { EnterpriseArchitecture, ARCHITECTURE_HEADLINE, } from './EnterpriseArchitecture'; -import { CARDS, ZONES, diagramHrefs } from '../../lib/architecture-diagram'; +import { + CARDS, + COLUMNS, + MODEL_STRIP, + diagramHrefs, +} from '../../lib/architecture-diagram'; describe('EnterpriseArchitecture', () => { - it('renders the section with its heading id, every zone and card, and a link per card', () => { + it('renders the section with its heading id, every column label, card, and link', () => { render(); - const section = document.querySelector('section#architecture'); - expect(section).not.toBeNull(); + expect(document.querySelector('section#architecture')).not.toBeNull(); expect(document.querySelector('#architecture-heading')?.textContent).toBe( ARCHITECTURE_HEADLINE ); - expect(document.querySelectorAll('[data-zone]')).toHaveLength(ZONES.length); + expect(document.querySelectorAll('[data-column]')).toHaveLength( + COLUMNS.length + ); expect(document.querySelectorAll('[data-card]')).toHaveLength(CARDS.length); const hrefs = new Set( [...document.querySelectorAll('a[href]')].map((a) => @@ -26,19 +32,28 @@ describe('EnterpriseArchitecture', () => { for (const href of diagramHrefs()) expect(hrefs.has(href), href).toBe(true); }); - it('marks the Threadplane card as the highlight and gives every other card a docs affordance', () => { + it('highlights Threadplane and the first-class LangGraph lane, and nests no anchors', () => { render(); expect( document.querySelector('[data-card="threadplane"] .arch-card--tp') ).not.toBeNull(); - const docs = [...document.querySelectorAll('.arch-docs')]; - expect(docs).toHaveLength(CARDS.filter((c) => c.docsLabel).length); expect( - document.querySelector('[data-card="threadplane"] .arch-docs') + document.querySelector('[data-card="langgraph-sdk"] .arch-card--tp') + ).not.toBeNull(); + expect( + document.querySelector('[data-card="ag-ui"] .arch-card--tp') ).toBeNull(); + expect(document.querySelector('a a')).toBeNull(); + // The Threadplane card's title is the link; its capabilities are links of their own. + expect( + document.querySelector('[data-card="threadplane"] a.arch-title-link') + ).not.toBeNull(); + expect( + document.querySelectorAll('[data-card="threadplane"] a.arch-cap') + ).toHaveLength(5); }); - it('draws every card rect at the data module coordinates', () => { + it('draws every card rect at the data module coordinates and the model strip chips', () => { render(); for (const c of CARDS) { const rect = document.querySelector( @@ -49,6 +64,9 @@ describe('EnterpriseArchitecture', () => { expect(rect?.getAttribute('width')).toBe(String(c.width)); expect(rect?.getAttribute('height')).toBe(String(c.height)); } + expect( + document.querySelectorAll('[data-model-strip] [data-chip]') + ).toHaveLength(MODEL_STRIP.chips.length); }); it('shows the alignment grid only when asked', () => { @@ -57,8 +75,5 @@ describe('EnterpriseArchitecture', () => { document.body.innerHTML = ''; render(); expect(document.querySelector('[data-alignment-grid]')).not.toBeNull(); - expect( - document.querySelector('[data-diagram]')?.getAttribute('data-grid') - ).toBe('true'); }); }); diff --git a/apps/website/src/components/landing/EnterpriseArchitecture.tsx b/apps/website/src/components/landing/EnterpriseArchitecture.tsx index d5ee17bb9..d3ae07caf 100644 --- a/apps/website/src/components/landing/EnterpriseArchitecture.tsx +++ b/apps/website/src/components/landing/EnterpriseArchitecture.tsx @@ -5,68 +5,71 @@ import { ARROWS, CARDS, CARD_PAD, - CHIP_GAP, - CHIP_H, + COLUMNS, + COLUMN_LABEL_Y, + CONTRACT_CAPTION, GRID, LOGOS, MAJOR, + MODEL_STRIP, + STRIP_CHIP_H, + STRIP_GAP, TITLE_DY, VIEW, - ZONES, - chipWidth, + stripChipWidth, type Card, type IconKey, - type Row, + type LogoKey, } from '../../lib/architecture-diagram'; export const ARCHITECTURE_EYEBROW = 'Architecture'; export const ARCHITECTURE_HEADLINE = - 'Where Threadplane fits in your agent platform.'; + 'The UI layer between your users and your agents.'; export const ARCHITECTURE_BODY = - 'One highlighted box inside your Angular application. Everything else is yours or your runtime’s, and the docs say what crosses each line.'; + 'Threadplane lives inside your Angular application and talks to your agents through the LangGraph SDK or AG-UI. Everything on the right is yours.'; export const ARCHITECTURE_LABEL = - 'Where Threadplane fits in an enterprise agent architecture: your Angular application with Threadplane as its only vendor box, your platform edge with a same-origin proxy and identity, and the agent platform beneath with the runtime, models, tools, observability and durable state.'; + 'Threadplane is the UI layer between your users and your agents: it lives inside your Angular application, reaches LangGraph agents first-class through the LangGraph SDK and any AG-UI server through the AG-UI protocol, and leaves the model choice to your runtime.'; const SLUG = 'enterprise-architecture'; /** Line icons (24-unit paths) for the roles that have no mark. */ const ICONS: Readonly> = { - key: 'M21 2l-2 2m-7.6 7.6a5 5 0 1 1-7.1 7.1 5 5 0 0 1 7.1-7.1zm0 0L19 3m-3 3l2 2', - gateway: 'M3 4h18v6H3zM3 14h18v6H3zM7 7h.01M7 17h.01', - trace: 'M3 12h4l3-8 4 16 3-8h4', - plug: 'M9 2v6m6-6v6M5 8h14l-1 5a6 6 0 0 1-12 0zM12 19v3', - db: 'M4 5a8 3 0 1 0 16 0a8 3 0 1 0-16 0M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3', - layers: 'M12 2l10 5-10 5L2 7zM2 12l10 5 10-5M2 17l10 5 10-5', - cpu: 'M5 5h14v14H5zM9 9h6v6H9zM9 2v3m6-3v3M9 19v3m6-3v3M2 9h3m-3 6h3m14-6h3m-3 6h3', + users: + 'M17 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8a4 4 0 1 0 0 8M23 21v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8', chat: 'M21 12a8 8 0 0 1-8 8H8l-5 3 1-5A8 8 0 1 1 21 12z', pause: 'M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18M10 9v6m4-6v6', branch: 'M6 2a2 2 0 1 0 0 4a2 2 0 1 0 0-4M6 18a2 2 0 1 0 0 4a2 2 0 1 0 0-4M18 7a2 2 0 1 0 0 4a2 2 0 1 0 0-4M6 6v12M18 11a6 6 0 0 1-6 6h-1', sparkles: 'M12 3l2 5 5 2-5 2-2 5-2-5-5-2 5-2z', wrench: 'M14.7 6.3a4 4 0 0 0 5 5L13 18l-2 2-4-4 2-2 6.7-7.7zM3 21l4-4', - sparkle: - 'M12 3l2 5 5 2-5 2-2 5-2-5-5-2 5-2zM19 16l1 2 2 1-2 1-1 2-1-2-2-1 2-1z', }; function Icon({ name, x, y, - size = 32, + size, bg, fg, }: { name: IconKey; x: number; y: number; - size?: number; + size: number; bg: string; fg: string; }) { const s = (size - 8) / 24; return ( - + ; + mark: LogoKey; + x: number; + y: number; + size: number; }) { - let x = card.x + CARD_PAD; - const out: ReactNode[] = []; - for (const chip of row.chips) { - const w = chipWidth(chip.label, !!chip.mark); - out.push( - - - {chip.mark ? ( - - ) : null} - - {chip.label} - - - ); - x += w + CHIP_GAP; - } - if (row.trailing) { - out.push( - - {row.trailing} - - ); - } - return <>{out}; + return ( + + + + + ); } function CardRows({ card }: { card: Card }) { @@ -169,47 +153,45 @@ function CardRows({ card }: { card: Card }) { {row.text} ); - case 'chips': - return ; + case 'badge': + return ( + + + + {row.label} + + + ); case 'marks': return ( {row.marks.map((m, j) => ( - - - - + ))} ); - case 'caps': { - let x = card.x + CARD_PAD; + case 'caps': return ( - {row.caps.map((cap) => { - const cx = x; - x += 42 + Math.round(cap.label.length * 7.2) + 28; + {row.caps.map((cap, j) => { + const y = row.y + j * 40; return (
- + {cap.label} @@ -217,7 +199,6 @@ function CardRows({ card }: { card: Card }) { })} ); - } } })} @@ -225,8 +206,15 @@ function CardRows({ card }: { card: Card }) { } function CardView({ card }: { card: Card }) { - const titleY = card.y + TITLE_DY; const hasInnerLinks = card.rows.some((r) => r.kind === 'caps'); + const titleX = card.x + CARD_PAD + (card.mark ? 52 : 0); + const titleY = card.y + TITLE_DY; + const title = ( + + {card.highlight && !card.mark ? '\u{1F6E9}️ ' : ''} + {card.title} + + ); const body = ( + {card.id === 'threadplane' ? ( + <> + + + YOUR ANGULAR APP + + + ) : null} {card.icon ? ( ) : null} + {card.mark ? ( + + ) : null} {hasInnerLinks ? ( // A card whose rows carry their own links cannot itself be a link // (anchors do not nest), so its title is the link instead. - {card.highlight ? '\u{1F6E9}\uFE0F ' : ''} + {'\u{1F6E9}️ '} {card.title} - ) : ( - - {card.highlight ? '\u{1F6E9}\uFE0F ' : ''} + ) : card.icon ? ( + {card.title} + ) : ( + title )} + {card.title2 ? ( + + {card.title2} + + ) : null} {card.tag ? ( {card.tag} ) : null} - {card.docsLabel ? ( - - docs ↗ - - ) : null} ); @@ -305,6 +312,50 @@ function CardView({ card }: { card: Card }) { ); } +function ModelStrip() { + let x: number = MODEL_STRIP.x; + const chips: ReactNode[] = []; + for (const chip of MODEL_STRIP.chips) { + const w = stripChipWidth(chip.label); + chips.push( + + + + + {chip.label} + + + ); + x += w + STRIP_GAP; + } + return ( + + + {MODEL_STRIP.label} + + {chips} + + {MODEL_STRIP.caption} + + + ); +} + function AlignmentGrid() { const lines: ReactNode[] = []; for (let x = 0; x <= VIEW.width; x += GRID) { @@ -344,10 +395,10 @@ interface Props { } /** - * The homepage architecture section (spec 2026-09-07): three zones, one - * highlighted box, every card a link to the docs page that backs its wording. - * Geometry comes from `lib/architecture-diagram.ts`, which the unit spec and - * the e2e read too. + * The homepage architecture section (spec 2026-09-07): your users, your + * Angular application with Threadplane as its UI layer, the two adapters, + * your agents, and the model strip. Geometry comes from + * `lib/architecture-diagram.ts`, which the unit spec and the e2e read too. */ export function EnterpriseArchitecture({ grid = false }: Props) { return ( @@ -372,69 +423,50 @@ export function EnterpriseArchitecture({ grid = false }: Props) { - - - - - - - - - - - - - + - {ZONES.map((z) => ( - - - {z.mark ? ( - - ) : null} - - {z.label} - - - {z.owner} - - + {COLUMNS.map((c) => ( + + {c.label} + ))} {CARDS.map((c) => ( ))} {ARROWS.map((a) => ( - - - - {a.caption} - - + ))} + + {CONTRACT_CAPTION.lines[0]} + + + {CONTRACT_CAPTION.lines[1]} + + {grid ? : null} diff --git a/apps/website/src/lib/architecture-diagram.spec.ts b/apps/website/src/lib/architecture-diagram.spec.ts index c5b548714..074f6f4b4 100644 --- a/apps/website/src/lib/architecture-diagram.spec.ts +++ b/apps/website/src/lib/architecture-diagram.spec.ts @@ -5,27 +5,23 @@ import { ARROWS, CARDS, CARD_GAP, + CARD_PAD, + COLUMNS, GRID, LOGOS, + MODEL_STRIP, + STRIP_GAP, VIEW, - ZONES, - ZONE_HEAD, - ZONE_INSET, - chipWidth, diagramHrefs, - CHIP_GAP, - CARD_PAD, + stripChipWidth, } from './architecture-diagram'; const WEBSITE = resolve(__dirname, '../..'); const onGrid = (n: number) => n % GRID === 0; +const card = (id: string) => CARDS.find((c) => c.id === id)!; describe('architecture diagram geometry', () => { - it('puts every zone and card on the 8px grid', () => { - for (const z of ZONES) { - expect(onGrid(z.y), `${z.id}.y`).toBe(true); - expect(onGrid(z.height), `${z.id}.height`).toBe(true); - } + it('puts every card on the 8px grid, inside the view with a 40px margin', () => { for (const c of CARDS) { for (const [k, v] of Object.entries({ x: c.x, @@ -35,34 +31,19 @@ describe('architecture diagram geometry', () => { })) { expect(onGrid(v), `${c.id}.${k} = ${v}`).toBe(true); } - } - expect(onGrid(VIEW.width) && onGrid(VIEW.height)).toBe(true); - }); - - it('lays the zones end to end with one 40px gap and a 40px margin', () => { - expect(ZONES[0].y).toBe(40); - ZONES.slice(1).forEach((z, i) => - expect(z.y).toBe(ZONES[i].y + ZONES[i].height + CARD_GAP) - ); - const last = ZONES[ZONES.length - 1]; - expect(last.y + last.height + 40).toBe(VIEW.height); - }); - - it('keeps every card inside its zone with the zone inset, below the zone head', () => { - for (const c of CARDS) { - const z = ZONES.find((x) => x.id === c.zone)!; - expect(c.x, `${c.id} left`).toBeGreaterThanOrEqual(40 + ZONE_INSET); + expect(c.x, `${c.id} left`).toBeGreaterThanOrEqual(40); expect(c.x + c.width, `${c.id} right`).toBeLessThanOrEqual( - 40 + 1200 - ZONE_INSET + VIEW.width - 40 ); - expect(c.y, `${c.id} top`).toBeGreaterThanOrEqual(z.y + ZONE_HEAD); + expect(c.y, `${c.id} top`).toBeGreaterThanOrEqual(40); expect(c.y + c.height, `${c.id} bottom`).toBeLessThanOrEqual( - z.y + z.height - 24 + VIEW.height - 40 ); } + expect(onGrid(VIEW.width) && onGrid(VIEW.height)).toBe(true); }); - it('never overlaps two cards, and separates horizontal neighbours by exactly the card gap', () => { + it('never overlaps two cards, and separates vertical neighbours by at least the card gap', () => { const overlap = (a: (typeof CARDS)[number], b: (typeof CARDS)[number]) => a.x < b.x + b.width && b.x < a.x + a.width && @@ -72,41 +53,82 @@ describe('architecture diagram geometry', () => { for (const b of CARDS) if (a !== b) expect(overlap(a, b), `${a.id} vs ${b.id}`).toBe(false); for (const a of CARDS) { - const right = CARDS.filter((b) => b.y === a.y && b.x > a.x).sort( - (p, q) => p.x - q.x + const below = CARDS.filter((b) => b.x === a.x && b.y > a.y).sort( + (p, q) => p.y - q.y )[0]; - if (right) - expect(right.x - (a.x + a.width), `${a.id} → ${right.id}`).toBe( + if (below) + expect(below.y - (a.y + a.height), `${a.id} ↓ ${below.id}`).toBe( CARD_GAP ); } }); - it('draws each arrow in the gap between two zones, at a grid x', () => { + it('levels the columns: users and Threadplane span the adapter stack exactly', () => { + const top = card('langgraph-sdk'); + const bottom = card('ag-ui'); + for (const id of ['users', 'threadplane']) { + expect(card(id).y).toBe(top.y); + expect(card(id).y + card(id).height).toBe(bottom.y + bottom.height); + } + expect(card('langsmith').y).toBe(top.y); + expect(card('ag-ui-servers').y).toBe(bottom.y); + }); + + it('draws every arrow from one card edge to the next card edge, on a grid row', () => { for (const a of ARROWS) { - expect(onGrid(a.x)).toBe(true); - const from = ZONES.find( - (z) => z.y + z.height <= a.y1 + 40 && z.y < a.y1 - )!; - const to = ZONES.find((z) => z.y === a.y2)!; - expect(from, `arrow at ${a.y1} leaves a zone`).toBeDefined(); - expect(to, `arrow at ${a.y2} enters a zone`).toBeDefined(); + expect(onGrid(a.y), `arrow y ${a.y}`).toBe(true); + const from = CARDS.find( + (c) => c.x + c.width === a.x1 && a.y > c.y && a.y < c.y + c.height + ); + const to = CARDS.find( + (c) => c.x === a.x2 && a.y > c.y && a.y < c.y + c.height + ); + expect(from, `arrow at ${a.x1} leaves a card`).toBeDefined(); + expect(to, `arrow at ${a.x2} enters a card`).toBeDefined(); + expect(a.y).toBeGreaterThan(from!.y); + expect(a.y).toBeLessThan(from!.y + from!.height); + } + }); + + it("keeps the column labels at their column's left edge", () => { + for (const col of COLUMNS) { + expect( + CARDS.some((c) => c.x === col.x), + col.label + ).toBe(true); } }); - it('keeps every chip row inside its card', () => { + it('fits the model strip inside the view', () => { + let x: number = MODEL_STRIP.x; + for (const chip of MODEL_STRIP.chips) + x += stripChipWidth(chip.label) + STRIP_GAP; + // The caption follows the last chip; leave it room. + expect(x + 8 + MODEL_STRIP.caption.length * 7).toBeLessThanOrEqual( + VIEW.width - 40 + ); + expect(MODEL_STRIP.chipY + 36).toBeLessThanOrEqual(VIEW.height - 24); + }); + + it('keeps every text row inside its card vertically', () => { for (const c of CARDS) { for (const r of c.rows) { - if (r.kind !== 'chips') continue; - let x = c.x + CARD_PAD; - for (const chip of r.chips) - x += chipWidth(chip.label, !!chip.mark) + CHIP_GAP; - expect(x - CHIP_GAP, `${c.id} chip row at ${r.y}`).toBeLessThanOrEqual( - c.x + c.width - CARD_PAD - ); - expect(r.y + 28, `${c.id} chip row bottom`).toBeLessThanOrEqual( - c.y + c.height - 16 - ); + if (r.kind === 'text' || r.kind === 'mono') { + expect(r.y, `${c.id} row ${r.text}`).toBeGreaterThan(c.y + 40); + expect(r.y, `${c.id} row ${r.text}`).toBeLessThanOrEqual( + c.y + c.height - 12 + ); + } + if (r.kind === 'caps') { + expect(r.y + r.caps.length * 40).toBeLessThanOrEqual( + c.y + c.height - 24 + ); + } + if (r.kind === 'marks') { + expect( + c.x + CARD_PAD + (r.marks.length - 1) * r.step + r.size + ).toBeLessThanOrEqual(c.x + c.width - CARD_PAD); + } } } }); @@ -122,7 +144,7 @@ describe('architecture diagram links and marks', () => { const page = resolve(WEBSITE, `src/app${href}/page.tsx`); expect(existsSync(mdx) || existsSync(page), `${href}`).toBe(true); } - expect(diagramHrefs().length).toBeGreaterThanOrEqual(10); + expect(diagramHrefs().length).toBeGreaterThanOrEqual(9); }); it('uses only marks that exist under /logos', () => { @@ -130,13 +152,14 @@ describe('architecture diagram links and marks', () => { expect(existsSync(resolve(WEBSITE, `public${path}`)), key).toBe(true); } for (const c of CARDS) { + if (c.mark) expect(LOGOS[c.mark]).toBeDefined(); for (const r of c.rows) { - if (r.kind === 'chips') - for (const chip of r.chips) - if (chip.mark) expect(LOGOS[chip.mark]).toBeDefined(); + if (r.kind === 'badge') expect(LOGOS[r.mark]).toBeDefined(); if (r.kind === 'marks') for (const m of r.marks) expect(LOGOS[m]).toBeDefined(); } } + for (const chip of MODEL_STRIP.chips) + expect(LOGOS[chip.mark]).toBeDefined(); }); }); diff --git a/apps/website/src/lib/architecture-diagram.ts b/apps/website/src/lib/architecture-diagram.ts index c8821793d..ba270d96c 100644 --- a/apps/website/src/lib/architecture-diagram.ts +++ b/apps/website/src/lib/architecture-diagram.ts @@ -1,51 +1,41 @@ /** - * Geometry and copy for the homepage enterprise architecture diagram + * Geometry and copy for the homepage architecture diagram * (spec: docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md). * * One table, three readers: the component draws it, the unit spec checks that - * every rectangle lands on the 8px grid inside its zone, and the e2e measures - * the rendered text against these same boxes. Coordinates are viewBox units; - * the SVG scales with its container. + * every rectangle lands on the 8px grid inside the view without overlapping, + * and the e2e measures the rendered text against these same boxes. + * Coordinates are viewBox units; the SVG scales with its container. * - * Every card links to the docs page that backs its wording. Third-party - * products are examples of the role a card describes, not integrations the - * library claims; the spec §3 lists the source page for each line. + * The story is four columns, left to right: your users, your Angular + * application with Threadplane as its UI layer, the two adapters, and your + * agents — with a strip of model providers beneath. Every card links to the + * docs page that backs its wording. Third-party products appear as examples + * of a role, never as integrations the library claims. */ -export const VIEW = { width: 1280, height: 1088 } as const; +export const VIEW = { width: 1280, height: 624 } as const; export const GRID = 8; export const MAJOR = 40; -/** Distance from a zone's edge to its cards' outer edges (left/right/bottom). */ -export const ZONE_INSET = 32; -/** Vertical room a zone keeps above its first row of cards for its label. */ -export const ZONE_HEAD = 72; export const CARD_GAP = 40; - -export type ZoneId = 'app' | 'edge' | 'platform'; - -export interface Zone { - readonly id: ZoneId; - readonly label: string; - readonly owner: string; - readonly y: number; - readonly height: number; - /** Gradient id in the component's defs. */ - readonly fill: 'app' | 'edge' | 'plat'; - readonly stroke: string; - /** A logo file under /logos to show beside the label. */ - readonly mark?: LogoKey; -} +/** Left inset of card content. */ +export const CARD_PAD = 24; +/** Baseline of a card's title row, below the card's top. */ +export const TITLE_DY = 42; export type LogoKey = | 'angular' | 'vercel' | 'google' | 'langgraph' + | 'langchain' | 'agui' | 'bedrock' | 'azure' | 'microsoft' | 'mastra' + | 'crewai' + | 'pydantic' | 'openai' | 'anthropic'; @@ -55,34 +45,25 @@ export const LOGOS: Readonly> = { vercel: '/logos/surface/vercel.svg', google: '/logos/providers/google.svg', langgraph: '/logos/langgraph.svg', + langchain: '/logos/langchain.svg', agui: '/logos/ag-ui.svg', bedrock: '/logos/providers/bedrock.svg', azure: '/logos/providers/azure.svg', microsoft: '/logos/runtimes/microsoft.svg', mastra: '/logos/runtimes/mastra.svg', + crewai: '/logos/runtimes/crewai.svg', + pydantic: '/logos/runtimes/pydantic.svg', openai: '/logos/providers/openai.svg', anthropic: '/logos/providers/anthropic.svg', }; export type IconKey = - | 'key' - | 'gateway' - | 'trace' - | 'plug' - | 'db' - | 'layers' - | 'cpu' + | 'users' | 'chat' | 'pause' | 'branch' | 'sparkles' - | 'wrench' - | 'sparkle'; - -export interface Chip { - readonly mark?: LogoKey; - readonly label: string; -} + | 'wrench'; export interface Capability { readonly icon: IconKey; @@ -91,113 +72,109 @@ export interface Capability { } export type Row = - /** A row of pill chips, each with an optional mark. `trailing` is a sentence set after the last chip. */ + | { readonly kind: 'text'; readonly y: number; readonly text: string } + | { readonly kind: 'mono'; readonly y: number; readonly text: string } + /** Capability badges stacked vertically from `y`, 40 apart, each its own link. */ | { - readonly kind: 'chips'; + readonly kind: 'caps'; readonly y: number; - readonly chips: readonly Chip[]; - readonly trailing?: string; - readonly tone?: 'light' | 'tp'; + readonly caps: readonly Capability[]; } - /** A row of square mark badges, no label. */ + /** A mark badge with a label beside it, at an absolute x within the card. */ | { - readonly kind: 'marks'; - readonly y: number; + readonly kind: 'badge'; readonly x: number; - readonly marks: readonly LogoKey[]; + readonly y: number; + readonly mark: LogoKey; + readonly label: string; } - | { readonly kind: 'text'; readonly y: number; readonly text: string } - | { readonly kind: 'mono'; readonly y: number; readonly text: string } - /** The Threadplane card's capability badges, each its own link. */ + /** A row of mark badges with no labels. */ | { - readonly kind: 'caps'; + readonly kind: 'marks'; readonly y: number; - readonly caps: readonly Capability[]; + readonly marks: readonly LogoKey[]; + readonly size: number; + readonly step: number; }; export interface Card { readonly id: string; - readonly zone: ZoneId; readonly x: number; readonly y: number; readonly width: number; readonly height: number; readonly title: string; + /** Second title line (the adapter cards break their names over two lines). */ + readonly title2?: string; readonly href: string; - /** Icon badge beside the title; absent on the Threadplane card, which carries the brand mark. */ + /** A mark badge beside the title (40px), or an icon badge. */ + readonly mark?: LogoKey; readonly icon?: { readonly name: IconKey; readonly bg: string; readonly fg: string; }; - /** Right-aligned tag on the title line (only the Threadplane card). */ + /** Right-aligned tag on the card's first line. */ readonly tag?: string; - /** Whether to render the "docs ↗" affordance at the top-right. */ - readonly docsLabel: boolean; readonly highlight?: boolean; readonly rows: readonly Row[]; } -export interface Arrow { +export interface ColumnLabel { readonly x: number; - readonly y1: number; - readonly y2: number; - readonly caption: string; + readonly label: string; } -export const ZONES: readonly Zone[] = [ - { - id: 'app', - label: 'YOUR ANGULAR APPLICATION', - owner: 'you own this zone', - y: 40, - height: 280, - fill: 'app', - stroke: '#d6deee', - mark: 'angular', - }, - { - id: 'edge', - label: 'YOUR PLATFORM EDGE', - owner: 'you own this zone', - y: 360, - height: 224, - fill: 'edge', - stroke: '#d9dee6', - }, - { - id: 'platform', - label: 'AGENT PLATFORM', - owner: 'your runtime owns this zone', - y: 624, - height: 424, - fill: 'plat', - stroke: '#d3e3d9', - }, +export interface Arrow { + readonly x1: number; + readonly x2: number; + readonly y: number; +} + +export interface StripChip { + readonly mark: LogoKey; + readonly label: string; +} + +export const COLUMNS: readonly ColumnLabel[] = [ + { x: 64, label: 'YOUR USERS' }, + { x: 304, label: 'YOUR ANGULAR APPLICATION' }, + { x: 776, label: 'ADAPTERS' }, + { x: 1024, label: 'YOUR AGENTS' }, ]; +export const COLUMN_LABEL_Y = 72; export const CARDS: readonly Card[] = [ + { + id: 'users', + x: 64, + y: 104, + width: 176, + height: 392, + title: 'People', + href: '/docs/chat/getting-started/introduction', + icon: { name: 'users', bg: '#fff3e0', fg: '#c2410c' }, + rows: [{ kind: 'text', y: 248, text: 'web · mobile · desktop' }], + }, { id: 'threadplane', - zone: 'app', - x: 72, - y: 112, - width: 744, - height: 176, + x: 304, + y: 104, + width: 408, + height: 392, title: 'Threadplane', href: '/docs/chat/getting-started/introduction', - tag: 'THE FINAL MILE', - docsLabel: false, + tag: 'THE UI LAYER', highlight: true, rows: [ { kind: 'caps', - y: 168, + y: 224, caps: [ { icon: 'chat', label: 'Chat', href: '/docs/chat/components/chat' }, { icon: 'pause', - label: 'Interrupts', + label: 'Approvals', href: '/docs/langgraph/guides/interrupts', }, { @@ -217,228 +194,121 @@ export const CARDS: readonly Card[] = [ }, ], }, + { kind: 'badge', x: 540, y: 224, mark: 'google', label: 'A2UI' }, + { kind: 'badge', x: 540, y: 272, mark: 'vercel', label: 'json-render' }, { kind: 'mono', - y: 228, - text: '@threadplane/chat · render · langgraph · ag-ui · middleware', - }, - { - kind: 'chips', - y: 244, - tone: 'tp', - chips: [ - { mark: 'google', label: 'A2UI v0.9' }, - { mark: 'vercel', label: 'json-render' }, - ], - trailing: - 'generative UI on two open standards · one Agent contract for every adapter', + y: 470, + text: '@threadplane/chat · render · langgraph · ag-ui', }, ], }, { - id: 'components', - zone: 'app', - x: 856, - y: 112, - width: 352, - height: 176, - title: 'Your components', - href: '/docs/chat/guides/client-tools', - icon: { name: 'layers', bg: '#ecfdf5', fg: '#047857' }, - docsLabel: true, - rows: [ - { - kind: 'text', - y: 192, - text: 'design system · tool views · client-tool handlers', - }, - { - kind: 'text', - y: 218, - text: 'pages · routing · state · APM on status() and error()', - }, - { kind: 'text', y: 244, text: 'unchanged by Threadplane' }, - ], - }, - { - id: 'gateway', - zone: 'edge', - x: 72, - y: 432, - width: 744, - height: 128, - title: 'Same-origin proxy or API gateway', - href: '/docs/langgraph/guides/deployment', - icon: { name: 'gateway', bg: '#eef2f7', fg: '#4a5568' }, - docsLabel: true, - rows: [ - { - kind: 'chips', - y: 494, - chips: [ - { mark: 'azure', label: 'Azure API Management' }, - { mark: 'bedrock', label: 'Amazon API Gateway' }, - { mark: 'google', label: 'Apigee' }, - ], - }, - { - kind: 'text', - y: 544, - text: 'adds the deployment credentials · forwards user identity · keys never reach the browser', - }, - ], - }, - { - id: 'identity', - zone: 'edge', - x: 856, - y: 432, - width: 352, - height: 128, - title: 'Identity & session', - href: '/docs/langgraph/guides/deployment', - icon: { name: 'key', bg: '#fff3e0', fg: '#c2410c' }, - docsLabel: true, - rows: [ - { - kind: 'chips', - y: 494, - chips: [{ mark: 'microsoft', label: 'Microsoft Entra ID' }], - }, - { kind: 'text', y: 544, text: 'or Okta · session as HTTP-only cookies' }, - ], - }, - { - id: 'runtime', - zone: 'platform', - x: 72, - y: 696, - width: 408, - height: 152, - title: 'Agent runtime', + id: 'langgraph-sdk', + x: 776, + y: 104, + width: 200, + height: 208, + title: 'LangGraph', + title2: 'SDK', href: '/docs/langgraph/getting-started/introduction', - icon: { name: 'cpu', bg: '#e0f2fe', fg: '#0369a1' }, - docsLabel: true, - rows: [ - { - kind: 'chips', - y: 762, - chips: [ - { mark: 'langgraph', label: 'LangGraph Platform' }, - { mark: 'agui', label: 'AG-UI' }, - ], - }, - { - kind: 'chips', - y: 798, - chips: [ - { mark: 'bedrock', label: 'Strands' }, - { mark: 'microsoft', label: 'Agent Framework' }, - { mark: 'mastra', label: 'Mastra' }, - ], - }, - ], - }, - { - id: 'models', - zone: 'platform', - x: 520, - y: 696, - width: 336, - height: 152, - title: 'Models', - href: '/docs/runtimes/getting-started/introduction', - icon: { name: 'sparkle', bg: '#f5f3ff', fg: '#6d28d9' }, - docsLabel: true, + mark: 'langgraph', + tag: 'FIRST-CLASS', + highlight: true, rows: [ - { - kind: 'chips', - y: 762, - chips: [ - { mark: 'azure', label: 'Azure OpenAI' }, - { mark: 'bedrock', label: 'Amazon Bedrock' }, - ], - }, - { - kind: 'chips', - y: 798, - chips: [{ mark: 'google', label: 'Vertex AI' }], - }, - { kind: 'marks', y: 796, x: 664, marks: ['openai', 'anthropic'] }, + { kind: 'text', y: 204, text: 'threads · checkpoints' }, + { kind: 'text', y: 228, text: 'interrupts · streaming' }, + { kind: 'text', y: 252, text: 'time travel · branch' }, + { kind: 'text', y: 276, text: 'memory · subgraphs' }, + { kind: 'text', y: 300, text: 'durable execution' }, ], }, { - id: 'tools', - zone: 'platform', - x: 896, - y: 696, - width: 312, - height: 152, - title: 'Tools · MCP · data', - href: '/docs/middleware/getting-started/introduction', - icon: { name: 'plug', bg: '#fff7ed', fg: '#c2410c' }, - docsLabel: true, + id: 'ag-ui', + x: 776, + y: 352, + width: 200, + height: 144, + title: 'AG-UI', + title2: 'protocol', + href: '/docs/ag-ui/getting-started/introduction', + mark: 'agui', rows: [ - { kind: 'text', y: 780, text: 'server tools run here, on your systems' }, - { kind: 'text', y: 808, text: 'client tools round-trip to the browser' }, + { kind: 'text', y: 452, text: 'events · tool calls' }, + { kind: 'text', y: 476, text: 'state · interrupts' }, ], }, { - id: 'observability', - zone: 'platform', - x: 72, - y: 888, - width: 352, - height: 128, - title: 'Observability', + id: 'langsmith', + x: 1024, + y: 104, + width: 216, + height: 208, + title: 'LangSmith', href: '/docs/langgraph/guides/deployment', - icon: { name: 'trace', bg: '#fdf2f8', fg: '#be185d' }, - docsLabel: true, + mark: 'langchain', rows: [ - { - kind: 'chips', - y: 952, - chips: [{ label: 'LangSmith' }], - trailing: 'traces every run · evals · token cost', - }, + { kind: 'text', y: 172, text: 'deploy · observe' }, + { kind: 'text', y: 204, text: 'LangGraph agents' }, + { kind: 'text', y: 228, text: 'traces · evals' }, + { kind: 'text', y: 252, text: 'or self-hosted' }, ], }, { - id: 'state', - zone: 'platform', - x: 464, - y: 888, - width: 744, - height: 128, - title: 'Durable state', - href: '/docs/langgraph/guides/persistence', - icon: { name: 'db', bg: '#f1f5f9', fg: '#334155' }, - docsLabel: true, + id: 'ag-ui-servers', + x: 1024, + y: 352, + width: 216, + height: 144, + title: 'AG-UI servers', + href: '/docs/runtimes/getting-started/introduction', rows: [ { - kind: 'text', - y: 968, - text: 'checkpoints at every super-step, keyed by thread · platform-managed, or Postgres / SQLite when you embed the graph', + kind: 'marks', + y: 400, + marks: ['crewai', 'mastra', 'microsoft', 'bedrock', 'pydantic'], + size: 30, + step: 34, }, - { kind: 'text', y: 994, text: 'exposed by Threadplane, never faked' }, + { kind: 'text', y: 460, text: 'CrewAI · Mastra · Microsoft' }, + { kind: 'text', y: 482, text: 'Strands · Pydantic AI' }, ], }, ]; export const ARROWS: readonly Arrow[] = [ - { - x: 440, - y1: 288, - y2: 360, - caption: 'relative apiUrl · POST + SSE via the LangGraph SDK', - }, - { - x: 440, - y1: 560, - y2: 624, - caption: 'credentials added server-side · CORS on the runtime', - }, + { x1: 240, x2: 304, y: 272 }, + { x1: 712, x2: 776, y: 208 }, + { x1: 712, x2: 776, y: 424 }, + { x1: 976, x2: 1024, y: 208 }, + { x1: 976, x2: 1024, y: 424 }, ]; +/** The two-line caption between the adapter arrows. */ +export const CONTRACT_CAPTION = { + x: 744, + y: 306, + lines: ['one Agent', 'contract'], +} as const; + +export const MODEL_STRIP = { + label: 'ANY MODEL', + labelY: 540, + chipY: 556, + x: 64, + chips: [ + { mark: 'openai', label: 'OpenAI' }, + { mark: 'anthropic', label: 'Anthropic' }, + { mark: 'google', label: 'Google' }, + { mark: 'azure', label: 'Azure OpenAI' }, + { mark: 'bedrock', label: 'Amazon Bedrock' }, + ] as readonly StripChip[], + caption: 'chosen by your runtime, never by the UI', +} as const; + +export const STRIP_CHIP_H = 36; +export const stripChipWidth = (label: string): number => + Math.round(label.length * 7.2) + 48; +export const STRIP_GAP = 12; /** Every docs href the diagram links to, deduplicated, for the link-resolution spec. */ export function diagramHrefs(): readonly string[] { @@ -450,13 +320,3 @@ export function diagramHrefs(): readonly string[] { } return [...out]; } - -/** Baseline of a card's title row: 38 below the card's top. */ -export const TITLE_DY = 38; -/** Left inset of card content. */ -export const CARD_PAD = 24; -/** Chip geometry: height and the width formula the component and the e2e share. */ -export const CHIP_H = 28; -export const chipWidth = (label: string, withMark: boolean): number => - Math.round(label.length * 7.2) + (withMark ? 44 : 24); -export const CHIP_GAP = 12; diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index 0b3f65114..3cf264540 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1695,6 +1695,17 @@ font-size: 15px; font-weight: 600; } +.arch-figure .arch-title--lg { + font-size: 19px; +} +.arch-figure .arch-zone-label--app { + font-size: 10.5px; + fill: #2f5fa8; +} +.arch-figure .arch-caption--mid { + text-anchor: middle; + font-size: 11.5px; +} .arch-figure .arch-body { font-size: 12.5px; fill: var(--color-text-secondary); @@ -1758,7 +1769,7 @@ stroke: #c9d6ee; } .arch-figure .arch-chip text { - font-size: 11.5px; + font-size: 12.5px; font-weight: 500; fill: #2a2f3a; } @@ -1767,14 +1778,14 @@ stroke: #e2e6ec; } .arch-figure .arch-cap text { - font-size: 13px; + font-size: 14px; font-weight: 500; fill: var(--color-text-primary); } .arch-figure .arch-cap:hover text { fill: #2f5fa8; } -.arch-figure .arch-arrow path { +.arch-figure .arch-arrow { stroke: #7d8492; stroke-width: 1.5; fill: none; diff --git a/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md b/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md index 9d0f2f4f8..573039209 100644 --- a/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md +++ b/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md @@ -7,47 +7,42 @@ ## 1. Why -The homepage tells a developer what Threadplane does (the stage) but not where it sits in the estate they already run. Architects need one picture: three zones they recognise, one highlighted box inside their own application, and every named product in the place it actually occupies. The old scope table answered "what do we add" as prose; the diagram answers "where does it go" spatially, which is the final-mile argument made visible. +The homepage tells a developer what Threadplane does (the stage) but not where it sits in the estate they already run. Buyers need one picture they can find themselves in: their users, their Angular application with Threadplane as its UI layer, the adapter to their runtime, their agents, and their model provider, each by its mark. The old scope table answered "what do we add" as prose; the diagram answers "where does it go" spatially, which is the final-mile argument made visible. ## 2. Decisions | Decision | Choice | |---|---| -| Framing | Layers and boundaries (framing A): three horizontal zones, your Angular application, your platform edge, the agent platform, with Threadplane the only highlighted box, inside your application zone. | -| Source of truth for contents | The docs. Every box links to the page that backs its wording; nothing on the diagram claims what a docs page does not say. Named third-party products appear as examples of a role, never as integrations Threadplane claims. | -| Marks | Only marks the site already holds under `apps/website/public/logos/` (README documents their sources). Generic enterprise boxes get a consistent line icon in a tinted badge, never a fake logo. LangSmith is a text chip until its mark is sourced under the same rules. | -| Precision | Coordinates on an 8px grid with 40px majors; a hidden alignment grid is part of the component (`data-grid` attribute toggles it) for review; a unit test asserts every rect lands on the grid; an e2e measures every text run and asserts it is inside its box. | -| Colour | Tinted gradient zones (blue-violet application, neutral edge, green-to-blue platform), white cards with a faint vertical gradient, a light blue tint with a blue hairline for Threadplane, the site's dot grid behind. | -| Interaction | Every card is a link to its docs page with a "docs ↗" affordance at the top-right; no hover states beyond the link. No animation. | -| Placement | Where `ScopeTable` was: between Reliability and the stage. Eyebrow "Architecture", headline "Where Threadplane fits in your agent platform.", body: "One highlighted box inside your Angular application. Everything else is yours or your runtime's, and the docs say what crosses each line." | -| Responsive | The SVG scales with its container down to 1024px; below that it scrolls horizontally inside the diagram frame at a 1024px minimum width (the kit's scroll-shadow treatment), so text never drops below legibility. | - -## 3. Contents (verbatim, with the source page each links to) - -**Zone 1, YOUR ANGULAR APPLICATION (Angular mark), "you own this zone".** -- **Threadplane** card, tag THE FINAL MILE; capabilities as icon badges, each a link: Chat (`/docs/chat/components/chat`), Interrupts (`/docs/langgraph/guides/interrupts`), Threads (`/docs/langgraph/guides/persistence`), Generative UI (`/docs/chat/guides/generative-ui`), Client tools (`/docs/chat/guides/client-tools`); mono line `@threadplane/chat · render · langgraph · ag-ui · middleware`; chips A2UI v0.9 (Google mark) and json-render (Vercel mark); caption "generative UI on two open standards · one Agent contract for every adapter". Card links to `/docs/chat/getting-started/introduction`. -- **Your components** (layers icon): "design system · tool views · client-tool handlers", "pages · routing · state · your APM on status() and error()", "unchanged by Threadplane". Links `/docs/chat/guides/client-tools`. -- Arrow to zone 2, caption "relative apiUrl · POST + SSE via the LangGraph SDK" (langgraph-basics, angular-signals, deployment). - -**Zone 2, YOUR PLATFORM EDGE, "you own this zone".** -- **Same-origin proxy or API gateway** (gateway icon): chips Azure API Management, Amazon API Gateway, Apigee; line "adds the deployment credentials · forwards user identity · keys never reach the browser". Links `/docs/langgraph/guides/deployment`. -- **Identity & session** (key icon): chip Microsoft Entra ID; line "or Okta · session as HTTP-only cookies". Links `/docs/langgraph/guides/deployment`. -- Arrow to zone 3, caption "credentials added server-side · CORS on the runtime". - -**Zone 3, AGENT PLATFORM, "your runtime owns this zone".** -- **Agent runtime** (cpu icon): chips LangGraph Platform, AG-UI; Strands (AWS mark), Agent Framework (Microsoft mark), Mastra. Links `/docs/langgraph/getting-started/introduction`. -- **Models** (sparkle icon): chips Azure OpenAI, Amazon Bedrock, Vertex AI; OpenAI and Anthropic marks as badges. Links `/docs/runtimes/getting-started/introduction`. -- **Tools · MCP · data** (plug icon): "server tools run here, against your systems", "client tools round-trip to the browser". Links `/docs/middleware/getting-started/introduction`. -- **Observability** (trace icon): text chip LangSmith; "traces every run · evals · token cost". Links `/docs/langgraph/guides/deployment`. -- **Durable state** (database icon): "checkpoints at every super-step, keyed by thread · platform-managed, or Postgres / SQLite when you embed the graph", "exposed by Threadplane, never faked". Links `/docs/langgraph/guides/persistence`. +| Framing | A left-to-right flow for a buyer scanning for their own logos: your users, your Angular application with Threadplane as its UI layer, the two adapters, your agents, and a strip of model providers beneath. The earlier three-zone enterprise map was drawn, reviewed, and scaled back at the user's direction: less text, broader architecture, more marks. | +| Source of truth for contents | The docs. Every card links to the page that backs its wording. Named third-party products are examples of a role, never integrations Threadplane claims. | +| The first-class lane | The LangGraph SDK adapter is highlighted like Threadplane and tagged FIRST-CLASS, listing what the docs reserve for a checkpoint-aware runtime beyond the AG-UI event stream: time travel and branch, memory and subgraphs, durable execution. The AG-UI card lists events, tool calls, state, interrupts. | +| Marks | Only marks held under `apps/website/public/logos/` (README documents sources). LangSmith uses the LangChain mark from Simple Icons, which has no LangSmith slug; the README records that. Generic roles get a line icon in a tinted badge. | +| Precision | Coordinates on an 8px grid with 40px majors; a hidden alignment grid is part of the component for review; a unit spec asserts every card on the grid inside the view, no overlaps, 40px stack gaps, arrows leaving and entering card edges, and every href resolving; an e2e measures every rendered text run and chip inside its card. | +| Colour | The kit's dot grid, white cards with a faint vertical gradient, a light blue tint with a blue hairline for the two highlighted cards. No zone fills. | +| Interaction | Cards are links to their docs pages; the Threadplane card's five capabilities are their own links, so its title carries the card link (anchors do not nest). No hover states beyond links, no animation. | +| Placement | Where the scope table was: between Reliability and the stage. Eyebrow "Architecture", headline "The UI layer between your users and your agents.", body "Threadplane lives inside your Angular application and talks to your agents through the LangGraph SDK or AG-UI. Everything on the right is yours." | +| Responsive | The SVG scales with its container down to 1024px; below that it scrolls horizontally inside the diagram frame at a 1024px minimum width. | + +## 3. Contents + +Column labels: YOUR USERS, YOUR ANGULAR APPLICATION, ADAPTERS, YOUR AGENTS. + +- **People** (users icon): "web · mobile · desktop". Links `/docs/chat/getting-started/introduction`. +- **Threadplane** (Angular mark, "YOUR ANGULAR APP", tag THE UI LAYER): five capability links, Chat, Approvals, Threads, Generative UI, Client tools (`/docs/chat/components/chat`, `/docs/langgraph/guides/interrupts`, `/docs/langgraph/guides/persistence`, `/docs/chat/guides/generative-ui`, `/docs/chat/guides/client-tools`); A2UI (Google mark) and json-render (Vercel mark); the package line `@threadplane/chat · render · langgraph · ag-ui`. Title links `/docs/chat/getting-started/introduction`. +- **LangGraph SDK** (LangGraph mark, tag FIRST-CLASS, highlighted): "threads · checkpoints", "interrupts · streaming", "time travel · branch", "memory · subgraphs", "durable execution". Links `/docs/langgraph/getting-started/introduction`. +- **AG-UI protocol** (AG-UI mark): "events · tool calls", "state · interrupts". Links `/docs/ag-ui/getting-started/introduction`. +- **LangSmith** (LangChain mark): "deploy · observe", "LangGraph agents", "traces · evals", "or self-hosted". Links `/docs/langgraph/guides/deployment`. +- **AG-UI servers**: five marks, CrewAI, Mastra, Microsoft, AWS (Strands), Pydantic AI, then "CrewAI · Mastra · Microsoft", "Strands · Pydantic AI". Links `/docs/runtimes/getting-started/introduction`. +- Arrows: users → Threadplane; Threadplane → each adapter with the caption "one Agent contract" between them; each adapter → its agents card. +- **ANY MODEL** strip: OpenAI, Anthropic, Google, Azure OpenAI, Amazon Bedrock as mark chips, caption "chosen by your runtime, never by the UI". ## 4. Component -`apps/website/src/components/landing/EnterpriseArchitecture.tsx`, a server component rendering one `` inside the kit's `DiagramFrame` at `scale="marketing"`, framed by `DiagramSection`. Logos are `` referencing the public files. The diagram's geometry lives in a data module, `apps/website/src/lib/architecture-diagram.ts`, as typed zone/card/chip records so the tests can read the same numbers the component draws. The line icons are inline paths in the component (five to seven, matching the mockup). +`apps/website/src/components/landing/EnterpriseArchitecture.tsx`, a server component rendering one `` inside the kit's `DiagramFrame` at `scale="marketing"`, framed by `DiagramSection`. Logos are `` referencing the public files. The diagram's geometry lives in a data module, `apps/website/src/lib/architecture-diagram.ts`, as typed card/arrow/strip records so the tests can read the same numbers the component draws. The line icons are inline paths in the component (five to seven, matching the mockup). ## 5. Verification -- **Unit:** every zone and card rect has `x, y, width, height` divisible by 8, zone padding is 32 and card gaps 40, no two cards overlap, and every card sits inside its zone (from the data module). The component renders 12 links whose hrefs each resolve to an existing docs page or route (the spec reads `apps/website/content/docs` and `src/app`). Public-copy scan stays green. +- **Unit:** every card rect has `x, y, width, height` divisible by 8 inside the view's 40px margin, stacked cards are 40 apart, no two cards overlap, the users and Threadplane cards span the adapter stack exactly, every arrow leaves one card edge and enters the next, and the model strip fits (from the data module). The component renders every link whose hrefs each resolve to an existing docs page or route (the spec reads `apps/website/content/docs` and `src/app`). Public-copy scan stays green. - **e2e (`home-architecture.spec.ts`):** at 1440×900 every `` in the diagram has a `getBBox()` inside its parent card's rect with 8px of margin, every chip rect is inside its card, every `` loaded (`naturalWidth`/complete via the parent's `getBBox` non-zero), and the section's links have the expected hrefs. At 390px the frame scrolls horizontally and the SVG is at least 1024px wide. - **Visual sign-off:** rendered frames at 1440 and 390 shown to the user before the PR merges; the PR is opened without auto-merge. - **Spine test:** the homepage heading order replaces `why-heading` with `architecture-heading`. From 32dfb8359aff80451670640c83d0763eb36784c5 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 7 Sep 2026 13:36:40 -0700 Subject: [PATCH 4/8] =?UTF-8?q?refactor(website):=20architecture=20diagram?= =?UTF-8?q?=20=E2=80=94=20fainter=20dots,=20flatter=20gradients,=20larger?= =?UTF-8?q?=20type,=20a=20stacked=20phone=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The phone renders the same cards as an HTML stack in reading order, driven by the same data module, instead of scrolling the drawing sideways; the SVG is hidden under 768px. Type is one step larger across the diagram for legibility at page scale. Co-Authored-By: Claude Fable 5.1 --- apps/website/e2e/home-architecture.spec.ts | 19 +-- .../landing/EnterpriseArchitecture.tsx | 100 ++++++++++++- apps/website/src/styles/landing.css | 132 ++++++++++++++++-- 3 files changed, 228 insertions(+), 23 deletions(-) diff --git a/apps/website/e2e/home-architecture.spec.ts b/apps/website/e2e/home-architecture.spec.ts index 2342b67ff..c0e269258 100644 --- a/apps/website/e2e/home-architecture.spec.ts +++ b/apps/website/e2e/home-architecture.spec.ts @@ -86,18 +86,19 @@ test.describe('homepage architecture', () => { for (const w of report.images) expect(w).toBeGreaterThan(0); }); - test('scrolls horizontally on a phone instead of shrinking the type', async ({ + test('stacks the same cards on a phone instead of scrolling the drawing sideways', async ({ page, }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/'); - const fig = page.locator(`${DIAGRAM} .tp-diagram-figure`); - await fig.scrollIntoViewIfNeeded(); - const size = await fig.evaluate((el) => ({ - scroll: el.scrollWidth, - client: el.clientWidth, - })); - expect(size.scroll).toBeGreaterThanOrEqual(1024); - expect(size.scroll).toBeGreaterThan(size.client); + const stack = page.locator(`${DIAGRAM} [data-arch-stack]`); + await stack.scrollIntoViewIfNeeded(); + await expect(stack).toBeVisible(); + await expect(page.locator(`${DIAGRAM} .tp-diagram-figure`)).toBeHidden(); + await expect(stack.locator('a.arch-stack-card')).toHaveCount(CARDS.length); + const wide = await page.evaluate( + () => document.documentElement.scrollWidth > window.innerWidth + ); + expect(wide, 'no horizontal page scroll on a phone').toBe(false); }); }); diff --git a/apps/website/src/components/landing/EnterpriseArchitecture.tsx b/apps/website/src/components/landing/EnterpriseArchitecture.tsx index d3ae07caf..ff94dd9ab 100644 --- a/apps/website/src/components/landing/EnterpriseArchitecture.tsx +++ b/apps/website/src/components/landing/EnterpriseArchitecture.tsx @@ -356,6 +356,99 @@ function ModelStrip() { ); } +const STACK_ORDER = [ + 'users', + 'threadplane', + 'langgraph-sdk', + 'ag-ui', + 'langsmith', + 'ag-ui-servers', +] as const; + +/** + * The phone form of the diagram: the same cards, in reading order, as an + * HTML stack. Shown under 768px by CSS; the SVG is hidden there. + */ +function ArchitectureStack() { + const byId = new Map(CARDS.map((c) => [c.id, c])); + return ( +
+ {STACK_ORDER.map((id, i) => { + const c = byId.get(id)!; + const col = COLUMNS.find((col) => col.x === c.x); + const caps = c.rows.find((r) => r.kind === 'caps'); + const marks = c.rows.find((r) => r.kind === 'marks'); + const texts = c.rows.filter((r) => r.kind === 'text'); + const mono = c.rows.find((r) => r.kind === 'mono'); + const badges = c.rows.filter((r) => r.kind === 'badge'); + return ( + + ); + })} +

{MODEL_STRIP.label}

+
+
+ {MODEL_STRIP.chips.map((chip) => ( + {chip.label} + ))} +
+
    +
  • {MODEL_STRIP.chips.map((c) => c.label).join(' · ')}
  • +
  • {MODEL_STRIP.caption}
  • +
+
+
+ ); +} + function AlignmentGrid() { const lines: ReactNode[] = []; for (let x = 0; x <= VIEW.width; x += GRID) { @@ -423,11 +516,11 @@ export function EnterpriseArchitecture({ grid = false }: Props) { - + - - + + {COLUMNS.map((c) => ( @@ -469,6 +562,7 @@ export function EnterpriseArchitecture({ grid = false }: Props) { {grid ? : null} + ); diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index 3cf264540..5689ed455 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1688,15 +1688,15 @@ } .arch-figure text { font-family: var(--font-inter); - font-size: 12.5px; + font-size: 13.5px; fill: var(--color-text-primary); } .arch-figure .arch-title { - font-size: 15px; + font-size: 16px; font-weight: 600; } .arch-figure .arch-title--lg { - font-size: 19px; + font-size: 20px; } .arch-figure .arch-zone-label--app { font-size: 10.5px; @@ -1704,24 +1704,24 @@ } .arch-figure .arch-caption--mid { text-anchor: middle; - font-size: 11.5px; + font-size: 12.5px; } .arch-figure .arch-body { - font-size: 12.5px; + font-size: 13.5px; fill: var(--color-text-secondary); } .arch-figure .arch-caption { - font-size: 12px; + font-size: 13px; font-style: italic; fill: var(--color-text-secondary); } .arch-figure .arch-mono { font-family: var(--font-mono); - font-size: 11.5px; + font-size: 12px; fill: #2a3a5c; } .arch-figure .arch-zone-label { - font-size: 11.5px; + font-size: 12px; font-weight: 700; letter-spacing: 0.14em; fill: var(--color-text-secondary); @@ -1732,7 +1732,7 @@ fill: var(--color-text-muted, #8a8f98); } .arch-figure .arch-tag { - font-size: 10.5px; + font-size: 11px; font-weight: 700; letter-spacing: 0.12em; text-anchor: end; @@ -1769,7 +1769,7 @@ stroke: #c9d6ee; } .arch-figure .arch-chip text { - font-size: 12.5px; + font-size: 13px; font-weight: 500; fill: #2a2f3a; } @@ -1778,7 +1778,7 @@ stroke: #e2e6ec; } .arch-figure .arch-cap text { - font-size: 14px; + font-size: 15px; font-weight: 500; fill: var(--color-text-primary); } @@ -1800,6 +1800,116 @@ stroke-width: 0.5; opacity: 0.6; } +/* The dot ground stays, one step fainter than the docs kit's. */ +.arch-figure .tp-diagram-dot { + fill: #e3e7ed; +} +/* Phone form: the same cards as an HTML stack, driven by the same data. + * The SVG is hidden here instead of scrolled sideways. */ +.arch-stack { + display: none; +} +@media (max-width: 767px) { + .arch-figure .tp-diagram-figure { + display: none; + } + .arch-stack { + display: grid; + gap: 12px; + margin-top: 8px; + } +} +.arch-stack-label { + text-align: left; + font-family: var(--font-inter); + font-size: 11px; + letter-spacing: 0.14em; + font-weight: 700; + color: var(--color-text-secondary); + margin: 12px 0 0; +} +.arch-stack-card { + display: block; + text-align: left; + text-decoration: none; + color: inherit; + border: 1px solid #dfe3ea; + border-radius: 14px; + background: #fff; + padding: 16px; +} +.arch-stack-card[data-highlight] { + background: #f2f6fd; + border-color: #b7c8e6; +} +.arch-stack-head { + display: flex; + align-items: center; + gap: 12px; +} +.arch-stack-head img, +.arch-stack-marks img { + width: 20px; + height: 20px; +} +.arch-stack-title { + font-family: var(--font-inter); + font-size: 15px; + font-weight: 600; + margin: 0; +} +.arch-stack-tag { + margin-left: auto; + font-size: 10px; + letter-spacing: 0.12em; + font-weight: 700; + color: #2f5fa8; +} +.arch-stack-rows { + margin: 8px 0 0; + padding: 0; + list-style: none; + font-family: var(--font-inter); + font-size: 13px; + color: var(--color-text-secondary); + display: grid; + gap: 4px; +} +.arch-stack-caps { + display: flex; + flex-wrap: wrap; + gap: 6px 10px; + margin: 10px 0 0; + padding: 0; + list-style: none; + font-family: var(--font-inter); + font-size: 13px; + font-weight: 500; +} +.arch-stack-caps li { + background: #e4ecfa; + color: #2f5fa8; + border-radius: 999px; + padding: 4px 10px; +} +.arch-stack-marks { + display: flex; + gap: 8px; + margin-top: 10px; +} +.arch-stack-mono { + font-family: var(--font-mono); + font-size: 11.5px; + color: #2a3a5c; + margin: 10px 0 0; +} +.arch-stack-arrow { + text-align: center; + color: #7d8492; + font-size: 16px; + line-height: 1; +} + /* HomeConceptGrid — components/landing/HomeConceptGrid.tsx */ .home-concept { From 96677bf812be3b81807b3dbac727bf727aae29f8 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 7 Sep 2026 13:49:15 -0700 Subject: [PATCH 5/8] =?UTF-8?q?refactor(website):=20architecture=20diagram?= =?UTF-8?q?=20=E2=80=94=20less=20copy,=20a=20soft=20ground=20under=20the?= =?UTF-8?q?=20dot=20texture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every card is down to its title, marks, and one to three short lines; the model strip's caveat is two words. A faint radial gradient sits under the dot pattern so the figure reads as a surface, not a sheet. Co-Authored-By: Claude Fable 5.1 --- .../landing/EnterpriseArchitecture.tsx | 22 +++++++++++++++ apps/website/src/lib/architecture-diagram.ts | 27 +++++++------------ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/apps/website/src/components/landing/EnterpriseArchitecture.tsx b/apps/website/src/components/landing/EnterpriseArchitecture.tsx index ff94dd9ab..b58c3989b 100644 --- a/apps/website/src/components/landing/EnterpriseArchitecture.tsx +++ b/apps/website/src/components/landing/EnterpriseArchitecture.tsx @@ -518,11 +518,33 @@ export function EnterpriseArchitecture({ grid = false }: Props) { + + + + + + + {COLUMNS.map((c) => ( Date: Mon, 7 Sep 2026 18:17:33 -0700 Subject: [PATCH 6/8] refactor(website): the adapter and agent rows read as a banded feature list Each capability line sits in its own soft band with a left accent bar, white-on-tint inside the first-class LangGraph lane and grey elsewhere, so the lines read as a list rather than loose paragraph text. The phone stack mirrors the same treatment from the same data. Co-Authored-By: Claude Opus 5 --- .../landing/EnterpriseArchitecture.tsx | 43 +++++++++++++++++++ .../src/lib/architecture-diagram.spec.ts | 7 +++ apps/website/src/lib/architecture-diagram.ts | 41 ++++++++++++++---- apps/website/src/styles/landing.css | 34 +++++++++++++++ 4 files changed, 117 insertions(+), 8 deletions(-) diff --git a/apps/website/src/components/landing/EnterpriseArchitecture.tsx b/apps/website/src/components/landing/EnterpriseArchitecture.tsx index b58c3989b..c861ac888 100644 --- a/apps/website/src/components/landing/EnterpriseArchitecture.tsx +++ b/apps/website/src/components/landing/EnterpriseArchitecture.tsx @@ -162,6 +162,41 @@ function CardRows({ card }: { card: Card }) { ); + case 'items': + return ( + + {row.items.map((item, j) => { + const y = row.y + j * row.step; + return ( + + + + + {item} + + + ); + })} + + ); case 'marks': return ( @@ -379,6 +414,7 @@ function ArchitectureStack() { const caps = c.rows.find((r) => r.kind === 'caps'); const marks = c.rows.find((r) => r.kind === 'marks'); const texts = c.rows.filter((r) => r.kind === 'text'); + const items = c.rows.find((r) => r.kind === 'items'); const mono = c.rows.find((r) => r.kind === 'mono'); const badges = c.rows.filter((r) => r.kind === 'badge'); return ( @@ -409,6 +445,13 @@ function ArchitectureStack() { )} ) : null} + {items && items.kind === 'items' ? ( +
    + {items.items.map((item) => ( +
  • {item}
  • + ))} +
+ ) : null} {caps && caps.kind === 'caps' ? (
    {caps.caps.map((cap) => ( diff --git a/apps/website/src/lib/architecture-diagram.spec.ts b/apps/website/src/lib/architecture-diagram.spec.ts index 074f6f4b4..edc16a82b 100644 --- a/apps/website/src/lib/architecture-diagram.spec.ts +++ b/apps/website/src/lib/architecture-diagram.spec.ts @@ -119,6 +119,13 @@ describe('architecture diagram geometry', () => { c.y + c.height - 12 ); } + if (r.kind === 'items') { + const last = r.y + (r.items.length - 1) * r.step; + expect(r.y - 18, `${c.id} items top`).toBeGreaterThan(c.y + 40); + expect(last + 8, `${c.id} items bottom`).toBeLessThanOrEqual( + c.y + c.height - 8 + ); + } if (r.kind === 'caps') { expect(r.y + r.caps.length * 40).toBeLessThanOrEqual( c.y + c.height - 24 diff --git a/apps/website/src/lib/architecture-diagram.ts b/apps/website/src/lib/architecture-diagram.ts index 6e51a19ec..fbee44ad9 100644 --- a/apps/website/src/lib/architecture-diagram.ts +++ b/apps/website/src/lib/architecture-diagram.ts @@ -88,6 +88,14 @@ export type Row = readonly mark: LogoKey; readonly label: string; } + /** A banded list: one row per item, each with a left accent bar. */ + | { + readonly kind: 'items'; + readonly y: number; + readonly items: readonly string[]; + /** Vertical pitch between rows. */ + readonly step: number; + } /** A row of mark badges with no labels. */ | { readonly kind: 'marks'; @@ -216,9 +224,16 @@ export const CARDS: readonly Card[] = [ tag: 'FIRST-CLASS', highlight: true, rows: [ - { kind: 'text', y: 212, text: 'checkpoints · interrupts' }, - { kind: 'text', y: 240, text: 'time travel · memory' }, - { kind: 'text', y: 268, text: 'subgraphs · durable runs' }, + { + kind: 'items', + y: 212, + step: 32, + items: [ + 'checkpoints · interrupts', + 'time travel · memory', + 'subgraphs · durable runs', + ], + }, ], }, { @@ -231,7 +246,9 @@ export const CARDS: readonly Card[] = [ title2: 'protocol', href: '/docs/ag-ui/getting-started/introduction', mark: 'agui', - rows: [{ kind: 'text', y: 460, text: 'events · tools · state' }], + rows: [ + { kind: 'items', y: 460, step: 32, items: ['events · tools · state'] }, + ], }, { id: 'langsmith', @@ -243,9 +260,12 @@ export const CARDS: readonly Card[] = [ href: '/docs/langgraph/guides/deployment', mark: 'langchain', rows: [ - { kind: 'text', y: 172, text: 'deploy · observe' }, - { kind: 'text', y: 212, text: 'traces · evals' }, - { kind: 'text', y: 240, text: 'or self-hosted' }, + { + kind: 'items', + y: 200, + step: 32, + items: ['deploy · observe', 'traces · evals', 'or self-hosted'], + }, ], }, { @@ -264,7 +284,12 @@ export const CARDS: readonly Card[] = [ size: 30, step: 34, }, - { kind: 'text', y: 468, text: 'CrewAI · Mastra · Microsoft' }, + { + kind: 'items', + y: 468, + step: 32, + items: ['CrewAI · Mastra · Microsoft'], + }, ], }, ]; diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index 5689ed455..9fed94440 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -1773,6 +1773,40 @@ font-weight: 500; fill: #2a2f3a; } +/* Adapter and agent capability rows: a banded list with a left accent, so + * the lines read as a feature list rather than loose paragraph text. */ +.arch-figure .arch-item-band { + fill: #f2f5f8; +} +.arch-figure .arch-item-bar { + fill: #c2cad6; +} +.arch-figure .arch-items[data-tone='tp'] .arch-item-band { + fill: rgba(255, 255, 255, 0.75); +} +.arch-figure .arch-items[data-tone='tp'] .arch-item-bar { + fill: #2f5fa8; +} +.arch-stack-items { + list-style: none; + margin: 10px 0 0; + padding: 0; + display: grid; + gap: 6px; + font-family: var(--font-inter); + font-size: 13px; + color: var(--color-text-secondary); +} +.arch-stack-items li { + background: #f2f5f8; + border-left: 3px solid #c2cad6; + border-radius: 8px; + padding: 6px 10px; +} +.arch-stack-card[data-highlight] .arch-stack-items li { + background: rgba(255, 255, 255, 0.75); + border-left-color: #2f5fa8; +} .arch-figure .arch-badge rect { fill: #fff; stroke: #e2e6ec; From 373ebff76a8647b946c8e568346f30f28e6dabd3 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 7 Sep 2026 18:29:33 -0700 Subject: [PATCH 7/8] refactor(website): even columns, symmetric margins, and centred arrows in the architecture diagram Measured the geometry and fixed what it found: the adapters-to-agents gap was 48 against 64 elsewhere, the drawing sat 24px off centre (64 left, 40 right), the users arrow was 28px above the two card centres it joins, the LangSmith rows started 12px above the LangGraph rows beside them, and the People card's content floated at the top of a 392px card. Columns are now 64 apart with 48px margins on all four sides, every arrow is 64 long and lands on the centre of the card it enters, the paired cards share their row positions, and the People block is centred where its arrow leaves. The spec asserts each of those, so they cannot drift back. Co-Authored-By: Claude Opus 5 --- .../landing/EnterpriseArchitecture.tsx | 8 ++- .../src/lib/architecture-diagram.spec.ts | 56 ++++++++++++++--- apps/website/src/lib/architecture-diagram.ts | 62 ++++++++++--------- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/apps/website/src/components/landing/EnterpriseArchitecture.tsx b/apps/website/src/components/landing/EnterpriseArchitecture.tsx index c861ac888..1a623d811 100644 --- a/apps/website/src/components/landing/EnterpriseArchitecture.tsx +++ b/apps/website/src/components/landing/EnterpriseArchitecture.tsx @@ -287,7 +287,7 @@ function CardView({ card }: { card: Card }) { ) : card.icon ? ( - + {card.title} ) : ( diff --git a/apps/website/src/lib/architecture-diagram.spec.ts b/apps/website/src/lib/architecture-diagram.spec.ts index edc16a82b..2f702c6a7 100644 --- a/apps/website/src/lib/architecture-diagram.spec.ts +++ b/apps/website/src/lib/architecture-diagram.spec.ts @@ -3,6 +3,7 @@ import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; import { ARROWS, + STRIP_CHIP_H, CARDS, CARD_GAP, CARD_PAD, @@ -74,19 +75,60 @@ describe('architecture diagram geometry', () => { expect(card('ag-ui-servers').y).toBe(bottom.y); }); - it('draws every arrow from one card edge to the next card edge, on a grid row', () => { + it('lands every arrow on the vertical centre of the card it enters, at one length', () => { + const lengths = new Set(); for (const a of ARROWS) { - expect(onGrid(a.y), `arrow y ${a.y}`).toBe(true); const from = CARDS.find( (c) => c.x + c.width === a.x1 && a.y > c.y && a.y < c.y + c.height ); - const to = CARDS.find( - (c) => c.x === a.x2 && a.y > c.y && a.y < c.y + c.height - ); + const to = CARDS.find((c) => c.x === a.x2); expect(from, `arrow at ${a.x1} leaves a card`).toBeDefined(); expect(to, `arrow at ${a.x2} enters a card`).toBeDefined(); - expect(a.y).toBeGreaterThan(from!.y); - expect(a.y).toBeLessThan(from!.y + from!.height); + const enters = CARDS.filter( + (c) => c.x === a.x2 && a.y > c.y && a.y < c.y + c.height + )[0]; + expect(a.y, `arrow into ${enters?.id}`).toBe( + enters!.y + enters!.height / 2 + ); + lengths.add(a.x2 - a.x1); + } + expect([...lengths], 'every arrow is the same length').toHaveLength(1); + }); + + it('spaces the columns evenly and centres the drawing in the view', () => { + const xs = [...new Set(CARDS.map((c) => c.x))].sort((a, b) => a - b); + const gaps = xs.slice(1).map((x, i) => { + const right = Math.max( + ...CARDS.filter((c) => c.x === xs[i]).map((c) => c.x + c.width) + ); + return x - right; + }); + expect([...new Set(gaps)], `column gaps ${gaps}`).toHaveLength(1); + expect(onGrid(gaps[0])).toBe(true); + const left = xs[0]; + const right = VIEW.width - Math.max(...CARDS.map((c) => c.x + c.width)); + expect(right, 'left and right margins match').toBe(left); + expect( + VIEW.height - (MODEL_STRIP.chipY + STRIP_CHIP_H), + 'bottom margin matches the sides' + ).toBe(left); + expect(MODEL_STRIP.x, 'the strip starts at the first column').toBe(left); + }); + + it('aligns the rows of side-by-side cards', () => { + const firstItemY = (id: string) => { + const r = card(id).rows.find((row) => row.kind === 'items'); + return r && r.kind === 'items' ? r.y : null; + }; + expect(firstItemY('langsmith')).toBe(firstItemY('langgraph-sdk')); + expect(firstItemY('ag-ui-servers')).toBe(firstItemY('ag-ui')); + // Both rows of the stack share their tops and bottoms across the columns. + for (const [a, b] of [ + ['langgraph-sdk', 'langsmith'], + ['ag-ui', 'ag-ui-servers'], + ] as const) { + expect(card(b).y).toBe(card(a).y); + expect(card(b).height).toBe(card(a).height); } }); diff --git a/apps/website/src/lib/architecture-diagram.ts b/apps/website/src/lib/architecture-diagram.ts index fbee44ad9..846f5418a 100644 --- a/apps/website/src/lib/architecture-diagram.ts +++ b/apps/website/src/lib/architecture-diagram.ts @@ -14,7 +14,7 @@ * of a role, never as integrations the library claims. */ -export const VIEW = { width: 1280, height: 624 } as const; +export const VIEW = { width: 1280, height: 640 } as const; export const GRID = 8; export const MAJOR = 40; export const CARD_GAP = 40; @@ -122,6 +122,8 @@ export interface Card { readonly bg: string; readonly fg: string; }; + /** Pushes an icon card's icon and title down, to centre a short card's content. */ + readonly contentDy?: number; /** Right-aligned tag on the card's first line. */ readonly tag?: string; readonly highlight?: boolean; @@ -145,30 +147,33 @@ export interface StripChip { } export const COLUMNS: readonly ColumnLabel[] = [ - { x: 64, label: 'YOUR USERS' }, - { x: 304, label: 'YOUR ANGULAR APPLICATION' }, - { x: 776, label: 'ADAPTERS' }, - { x: 1024, label: 'YOUR AGENTS' }, + { x: 48, label: 'YOUR USERS' }, + { x: 288, label: 'YOUR ANGULAR APPLICATION' }, + { x: 744, label: 'ADAPTERS' }, + { x: 1008, label: 'YOUR AGENTS' }, ]; export const COLUMN_LABEL_Y = 72; export const CARDS: readonly Card[] = [ { id: 'users', - x: 64, + x: 48, y: 104, width: 176, height: 392, title: 'People', href: '/docs/chat/getting-started/introduction', icon: { name: 'users', bg: '#fff3e0', fg: '#c2410c' }, - rows: [{ kind: 'text', y: 248, text: 'web · mobile' }], + // The column's single node: its block is centred on the card, where its + // outgoing arrow leaves. + contentDy: 104, + rows: [{ kind: 'text', y: 352, text: 'web · mobile' }], }, { id: 'threadplane', - x: 304, + x: 288, y: 104, - width: 408, + width: 392, height: 392, title: 'Threadplane', href: '/docs/chat/getting-started/introduction', @@ -202,8 +207,8 @@ export const CARDS: readonly Card[] = [ }, ], }, - { kind: 'badge', x: 540, y: 224, mark: 'google', label: 'A2UI' }, - { kind: 'badge', x: 540, y: 272, mark: 'vercel', label: 'json-render' }, + { kind: 'badge', x: 524, y: 224, mark: 'google', label: 'A2UI' }, + { kind: 'badge', x: 524, y: 272, mark: 'vercel', label: 'json-render' }, { kind: 'mono', y: 470, @@ -213,7 +218,7 @@ export const CARDS: readonly Card[] = [ }, { id: 'langgraph-sdk', - x: 776, + x: 744, y: 104, width: 200, height: 208, @@ -238,7 +243,7 @@ export const CARDS: readonly Card[] = [ }, { id: 'ag-ui', - x: 776, + x: 744, y: 352, width: 200, height: 144, @@ -247,14 +252,14 @@ export const CARDS: readonly Card[] = [ href: '/docs/ag-ui/getting-started/introduction', mark: 'agui', rows: [ - { kind: 'items', y: 460, step: 32, items: ['events · tools · state'] }, + { kind: 'items', y: 468, step: 32, items: ['events · tools · state'] }, ], }, { id: 'langsmith', - x: 1024, + x: 1008, y: 104, - width: 216, + width: 224, height: 208, title: 'LangSmith', href: '/docs/langgraph/guides/deployment', @@ -262,7 +267,7 @@ export const CARDS: readonly Card[] = [ rows: [ { kind: 'items', - y: 200, + y: 212, step: 32, items: ['deploy · observe', 'traces · evals', 'or self-hosted'], }, @@ -270,16 +275,16 @@ export const CARDS: readonly Card[] = [ }, { id: 'ag-ui-servers', - x: 1024, + x: 1008, y: 352, - width: 216, + width: 224, height: 144, title: 'AG-UI servers', href: '/docs/runtimes/getting-started/introduction', rows: [ { kind: 'marks', - y: 400, + y: 408, marks: ['crewai', 'mastra', 'microsoft', 'bedrock', 'pydantic'], size: 30, step: 34, @@ -294,25 +299,26 @@ export const CARDS: readonly Card[] = [ }, ]; +/** Each arrow lands on the vertical centre of the card it enters. */ export const ARROWS: readonly Arrow[] = [ - { x1: 240, x2: 304, y: 272 }, - { x1: 712, x2: 776, y: 208 }, - { x1: 712, x2: 776, y: 424 }, - { x1: 976, x2: 1024, y: 208 }, - { x1: 976, x2: 1024, y: 424 }, + { x1: 224, x2: 288, y: 300 }, + { x1: 680, x2: 744, y: 208 }, + { x1: 680, x2: 744, y: 424 }, + { x1: 944, x2: 1008, y: 208 }, + { x1: 944, x2: 1008, y: 424 }, ]; /** The two-line caption between the adapter arrows. */ export const CONTRACT_CAPTION = { - x: 744, + x: 712, y: 306, lines: ['one Agent', 'contract'], } as const; export const MODEL_STRIP = { label: 'ANY MODEL', - labelY: 540, + labelY: 524, chipY: 556, - x: 64, + x: 48, chips: [ { mark: 'openai', label: 'OpenAI' }, { mark: 'anthropic', label: 'Anthropic' }, From 6f96f65e1feec39f608a0c8dfca88671a2cbc65e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Mon, 7 Sep 2026 18:47:25 -0700 Subject: [PATCH 8/8] =?UTF-8?q?docs:=20the=20architecture=20spec=20matches?= =?UTF-8?q?=20what=20shipped=20=E2=80=94=20banded=20rows,=20the=20grid=20r?= =?UTF-8?q?ules,=20the=20phone=20stack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- ...7-enterprise-architecture-diagram-design.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md b/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md index 573039209..cb74e7ae7 100644 --- a/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md +++ b/docs/superpowers/specs/2026-09-07-enterprise-architecture-diagram-design.md @@ -1,7 +1,7 @@ # Enterprise architecture diagram on the homepage **Date:** 2026-09-07 -**Status:** Design approved in brainstorming (iterated through six mockups with the user); implementation gated on the user's visual sign-off before merge. +**Status:** Built on `blove/architecture-diagram` (PR #1048). Iterated through eight mockups and four review passes with the user; the final direction scaled the three-zone enterprise map back to this four-column flow. Merge is gated on the user's visual sign-off. **Surface:** `apps/website` only. **Replaces:** the "Where Threadplane fits" section (`ScopeTable`, section id `why`) on the homepage. `ScopeTable` and the `FINAL_MILE_*` copy leave with it. @@ -38,15 +38,23 @@ Column labels: YOUR USERS, YOUR ANGULAR APPLICATION, ADAPTERS, YOUR AGENTS. ## 4. Component -`apps/website/src/components/landing/EnterpriseArchitecture.tsx`, a server component rendering one `` inside the kit's `DiagramFrame` at `scale="marketing"`, framed by `DiagramSection`. Logos are `` referencing the public files. The diagram's geometry lives in a data module, `apps/website/src/lib/architecture-diagram.ts`, as typed card/arrow/strip records so the tests can read the same numbers the component draws. The line icons are inline paths in the component (five to seven, matching the mockup). +`apps/website/src/components/landing/EnterpriseArchitecture.tsx`, a server component rendering one `` inside the kit's `DiagramFrame` at `scale="marketing"`, framed by `DiagramSection`. Logos are `` referencing the public files; line icons are inline paths in the component. The geometry lives in a data module, `apps/website/src/lib/architecture-diagram.ts`, as typed card/arrow/strip records, so the component, the unit spec and the e2e all read the same numbers. + +Row kinds: `text`, `mono`, `caps` (the Threadplane card's five capability links), `badge` (a mark with a label), `marks` (a row of mark tiles), and `items` — the banded list used by the adapter and agent cards, one soft band per line with a left accent bar, white-on-tint inside a highlighted card. + +Ground: a faint radial gradient under the kit's dot pattern, with the dots one step lighter than the docs default, so the figure reads as a surface. + +**Grid.** Four columns 64 apart, 48px margins on all four sides, stacked cards 40 apart, every arrow 64 long landing on the vertical centre of the card it enters, and the two paired cards in each row sharing their tops, heights and row positions. The People card's block is centred on its card, where its arrow leaves. + +**Phone form.** Under 768px the SVG is hidden and `ArchitectureStack` renders the same cards as an HTML stack in reading order, from the same data module, so the two forms cannot drift. ## 5. Verification -- **Unit:** every card rect has `x, y, width, height` divisible by 8 inside the view's 40px margin, stacked cards are 40 apart, no two cards overlap, the users and Threadplane cards span the adapter stack exactly, every arrow leaves one card edge and enters the next, and the model strip fits (from the data module). The component renders every link whose hrefs each resolve to an existing docs page or route (the spec reads `apps/website/content/docs` and `src/app`). Public-copy scan stays green. -- **e2e (`home-architecture.spec.ts`):** at 1440×900 every `` in the diagram has a `getBBox()` inside its parent card's rect with 8px of margin, every chip rect is inside its card, every `` loaded (`naturalWidth`/complete via the parent's `getBBox` non-zero), and the section's links have the expected hrefs. At 390px the frame scrolls horizontally and the SVG is at least 1024px wide. +- **Unit:** every card rect is divisible by 8 and sits inside the view's margin; stacked cards are 40 apart; no two cards overlap; the users and Threadplane cards span the adapter stack exactly; every column gap is equal; the left, right and bottom margins match and the model strip starts at the first column; every arrow is one length and lands on the vertical centre of the card it enters; the paired cards share their tops, heights and first row positions; every row sits inside its card. Every href resolves to a docs page or route on disk, and every mark exists under `/logos`. Public-copy scan stays green. +- **e2e (`home-architecture.spec.ts`):** at 1440×900, with fonts loaded, every `` and every chip rect in the diagram has a `getBBox()` inside its own card with margin, every `` has a non-zero box, and the cards carry the expected hrefs. At 390px the HTML stack is visible, the SVG is hidden, it renders one card per data entry, and the page has no horizontal scroll. - **Visual sign-off:** rendered frames at 1440 and 390 shown to the user before the PR merges; the PR is opened without auto-merge. - **Spine test:** the homepage heading order replaces `why-heading` with `architecture-heading`. ## 6. Out of scope -Hover interactions, a mobile-specific stacked variant, sourcing the LangSmith mark, changes to the docs pages linked. +Hover interactions beyond the links, animation, changes to the docs pages linked. LangSmith has no mark under the site's sourcing rules (Simple Icons carries no `langsmith` slug), so its card uses the LangChain mark and the logo README records why; swapping in a real LangSmith SVG is a later one-line change.