From 7b7e8c2621be7fa013b3f4c29a3c15a8f987b7c4 Mon Sep 17 00:00:00 2001 From: Joseph Dale Banares Date: Wed, 30 Sep 2026 21:56:12 +0800 Subject: [PATCH 1/7] docs(site): draw one checkout as each kind for the landing --- site/content/checkout/architecture.json | 21 +++++++ site/content/checkout/dataflow.json | 24 ++++++++ site/content/checkout/lifecycle.json | 31 ++++++++++ site/content/checkout/sequence.json | 80 +++++++++++++++++++++++++ site/content/checkout/workflow.json | 32 ++++++++++ site/content/content.test.ts | 2 +- site/lib/checkout.ts | 15 +++++ site/lib/data/data.test.ts | 4 +- site/lib/data/diagrams.ts | 5 +- site/lib/data/paths.ts | 1 + site/tests/checkout.test.ts | 20 +++++++ 11 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 site/content/checkout/architecture.json create mode 100644 site/content/checkout/dataflow.json create mode 100644 site/content/checkout/lifecycle.json create mode 100644 site/content/checkout/sequence.json create mode 100644 site/content/checkout/workflow.json create mode 100644 site/lib/checkout.ts create mode 100644 site/tests/checkout.test.ts diff --git a/site/content/checkout/architecture.json b/site/content/checkout/architecture.json new file mode 100644 index 0000000..b0788fd --- /dev/null +++ b/site/content/checkout/architecture.json @@ -0,0 +1,21 @@ +{ + "kind": "architecture", + "title": "Checkout", + "subtitle": "One checkout, as components", + "density": "compact", + "nodes": [ + { "id": "storefront", "type": "client", "card": { "title": "Storefront", "subtitle": "Next.js" } }, + { "id": "edge", "type": "gateway", "card": { "title": "Edge gateway", "subtitle": "Public routing" } }, + { "id": "api", "type": "service", "card": { "title": "shop-api", "subtitle": "REST API" } }, + { "id": "stripe", "type": "external", "card": { "title": "Stripe", "subtitle": "Payments" } }, + { "id": "orders", "type": "database", "card": { "title": "Orders DB", "subtitle": "PostgreSQL" } }, + { "id": "events", "type": "queue", "card": { "title": "Order events", "subtitle": "Async queue" } } + ], + "edges": [ + { "id": "a1", "from": "storefront", "to": "edge" }, + { "id": "a2", "from": "edge", "to": "api" }, + { "id": "a3", "from": "api", "to": "stripe" }, + { "id": "a4", "from": "api", "to": "orders" }, + { "id": "a5", "from": "api", "to": "events", "kind": "async" } + ] +} diff --git a/site/content/checkout/dataflow.json b/site/content/checkout/dataflow.json new file mode 100644 index 0000000..dfefd83 --- /dev/null +++ b/site/content/checkout/dataflow.json @@ -0,0 +1,24 @@ +{ + "kind": "dataflow", + "title": "Checkout", + "subtitle": "One checkout, as data moving", + "density": "compact", + "phases": [ + { "id": "collect", "label": "Collect", "nodes": ["cart", "priced"] }, + { "id": "charge", "label": "Charge", "nodes": ["payment"] }, + { "id": "record", "label": "Record", "nodes": ["row", "placed"] } + ], + "nodes": [ + { "id": "cart", "type": "client", "card": { "title": "Cart", "subtitle": "Line items" } }, + { "id": "priced", "type": "service", "card": { "title": "Priced order", "subtitle": "Tax and totals" } }, + { "id": "payment", "type": "external", "card": { "title": "Payment", "subtitle": "Stripe charge" } }, + { "id": "row", "type": "database", "card": { "title": "Order row", "subtitle": "Orders DB" } }, + { "id": "placed", "type": "queue", "card": { "title": "Order placed", "subtitle": "Event" } } + ], + "edges": [ + { "id": "d1", "from": "cart", "to": "priced" }, + { "id": "d2", "from": "priced", "to": "payment" }, + { "id": "d3", "from": "payment", "to": "row" }, + { "id": "d4", "from": "row", "to": "placed", "kind": "async" } + ] +} diff --git a/site/content/checkout/lifecycle.json b/site/content/checkout/lifecycle.json new file mode 100644 index 0000000..ca4040f --- /dev/null +++ b/site/content/checkout/lifecycle.json @@ -0,0 +1,31 @@ +{ + "kind": "lifecycle", + "title": "Checkout", + "subtitle": "One checkout, as the states of an order", + "lanes": [ + { "id": "order", "label": "Order" }, + { "id": "waiting", "label": "Waiting" }, + { "id": "exits", "label": "Exits" } + ], + "phases": [ + { "id": "open", "label": "Open", "nodes": ["created", "awaiting"] }, + { "id": "settled", "label": "Settled", "nodes": ["paid", "failed"] }, + { "id": "done", "label": "Done", "nodes": ["fulfilled", "cancelled"] } + ], + "nodes": [ + { "id": "created", "type": "start", "lane": "order", "card": { "title": "Created", "subtitle": "Cart submitted" } }, + { "id": "awaiting", "type": "waiting", "lane": "waiting", "card": { "title": "Awaiting payment", "subtitle": "Card pending" } }, + { "id": "failed", "type": "failure", "lane": "exits", "card": { "title": "Payment failed", "subtitle": "Declined" } }, + { "id": "fulfilled", "type": "success", "lane": "order", "card": { "title": "Fulfilled", "subtitle": "Shipped" } }, + { "id": "paid", "type": "active", "lane": "order", "card": { "title": "Paid", "subtitle": "Charge captured" } }, + { "id": "cancelled", "type": "failure", "lane": "exits", "card": { "title": "Cancelled", "subtitle": "Timed out" } } + ], + "edges": [ + { "id": "l1", "from": "created", "to": "awaiting" }, + { "id": "l2", "from": "awaiting", "to": "paid", "label": "charged" }, + { "id": "l3", "from": "awaiting", "to": "failed", "label": "declined" }, + { "id": "l4", "from": "paid", "to": "fulfilled" }, + { "id": "l5", "from": "failed", "to": "awaiting", "kind": "return", "label": "retry" }, + { "id": "l6", "from": "awaiting", "to": "cancelled", "label": "timeout" } + ] +} diff --git a/site/content/checkout/sequence.json b/site/content/checkout/sequence.json new file mode 100644 index 0000000..81cd7b0 --- /dev/null +++ b/site/content/checkout/sequence.json @@ -0,0 +1,80 @@ +{ + "kind": "sequence", + "title": "Checkout", + "subtitle": "One checkout, as messages over time", + "nodes": [ + { + "id": "storefront", + "type": "client", + "card": { + "title": "Storefront", + "subtitle": "Next.js" + } + }, + { + "id": "api", + "type": "service", + "card": { + "title": "shop-api", + "subtitle": "REST API" + } + }, + { + "id": "stripe", + "type": "external", + "card": { + "title": "Stripe", + "subtitle": "Payments" + } + }, + { + "id": "orders", + "type": "database", + "card": { + "title": "Orders DB", + "subtitle": "PostgreSQL" + } + } + ], + "edges": [ + { + "id": "s1", + "from": "storefront", + "to": "api", + "label": "POST /checkout" + }, + { + "id": "s2", + "from": "api", + "to": "stripe", + "label": "charge" + }, + { + "id": "s3", + "from": "stripe", + "to": "api", + "kind": "return", + "label": "charged" + }, + { + "id": "s4", + "from": "api", + "to": "orders", + "label": "insert order" + }, + { + "id": "s5", + "from": "orders", + "to": "api", + "kind": "return", + "label": "order id" + }, + { + "id": "s6", + "from": "api", + "to": "storefront", + "kind": "return", + "label": "201 Created" + } + ] +} diff --git a/site/content/checkout/workflow.json b/site/content/checkout/workflow.json new file mode 100644 index 0000000..3a6c7e1 --- /dev/null +++ b/site/content/checkout/workflow.json @@ -0,0 +1,32 @@ +{ + "kind": "workflow", + "title": "Checkout", + "subtitle": "One checkout, as steps by owner", + "lanes": [ + { "id": "customer", "label": "Customer" }, + { "id": "shop", "label": "shop-api" }, + { "id": "payments", "label": "Payments" }, + { "id": "failures", "label": "Failures", "tone": "exception" } + ], + "phases": [ + { "id": "cart", "label": "Cart", "nodes": ["add", "validate"] }, + { "id": "payment", "label": "Payment", "nodes": ["pay", "charge"] }, + { "id": "order", "label": "Order", "nodes": ["create"] } + ], + "nodes": [ + { "id": "add", "type": "client", "lane": "customer", "card": { "title": "Add to cart", "subtitle": "Storefront" } }, + { "id": "validate", "type": "service", "lane": "shop", "card": { "title": "Validate cart", "subtitle": "Stock and price" } }, + { "id": "charge", "type": "external", "lane": "payments", "card": { "title": "Charge card", "subtitle": "Stripe" } }, + { "id": "create", "type": "service", "lane": "shop", "card": { "title": "Create order", "subtitle": "Orders DB" } }, + { "id": "pay", "type": "client", "lane": "customer", "card": { "title": "Pay", "subtitle": "Card details" } }, + { "id": "declined", "type": "security", "lane": "failures", "card": { "title": "Declined", "subtitle": "Ask again" } } + ], + "edges": [ + { "id": "w1", "from": "add", "to": "validate" }, + { "id": "w2", "from": "validate", "to": "pay" }, + { "id": "w3", "from": "pay", "to": "charge" }, + { "id": "w4", "from": "charge", "to": "create", "tone": "main" }, + { "id": "w5", "from": "charge", "to": "declined", "tone": "error" }, + { "id": "w6", "from": "declined", "to": "pay", "kind": "return", "label": "retry" } + ] +} diff --git a/site/content/content.test.ts b/site/content/content.test.ts index 84fed91..f195b14 100644 --- a/site/content/content.test.ts +++ b/site/content/content.test.ts @@ -13,7 +13,7 @@ describe('site content', () => { expect(files('examples')).toEqual(EXAMPLES); }); - for (const [sub, file] of [...EXAMPLES.map((f) => ['examples', f]), ['demo', 'commerce-api.json'], ['docs', 'quick-start.json']] as const) { + for (const [sub, file] of [...EXAMPLES.map((f) => ['examples', f]), ['demo', 'commerce-api.json'], ['docs', 'quick-start.json'], ...files('checkout').map((f) => ['checkout', f])] as const) { it(`${sub}/${file} validates with no diagnostics`, () => { const result = validateDiagram(read(sub, file)); expect(result.diagnostics).toEqual([]); diff --git a/site/lib/checkout.ts b/site/lib/checkout.ts new file mode 100644 index 0000000..ca36d01 --- /dev/null +++ b/site/lib/checkout.ts @@ -0,0 +1,15 @@ +import type { DiagramKind } from '@stackmap/core'; + +/** + * The #kinds scene's card slots: slot i holds each kind's node here, or nothing. A card keeps its slot + * from one kind to the next, so it glides to its new place instead of swapping (Motion board, #kinds). + */ +export const SLOTS: Record = { + architecture: ['storefront', 'edge', 'api', 'stripe', 'orders', 'events'], + dataflow: ['cart', 'priced', 'payment', null, 'row', 'placed'], + workflow: ['add', 'validate', 'charge', 'create', 'pay', 'declined'], + lifecycle: ['created', 'awaiting', 'failed', 'fulfilled', 'paid', 'cancelled'], + sequence: ['api', 'storefront', null, 'orders', 'stripe', null], +}; + +export const SLOT_COUNT = 6; diff --git a/site/lib/data/data.test.ts b/site/lib/data/data.test.ts index 649add5..e4a35d4 100644 --- a/site/lib/data/data.test.ts +++ b/site/lib/data/data.test.ts @@ -34,8 +34,10 @@ describe('layoutSite', () => { expect(Object.keys(d.examples).sort()).toEqual(['bookshop', 'food-delivery', 'ml-feature-platform', 'production-vpc', 'repo-architecture']); expect(Object.keys(d.skill).sort()).toEqual(['checkout.sequence', 'clickstream.dataflow', 'job.lifecycle', 'release.workflow', 'web-app.architecture']); expect(d.quickStart.draft.nodes.map((n) => n.id)).toEqual(['api', 'db']); + expect(Object.keys(d.checkout).sort()).toEqual(['architecture', 'dataflow', 'lifecycle', 'sequence', 'workflow']); + for (const [kind, c] of Object.entries(d.checkout)) expect(c.draft.kind).toBe(kind); expect(Object.keys(d.demo.nodes)).toHaveLength(11); - for (const diagram of [d.demo, d.quickStart, ...Object.values(d.gallery), ...Object.values(d.examples), ...Object.values(d.skill)]) { + for (const diagram of [d.demo, d.quickStart, ...Object.values(d.gallery), ...Object.values(d.examples), ...Object.values(d.skill), ...Object.values(d.checkout)]) { expect(diagram.bounds.width).toBeGreaterThan(0); } }, 60_000); diff --git a/site/lib/data/diagrams.ts b/site/lib/data/diagrams.ts index f609421..6420fb6 100644 --- a/site/lib/data/diagrams.ts +++ b/site/lib/data/diagrams.ts @@ -6,7 +6,7 @@ import { GALLERY } from '@stackmap/core/gallery'; import { layoutDiagram } from '@stackmap/layout'; import { validateDiagram } from '@stackmap/schema'; import { formatJson } from './json-format'; -import { DEMO, EXAMPLES_DIR, QUICK_START, SKILL_EXAMPLES_DIR } from './paths'; +import { CHECKOUT_DIR, DEMO, EXAMPLES_DIR, QUICK_START, SKILL_EXAMPLES_DIR } from './paths'; export interface SiteDiagrams { demo: LaidOutDiagram; @@ -15,6 +15,8 @@ export interface SiteDiagrams { /** the skill's own example per kind, keyed by file name without `.json` */ skill: Record; quickStart: LaidOutDiagram; + /** the landing's one checkout, drawn as each kind, keyed by kind */ + checkout: Record; } const jsonFiles = (dir: URL) => readdirSync(dir).filter((f) => f.endsWith('.json')).sort(); @@ -45,6 +47,7 @@ export async function layoutSite({ examplesDir = EXAMPLES_DIR, demo = DEMO }: { examples: await layoutDir(examplesDir), skill: await layoutDir(SKILL_EXAMPLES_DIR), quickStart: await layoutFile(QUICK_START), + checkout: await layoutDir(CHECKOUT_DIR), }; } diff --git a/site/lib/data/paths.ts b/site/lib/data/paths.ts index 4726595..9f0801f 100644 --- a/site/lib/data/paths.ts +++ b/site/lib/data/paths.ts @@ -4,6 +4,7 @@ export const CLI_PACKAGE = new URL('packages/cli/package.json', repo); export const VIEWER_TEMPLATE = new URL('packages/viewer/dist/index.html', repo); export const EXAMPLES_DIR = new URL('site/content/examples/', repo); export const DEMO = new URL('site/content/demo/commerce-api.json', repo); +export const CHECKOUT_DIR = new URL('site/content/checkout/', repo); export const QUICK_START = new URL('site/content/docs/quick-start.json', repo); export const SKILL_EXAMPLES_DIR = new URL('skill/examples/', repo); export const GENERATED_DIR = new URL('site/generated/', repo); diff --git a/site/tests/checkout.test.ts b/site/tests/checkout.test.ts new file mode 100644 index 0000000..9405f3c --- /dev/null +++ b/site/tests/checkout.test.ts @@ -0,0 +1,20 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { DIAGRAM_KINDS } from '@stackmap/core'; +import { SLOT_COUNT, SLOTS } from '../lib/checkout'; + +const draft = (kind: string) => JSON.parse(readFileSync(new URL(`../content/checkout/${kind}.json`, import.meta.url), 'utf8')) as { kind: string; nodes: { id: string }[] }; + +describe('checkout slots', () => { + it('has one checkout per kind', () => { + for (const kind of DIAGRAM_KINDS) expect(draft(kind).kind).toBe(kind); + }); + + it('puts every node of every kind in exactly one slot', () => { + for (const kind of DIAGRAM_KINDS) { + const slots = SLOTS[kind]; + expect(slots).toHaveLength(SLOT_COUNT); + expect(slots.filter(Boolean).sort()).toEqual(draft(kind).nodes.map((n) => n.id).sort()); + } + }); +}); From f499f569bca408c2e0a9e77122dbec9629b295c0 Mon Sep 17 00:00:00 2001 From: Joseph Dale Banares Date: Wed, 30 Sep 2026 22:06:11 +0800 Subject: [PATCH 2/7] docs(site): build the landing's desktop scenes and their scroll timelines --- site/app/page.tsx | 13 +- site/components/diagram/StaticScene.tsx | 4 + site/components/landing/BoardCopy.tsx | 29 + site/components/landing/KindPills.tsx | 43 ++ site/components/landing/KindsStage.tsx | 59 ++ site/components/landing/Landing.tsx | 35 + site/components/landing/LandingMotion.tsx | 255 +++++++ site/components/landing/LazyViewer.tsx | 37 + site/components/landing/desktop.css | 762 +++++++++++++++++++ site/components/landing/desktop/Agent.tsx | 291 +++++++ site/components/landing/desktop/Features.tsx | 217 ++++++ site/components/landing/desktop/Files.tsx | 215 ++++++ site/components/landing/desktop/Hero.tsx | 173 +++++ site/components/landing/desktop/Install.tsx | 113 +++ site/components/landing/desktop/Kinds.tsx | 60 ++ site/components/landing/desktop/Unmapped.tsx | 285 +++++++ site/components/landing/desktop/Viewer.tsx | 74 ++ site/components/landing/drops.ts | 20 + site/components/landing/kinds-geometry.ts | 118 +++ site/components/landing/mobile.css | 601 +++++++++++++++ site/components/landing/site.css | 146 ++++ site/components/landing/style.ts | 4 + site/components/site/Nav.tsx | 13 +- site/lib/diagrams.ts | 2 + site/tests/kinds-geometry.test.ts | 53 ++ 25 files changed, 3610 insertions(+), 12 deletions(-) create mode 100644 site/components/diagram/StaticScene.tsx create mode 100644 site/components/landing/BoardCopy.tsx create mode 100644 site/components/landing/KindPills.tsx create mode 100644 site/components/landing/KindsStage.tsx create mode 100644 site/components/landing/Landing.tsx create mode 100644 site/components/landing/LandingMotion.tsx create mode 100644 site/components/landing/LazyViewer.tsx create mode 100644 site/components/landing/desktop.css create mode 100644 site/components/landing/desktop/Agent.tsx create mode 100644 site/components/landing/desktop/Features.tsx create mode 100644 site/components/landing/desktop/Files.tsx create mode 100644 site/components/landing/desktop/Hero.tsx create mode 100644 site/components/landing/desktop/Install.tsx create mode 100644 site/components/landing/desktop/Kinds.tsx create mode 100644 site/components/landing/desktop/Unmapped.tsx create mode 100644 site/components/landing/desktop/Viewer.tsx create mode 100644 site/components/landing/drops.ts create mode 100644 site/components/landing/kinds-geometry.ts create mode 100644 site/components/landing/mobile.css create mode 100644 site/components/landing/site.css create mode 100644 site/components/landing/style.ts create mode 100644 site/tests/kinds-geometry.test.ts diff --git a/site/app/page.tsx b/site/app/page.tsx index 1fbdeca..238b1e3 100644 --- a/site/app/page.tsx +++ b/site/app/page.tsx @@ -1,4 +1,6 @@ import type { Metadata } from 'next'; +import { Landing } from '@/components/landing/Landing'; +import { CHECKOUT, DEMO } from '@/lib/diagrams'; import { SITE } from '@/lib/site-data'; import { OPEN_GRAPH, siteUrl, softwareApplication } from '@/lib/seo'; @@ -12,16 +14,9 @@ const jsonLd = JSON.stringify(softwareApplication(siteUrl(), SITE.version)).repl export default function Home() { return ( -
+