From caac484815b2e6dc92996d76df2ce85b9ea95eea Mon Sep 17 00:00:00 2001 From: Bartek Kus <7887446+bartekus@users.noreply.github.com> Date: Thu, 16 Jul 2026 04:20:18 -0600 Subject: [PATCH 1/3] feat(004): restore rich marketing surfaces (products, whitepaper, get-started, sign-in) Harvest the OAP-era stagecraft web app's richer public experience onto the static apex, re-authored to be checkable against the real product family. - Products/architecture page: the family as layers (governance, substrate, control plane, interface, verification primitives) with a governed delivery flow whose maturity chips roll up from the baked registry, and a repo catalog derived from the spec-003 roster (no drift). - Papers + a full whitepaper reader (sticky TOC, reading-progress, scroll-spy, inline references, comparison table) with an interactive clickable-SVG architecture explorer. The flagship paper is re-authored around spec-spine, enrahitu, the attestation primitives, and live Rauthy; no fabricated hashes, spec counts, signatures, or dead subsystems. - Get started: an honest split between what runs today (spec-spine, the enrahitu substrate) and what is on the ladder, each step linking to its governing spec. - Chrome: Products, Papers, Get Started in the nav, and a Sign in link that hands off to the control plane's Rauthy OIDC kickoff at app.stagecraft.ing/auth/rauthy. The static site runs no auth flow. Rename is total: Open Agentic Platform becomes Stagecraft; factory-encore and template-encore become enrahitu. Icons are inline SVG (no new deps); every new route is prerendered; zero runtime off-origin requests. Spec 004 owns the new files and extends spec 001 on the route table, prerender list, and chrome. --- .../by-spec/004-marketing-surfaces.json | 289 +++++++++++++++++ .../by-spec/004-marketing-surfaces.json | 109 +++++++ app/components/architecture-explorer.tsx | 184 +++++++++++ app/components/icons.tsx | 213 ++++++++++++ app/components/paper-reader.tsx | 307 ++++++++++++++++++ app/components/sign-in-link.tsx | 22 ++ app/components/site-chrome.tsx | 21 +- app/lib/explorer-diagrams.ts | 80 +++++ app/lib/get-started.ts | 76 +++++ app/lib/papers.ts | 112 +++++++ app/lib/products.ts | 224 +++++++++++++ app/lib/whitepaper.ts | 212 ++++++++++++ app/routes.ts | 6 + app/routes/get-started.tsx | 151 +++++++++ app/routes/papers.$slug.tsx | 44 +++ app/routes/papers.tsx | 92 ++++++ app/routes/products.tsx | 292 +++++++++++++++++ react-router.config.ts | 12 + specs/004-marketing-surfaces/spec.md | 200 ++++++++++++ 19 files changed, 2637 insertions(+), 9 deletions(-) create mode 100644 .derived/codebase-index/by-spec/004-marketing-surfaces.json create mode 100644 .derived/spec-registry/by-spec/004-marketing-surfaces.json create mode 100644 app/components/architecture-explorer.tsx create mode 100644 app/components/icons.tsx create mode 100644 app/components/paper-reader.tsx create mode 100644 app/components/sign-in-link.tsx create mode 100644 app/lib/explorer-diagrams.ts create mode 100644 app/lib/get-started.ts create mode 100644 app/lib/papers.ts create mode 100644 app/lib/products.ts create mode 100644 app/lib/whitepaper.ts create mode 100644 app/routes/get-started.tsx create mode 100644 app/routes/papers.$slug.tsx create mode 100644 app/routes/papers.tsx create mode 100644 app/routes/products.tsx create mode 100644 specs/004-marketing-surfaces/spec.md 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..26bfe39 --- /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": "a0d1ca188aff5a45b52ccf05bc7df0b5d884ea592dc155980fbcbc2d94b9f948" +} 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..be73bac --- /dev/null +++ b/.derived/spec-registry/by-spec/004-marketing-surfaces.json @@ -0,0 +1,109 @@ +{ + "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", + "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): in progress" + ], + "specPath": "specs/004-marketing-surfaces/spec.md", + "status": "approved", + "summary": "Restores the richer marketing experience the OAP-era stagecraft 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.stagecraft.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 Stagecraft, 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": "2cbfcdda99c05400a095a0563a9cbd5f66fa774ddac6a79ba6dd8da2547ced1d", + "specVersion": "1.1.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..70d43fa --- /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.stagecraft.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..7d5e472 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) { @@ -98,19 +102,14 @@ export function SiteHeader() {
- + spec-governed - - GitHub - + + Sign in +