diff --git a/.claude/skills/init/SKILL.md b/.claude/skills/init/SKILL.md index c2bd40e..77f9c8c 100644 --- a/.claude/skills/init/SKILL.md +++ b/.claude/skills/init/SKILL.md @@ -15,7 +15,7 @@ Claude Code, Codex CLI, Cursor, Copilot, and any future agent). `## ` heading exclusive. That section is the step list. 2. Execute the protocol described there, using parallel tool calls where steps are independent. -3. Emit a structured summary as a `## initialized: stagecraft.ing` block: +3. Emit a structured summary as a `## initialized: statecraft.ing` block: rules loaded, registry freshness, lifecycle counts (from `spec-spine registry status-report --json --nonzero-only`), and the next pending spec per `AGENTS.md` § Working the backlog. diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md index beb0536..63a45af 100644 --- a/.claude/skills/setup/SKILL.md +++ b/.claude/skills/setup/SKILL.md @@ -21,7 +21,7 @@ in `.github/workflows/spec-spine.yml`; match it): ```bash cargo install spec-spine-cli # from crates.io (needs a Rust toolchain) # or, no Rust toolchain: the prebuilt installer CI uses -curl -fsSL https://raw.githubusercontent.com/stagecraft-ing/spec-spine/main/install.sh | \ +curl -fsSL https://raw.githubusercontent.com/statecrafting/spec-spine/main/install.sh | \ SPEC_SPINE_VERSION=v0.10.0 SPEC_SPINE_BIN_DIR="$HOME/.local/bin" sh ``` @@ -69,7 +69,7 @@ re-check. Do not parse `.derived/**/*.json` directly to "verify" success. Report exactly: ``` -## setup: stagecraft.ing +## setup: statecraft.ing **Install:** {ok / failed at } **Node:** {node + npm / absent (needed once spec 001 lands)} diff --git a/.derived/codebase-index/by-package/stagecraft-ing.json b/.derived/codebase-index/by-package/stagecraft-ing.json index 3135379..b5a5fed 100644 --- a/.derived/codebase-index/by-package/stagecraft-ing.json +++ b/.derived/codebase-index/by-package/stagecraft-ing.json @@ -1,7 +1,7 @@ { "package": { "kind": "npm-package", - "name": "stagecraft-ing", + "name": "statecrafting", "path": "" }, "schemaVersion": "1.1.0", diff --git a/.derived/codebase-index/by-spec/004-marketing-surfaces.json b/.derived/codebase-index/by-spec/004-marketing-surfaces.json new file mode 100644 index 0000000..bf1eb1c --- /dev/null +++ b/.derived/codebase-index/by-spec/004-marketing-surfaces.json @@ -0,0 +1,289 @@ +{ + "mapping": { + "dependsOn": [ + "001-site-scaffold", + "002-launch-content", + "003-product-family-registry" + ], + "implementingPaths": [ + { + "path": "app/components/architecture-explorer.tsx", + "source": "spec-edge" + }, + { + "path": "app/components/icons.tsx", + "source": "spec-edge" + }, + { + "path": "app/components/paper-reader.tsx", + "source": "spec-edge" + }, + { + "path": "app/components/sign-in-link.tsx", + "source": "spec-edge" + }, + { + "path": "app/components/site-chrome.tsx", + "source": "spec-edge" + }, + { + "path": "app/lib/explorer-diagrams.ts", + "source": "spec-edge" + }, + { + "path": "app/lib/get-started.ts", + "source": "spec-edge" + }, + { + "path": "app/lib/papers.ts", + "source": "spec-edge" + }, + { + "path": "app/lib/products.ts", + "source": "spec-edge" + }, + { + "path": "app/lib/whitepaper.ts", + "source": "spec-edge" + }, + { + "path": "app/routes.ts", + "source": "spec-edge" + }, + { + "path": "app/routes/get-started.tsx", + "source": "spec-edge" + }, + { + "path": "app/routes/papers.$slug.tsx", + "source": "spec-edge" + }, + { + "path": "app/routes/papers.tsx", + "source": "spec-edge" + }, + { + "path": "app/routes/products.tsx", + "source": "spec-edge" + }, + { + "path": "react-router.config.ts", + "source": "spec-edge" + } + ], + "resolvedUnits": [ + { + "locations": [ + { + "file": "app/components/architecture-explorer.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/components/architecture-explorer.tsx" + } + }, + { + "locations": [ + { + "file": "app/components/icons.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/components/icons.tsx" + } + }, + { + "locations": [ + { + "file": "app/components/paper-reader.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/components/paper-reader.tsx" + } + }, + { + "locations": [ + { + "file": "app/components/sign-in-link.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/components/sign-in-link.tsx" + } + }, + { + "locations": [ + { + "file": "app/lib/explorer-diagrams.ts" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/lib/explorer-diagrams.ts" + } + }, + { + "locations": [ + { + "file": "app/lib/get-started.ts" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/lib/get-started.ts" + } + }, + { + "locations": [ + { + "file": "app/lib/papers.ts" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/lib/papers.ts" + } + }, + { + "locations": [ + { + "file": "app/lib/products.ts" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/lib/products.ts" + } + }, + { + "locations": [ + { + "file": "app/lib/whitepaper.ts" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/lib/whitepaper.ts" + } + }, + { + "locations": [ + { + "file": "app/routes/get-started.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/routes/get-started.tsx" + } + }, + { + "locations": [ + { + "file": "app/routes/papers.$slug.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/routes/papers.$slug.tsx" + } + }, + { + "locations": [ + { + "file": "app/routes/papers.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/routes/papers.tsx" + } + }, + { + "locations": [ + { + "file": "app/routes/products.tsx" + } + ], + "ownership": true, + "sourceField": "establishes", + "unit": { + "kind": "file", + "path": "app/routes/products.tsx" + } + }, + { + "locations": [ + { + "file": "app/components/site-chrome.tsx" + } + ], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "app/components/site-chrome.tsx" + } + }, + { + "locations": [ + { + "file": "app/routes.ts" + } + ], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "app/routes.ts" + } + }, + { + "locations": [ + { + "file": "react-router.config.ts" + } + ], + "ownership": true, + "sourceField": "extends", + "unit": { + "kind": "file", + "path": "react-router.config.ts" + } + } + ], + "specId": "004-marketing-surfaces", + "specStatus": "approved" + }, + "schemaVersion": "1.1.0", + "shardHash": "17cae3aae14beeeda1c9cc036ee10a1fa5f8bb808278529665a3ab98182a78be" +} diff --git a/.derived/spec-registry/by-spec/001-site-scaffold.json b/.derived/spec-registry/by-spec/001-site-scaffold.json index 4f03a7f..4b3e418 100644 --- a/.derived/spec-registry/by-spec/001-site-scaffold.json +++ b/.derived/spec-registry/by-spec/001-site-scaffold.json @@ -43,12 +43,12 @@ "3. Behavior", "4. Acceptance", "5. Out of scope", - "6. Status (2026-07-14): complete, live at https://stagecraft.ing" + "6. Status (2026-07-14): complete, live at https://statecraft.ing" ], "specPath": "specs/001-site-scaffold/spec.md", "status": "approved", - "summary": "The marketing and docs site, built as a fully static React Router v7 app (framework mode, prerendered, no SSR at runtime) deployed to GitHub Pages under stagecraft.ing. Amended 2026-07-14: React Router v7 replaces the earlier Astro choice, harvesting the OAP-era stagecraft web app (ecosystem content, the spec-registry viewer) and keeping the whole product family on one frontend stack. The registry viewer runs over build-time-baked shards from the public repos, so the site stays static and every claim stays checkable. The apex DNS cutover is authorized: the legacy control plane no longer owns the domain.\n", - "title": "stagecraft.ing: React Router v7 static site, Pages deploy, apex cutover" + "summary": "The marketing and docs site, built as a fully static React Router v7 app (framework mode, prerendered, no SSR at runtime) deployed to GitHub Pages under statecraft.ing. Amended 2026-07-14: React Router v7 replaces the earlier Astro choice, harvesting the OAP-era statecraft web app (ecosystem content, the spec-registry viewer) and keeping the whole product family on one frontend stack. The registry viewer runs over build-time-baked shards from the public repos, so the site stays static and every claim stays checkable. The apex DNS cutover is authorized: the legacy control plane no longer owns the domain.\n", + "title": "statecraft.ing: React Router v7 static site, Pages deploy, apex cutover" }, "shardHash": "b49f22920be2a1d5873fe38da81d44466129e9422159399541c6f41c3dc4e69f", "specVersion": "1.1.0" diff --git a/.derived/spec-registry/by-spec/002-launch-content.json b/.derived/spec-registry/by-spec/002-launch-content.json index 8fa3682..dfa3bd5 100644 --- a/.derived/spec-registry/by-spec/002-launch-content.json +++ b/.derived/spec-registry/by-spec/002-launch-content.json @@ -40,7 +40,7 @@ ], "specPath": "specs/002-launch-content/spec.md", "status": "approved", - "summary": "The words on the site at launch. One index page that states what Stagecraft is in the builder's own register (creator-led, OSS-credible, no startup theater), a product-family section presenting the roster owned by spec 003, and an honest status section tied to the public milestone ladder. The positioning facts are inlined here so the implementing session needs no external archive.\n", + "summary": "The words on the site at launch. One index page that states what Statecraft is in the builder's own register (creator-led, OSS-credible, no startup theater), a product-family section presenting the roster owned by spec 003, and an honest status section tied to the public milestone ladder. The positioning facts are inlined here so the implementing session needs no external archive.\n", "title": "Launch content: positioning, product family, honest status" }, "shardHash": "69cf21c7547a8181c8c517b14673b7776e8e5379a8075a849ea9165a370a9bff", diff --git a/.derived/spec-registry/by-spec/003-product-family-registry.json b/.derived/spec-registry/by-spec/003-product-family-registry.json index 2b025f7..0f0a4d7 100644 --- a/.derived/spec-registry/by-spec/003-product-family-registry.json +++ b/.derived/spec-registry/by-spec/003-product-family-registry.json @@ -27,7 +27,7 @@ ], "specPath": "specs/003-product-family-registry/spec.md", "status": "approved", - "summary": "The canonical registry of the Stagecraft product family: each repo's name, role, SPDX license, and URL, encoded once in app/lib/product-family.ts and consumed by the index family section, the footer, and the /registry viewer's repoMeta lookup. This spec gives that module a single explicit owner so a roster change (a repo added, removed, or re-described) is an ordinary authoring edit here plus the module, with no coupling waiver and no edit to the scaffold (001) or launch-content (002) specs. It owns the roster data, not the rendering.\n", + "summary": "The canonical registry of the Statecraft product family: each repo's name, role, SPDX license, and URL, encoded once in app/lib/product-family.ts and consumed by the index family section, the footer, and the /registry viewer's repoMeta lookup. This spec gives that module a single explicit owner so a roster change (a repo added, removed, or re-described) is an ordinary authoring edit here plus the module, with no coupling waiver and no edit to the scaffold (001) or launch-content (002) specs. It owns the roster data, not the rendering.\n", "title": "Product-family registry: a single owner for the shared repo list" }, "shardHash": "cb8d0cd1171faee3ce246ccacab44f0ecdceca15d9c52d962ca22250e0e51cb8", diff --git a/.derived/spec-registry/by-spec/004-marketing-surfaces.json b/.derived/spec-registry/by-spec/004-marketing-surfaces.json new file mode 100644 index 0000000..01b7404 --- /dev/null +++ b/.derived/spec-registry/by-spec/004-marketing-surfaces.json @@ -0,0 +1,110 @@ +{ + "record": { + "created": "2026-07-16", + "dependsOn": [ + "001-site-scaffold", + "002-launch-content", + "003-product-family-registry" + ], + "establishes": [ + { + "kind": "file", + "path": "app/routes/products.tsx" + }, + { + "kind": "file", + "path": "app/routes/papers.tsx" + }, + { + "kind": "file", + "path": "app/routes/papers.$slug.tsx" + }, + { + "kind": "file", + "path": "app/routes/get-started.tsx" + }, + { + "kind": "file", + "path": "app/components/paper-reader.tsx" + }, + { + "kind": "file", + "path": "app/components/architecture-explorer.tsx" + }, + { + "kind": "file", + "path": "app/components/sign-in-link.tsx" + }, + { + "kind": "file", + "path": "app/components/icons.tsx" + }, + { + "kind": "file", + "path": "app/lib/whitepaper.ts" + }, + { + "kind": "file", + "path": "app/lib/papers.ts" + }, + { + "kind": "file", + "path": "app/lib/products.ts" + }, + { + "kind": "file", + "path": "app/lib/explorer-diagrams.ts" + }, + { + "kind": "file", + "path": "app/lib/get-started.ts" + } + ], + "extends": [ + { + "spec": "001-site-scaffold", + "unit": { + "kind": "file", + "path": "app/routes.ts" + } + }, + { + "spec": "001-site-scaffold", + "unit": { + "kind": "file", + "path": "react-router.config.ts" + } + }, + { + "spec": "001-site-scaffold", + "unit": { + "kind": "file", + "path": "app/components/site-chrome.tsx" + } + } + ], + "id": "004-marketing-surfaces", + "implementation": "complete", + "sectionHeadings": [ + "004: Rich marketing surfaces", + "1. Purpose", + "2. Territory", + "3. Behavior", + "3.1 Chrome and navigation", + "3.2 Sign-in", + "3.3 Products / architecture (`/products`)", + "3.4 Papers and the whitepaper reader (`/papers`, `/papers/:slug`)", + "3.5 Get started (`/get-started`)", + "3.6 Static and dependency posture", + "4. Acceptance", + "5. Out of scope", + "6. Status (2026-07-16): complete" + ], + "specPath": "specs/004-marketing-surfaces/spec.md", + "status": "approved", + "summary": "Restores the richer marketing experience the OAP-era statecraft web app carried, ported onto the static apex: a products/architecture page, a papers index with a full whitepaper reader (sticky TOC, reading-progress, scroll-spy, inline references) and an interactive clickable-SVG architecture explorer, and a get-started walkthrough. It expands the site chrome (Products, Papers, Get Started in the nav) and adds a sign-in link that hands off to the control plane's Rauthy OIDC flow at app.statecraft.ing/auth/rauthy. All content is re-authored to be truthful and checkable against the current public repos (no fabricated hashes, spec counts, signatures, or dead subsystems): the OAP name becomes Statecraft, and factory-encore / template-encore become enrahitu. The site stays fully static and prerendered, with zero runtime off-origin requests; sign-in is a plain outbound link, not an auth flow this site runs.\n", + "title": "Rich marketing surfaces: products, whitepaper, get-started, sign-in" + }, + "shardHash": "3448aef607e5b5bf973fa2570dc2717d151c9b47d4a6d751b8a187f90da808c2", + "specVersion": "1.1.0" +} diff --git a/.github/workflows/spec-spine.yml b/.github/workflows/spec-spine.yml index e586831..074888f 100644 --- a/.github/workflows/spec-spine.yml +++ b/.github/workflows/spec-spine.yml @@ -13,7 +13,7 @@ jobs: fetch-depth: 0 # full history so the gate can diff the merge base - name: Install spec-spine (prebuilt, pinned) run: | - curl -fsSL https://raw.githubusercontent.com/stagecraft-ing/spec-spine/main/install.sh | \ + curl -fsSL https://raw.githubusercontent.com/statecrafting/spec-spine/main/install.sh | \ SPEC_SPINE_VERSION=v0.10.0 SPEC_SPINE_BIN_DIR="$HOME/.local/bin" sh echo "$HOME/.local/bin" >> "$GITHUB_PATH" - run: spec-spine compile diff --git a/AGENTS.md b/AGENTS.md index 35aab99..c52488c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# AGENTS.md: stagecraft.ing +# AGENTS.md: statecraft.ing This file is the cross-agent session-init protocol authority, read by Claude Code, Codex CLI, Cursor, and GitHub Copilot via the AAIF/Linux diff --git a/CLAUDE.md b/CLAUDE.md index fb574dc..ea7fbc1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,9 @@ -# CLAUDE.md: stagecraft.ing +# CLAUDE.md: statecraft.ing ## Project Overview -The marketing and docs site for the Stagecraft product family: a fully -static React Router v7 site (framework mode, prerendered, harvesting the OAP-era stagecraft web app and its spec-registry viewer) deployed to GitHub Pages under stagecraft.ing. The +The marketing and docs site for the Statecraft product family: a fully +static React Router v7 site (framework mode, prerendered, harvesting the OAP-era statecraft web app and its spec-registry viewer) deployed to GitHub Pages under statecraft.ing. The scaffold is governed by `specs/001-site-scaffold/spec.md`, the launch copy by `specs/002-launch-content/spec.md`. No backend, no analytics, no runtime external requests: the site must never become operational diff --git a/README.md b/README.md index 2425232..5cd6583 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,31 @@ -# stagecraft.ing +# statecraft.ing -The website and docs for [Stagecraft](https://github.com/stagecraft-ing/stagecraft), +The website and docs for [Statecraft](https://github.com/statecrafting/statecraft), the governed agentic delivery control plane, and its product family: -- [stagecraft](https://github.com/stagecraft-ing/stagecraft): the control +- [statecraft](https://github.com/statecrafting/statecraft): the control plane (AGPL-3.0) -- [enrahitu](https://github.com/stagecraft-ing/enrahitu): the EnRaHiTu +- [enrahitu](https://github.com/statecrafting/enrahitu): the EnRaHiTu template chassis: Encore.ts + rauthy + hiqlite + Turso (Apache-2.0) -- [stagecraft-cli](https://github.com/stagecraft-ing/stagecraft-cli): the +- [statecraft-cli](https://github.com/statecrafting/statecraft-cli): the CLI + MCP server (Apache-2.0) -- [spec-spine](https://github.com/stagecraft-ing/spec-spine): the +- [spec-spine](https://github.com/statecrafting/spec-spine): the spec-governance toolchain everything above is governed by -- [tenant-emit](https://github.com/stagecraft-ing/tenant-emit): the tenant +- [tenant-emit](https://github.com/statecrafting/tenant-emit): the tenant certificate emitter, signing a produced app's governance certificate (Apache-2.0) -- [tenant-tail](https://github.com/stagecraft-ing/tenant-tail): the tenant +- [tenant-tail](https://github.com/statecrafting/tenant-tail): the tenant certificate verifier, re-checking the factory's paperwork with no trust in the producer (Apache-2.0) -- [action-gate](https://github.com/stagecraft-ing/action-gate): a pure, +- [action-gate](https://github.com/statecrafting/action-gate): a pure, deterministic decision gate, evaluate(context, checks) returning Allow, Deny, or Degrade (Apache-2.0) -- [attest-ledger](https://github.com/stagecraft-ing/attest-ledger): a +- [attest-ledger](https://github.com/statecrafting/attest-ledger): a tamper-evident record ledger, append-only, hash-linked, Ed25519-signed, with an independent verifier (Apache-2.0) -- [canonical-keysort-json](https://github.com/stagecraft-ing/canonical-keysort-json): +- [canonical-keysort-json](https://github.com/statecrafting/canonical-keysort-json): deterministic canonical JSON, a lexicographic key sort at the serialization boundary so record hashes agree (Apache-2.0) -- [trust-window](https://github.com/stagecraft-ing/trust-window): a +- [trust-window](https://github.com/statecrafting/trust-window): a rolling-window trust scorer, weighted samples mapping to a graduated privilege level (Apache-2.0) diff --git a/app/components/architecture-explorer.tsx b/app/components/architecture-explorer.tsx new file mode 100644 index 0000000..5b5bac7 --- /dev/null +++ b/app/components/architecture-explorer.tsx @@ -0,0 +1,184 @@ +import { useState } from "react"; +import { X } from "./icons"; + +// Interactive inline-SVG diagram (spec 004 section 3.4): clickable nodes reveal +// a description. Harvested from the OAP whitepaper site, with framer-motion +// swapped for a plain CSS transition and the lucide icon swapped for the inline +// set, so the reader carries no animation or icon-library dependency. + +export interface ArchNode { + id: string; + label: string; + x: number; + y: number; + width: number; + height: number; + description: string; + color: string; +} + +export interface ArchEdge { + from: string; + to: string; + label?: string; +} + +export interface ArchDiagram { + title: string; + caption: string; + viewBox: string; + nodes: ArchNode[]; + edges: ArchEdge[]; +} + +function nodeCenter(node: ArchNode) { + return { x: node.x + node.width / 2, y: node.y + node.height / 2 }; +} + +export function ArchitectureExplorer({ title, caption, nodes, edges, viewBox }: ArchDiagram) { + const [activeNode, setActiveNode] = useState(null); + + return ( +
+
+

+ {title} +

+ + Click nodes to explore + +
+ +
+ + + + + + + + + + + + + + + + + + + + + {edges.map((edge, i) => { + const fromNode = nodes.find((n) => n.id === edge.from); + const toNode = nodes.find((n) => n.id === edge.to); + if (!fromNode || !toNode) return null; + const from = nodeCenter(fromNode); + const to = nodeCenter(toNode); + return ( + + + {edge.label && ( + + {edge.label} + + )} + + ); + })} + + {nodes.map((node) => { + const isActive = activeNode?.id === node.id; + return ( + setActiveNode(isActive ? null : node)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setActiveNode(isActive ? null : node); + } + }} + className="cursor-pointer" + role="button" + tabIndex={0} + aria-pressed={isActive} + aria-label={node.label} + filter={isActive ? "url(#node-glow)" : undefined} + > + + + {node.label} + + + ); + })} + + + {activeNode && ( +
+
+
+
{activeNode.label}
+

+ {activeNode.description} +

+
+ +
+
+ )} +
+ +
+

{caption}

+
+
+ ); +} diff --git a/app/components/icons.tsx b/app/components/icons.tsx new file mode 100644 index 0000000..dd99252 --- /dev/null +++ b/app/components/icons.tsx @@ -0,0 +1,213 @@ +// Inline SVG icons (spec 004 section 3.6). The site ships zero off-origin +// requests and no icon-font or component library, so the handful of glyphs the +// marketing surfaces need are hand-inlined here, matching the stroke style of +// the chrome logo. Paths follow the lucide geometry (ISC-licensed) at +// strokeWidth 1.75 on a 24x24 grid. + +import type { SVGProps } from "react"; + +type IconProps = SVGProps & { className?: string }; + +function Icon({ className, children, ...rest }: IconProps & { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +export function ArrowRight(p: IconProps) { + return ( + + + + + ); +} + +export function ArrowLeft(p: IconProps) { + return ( + + + + + ); +} + +export function ArrowDown(p: IconProps) { + return ( + + + + + ); +} + +export function ChevronDown(p: IconProps) { + return ( + + + + ); +} + +export function ExternalLink(p: IconProps) { + return ( + + + + + + ); +} + +export function Clock(p: IconProps) { + return ( + + + + + ); +} + +export function FileText(p: IconProps) { + return ( + + + + + + + ); +} + +export function Shield(p: IconProps) { + return ( + + + + ); +} + +export function ShieldCheck(p: IconProps) { + return ( + + + + + ); +} + +export function CheckCircle(p: IconProps) { + return ( + + + + + ); +} + +export function GitBranch(p: IconProps) { + return ( + + + + + + + ); +} + +export function Layers(p: IconProps) { + return ( + + + + + + ); +} + +export function Package(p: IconProps) { + return ( + + + + + + ); +} + +export function Server(p: IconProps) { + return ( + + + + + + + ); +} + +export function Terminal(p: IconProps) { + return ( + + + + + ); +} + +export function Cpu(p: IconProps) { + return ( + + + + + + + + + + + + + ); +} + +export function Activity(p: IconProps) { + return ( + + + + ); +} + +export function Printer(p: IconProps) { + return ( + + + + + + ); +} + +export function X(p: IconProps) { + return ( + + + + + ); +} diff --git a/app/components/paper-reader.tsx b/app/components/paper-reader.tsx new file mode 100644 index 0000000..670945f --- /dev/null +++ b/app/components/paper-reader.tsx @@ -0,0 +1,307 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router"; +import { ArrowLeft, Clock, Printer } from "./icons"; +import { ArchitectureExplorer } from "./architecture-explorer"; +import type { ReaderPaper } from "~/lib/papers"; + +// The whitepaper reader (spec 004 section 3.4): sticky TOC, reading-progress, +// scroll-spy, inline **bold** and [ref:N] markers, a positioning table, an +// interactive architecture diagram per section, and a references list. All +// client behaviour is confined to effects, so the article prerenders fully. + +// Renders `**bold**` and `[ref:N]` footnote markers inline. +function InlineText({ text }: { text: string }) { + const parts = text.split(/(\*\*[^*]+\*\*|\[ref:\d+\])/g); + return ( + <> + {parts.map((part, i) => { + if (/^\*\*[^*]+\*\*$/.test(part)) { + return ( + + {part.slice(2, -2)} + + ); + } + const ref = /^\[ref:(\d+)\]$/.exec(part); + if (ref) { + return ( + + + {ref[1]} + + + ); + } + return {part}; + })} + + ); +} + +function useReadingProgress() { + const [progress, setProgress] = useState(0); + useEffect(() => { + function onScroll() { + const h = document.documentElement; + const max = h.scrollHeight - h.clientHeight; + setProgress(max > 0 ? (h.scrollTop / max) * 100 : 0); + } + onScroll(); + window.addEventListener("scroll", onScroll, { passive: true }); + return () => window.removeEventListener("scroll", onScroll); + }, []); + return progress; +} + +function useScrollSpy(ids: string[]) { + const [activeId, setActiveId] = useState(ids[0] ?? ""); + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + const visible = entries + .filter((e) => e.isIntersecting) + .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); + if (visible[0]) setActiveId(visible[0].target.id); + }, + { rootMargin: "-80px 0px -70% 0px", threshold: 0 } + ); + for (const id of ids) { + const el = document.getElementById(id); + if (el) observer.observe(el); + } + return () => observer.disconnect(); + }, [ids.join(",")]); + return activeId; +} + +export function PaperReader({ paper }: { paper: ReaderPaper }) { + const progress = useReadingProgress(); + const sectionIds = paper.sections.map((s) => s.id); + const activeId = useScrollSpy(sectionIds); + + return ( +
+ {/* Reading progress */} +
+
+
+ +
+ + All papers + + +
+ {/* TOC */} + + + {/* Article */} +
+ {/* Header */} +
+ {paper.featured && ( + + Featured whitepaper + + )} +

+ {paper.title} +

+

{paper.subtitle}

+
+ {paper.author} + {paper.date} + + {paper.readingTime} min read + +
+
+ + {/* Abstract */} +
+
+ Abstract +
+

+ {paper.abstract} +

+
+ + {/* Stats */} + {paper.stats && ( +
+ {paper.stats.map((stat) => ( +
+
+ {stat.value} +
+
+ {stat.label} +
+
+ ))} +
+ )} + + {/* Sections */} + {paper.sections.map((section) => ( +
+
+ + {section.number} + +

+ {section.title} +

+
+ {section.blocks.map((block, bi) => ( +
+ {block.heading && ( +

{block.heading}

+ )} + {block.paragraphs.map((p, pi) => ( +

+ +

+ ))} +
+ ))} + {section.explorer && } +
+ ))} + + {/* Positioning table */} + {paper.comparisonTable && ( +
+

+ {paper.comparisonTable.title} +

+

+ {paper.comparisonTable.description} +

+
+ + + + + {paper.comparisonTable.approaches.map((a) => ( + + ))} + + + + {paper.comparisonTable.dimensions.map((dim, di) => ( + + + {paper.comparisonTable!.approaches.map((a) => ( + + ))} + + ))} + +
+ Dimension + + {a.name} +
+ {dim} + + {a.values[di]} +
+
+
+ )} + + {/* References */} + {paper.references && ( +
+

References

+
    + {paper.references.map((ref) => ( +
  1. + [{ref.id}]{" "} + {ref.label}{" "} + + {ref.title} + {" "} + + (accessed {ref.accessed}) + +
  2. + ))} +
+
+ )} +
+
+
+
+ ); +} diff --git a/app/components/sign-in-link.tsx b/app/components/sign-in-link.tsx new file mode 100644 index 0000000..0d03cc0 --- /dev/null +++ b/app/components/sign-in-link.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from "react"; + +// Sign-in hand-off (spec 004 section 3.2). The static apex runs no auth flow: +// it links out to the control plane's Rauthy OIDC kickoff at the same +// /auth/rauthy path the app used before the apex cutover, now on the app host. +// A plain absolute URL so it renders identically in prerendered HTML with no +// client JavaScript, and so middle-click / cmd-click behave. +export const SIGN_IN_URL = "https://app.statecraft.ing/auth/rauthy"; + +export function SignInLink({ + className, + children, +}: { + className?: string; + children: ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/app/components/site-chrome.tsx b/app/components/site-chrome.tsx index 114c1b1..ab99359 100644 --- a/app/components/site-chrome.tsx +++ b/app/components/site-chrome.tsx @@ -1,12 +1,16 @@ import { useEffect, useState } from "react"; import { Link, useLocation } from "react-router"; import { ThemeToggle } from "./theme-toggle"; +import { SignInLink } from "./sign-in-link"; import { ORG_URL, PRODUCT_FAMILY } from "~/lib/product-family"; const NAV_ITEMS = [ { path: "/", label: "Overview" }, + { path: "/products", label: "Products" }, + { path: "/papers", label: "Papers" }, { path: "/registry", label: "Registry" }, { path: "/docs", label: "Docs" }, + { path: "/get-started", label: "Get Started" }, ]; function isActive(pathname: string, path: string) { @@ -37,7 +41,7 @@ function Wordmark() { - Stagecraft + Statecraft ); @@ -98,19 +102,14 @@ export function SiteHeader() {
- + spec-governed - - GitHub - + + Sign in +