diff --git a/.omo/evidence/v11-consolidation.md b/.omo/evidence/v11-consolidation.md new file mode 100644 index 0000000..e253ef0 --- /dev/null +++ b/.omo/evidence/v11-consolidation.md @@ -0,0 +1,35 @@ +# v11 design-system consolidation — evidence + +Lens: omo-programming (TS iron list, no speculative API) + omo-frontend (DESIGN.md §7: pattern twice => design-system). + +## Folds (rendered class sets byte-equal; DOM tags unchanged) +- NEW `MonoTag` (surfaces.tsx): mono chip
  • — folds ulw-research lane chips + feature-workflows skills chips (2 sites) +- NEW `CardLabel` (typography.tsx, tone: default|accent): mono uppercase card

    — folds hephaestus omoLabel/lazyLabel + team-mode whenTitle (3 sites) +- `AccentSurface` gains `as: "div"|"li"` + `padding` (default p-5, existing consumers unchanged) — absorbs ulw-demo example chip (px-6 py-3) + 5 Hephaestus loop tiles (as="li" p-4) + +## Prop-surface tightening (all zero-consumer, DOM-identical) +- CommandCodeSurface / IconWell: no longer accept a className they silently ignored (ChildrenOnlyProps) +- SkipLink: unused children/href props inlined ("Skip to main content", "#content") +- LinkAction: speculative `prefetch?: false` prop inlined to `prefetch={false}` +- KEPT: FactList.dotClassName (shared FactListProps contract; CompactDotList consumer live), className passthrough on rendered primitives (layer-wide idiom) + +## Dead CSS/tokens removed (verified 0 source consumers) +- .card-gradient-base/-beam/-sheen/-pools (hero went open-canvas in e425a68) +- --accent-cyan, --accent-teal (compat aliases), --surface-3, --surface-panel-alt, --surface-panel-deep + +## DESIGN.md truth-up +- §2 palette table + alias rule reconciled with removed tokens; adapter-token intro now states the fixed-dark reality (light block defined-but-unmounted) +- §5 CodexWindow rewritten for the v10 appending chat replay (was still describing the v1 slide model with toggle + play/pause) +- §5 component lists gained MonoTag / CardLabel / AccentSurface variants +- §6 ulw-demo timeline rewritten (ENTRY_MS 900 append cadence, 4s rest, loop, reduced-motion static) +- §7 hero row + rules reflect the open-canvas hero (no card gradients) + +## Gates (all after the change) +- biome lint app e2e components lib: PASS (59 files) +- tsc --noEmit: PASS +- new-string audit: 32 candidates, NEW-STRINGS-OK (no new visible copy) +- Full sweep: 57/57 PASS (v11-full-e2e.txt) +- Lighthouse: 100x4 (v11-lighthouse.txt) +- Visual eyeball on built server :4341 — v11-heph-loop.png / v11-skills-band.png / v11-demo-chip.png (all identical to pre-change rendering) + +Cleanup: screenshot server on :4341 killed; playwright self-managed servers torn down (no LISTEN on 4340/4341). diff --git a/.omo/evidence/v11-demo-chip.png b/.omo/evidence/v11-demo-chip.png new file mode 100644 index 0000000..53a6ce9 Binary files /dev/null and b/.omo/evidence/v11-demo-chip.png differ diff --git a/.omo/evidence/v11-full-e2e.txt b/.omo/evidence/v11-full-e2e.txt new file mode 100644 index 0000000..f98597b --- /dev/null +++ b/.omo/evidence/v11-full-e2e.txt @@ -0,0 +1,101 @@ +[WebServer] $ node ./scripts/generate-docs-content.mjs +[WebServer] Docs content already current with 20 HTML-compiled docs +[WebServer] $ NODE_OPTIONS=--no-deprecation next build +[WebServer] ▲ Next.js 16.2.9 (Turbopack) +[WebServer] +[WebServer] Creating an optimized production build ... +[WebServer] ✓ Compiled successfully in 1076ms +[WebServer] Running TypeScript ... +[WebServer] Finished TypeScript in 2.2s ... +[WebServer] Collecting page data using 13 workers ... +[WebServer] Generating static pages using 13 workers (0/11) ... +[WebServer] Generating static pages using 13 workers (2/11) +[WebServer] Generating static pages using 13 workers (5/11) +[WebServer] Generating static pages using 13 workers (8/11) +[WebServer] ✓ Generating static pages using 13 workers (11/11) in 719ms +[WebServer] Finalizing page optimization ... +[WebServer] +[WebServer] Route (app) +[WebServer] ┌ ○ / +[WebServer] ├ ○ /_not-found +[WebServer] ├ ƒ /api/github-stars +[WebServer] ├ ○ /apple-icon.png +[WebServer] ├ ○ /docs +[WebServer] ├ ○ /icon.svg +[WebServer] ├ ○ /manifest.webmanifest +[WebServer] ├ ○ /opengraph-image +[WebServer] ├ ○ /robots.txt +[WebServer] ├ ○ /sitemap.xml +[WebServer] └ ○ /twitter-image +[WebServer] +[WebServer] +[WebServer] ○ (Static) prerendered as static content +[WebServer] ƒ (Dynamic) server-rendered on demand +[WebServer] +[WebServer] $ NODE_OPTIONS=--no-deprecation next start +[WebServer] ▲ Next.js 16.2.9 +[WebServer] - Local: http://localhost:56465 +[WebServer] - Network: http://192.168.0.3:56465 +[WebServer] ✓ Ready in 94ms + +Running 57 tests using 1 worker + + ✓ 1 [chromium] › e2e/docs.spec.ts:15:3 › docs page — structure › responds 200 (19ms) + ✓ 2 [chromium] › e2e/docs.spec.ts:20:3 › docs page — structure › has exactly one h1 (220ms) + ✓ 3 [chromium] › e2e/docs.spec.ts:25:3 › docs page — structure › renders every section as a visible element carrying its id + title (168ms) + ✓ 4 [chromium] › e2e/docs.spec.ts:35:3 › docs page — structure › nav lists every section title as links or buttons (167ms) + ✓ 5 [chromium] › e2e/docs.spec.ts:46:3 › docs page — structure › documents lazycodex-ai as the npm install alias (118ms) + ✓ 6 [chromium] › e2e/docs.spec.ts:63:3 › docs page — structure › documents skills and built-in workflow usage (170ms) + ✓ 7 [chromium] › e2e/docs.spec.ts:80:3 › docs page — structure › orders Skills immediately after Commands and before Concepts (0ms) + ✓ 8 [chromium] › e2e/docs.spec.ts:91:3 › docs page — navigation › clicking the $ulw-loop nav entry jumps to that section (168ms) + ✓ 9 [chromium] › e2e/docs.spec.ts:106:3 › docs page — no-JS SSR › server-renders every section heading without JavaScript (144ms) + ✓ 10 [chromium] › e2e/github-stars.spec.ts:21:3 › github stars API › responds with a numeric live star count (13ms) + ✓ 11 [chromium] › e2e/github-stars.spec.ts:37:3 › github stars live source parsing › uses GH_TOKEN when GITHUB_TOKEN is absent (2ms) + ✓ 12 [chromium] › e2e/github-stars.spec.ts:63:3 › github stars live source parsing › falls back to Shields when GitHub rejects the request (1ms) + ✓ 13 [chromium] › e2e/github-stars.spec.ts:87:3 › github stars live source parsing › parses Shields comma and compact star payloads (0ms) + ✓ 14 [chromium] › e2e/github-stars.spec.ts:93:3 › github stars live source parsing › caches the non-zero fallback instead of serving a zero-star shell (0ms) + ✓ 15 [chromium] › e2e/github-stars.spec.ts:103:3 › github stars live source parsing › rejects zero-star upstream payloads as stale or broken source data (0ms) + ✓ 16 [chromium] › e2e/home.spec.ts:17:3 › home page — content › renders the wordmark, hero copy, and footer (106ms) + ✓ 17 [chromium] › e2e/home.spec.ts:37:3 › home page — content › does not show launch gating copy (90ms) + ✓ 18 [chromium] › e2e/home.spec.ts:44:3 › home page — content › has a single h1 and no broken landmarks (92ms) + ✓ 19 [chromium] › e2e/home.spec.ts:52:3 › home page — content › skip-link is hidden until focused (93ms) + ✓ 20 [chromium] › e2e/landing-sections.spec.ts:20:3 › team mode section › renders the grounded team mode copy (101ms) + ✓ 21 [chromium] › e2e/landing-sections.spec.ts:41:3 › ulw-research section › renders the grounded ulw-research copy (96ms) + ✓ 22 [chromium] › e2e/landing-sections.spec.ts:56:3 › information architecture › keeps the planned section order (528ms) + ✓ 23 [chromium] › e2e/landing.spec.ts:15:3 › landing page — hero › has exactly one h1 reading the wordmark (101ms) + ✓ 24 [chromium] › e2e/landing.spec.ts:23:3 › landing page — hero › shows the eyebrow and both hero lines (105ms) + ✓ 25 [chromium] › e2e/landing.spec.ts:41:3 › landing page — install + commands › shows the install command and a copy button (101ms) + ✓ 26 [chromium] › e2e/landing.spec.ts:51:3 › landing page — install + commands › renders every command with its name and syntax (104ms) + ✓ 27 [chromium] › e2e/landing.spec.ts:59:3 › landing page — install + commands › feature workflow guidance keeps the three command pillars first (101ms) + ✓ 28 [chromium] › e2e/landing.spec.ts:70:3 › landing page — install + commands › places skill coverage before the concept section (100ms) + ✓ 29 [chromium] › e2e/landing.spec.ts:87:3 › landing page — links + footer › github stars pill links to the stargazers url with a count (93ms) + ✓ 30 [chromium] › e2e/landing.spec.ts:96:3 › landing page — links + footer › updates the github stars pill from the live API (92ms) + ✓ 31 [chromium] › e2e/landing.spec.ts:104:3 › landing page — links + footer › has a Docs link pointing at /docs (95ms) + ✓ 32 [chromium] › e2e/landing.spec.ts:111:3 › landing page — links + footer › links to OmO and shows lazycodex.ai (93ms) + ✓ 33 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at mobile-small (360×640) (645ms) + ✓ 34 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at mobile-iphone-se (375×667) (644ms) + ✓ 35 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at mobile-iphone-14 (390×844) (622ms) + ✓ 36 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at mobile-large-android (412×915) (623ms) + ✓ 37 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at tablet-ipad-portrait (768×1024) (667ms) + ✓ 38 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at tablet-ipad-landscape (1024×768) (625ms) + ✓ 39 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at tablet-ipad-pro-portrait (1024×1366) (628ms) + ✓ 40 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at desktop-laptop (1280×800) (623ms) + ✓ 41 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at desktop-fullhd (1440×900) (622ms) + ✓ 42 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at desktop-wide (1536×864) (659ms) + ✓ 43 [chromium] › e2e/responsive.spec.ts:37:3 › @responsive renders correctly at desktop-ultrawide (1920×1080) (675ms) + ✓ 44 [chromium] › e2e/responsive.spec.ts:81:1 › @responsive iPhone-13 device profile (Playwright preset) (607ms) + ✓ 45 [chromium] › e2e/responsive.spec.ts:98:1 › @responsive iPad-Pro device profile (Playwright preset) (609ms) + ✓ 46 [chromium] › e2e/seo.spec.ts:25:3 › site SEO + metadata › has a unique , description, canonical, lang, viewport (99ms) + ✓ 47 [chromium] › e2e/seo.spec.ts:52:3 › site SEO + metadata › has OpenGraph and Twitter card tags (96ms) + ✓ 48 [chromium] › e2e/seo.spec.ts:77:3 › site SEO + metadata › has JSON-LD SoftwareApplication structured data (95ms) + ✓ 49 [chromium] › e2e/seo.spec.ts:87:3 › site SEO + metadata › /robots.txt and /sitemap.xml are reachable (8ms) + ✓ 50 [chromium] › e2e/seo.spec.ts:101:3 › site SEO + metadata › /docs route is reachable (7ms) + ✓ 51 [chromium] › e2e/seo.spec.ts:106:3 › site SEO + metadata › /manifest.webmanifest is reachable and valid (3ms) + ✓ 52 [chromium] › e2e/seo.spec.ts:114:3 › site SEO + metadata › opengraph image and twitter image render as PNGs (8ms) + ✓ 53 [chromium] › e2e/seo.spec.ts:135:3 › site SEO + metadata › serves the unified LazyCodex favicon assets (97ms) + ✓ 54 [chromium] › e2e/ulw-demo.spec.ts:35:3 › ulw demo — chat replay @happy › one ask, then the run appends beneath it (6.0s) + ✓ 55 [chromium] › e2e/ulw-demo.spec.ts:78:3 › ulw demo — chat replay @happy › walks the whole run to the checkpoint, then loops (46.9s) + ✓ 56 [chromium] › e2e/ulw-demo.spec.ts:99:3 › ulw demo — reduced motion + mobile @edge › reduced motion shows the completed run statically (3.1s) + ✓ 57 [chromium] › e2e/ulw-demo.spec.ts:117:3 › ulw demo — reduced motion + mobile @edge › no horizontal overflow at 390x844 and the sidebar collapses (2.0s) + + 57 passed (1.3m) diff --git a/.omo/evidence/v11-heph-loop.png b/.omo/evidence/v11-heph-loop.png new file mode 100644 index 0000000..49d84d9 Binary files /dev/null and b/.omo/evidence/v11-heph-loop.png differ diff --git a/.omo/evidence/v11-lighthouse.txt b/.omo/evidence/v11-lighthouse.txt new file mode 100644 index 0000000..2d8a9ec --- /dev/null +++ b/.omo/evidence/v11-lighthouse.txt @@ -0,0 +1,60 @@ +[WebServer] $ node ./scripts/generate-docs-content.mjs +[WebServer] Docs content already current with 20 HTML-compiled docs +[WebServer] $ NODE_OPTIONS=--no-deprecation next build +[WebServer] ▲ Next.js 16.2.9 (Turbopack) +[WebServer] +[WebServer] Creating an optimized production build ... +[WebServer] ✓ Compiled successfully in 1008ms +[WebServer] Running TypeScript ... +[WebServer] Finished TypeScript in 2.3s ... +[WebServer] Collecting page data using 13 workers ... +[WebServer] Generating static pages using 13 workers (0/11) ... +[WebServer] Generating static pages using 13 workers (2/11) +[WebServer] Generating static pages using 13 workers (5/11) +[WebServer] Generating static pages using 13 workers (8/11) +[WebServer] ✓ Generating static pages using 13 workers (11/11) in 689ms +[WebServer] Finalizing page optimization ... +[WebServer] +[WebServer] Route (app) +[WebServer] ┌ ○ / +[WebServer] ├ ○ /_not-found +[WebServer] ├ ƒ /api/github-stars +[WebServer] ├ ○ /apple-icon.png +[WebServer] ├ ○ /docs +[WebServer] ├ ○ /icon.svg +[WebServer] ├ ○ /manifest.webmanifest +[WebServer] ├ ○ /opengraph-image +[WebServer] ├ ○ /robots.txt +[WebServer] ├ ○ /sitemap.xml +[WebServer] └ ○ /twitter-image +[WebServer] +[WebServer] +[WebServer] ○ (Static) prerendered as static content +[WebServer] ƒ (Dynamic) server-rendered on demand +[WebServer] +[WebServer] $ NODE_OPTIONS=--no-deprecation next start +[WebServer] ▲ Next.js 16.2.9 +[WebServer] - Local: http://localhost:56958 +[WebServer] - Network: http://192.168.0.3:56958 +[WebServer] ✓ Ready in 70ms + +Running 2 tests using 1 worker + +[Lighthouse mobile] url=http://127.0.0.1:56958 perf=100 a11y=100 bp=100 seo=100 +[Lighthouse mobile] Failing audits: + - unused-javascript: score=0.5 (Reduce unused JavaScript) + - bf-cache: score=0 (Page prevented back/forward cache restoration) + - legacy-javascript-insight: score=0.5 (Legacy JavaScript) + - network-dependency-tree-insight: score=0 (Network dependency tree) + - render-blocking-insight: score=0.5 (Render-blocking requests) + ✓ 1 [chromium] › e2e/lighthouse.spec.ts:220:3 › @lighthouse — Lighthouse 100/100/100/100 (Playwright Chrome + CDP) › mobile preset hits 100 in every category (15.9s) +[Lighthouse desktop] url=http://127.0.0.1:56958 perf=100 a11y=100 bp=100 seo=100 +[Lighthouse desktop] Failing audits: + - unused-javascript: score=0.5 (Reduce unused JavaScript) + - bf-cache: score=0 (Page prevented back/forward cache restoration) + - legacy-javascript-insight: score=0.5 (Legacy JavaScript) + - network-dependency-tree-insight: score=0 (Network dependency tree) + - render-blocking-insight: score=0.5 (Render-blocking requests) + ✓ 2 [chromium] › e2e/lighthouse.spec.ts:226:3 › @lighthouse — Lighthouse 100/100/100/100 (Playwright Chrome + CDP) › desktop preset hits 100 in every category (15.8s) + + 2 passed (39.6s) diff --git a/.omo/evidence/v11-skills-band.png b/.omo/evidence/v11-skills-band.png new file mode 100644 index 0000000..5ed81f7 Binary files /dev/null and b/.omo/evidence/v11-skills-band.png differ diff --git a/.omo/evidence/v11-slop-report.md b/.omo/evidence/v11-slop-report.md new file mode 100644 index 0000000..ebdf7c2 --- /dev/null +++ b/.omo/evidence/v11-slop-report.md @@ -0,0 +1,40 @@ +# AI SLOP REMOVAL REPORT — v11 full-branch sweep + +Scope: branch diff vs merge-base main (37 source files, packages/web), 10 parallel deep agents in 2 batches of 5. +Behavior lock: green baseline at 4f6572d — 57/57 e2e (v10-full-e2e.txt) + Lighthouse 100x4 (v10-lighthouse.txt). No new tests needed (full coverage existed). + +## Per-file results (only files with changes) +- lib/ulw-demo-scenes.ts: dead code -24 LOC — ULW_DEMO_STEPS, ULW_DEMO_PROOFS, UlwStep (pre-v10 rendering model; zero consumers repo-wide, timeline derivation byte-identical) +- components/site/ulw-demo/window-icons.tsx: dead code -1 — unused "agent" icon path (v9 roster leftover; union narrowing proven safe) +- app/styles/ulw-demo-transcript.css: stale comment -1 — header still described v9 scene-variable reserves +- app/styles/ulw-demo-panel.css: obvious comment -2 — section divider restating the file header verbatim +- components/design-system/brand-mark.tsx: -1 — cx() wrapping a single static string + orphaned import +- components/site/hero.tsx: -1 — WHAT-restating JSX comment + +Net: -30 LOC (32 deleted, 2 added). 31 other files verified clean with per-category SKIP reasoning (agents' full reports in session log). + +## Notable justified SKIPs +- window-panes.tsx entry-kind if-chain: repo has no assertNever idiom; switch+default would change impossible-branch behavior — SKIP correct +- codex-window.tsx ?.>/?? guards: REQUIRED by noUncheckedIndexedAccess — not defensive slop +- command-card GlyphIcon default branch: runtime safety at data seam; removal trades no-op for potential render throw +- docs.css duplication (sticky columns, 4.5rem offsets): unmergeable without cascade reorder / new custom property + +## Flagged for the consolidation pass (handled in the follow-up commit) +- DEAD: .card-gradient-base/-beam/-sheen/-pools (design-system.css) — hero went open-canvas in e425a68; DESIGN.md §7 hero row stale +- DEAD tokens: --accent-cyan, --accent-teal (compat aliases, zero consumers), --surface-3, --surface-panel-alt, --surface-panel-deep +- Accept-but-ignore props: CommandCodeSurface.className, IconWell.className (accepted, never rendered) +- Zero-consumer props: FactList.dotClassName, SkipLink.children/href, LinkAction.prefetch (?: false single-literal) +- ChildrenProps defined in surfaces.tsx AND layout.tsx with different shapes (same name) + +## Quality gates +- Regression: green baseline held (behavior-preserving edits only; full sweep re-run scheduled after consolidation commit) +- Lint (biome app e2e components lib): PASS, 59 files +- Typecheck (tsc --noEmit): PASS +- Static/security scan: N/A (not configured) + +## Deferred debt ledger +- docs.css at 441 pure LOC (>250 ceiling): NOT split — DESIGN.md sanctions it as the page-specific composition layer; split risks cascade-order changes +- team-mode-section.tsx L89-92 hardcoded visible string (not ledger-sourced like siblings) — flag only, copy changes forbidden +- CompactDotList vs hand-rolled team-mode bullets: dot geometry differs visibly — distinct patterns, not folded + +Final Status: CLEAN diff --git a/packages/web/DESIGN.md b/packages/web/DESIGN.md index c8f9920..48a151f 100644 --- a/packages/web/DESIGN.md +++ b/packages/web/DESIGN.md @@ -4,48 +4,140 @@ Implementation sources: - Browser CSS tokens and shared utility layers live in `app/styles/design-system.css`, imported before page-specific styles by `app/globals.css`. - Reusable React primitives live in `components/design-system/`; landing and docs components compose from those primitives. - Social preview tokens live in `app/og-image-theme.ts` and intentionally mirror the browser palette. -- Page-specific composition styles live in `app/styles/landing.css` and `app/styles/docs.css`. +- Page-specific composition styles live in `app/styles/landing.css`, `app/styles/ulw-demo.css`, and `app/styles/docs.css`. ## 1. Atmosphere & Identity -LazyCodex feels like a serious command surface for complex codebases: near-black, quiet, technical, and lit by an emerald signal. The signature is a glowing green card-in-canvas composition with a geometric rounded-square `L` mark. The brand color is green, not teal, cyan, purple, or blue. +LazyCodex feels like a calm, precise productivity tool for complex codebases: a deep graphite +canvas, editorial structure with dotted column rules, hairline white borders, restrained accents, +and green as the single brand signal — crisp and quiet, in the spirit of a modern dark +productivity tool. The signature composition is the faithful LIGHT Codex window sitting on the +dark ground with the geometric rounded-square `L` mark: the light window is the page's hero +contrast, mirroring the real app frames. Elevated panels separate from the canvas through small +tonal lifts plus hairline borders, never heavy chrome. Light surfaces exist only as deliberate +accents — chiefly the demo window's light theme — small light windows on dark ground, never the +page itself. The brand color is green, not teal, cyan, purple, or blue. ## 2. Color +### Atmosphere + +The canvas is not flat black: a static emerald atmosphere restores the +original identity's luminous tone. `PageShell` mounts an `aria-hidden` +`.glow-backdrop` layer (absolute, `pointer-events-none`, painted below the +content wrapper by DOM order) carrying four low-alpha radial green washes — +an aurora behind the hero (peak `rgba(34,197,94,0.17)`), a faint right-side +pool near the demo, a mid-page pool, and a low anchor near the Hephaestus +band. Alphas stay ≤ 0.17 so every AA-audited text pair is unaffected, and the +layer never animates — zero paint cost after first frame, zero CLS. + + ### Palette | Role | Token | Value | Usage | | --- | --- | --- | --- | -| Surface/base | `--surface-base`, `--surface-night`, `--surface-0` | `#0a0c0b` | Page canvas and footer | -| Surface/subtle | `--surface-1` | `rgba(255,255,255,0.018)` | Hover and quiet fills | -| Surface/raised | `--surface-2` | `rgba(255,255,255,0.035)` | Secondary tonal layer | -| Surface/strong | `--surface-3` | `rgba(255,255,255,0.055)` | Stronger tonal layer | -| Surface/card | `--card-base`, `--surface-panel` | `#0E1411` | Hero card, command surfaces | -| Surface/alt | `--surface-panel-alt` | `#0C1310` | Alternate panel | -| Surface/deep | `--surface-panel-deep` | `#0D1310` | Deep panel | -| Brand/core | `--brand-core` | `#22c55e` | Green brand center | +| Surface/base | `--surface-base`, `--surface-0` | `#0a0b0d` | Page canvas | +| Surface/night | `--surface-night` | `#07080a` | Footer and deeper page bands | +| Surface/subtle | `--surface-1` | `rgba(255,255,255,0.04)` | Hover and quiet fills | +| Surface/raised | `--surface-2` | `rgba(255,255,255,0.06)` | Secondary tonal layer | +| Surface/card | `--card-base` | `#15171b` | Elevated dark panels, content cards | +| Surface/panel | `--surface-panel` | `#101216` | Panels, install bar | +| Brand/core | `--brand-core` | `#22c55e` | Green brand center (fills, gradients) | | Brand/mid | `--brand-mid` | `#16a34a` | Green gradient middle | -| Brand/outer | `--brand-outer` | `#15803d` | Selection and gradient edge | -| Accent/primary | `--accent-primary` | `#4ade80` | CTAs, focus, active docs links | -| Accent/soft | `--accent-primary-soft` | `rgba(74,222,128,0.1)` | Soft green fills | -| Accent/border | `--accent-primary-border` | `rgba(74,222,128,0.24)` | Soft green outlines | -| Accent/mint | `--accent-mint`, `--accent-glow` | `#86efac` | Highlights, glow text | -| Text/primary | `--text-primary` | `#ffffff` | Main text and headings | -| Text/secondary | `--text-secondary` | `#b8c2bc` | Supporting text | -| Text/tertiary | `--text-tertiary` | `#8b9690` | Labels, metadata | -| Text/muted | `--text-muted` | `rgba(255,255,255,0.74)` | Body copy on dark surfaces | -| Text/soft | `--text-soft` | `#dcfce7` | Mint-tinted text | -| Border/subtle | `--border-subtle` | `rgba(255,255,255,0.06)` | Dividers and quiet controls | -| Border/default | `--border-default` | `rgba(255,255,255,0.1)` | Panels and cards | -| Status/success | `--status-success` | `#22c55e` | Positive status | -| Status/warning | `--status-warning` | `#f59e0b` | Warnings | -| Status/error | `--status-error` | `#ef4444` | Errors | +| Brand/outer | `--brand-outer` | `#15803d` | Gradient edge | +| Accent/primary | `--accent-primary` | `#4ade80` | CTAs, focus, active docs links (AA on canvas AND elevated panels) | +| Accent/soft | `--accent-primary-soft` | `rgba(74,222,128,0.10)` | Soft green fills | +| Accent/border | `--accent-primary-border` | `rgba(74,222,128,0.32)` | Soft green outlines | +| Accent/mint | `--accent-mint` | `#86efac` | Fills and decoration first; interactive text stays on `--accent-primary` | +| Accent/glow | `--accent-glow` | `#bbf7d0` | Bright green emphasis | +| Text/primary | `--text-primary` | `#f7f8f8` | Main text and headings | +| Text/secondary | `--text-secondary` | `#b4bcc8` | Supporting text | +| Text/tertiary | `--text-tertiary` | `#98a1ab` | Labels, metadata | +| Text/muted | `--text-muted` | `rgba(247,248,248,0.78)` | Body copy | +| Text/soft | `--text-soft` | `#86efac` | Green-tinted text | +| Border/subtle | `--border-subtle` | `rgba(255,255,255,0.08)` | Hairline dividers, dotted rules, quiet controls | +| Border/default | `--border-default` | `rgba(255,255,255,0.12)` | Panels and cards | +| Status/success | `--status-success` | `#4ade80` | Positive status | +| Status/warning | `--status-warning` | `#fbbf24` | Warnings | +| Status/error | `--status-error` | `#f87171` | Errors | + +`::selection` uses a `#14532d` dark-green background with `#dcfce7` text. `:focus-visible` +outlines use `--accent-primary`. The `html` element declares `color-scheme: dark`; the site +identity is a FIXED dark canvas — there is no site-wide `prefers-color-scheme` flip. Light +appears only inside the sanctioned light surface below (the demo window's light theme). + +### Codex window adapter tokens (ulw-demo / team-mode mocks only) + +The Ultrawork demo and the Team Mode thread mock reproduce the Codex Desktop surface on the dark +canvas. The window carries its own isolated adapter palette with two theme blocks selected by +`data-window-theme="light|dark"` on `.ulw-window`. Both mounted windows are FIXED dark +(`data-window-theme="dark"` hardcoded — there is no theme toggle); the light block remains the +adapter's base token definition and the only sanctioned light surface should one return. +Adapter tokens never leak into ordinary landing/docs UI, and ordinary tokens never restyle the +window interior. + +Light theme (base block on `.ulw-window`, currently unmounted): + +| Role | Token | Value | Usage | +| --- | --- | --- | --- | +| Window/canvas | `--codex-window-bg` | `#ffffff` | Codex window body | +| Window/chrome | `--codex-window-chrome` | `#f6f7f6` | Title bar, sidebar, composer field | +| Window/border | `--codex-window-border` | `rgba(10,12,11,0.12)` | Window ring, pane dividers | +| Window/text | `--codex-window-text` | `#17211b` | Primary transcript text | +| Window/text-soft | `--codex-window-text-soft` | `#5b675f` | Tool rows, metadata, timestamps | +| Window/chip | `--codex-window-chip` | `rgba(10,12,11,0.06)` | Inline code chips, path chips | +| Window/active | `--codex-window-active` | `rgba(34,197,94,0.12)` | Active step, active roster row | +| Window/active-border | `--codex-window-active-border` | `rgba(22,101,52,0.28)` | Active step/proof outlines | +| Window/accent | `--codex-window-accent` | `#166534` | Active-state text on light surface (AA on white) | +| Window/glyph-text | `--codex-window-glyph-text` | `#ffffff` | Letters inside roster glyph squares | +| Window/traffic | `--codex-window-traffic-red/-amber/-green` | `#f87171` / `#fbbf24` / `#34d399` | macOS traffic-light ornaments | + +Dark theme (override block scoped `[data-window-theme="dark"]`, same 13 token names). It is +deliberately a touch LIGHTER than the page canvas (`#0a0b0d`) with a stronger hairline ring +(`rgba(255,255,255,0.18)`), so the dark window still reads as a distinct elevated layer instead +of dissolving into the page: + +| Role | Token | Value | +| --- | --- | --- | +| Window/canvas | `--codex-window-bg` | `#1a1d22` | +| Window/chrome | `--codex-window-chrome` | `#15181c` | +| Window/text | `--codex-window-text` | `#eef1f4` | +| Window/text-soft | `--codex-window-text-soft` | `#a9b2bd` | +| Window/accent | `--codex-window-accent` | `#4ade80` | +| Window/border, chip, active(+border), glyph-text, traffic | same names | tuned dark-elevated values — `app/styles/design-system.css` is authoritative | + +Every (text, background) pair in BOTH window themes must pass `.omo/scripts/contrast-check.mjs` +at ≥ 4.5:1 (≥ 3:1 only for display-size text). + +### Subagent lane glyph tokens + +The roster glyph squares use per-agent identity hues faithful to the Codex Desktop reference. +They are exposed as per-theme custom props (`--lane-<name>`) so the dark window block can re-tune +any glyph that fails contrast against `--codex-window-bg`: + +| Lane | Token | Light value | +| --- | --- | --- | +| Root | `--lane-root` | `#115e59` | +| Explore | `--lane-explore` | `#1d4ed8` | +| Library | `--lane-library` | `#92400e` | +| Plan | `--lane-plan` | `#6d28d9` | +| Todo | `--lane-todo` | `#334155` | +| Execute | `--lane-execute` | `#166534` | +| Test | `--lane-test` | `#b91c1c` | +| QA | `--lane-qa` | `#be185d` | +| Review | `--lane-review` | `#4338ca` | +| Continuation | `--lane-continuation` | `#475569` | + +These are identity badges scoped to the window adapter, not brand accents — the green-only brand +rule applies everywhere outside the window. ### Rules -- New UI uses `--accent-primary` and `--accent-mint`; `--accent-cyan` and `--accent-teal` remain green aliases only for compatibility. +- New UI uses `--accent-primary`. +- `--accent-mint` (`#86efac`) is a fill/decoration color first (glows, dots, code prompt glyphs). Interactive text and links stay on `--accent-primary` so the accent voice remains single and restrained. - Accent is reserved for interactivity, code emphasis, focus, and brand signal. -- Raw colors belong in this file, `design-system.css`, or OG theme tokens. Component code should reference tokens or shared primitives. +- Light surfaces are allowed ONLY through the `.ulw-window` light adapter block. Both mounted windows (demo + Team Mode mock) are fixed to the dark elevated theme — a full-white pane on the near-black canvas is glare — so the light block is currently a defined-but-unmounted sanctioned surface, never the page. Everything else sits on the dark canvas. Code blocks (`pre`), command surfaces (`CommandCodeSurface`), and the Hephaestus band (`ShowcaseSurface`) are slightly ELEVATED dark layers — a tonal lift plus a hairline ring, so they never vanish into the page. +- Raw colors belong in this file, `design-system.css`, or OG theme tokens. Component code references tokens or shared primitives. The sanctioned raw values in components are: `#16191e` (showcase band), `#1b1f24` (command code chip), `#15181d` (docs `pre`), `#dcfce7` (text on dark code chips), gradient stops `#86efac`/`#4ade80`/`#22c55e`, brand glow `rgba(74,222,128,0.16)`, card shadow `rgba(0,0,0,0.4)`, and the `white/10` hairline rings on elevated dark chips. ## 3. Typography @@ -67,7 +159,12 @@ LazyCodex feels like a serious command surface for complex codebases: near-black - Primary: `ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif` - Mono: `ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace` -- The landing wordmark intentionally uses the native primary stack so the LCP text has no webfont dependency. +- Display serif: `--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", serif` — declared in the `@theme` block; a SYSTEM stack, never a webfont (Lighthouse perf 100 depends on zero webfonts). + +### Rules + +- The serif stack is for section display headings only (via the `serif` option on `SectionHeading`), giving marketing bands the editorial voice. Body copy, UI chrome, cards, and docs prose stay on the sans stack. +- The landing wordmark/`h1` and hero lead intentionally stay on the native sans stack so the LCP text has no font-substitution or reflow risk. ## 4. Spacing & Layout @@ -83,49 +180,101 @@ All spacing resolves to a 4px rhythm. Existing Tailwind values map to the same r - Docs collapse: hide the ToC below `1100px`; single column and mobile menu below `768px`. - Full-height surfaces use `min-h-[100dvh]`, never `h-screen`. +### Dotted rule grid + +- The `.rule-grid-dotted` utility applies `border-left: 1px dotted var(--border-subtle)` to child columns — the editorial vertical column rule of the LazyCodex identity (the rule color flips with the token: hairline white on the dark canvas). +- Apply it through `MarketingRuleGrid` with `ruleStyle="dotted"` on multi-column marketing bands that need column separation without card chrome. Solid rules remain the default (`ruleStyle="solid"`). +- Dotted rules never appear inside the demo window or the docs layout. + ### Rules - `MarketingContainer`, `MarketingSection`, and `MarketingRuleGrid` in `components/design-system/layout.tsx` own the repeated page width and split-section geometry. - Use CSS Grid for multi-column layouts. Avoid percentage flex math. -- Preserve the existing information architecture: landing first, docs as a single richly-sectioned page. +- Landing IA, top to bottom: header → compact hero → `#ulw-demo` (interactive demo directly under the hero) → install → command cards → feature workflows (+ built-in skills band) → team mode → ulw-research → Hephaestus (+ OmO intro) → docs CTA → footer. Docs remain a single richly-sectioned page. +- Marketing sections must never wrap an `h2` in `<article>` — `e2e/landing.spec.ts` asserts `article h2` equals exactly the command-card names. ## 5. Components ### BrandMark - **Source**: `components/design-system/brand-mark.tsx`. -- **Structure**: inline SVG rounded square, `L` stroke, mint/green dot. -- **Variants**: `nav` 24px geometry, `hero` 160px geometry with `HeroBrandMark` glow wrapper. +- **Structure**: inline SVG rounded square, `L` stroke, mint/green dot; dark tile fill `var(--card-base)`, stroke `var(--accent-primary)`. +- **Variants**: `nav` 24px geometry, `hero` 160px geometry with `HeroBrandMark` glow wrapper (soft `rgba(74,222,128,0.16)` glow tuned for the dark canvas). - **States**: inherited from the containing link or surface. - **Accessibility**: decorative mark uses `aria-hidden`; header link owns the accessible label. ### Layout Primitives - **Source**: `components/design-system/layout.tsx`. -- **Components**: `PageShell`, `SkipLink`, `MarketingMain`, `MarketingContainer`, `MarketingSection`, `MarketingRuleGrid`. +- **Components**: `PageShell`, `SkipLink`, `MarketingMain`, `MarketingContainer`, `MarketingSection`, `MarketingRuleGrid` (with the `ruleStyle: "solid" | "dotted"` variant). - **Usage**: pages and repeated landing bands. They preserve the current DOM semantics while centralizing width, `dvh`, and split-grid rules. ### Typography Primitives - **Source**: `components/design-system/typography.tsx`. -- **Components**: `Kicker`, `SectionHeading`, `BodyText`, `GradientTitle`, `AccentBadge`, `InlineCode`. -- **Usage**: marketing sections, showcase titles, badges, and command/code snippets. +- **Components**: `Kicker`, `SectionHeading` (with the serif display option), `BodyText`, `GradientTitle`, `AccentBadge`, `CardLabel` (mono uppercase card sub-heading, `tone: "default" | "accent"`), `InlineCode`. +- **Usage**: marketing sections, showcase titles, badges, card labels, and command/code snippets. `GradientTitle` uses the dark-legible green gradient (`#86efac → #4ade80 → #22c55e`). - **Motion**: typography itself does not animate; reveal behavior remains in CSS utilities. ### Surface Primitives - **Source**: `components/design-system/surfaces.tsx`. -- **Components**: `SurfaceCard`, `AccentSurface`, `ShowcaseSurface`, `CommandCodeSurface`, `IconWell`, `FactList`, `CompactDotList`, `NumberedPoint`. -- **Usage**: command cards, OmO/Lazy comparison cards, Hephaestus and Ultrawork black showcases, numbered workflow rows. -- **Depth**: border plus tonal shift, with showcase shadows only where already present. +- **Components**: `SurfaceCard`, `AccentSurface` (polymorphic `as: "div" | "li"` + `padding` variant), `ShowcaseSurface`, `CommandCodeSurface`, `IconWell`, `FactList`, `CompactDotList`, `MonoTag` (mono chip `<li>` for lane/skill grids), `NumberedPoint`. +- **Usage**: elevated dark cards (`--card-base` + `--border-subtle` + soft black shadow) for command cards, comparison cards, and numbered workflow rows; `AccentSurface` also covers the demo's example-prompt chip and the Hephaestus loop tiles. `ShowcaseSurface` is a slightly elevated showcase band (`#16191e` + `white/10` ring) for the Hephaestus showcase; `CommandCodeSurface` is an elevated code chip (`#1b1f24` with `#dcfce7` text + `white/10` ring) — code surfaces read as distinct raised layers on the dark canvas, never dissolving into it. +- **Depth**: hairline border plus tonal lift, with showcase shadows only where already present. ### Action Primitives - **Source**: `components/design-system/actions.tsx`. - **Components**: `LinkAction`, `GlowActionFrame`. -- **Variants**: primary filled text button, secondary outlined button. +- **Variants**: primary is the token-inverted ink button (`--text-primary` fill, `--surface-base` text); secondary is outlined (`--border-default`, hover `--surface-1`). - **States**: hover scale or tonal shift, visible focus ring, no layout-property animation. +### CodexWindow (ulw-demo) + +- **Source**: `components/site/ulw-demo/codex-window.tsx` (client leaf), scene strings and the + derived replay timeline in `lib/ulw-demo-scenes.ts`. +- **Structure**: Codex Desktop window (adapter tokens above) on the dark canvas: sidebar with the + run's single constant session, per-session title bar with traffic lights, transcript pane, + right rail (Environment card, Subagents roster, narrative card), footer with the running line + and a decorative composer. +- **Replay model**: ONE appending chat replay — the user's ask renders as the opening message + bubble (`.ulw-app-user`), then `ULW_DEMO_TIMELINE` entries (mode flag, then per-phase status, + command, prose, tool-ledger rows, and JSON chips — every string derived verbatim from the 8 + grounded scenes) append one per tick (`ULW_DEMO_ENTRY_MS = 900`). Earlier entries persist and + the transcript follows the newest entry via INNER scroll only. At the finished checkpoint the + replay rests 4s, then loops back to the server-rendered opening state + (`ULW_DEMO_INITIAL_ENTRIES = 4`). +- **Window theme**: FIXED dark (`data-window-theme="dark"` hardcoded). There is no theme toggle + and no in-window control of any kind — zero `<button>`s inside `.ulw-window` (e2e-asserted). +- **Footer**: the app's running line ("Working for <elapsed>" with `.ulw-spinner`) plus a + `.ulw-run-progress` track that fills by phase — a run indicator, never a playback control. +- **States**: replay arms on scroll-into-view (IntersectionObserver, one-shot, 0.2 threshold); + per-lane `data-live` on the roster; `data-window-theme` on the window. +- **Accessibility**: non-playable by design; decorative regions (composer, spinner, run-progress) + are `aria-hidden`; the transcript is a labeled region; the opening entries are server-rendered + so content is readable with JS disabled. +- **Layout stability**: `.ulw-window` is a FIXED 680px box (560px at ≤ 768px); the appending + transcript scrolls internally so the outer box never changes (e2e asserts ≤ 1px drift while + entries append); panes stack single-column at ≤ 768px. +- **Integrity**: live DOM only — no raster screenshot, `<img>`, or `background-image` may stand in + for window content. +- **Roster glyph colors**: the subagent glyph squares use the per-theme `--lane-*` identity hues + (see § Subagent lane glyph tokens) faithful to the Codex Desktop reference. They are scoped to + the window adapter and are identity badges, not brand accents — the green-only brand rule applies + outside the window. + +### TeamModeSection / UlwResearchSection + +- **Source**: `components/site/team-mode-section.tsx`, `components/site/ulw-research-section.tsx`; + copy constants in `lib/site-config.ts`. +- **Structure**: TeamMode shows a leader thread plus member thread cards (window chrome via the same + adapter tokens, on the dark canvas) with a `Sent by Codex from another thread` note bubble; + UlwResearch is a compact feature band composed from existing surface primitives. +- **Copy rule**: every visible string traces to `plugins/omo/skills/teammode/SKILL.md`, + `plugins/omo/skills/ulw-research/SKILL.md`, or `content/docs/*.md` via the copy ledger — no + invented claims, metrics, customers, or dates. + ### DocsHero - **Source**: `components/design-system/docs-hero.tsx`. @@ -148,23 +297,44 @@ All spacing resolves to a 4px rhythm. Existing Tailwind values map to the same r - Respect `prefers-reduced-motion`; `splash-reveal` disables itself. - Focus states are visible through global `:focus-visible` and component-level rings. - Docs interactions must keep working: mobile menu, sidebar search, Cmd/Ctrl-K focus, hash navigation, scroll-spy, and prev/next cards. +- **Hover is an affordance, not decoration.** Hover feedback may exist only on elements with a real + action (links, buttons, tabs, inputs). A hover effect on a non-actionable element — cards, list + rows, headings, chips, roster rows — is a defect and must be removed. + +### ulw-demo timeline + +- Appended entries flow in via `opacity`/`transform` only (`.ulw-entry`, 420ms); the footer + run-progress track fills by `transform: scaleX` per phase. No layout-property animation + anywhere in the window. +- The replay arms when the demo scrolls into view (IntersectionObserver, one-shot at 0.2 + threshold), appends one entry per `ULW_DEMO_ENTRY_MS = 900`, rests 4s on the finished + checkpoint (`LOOP_REST_MS` in `codex-window.tsx`), then loops back to the opening ask. +- `prefers-reduced-motion: reduce` disables the replay entirely: the COMPLETED transcript renders + statically, nothing appends afterwards, and the entry flow-in animation is off. +- The transcript follows the newest entry via inner `scrollTo` (smooth; `auto` under reduced + motion) — the window's outer box never moves. ## 7. Depth & Surface ### Strategy -LazyCodex uses a mixed but constrained depth strategy: tonal-shift panels and borders for everyday surfaces, plus existing deep showcase shadows for the landing hero and black showcase panels. +LazyCodex uses a mixed but constrained depth strategy: elevated dark panels with hairline borders +and soft black shadows for everyday surfaces on the graphite canvas, plus the deliberate LIGHT +Codex window where real product chrome is shown. | Level | Treatment | Usage | | --- | --- | --- | -| Canvas | `--surface-base` | Whole site background | -| Panel | `--surface-panel` or `bg-white/[0.03]` with subtle border | Cards, install bar, docs input | +| Canvas | `--surface-base` (`#0a0b0d`) | Whole site background | +| Panel | `--card-base` / `--surface-panel` with `--border-subtle` and `rgba(0,0,0,0.4)` shadow | Cards, install bar, docs input | | Accent panel | `--accent-primary` soft fill and border | Built-in skills, Lazy comparison, workflow code | -| Showcase | black surface, ring, green radial glow, shadow | Hephaestus and Ultrawork feature blocks | -| Hero | `--card-base`, layered `.card-gradient-*`, large shadow | Landing hero card | +| Elevated dark chip | `#16191e` / `#1b1f24` / `#15181d` surface + `white/10` ring, light-on-dark text | Code blocks, command surfaces, Hephaestus showcase, demo window dark theme | +| Light accent | `.ulw-window` light adapter theme (`#ffffff` window on the dark ground) | Sanctioned but currently unmounted (both windows fixed dark) | +| Hero | open canvas — display type directly on `--surface-base` over the `.glow-backdrop` atmosphere | Landing hero | ### Rules -- Do not add generic white cards or purple-blue gradients. -- Do not replace the hero or brand mark with raster screenshots. +- The hero sits directly on the open canvas; its only glow is the shared `.glow-backdrop` atmosphere plus the brand-mark's `rgba(74,222,128,0.16)` halo — low alpha only, no light stops anywhere. +- Light surfaces may appear only through the `.ulw-window` light adapter theme (currently unmounted); the page canvas is always dark. Every other raised layer is an elevated dark surface: tonal lift + hairline ring. +- Do not add purple-blue gradients; green is the only brand hue. +- Do not replace the hero, the brand mark, or the demo window content with raster screenshots. - If a component pattern appears twice, it belongs in `components/design-system/` and this section. diff --git a/packages/web/app/globals.css b/packages/web/app/globals.css index 6eb9f03..3d51181 100644 --- a/packages/web/app/globals.css +++ b/packages/web/app/globals.css @@ -1,3 +1,4 @@ @import "tailwindcss"; @import "./styles/design-system.css"; @import "./styles/landing.css"; +@import "./styles/ulw-demo.css"; diff --git a/packages/web/app/icon.svg b/packages/web/app/icon.svg index 1263162..388ab07 100644 --- a/packages/web/app/icon.svg +++ b/packages/web/app/icon.svg @@ -3,10 +3,10 @@ <defs> <linearGradient id="lc-green" x1="0" y1="0" x2="1" y2="1"> <stop offset="0%" stop-color="#4ade80" /> - <stop offset="100%" stop-color="#16a34a" /> + <stop offset="100%" stop-color="#22c55e" /> </linearGradient> </defs> - <rect x="28" y="28" width="200" height="200" rx="48" fill="#0E1411" stroke="url(#lc-green)" stroke-width="14" /> + <rect x="28" y="28" width="200" height="200" rx="48" fill="#16181c" stroke="url(#lc-green)" stroke-width="14" /> <path d="M88 96 V160 H152" fill="none" stroke="url(#lc-green)" stroke-width="20" stroke-linecap="round" stroke-linejoin="round" /> - <circle cx="168" cy="96" r="18" fill="#86efac" /> + <circle cx="168" cy="96" r="18" fill="#4ade80" /> </svg> diff --git a/packages/web/app/layout.tsx b/packages/web/app/layout.tsx index 7d4ac73..39121e2 100644 --- a/packages/web/app/layout.tsx +++ b/packages/web/app/layout.tsx @@ -12,7 +12,7 @@ const DESCRIPTION = export const viewport: Viewport = { width: "device-width", initialScale: 1, - themeColor: "#22c55e", + themeColor: "#0e1012", colorScheme: "dark", } diff --git a/packages/web/app/manifest.ts b/packages/web/app/manifest.ts index 0643780..b410bda 100644 --- a/packages/web/app/manifest.ts +++ b/packages/web/app/manifest.ts @@ -7,8 +7,8 @@ export default function manifest(): MetadataRoute.Manifest { description: "Agent harness for complex codebases inside Codex.", start_url: "/", display: "standalone", - background_color: "#0a0a0a", - theme_color: "#22c55e", + background_color: "#0e1012", + theme_color: "#0e1012", // No icons array: the browser favicon comes from the app/icon.svg file // convention; duplicating it here triggers a second eager favicon fetch // that lands on the Lantern LCP critical path. PWA installability isn't a diff --git a/packages/web/app/og-image-theme.ts b/packages/web/app/og-image-theme.ts index 74051c7..e3a8ed8 100644 --- a/packages/web/app/og-image-theme.ts +++ b/packages/web/app/og-image-theme.ts @@ -1,29 +1,29 @@ export const OG_SIZE = { width: 1200, height: 630 } as const export const OG_PALETTE = { - surfaceBase: "#0a0c0b", - cardBase: "#0E1411", + surfaceBase: "#0e1012", + cardBase: "#16181c", brandCore: "#22c55e", brandMid: "#16a34a", brandOuter: "#15803d", - textPrimary: "#ffffff", - textSecondary: "#b8c2bc", - textSoft: "#dcfce7", - textMuted: "rgba(255, 255, 255, 0.74)", + textPrimary: "#f7f8f8", + textSecondary: "#b4bcc8", + textSoft: "#86efac", + textMuted: "rgba(247, 248, 248, 0.78)", accentCyan: "#4ade80", - accentGlow: "#86efac", - border: "rgba(255, 255, 255, 0.08)", + accentGlow: "#166534", + border: "rgba(255, 255, 255, 0.12)", } as const export const OG_GRADIENTS = { - base: `radial-gradient(120% 100% at 60% 65%, ${OG_PALETTE.brandCore} 0%, ${OG_PALETTE.brandMid} 35%, ${OG_PALETTE.brandOuter} 70%, ${OG_PALETTE.surfaceBase} 100%)`, + base: `radial-gradient(120% 100% at 60% 65%, rgba(74, 222, 128, 0.10) 0%, rgba(34, 197, 94, 0.06) 35%, rgba(34, 197, 94, 0.03) 70%, rgba(14, 16, 18, 0) 100%)`, beam: - "radial-gradient(55% 55% at 38% -8%, rgba(134,239,172,0.55) 0%, rgba(74,222,128,0.22) 35%, rgba(255,255,255,0) 65%), " + - "radial-gradient(32% 28% at 55% -5%, rgba(134,239,172,0.38) 0%, rgba(255,255,255,0) 70%)", + "radial-gradient(55% 55% at 38% -8%, rgba(74,222,128,0.16) 0%, rgba(74,222,128,0.06) 35%, rgba(14,16,18,0) 65%), " + + "radial-gradient(32% 28% at 55% -5%, rgba(74,222,128,0.10) 0%, rgba(14,16,18,0) 70%)", sheen: - "linear-gradient(118deg, transparent 18%, rgba(134,239,172,0.16) 26%, rgba(134,239,172,0.30) 30%, rgba(134,239,172,0.12) 35%, transparent 45%), " + - "linear-gradient(112deg, transparent 8%, rgba(74,222,128,0.12) 15%, transparent 28%)", + "linear-gradient(118deg, transparent 18%, rgba(74,222,128,0.06) 26%, rgba(74,222,128,0.10) 30%, rgba(74,222,128,0.05) 35%, transparent 45%), " + + "linear-gradient(112deg, transparent 8%, rgba(34,197,94,0.05) 15%, transparent 28%)", pools: - "radial-gradient(55% 50% at 8% 95%, rgba(34,197,94,0.26), transparent 70%), " + - "radial-gradient(45% 45% at 95% 40%, rgba(134,239,172,0.20), transparent 70%)", + "radial-gradient(55% 50% at 8% 95%, rgba(34,197,94,0.10), transparent 70%), " + + "radial-gradient(45% 45% at 95% 40%, rgba(74,222,128,0.08), transparent 70%)", } as const diff --git a/packages/web/app/page.tsx b/packages/web/app/page.tsx index ba9c8d1..29a4b7a 100644 --- a/packages/web/app/page.tsx +++ b/packages/web/app/page.tsx @@ -13,7 +13,10 @@ import { Hero } from "../components/site/hero" import { InstallBlock } from "../components/site/install-block" import { SiteFooter } from "../components/site/site-footer" import { SiteHeader } from "../components/site/site-header" +import { TeamModeSection } from "../components/site/team-mode-section" import { UltraworkSection } from "../components/site/ultrawork-section" +import { UlwDemoSection } from "../components/site/ulw-demo/ulw-demo-section" +import { UlwResearchSection } from "../components/site/ulw-research-section" export default function LandingPage(): JSX.Element { return ( @@ -26,9 +29,12 @@ export default function LandingPage(): JSX.Element { <MarketingContainer> <Hero /> </MarketingContainer> + <UlwDemoSection /> <InstallBlock /> <CommandCards /> <FeatureWorkflowsSection /> + <TeamModeSection /> + <UlwResearchSection /> <HephaestusSection /> <UltraworkSection /> <DocsCta /> diff --git a/packages/web/app/styles/design-system.css b/packages/web/app/styles/design-system.css index 7b554c9..7c3c444 100644 --- a/packages/web/app/styles/design-system.css +++ b/packages/web/app/styles/design-system.css @@ -6,51 +6,54 @@ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; + /* Editorial serif for section display headings ONLY (DESIGN.md §3). + System ui-serif stack — still zero webfonts. */ + --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", serif; } @layer base { :root { - /* Surfaces — near-black canvas with a faint cool-green undertone. */ - --surface-base: #0a0c0b; - --surface-0: #0a0c0b; - --surface-1: rgba(255, 255, 255, 0.018); - --surface-2: rgba(255, 255, 255, 0.035); - --surface-3: rgba(255, 255, 255, 0.055); - --card-base: #0E1411; - --surface-night: #0a0c0b; - --surface-panel: #0E1411; - --surface-panel-alt: #0C1310; - --surface-panel-deep: #0D1310; + /* Surfaces — near-black canvas where content emerges through luminance + (Linear-grammar darkness as the native medium), with the emerald + atmosphere restored by the .glow-backdrop layer below. */ + --surface-base: #0a0b0d; + --surface-0: #0a0b0d; + --surface-1: rgba(255, 255, 255, 0.04); + --surface-2: rgba(255, 255, 255, 0.06); + --card-base: #15171b; + --surface-night: #07080a; + --surface-panel: #101216; /* Brand — green core (green-500 family), clearly green not teal. */ --brand-core: #22c55e; --brand-mid: #16a34a; --brand-outer: #15803d; - /* Text. */ - --text-primary: #ffffff; - --text-secondary: #b8c2bc; - --text-tertiary: #8b9690; - --text-muted: rgba(255, 255, 255, 0.74); - --text-soft: #dcfce7; + /* Text — near-white inks, AA on the graphite canvas AND on --card-base. */ + --text-primary: #f7f8f8; + --text-secondary: #b4bcc8; + --text-tertiary: #98a1ab; + --text-muted: rgba(247, 248, 248, 0.78); + --text-soft: #86efac; - /* Accent — the live-wire green (green-400/300, unambiguously green). */ + /* Accent — green-400 on dark ground: AA (>= 4.5:1) on the canvas, on + --card-base, and on the elevated code chips (#1b1f24 family). + --accent-mint stays a fill/decoration hue by rule, though it is also + AA-legible on every dark surface here. */ --accent-primary: #4ade80; --accent-primary-soft: rgba(74, 222, 128, 0.1); - --accent-primary-border: rgba(74, 222, 128, 0.24); - --accent-cyan: #4ade80; - --accent-teal: #22c55e; + --accent-primary-border: rgba(74, 222, 128, 0.32); --accent-mint: #86efac; - --accent-glow: #86efac; + --accent-glow: #bbf7d0; - /* Borders. */ - --border-subtle: rgba(255, 255, 255, 0.06); - --border-default: rgba(255, 255, 255, 0.1); + /* Borders — hairline white washes. */ + --border-subtle: rgba(255, 255, 255, 0.08); + --border-default: rgba(255, 255, 255, 0.12); - /* Status. */ - --status-success: #22c55e; - --status-warning: #f59e0b; - --status-error: #ef4444; + /* Status — AA on dark. */ + --status-success: #4ade80; + --status-warning: #fbbf24; + --status-error: #f87171; } html { @@ -68,8 +71,8 @@ } ::selection { - background-color: var(--brand-outer); - color: var(--text-primary); + background-color: #14532d; + color: #dcfce7; } a { @@ -81,30 +84,96 @@ outline: 2px solid var(--accent-primary); outline-offset: 3px; } -} -@layer utilities { - .card-gradient-base { - background: radial-gradient(120% 100% at 60% 65%, #22c55e 0%, #16a34a 35%, #15803d 70%, #0a0c0b 100%); + /* Codex window adapter — the surface tokens the ulw-demo / team-mode + window mocks consume (see DESIGN.md § Codex window adapter tokens). + Default theme: LIGHT, faithful to the Codex desktop app frames — the + light window on the dark canvas is the page's hero contrast. */ + .ulw-window { + --codex-window-bg: #ffffff; + --codex-window-chrome: #f6f7f6; + --codex-window-border: rgba(10, 12, 11, 0.12); + --codex-window-text: #17211b; + --codex-window-text-soft: #5b675f; + --codex-window-chip: rgba(10, 12, 11, 0.06); + --codex-window-active: rgba(34, 197, 94, 0.12); + --codex-window-active-border: rgba(22, 101, 52, 0.28); + --codex-window-accent: #166534; + --codex-window-glyph-text: #ffffff; + --codex-window-traffic-red: #f87171; + --codex-window-traffic-amber: #fbbf24; + --codex-window-traffic-green: #34d399; + + /* Subagent lane-glyph identity colors — tuned for the white window + (each holds >= 3:1 against --codex-window-bg; the white glyph + letter stays >= 4.5:1 on every lane). */ + --lane-root: #115e59; + --lane-explore: #1d4ed8; + --lane-library: #92400e; + --lane-plan: #6d28d9; + --lane-todo: #334155; + --lane-execute: #166534; + --lane-test: #b91c1c; + --lane-qa: #be185d; + --lane-review: #4338ca; + --lane-continuation: #475569; } - .card-gradient-beam { - background: radial-gradient(55% 55% at 38% -8%, rgba(134,239,172,0.55) 0%, rgba(74,222,128,0.22) 35%, rgba(255,255,255,0) 65%), - radial-gradient(32% 28% at 55% -5%, rgba(134,239,172,0.38) 0%, rgba(255,255,255,0) 70%); - mix-blend-mode: screen; + /* Dark window variant — toggled via the role=group window-theme switch + (sets data-window-theme on .ulw-window). Slightly LIGHTER than the page + canvas (#0e1012) with a stronger hairline ring so the window still reads + as a distinct elevated layer instead of dissolving into the page. */ + [data-window-theme="dark"] { + --codex-window-bg: #1a1d22; + --codex-window-chrome: #15181c; + --codex-window-border: rgba(255, 255, 255, 0.18); + --codex-window-text: #eef1f4; + --codex-window-text-soft: #a9b2bd; + --codex-window-chip: rgba(255, 255, 255, 0.07); + --codex-window-active: rgba(74, 222, 128, 0.16); + --codex-window-active-border: rgba(74, 222, 128, 0.38); + --codex-window-accent: #4ade80; + --codex-window-glyph-text: #0a0c0b; + --codex-window-traffic-red: #f87171; + --codex-window-traffic-amber: #fbbf24; + --codex-window-traffic-green: #34d399; + + /* Lane glyphs re-tuned >= 3:1 against the elevated dark window ground + (same hue identity per lane, lifted two Tailwind stops). */ + --lane-root: #2dd4bf; + --lane-explore: #60a5fa; + --lane-library: #f59e0b; + --lane-plan: #a78bfa; + --lane-todo: #94a3b8; + --lane-execute: #4ade80; + --lane-test: #f87171; + --lane-qa: #f472b6; + --lane-review: #818cf8; + --lane-continuation: #cbd5e1; } +} - .card-gradient-sheen { - background: linear-gradient(118deg, transparent 18%, rgba(134,239,172,0.16) 26%, rgba(134,239,172,0.30) 30%, rgba(134,239,172,0.12) 35%, transparent 45%), - linear-gradient(112deg, transparent 8%, rgba(74,222,128,0.12) 15%, transparent 28%); - filter: blur(20px); - mix-blend-mode: screen; - opacity: 0.85; +@layer utilities { + /* ampcode-style dotted vertical column rules: every column after the + first gets a dotted left rule (MarketingRuleGrid ruleStyle="dotted"). + The rule color flips with --border-subtle (hairline white on dark). */ + .rule-grid-dotted > * + * { + border-left: 1px dotted var(--border-subtle); } - .card-gradient-pools { - background: radial-gradient(55% 50% at 8% 95%, rgba(34,197,94,0.26), transparent 70%), - radial-gradient(45% 45% at 95% 40%, rgba(134,239,172,0.20), transparent 70%); - mix-blend-mode: screen; + /* Page atmosphere — the emerald-lit darkness of the original identity. + Four static radial washes on an absolutely-positioned backdrop + (PageShell): the hero aurora (peak alpha 0.17), a faint right-side pool + near the demo, a mid-page pool, and a low anchor near the Hephaestus + band. At the aurora peak the canvas composites from #0a0b0d to roughly + #0e2b1b; text-primary still measures 14.3:1 there (18.5:1 on the pure + canvas), so every AA-checked pair keeps a wide margin. No animation: + zero paint cost after first frame. */ + .glow-backdrop { + background: + radial-gradient(72rem 30rem at 50% 4rem, rgba(34, 197, 94, 0.17), rgba(34, 197, 94, 0.06) 45%, transparent 68%), + radial-gradient(50rem 26rem at 78% 26rem, rgba(74, 222, 128, 0.07), transparent 66%), + radial-gradient(70rem 42rem at 10% 118rem, rgba(22, 163, 74, 0.09), transparent 68%), + radial-gradient(84rem 48rem at 70% 100%, rgba(34, 197, 94, 0.06), transparent 70%); } } diff --git a/packages/web/app/styles/docs.css b/packages/web/app/styles/docs.css index 31c8380..a12c8b9 100644 --- a/packages/web/app/styles/docs.css +++ b/packages/web/app/styles/docs.css @@ -303,14 +303,16 @@ .docs-content code { font-family: var(--font-mono); background: var(--surface-2); - color: var(--accent-mint); + color: var(--text-soft); padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.85em; } +/* Code blocks blend with the dark docs surface as slightly elevated + panels — hairline border keeps them a distinct layer. */ .docs-content pre { - background: #0c100e; + background: #15181d; border: 1px solid var(--border-subtle); padding: 1.1rem 1.25rem; border-radius: 10px; diff --git a/packages/web/app/styles/ulw-demo-app.css b/packages/web/app/styles/ulw-demo-app.css new file mode 100644 index 0000000..b4ff002 --- /dev/null +++ b/packages/web/app/styles/ulw-demo-app.css @@ -0,0 +1,168 @@ +/* App-shell chrome for the Ultrawork demo — the .ulw-app-* layer built to + the real Codex desktop anatomy (.omo/reference/app-frames/creation-03.png): + sidebar with the run's single session, per-session title bar, transcript + document flow, decorative composer. The demo is a staged recording with no + controls and no progress bar — it simply looks like a session in progress. + Split from ulw-demo.css to keep files under the size limit. + Motion: transform/opacity/color only. Zero outer layout shift: the + window box is fixed-height and the transcript scrolls internally. */ + +@layer components { + .ulw-window.ulw-app { + max-width: 1120px; + /* FIXED height: the appending transcript scrolls inside; the outer box + never grows (zero layout shift is a binding contract). */ + height: 680px; + display: flex; + flex-direction: column; + } + + .ulw-app-frame { + flex: 1; + display: grid; + grid-template-columns: 212px minmax(0, 1fr) 264px; + min-height: 0; + } + + /* ---- Left sidebar: traffic lights, nav list, Pinned/Projects groups. */ + + .ulw-app-sidebar { + display: flex; + flex-direction: column; + gap: 14px; + padding: 14px 10px; + background: var(--codex-window-chrome); + border-right: 1px solid var(--codex-window-border); + min-width: 0; + overflow: hidden; + } + + .ulw-app-sidebar .ulw-traffic { + padding-left: 4px; + } + + .ulw-app-nav, + .ulw-app-group { + display: flex; + flex-direction: column; + gap: 1px; + } + + .ulw-app-row { + display: flex; + align-items: center; + gap: 8px; + padding: 5px 8px; + border-radius: 7px; + font-size: 12px; + color: var(--codex-window-text-soft); + white-space: nowrap; + overflow: hidden; + } + + .ulw-app-group-label { + padding: 6px 8px 2px; + font-size: 11px; + color: var(--codex-window-text-soft); + } + + /* The ONE session of the run — static (not navigation; no hover): the + whole demo is a single goal being pursued, so the sidebar never changes. */ + .ulw-app-session { + display: flex; + align-items: center; + gap: 7px; + width: 100%; + text-align: left; + padding: 5px 8px 5px 14px; + border-radius: 7px; + font-size: 12px; + color: var(--codex-window-text-soft); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .ulw-app-session[aria-current="true"] { + background: var(--codex-window-active); + color: var(--codex-window-text); + font-weight: 600; + } + + /* Running-session spinner — the app's alive indicator. Transform-only + rotation (compositor-safe); reduced motion freezes it into a static + ring so the marker survives without movement. */ + .ulw-spinner { + flex-shrink: 0; + width: 9px; + height: 9px; + border-radius: 999px; + border: 1.5px solid var(--codex-window-border); + border-top-color: var(--codex-window-accent); + animation: ulw-spin 900ms linear infinite; + } + + @keyframes ulw-spin { + to { + transform: rotate(360deg); + } + } + + @media (prefers-reduced-motion: reduce) { + .ulw-spinner { + animation: none; + } + } + + .ulw-app-showmore { + padding-left: 30px; + } + + .ulw-app-main { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + } + + .ulw-app-titlebar { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 16px; + border-bottom: 1px solid var(--codex-window-border); + flex-shrink: 0; + } + + .ulw-app-title { + font-size: 13px; + font-weight: 600; + color: var(--codex-window-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .ulw-app-title-dots { + color: var(--codex-window-text-soft); + } + + .ulw-icon { + flex-shrink: 0; + } + + @media (max-width: 768px) { + .ulw-window.ulw-app { + height: 560px; + } + + .ulw-app-frame { + grid-template-columns: 1fr; + } + + .ulw-app-sidebar { + display: none; + } + } + +} diff --git a/packages/web/app/styles/ulw-demo-panel.css b/packages/web/app/styles/ulw-demo-panel.css new file mode 100644 index 0000000..078074f --- /dev/null +++ b/packages/web/app/styles/ulw-demo-panel.css @@ -0,0 +1,143 @@ +/* Right-hand panel for the interactive Ultrawork demo window — Environment + and Subagents groups styled after the app frames (subagents-03.png and + the desktop-app reference). Keeps the full 13-worker roster: delegation + made observable is the differentiator. Imported by ulw-demo.css (split + for the file-size limit). */ + +@layer components { + .ulw-side { + border-left: 1px solid var(--codex-window-border); + background: color-mix(in srgb, var(--codex-window-chrome) 55%, var(--codex-window-bg)); + padding: 16px 14px; + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; + } + + .ulw-side-card { + border: 1px solid var(--codex-window-border); + border-radius: 10px; + background: var(--codex-window-bg); + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 6px; + } + + .ulw-side-heading { + font-family: var(--font-mono); + font-size: 10.5px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--codex-window-text-soft); + } + + .ulw-side-row { + display: flex; + justify-content: space-between; + gap: 8px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--codex-window-text); + } + + .ulw-side-row span:last-child { + color: var(--codex-window-text-soft); + } + + .ulw-workers { + display: flex; + flex-direction: column; + gap: 3px; + } + + .ulw-worker { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + border-radius: 7px; + font-size: 11.5px; + color: var(--codex-window-text-soft); + transition: color 200ms ease, background-color 200ms ease; + } + + .ulw-worker[data-live="true"] { + background: var(--codex-window-active); + color: var(--codex-window-text); + font-weight: 500; + } + + .ulw-worker-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .ulw-worker small { + font-family: var(--font-mono); + font-size: 9.5px; + color: var(--codex-window-text-soft); + white-space: nowrap; + } + + .ulw-worker-glyph { + width: 16px; + height: 16px; + border-radius: 4px; + display: inline-flex; + align-items: center; + justify-content: center; + font-family: var(--font-mono); + font-size: 9px; + font-weight: 700; + color: var(--codex-window-glyph-text); + flex-shrink: 0; + } + + /* Lane identity colors are per-window-theme custom props defined next to + the --codex-window-* adapter blocks (design-system.css). */ + .ulw-worker-glyph[data-lane="root"] { background: var(--lane-root); } + .ulw-worker-glyph[data-lane="explore"] { background: var(--lane-explore); } + .ulw-worker-glyph[data-lane="library"] { background: var(--lane-library); } + .ulw-worker-glyph[data-lane="plan"] { background: var(--lane-plan); } + .ulw-worker-glyph[data-lane="todo"] { background: var(--lane-todo); } + .ulw-worker-glyph[data-lane="execute"] { background: var(--lane-execute); } + .ulw-worker-glyph[data-lane="test"] { background: var(--lane-test); } + .ulw-worker-glyph[data-lane="qa"] { background: var(--lane-qa); } + .ulw-worker-glyph[data-lane="review"] { background: var(--lane-review); } + .ulw-worker-glyph[data-lane="continuation"] { background: var(--lane-continuation); } + + .ulw-app-side-note { + /* Reserve for the tallest sideTitle+sideBody pair (zero layout shift): + the tallest measured scene is ~112px at the 264px desktop column. */ + min-height: 126px; + } + + .ulw-app-side-note strong { + font-size: 12px; + font-weight: 600; + } + + .ulw-app-side-note span { + font-size: 11px; + line-height: 1.45; + color: var(--codex-window-text-soft); + } + + @media (max-width: 768px) { + .ulw-side { + border-left: none; + border-top: 1px solid var(--codex-window-border); + } + } + + @media (prefers-reduced-motion: reduce) { + .ulw-worker { + transition: none; + } + } +} diff --git a/packages/web/app/styles/ulw-demo-transcript.css b/packages/web/app/styles/ulw-demo-transcript.css new file mode 100644 index 0000000..15a1c49 --- /dev/null +++ b/packages/web/app/styles/ulw-demo-transcript.css @@ -0,0 +1,225 @@ +/* Transcript + footer layer for the interactive Ultrawork demo window — + document flow faithful to the app frames (creation-03.png): user command + bubble, prose, tool-activity rows, inline code chip, then the Step pill, + Pursuing-goal row, and the decorative composer. Imported by ulw-demo.css + (split for the file-size limit). */ + +@layer components { + /* ---- Transcript document flow. */ + + .ulw-app-transcript { + flex: 1; + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px 22px 8px; + min-height: 0; + /* The replay APPENDS: the transcript scrolls internally while the + window's outer box never moves. */ + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + } + + .ulw-app-user { + align-self: flex-end; + max-width: 92%; + min-width: 0; + background: var(--codex-window-chip); + border-radius: 12px; + padding: 8px 12px; + } + + .ulw-app-user code { + display: block; + font-family: var(--font-mono); + font-size: 11.5px; + white-space: nowrap; + /* Horizontal scroll is allowed ONLY inside code rows. */ + overflow-x: auto; + } + + .ulw-app-tool { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--codex-window-text-soft); + } + + .ulw-app-tool span { + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .ulw-app-code { + background: var(--codex-window-chip); + border-radius: 8px; + padding: 7px 10px; + min-width: 0; + } + + .ulw-app-code code { + display: block; + font-family: var(--font-mono); + font-size: 11px; + color: var(--codex-window-text-soft); + white-space: nowrap; + /* Horizontal scroll is allowed ONLY inside code rows. */ + overflow-x: auto; + } + + /* ---- Footer: Step pill, Pursuing-goal row, decorative composer. */ + + .ulw-app-footer { + display: flex; + flex-direction: column; + gap: 10px; + padding: 4px 22px 16px; + flex-shrink: 0; + } + + .ulw-app-working { + display: flex; + align-items: center; + gap: 10px; + } + + .ulw-app-step { + display: inline-flex; + align-items: center; + gap: 7px; + font-family: var(--font-mono); + font-size: 10.5px; + padding: 3px 10px; + border-radius: 999px; + border: 1px solid var(--codex-window-border); + color: var(--codex-window-text-soft); + white-space: nowrap; + } + + /* Run progress — how far through the goal the recording is. The fill is + scaleX-driven (compositor-safe) and eases forward on each scene, so the + run visibly accumulates instead of the screen merely switching. */ + .ulw-run-progress { + flex: 1; + height: 3px; + border-radius: 999px; + background: var(--codex-window-chip); + overflow: hidden; + } + + .ulw-run-progress span { + display: block; + height: 100%; + border-radius: 999px; + background: var(--codex-window-accent); + transform-origin: left center; + transition: transform 600ms cubic-bezier(0.22, 1, 0.36, 1); + } + + /* Scene continuity: new transcript content flows up into place, reading + as the session carrying on rather than a slide swap. */ + .ulw-entry { + animation: ulw-flow-in 420ms ease-out; + } + + @keyframes ulw-flow-in { + from { + opacity: 0; + transform: translateY(10px); + } + } + + @media (prefers-reduced-motion: reduce) { + .ulw-entry { + animation: none; + } + + .ulw-run-progress span { + transition: none; + } + } + + .ulw-app-goal-elapsed { + margin-left: auto; + font-family: var(--font-mono); + font-size: 10.5px; + white-space: nowrap; + color: var(--codex-window-accent); + } + + .ulw-app-goal { + display: flex; + align-items: center; + gap: 8px; + border: 1px solid var(--codex-window-border); + border-radius: 10px; + padding: 7px 12px; + font-size: 11.5px; + color: var(--codex-window-text-soft); + } + + .ulw-app-goal strong { + font-weight: 600; + color: var(--codex-window-text); + white-space: nowrap; + } + + .ulw-app-goal span { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .ulw-app-composer { + border: 1px solid var(--codex-window-border); + border-radius: 14px; + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 10px; + background: var(--codex-window-bg); + } + + .ulw-app-composer-placeholder { + font-size: 13px; + color: var(--codex-window-text-soft); + } + + .ulw-app-composer-row { + display: flex; + align-items: center; + gap: 10px; + } + + .ulw-app-composer-chip { + display: inline-flex; + align-items: center; + gap: 5px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--codex-window-text-soft); + white-space: nowrap; + } + + .ulw-app-composer-grow { + flex: 1; + } + + .ulw-app-composer-send { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 999px; + background: var(--codex-window-text); + color: var(--codex-window-bg); + } +} diff --git a/packages/web/app/styles/ulw-demo.css b/packages/web/app/styles/ulw-demo.css new file mode 100644 index 0000000..d7e5759 --- /dev/null +++ b/packages/web/app/styles/ulw-demo.css @@ -0,0 +1,116 @@ +/* Interactive Ultrawork demo — Codex Desktop window on the site canvas, + themed light (default, faithful to the app frames) or dark via + data-window-theme on .ulw-window. Tokens: the --codex-window-* adapter + palette (DESIGN.md § Codex window adapter). The .ulw-app-* window-shell + layer lives in ulw-demo-app.css (split for the file-size limit). + .ulw-window/.ulw-titlebar/.ulw-traffic/.ulw-window-tab(s) below are ALSO + used by team-mode-section.tsx — keep them stable. */ + +@import "./ulw-demo-app.css"; +@import "./ulw-demo-transcript.css"; +@import "./ulw-demo-panel.css"; + +@layer components { + .ulw-window { + width: 100%; + max-width: 1060px; + min-height: 560px; + border-radius: 14px; + border: 1px solid var(--border-default); + background: var(--codex-window-bg); + color: var(--codex-window-text); + box-shadow: + 0 24px 70px rgba(16, 25, 20, 0.16), + 0 2px 8px rgba(16, 25, 20, 0.06); + overflow: hidden; + text-align: left; + } + + .ulw-titlebar { + display: flex; + align-items: center; + gap: 14px; + padding: 8px 14px; + background: var(--codex-window-chrome); + border-bottom: 1px solid var(--codex-window-border); + } + + .ulw-traffic { + display: inline-flex; + gap: 6px; + } + + .ulw-traffic span { + width: 10px; + height: 10px; + border-radius: 999px; + background: var(--codex-window-chip); + } + + .ulw-traffic span:nth-child(1) { background: var(--codex-window-traffic-red); } + .ulw-traffic span:nth-child(2) { background: var(--codex-window-traffic-amber); } + .ulw-traffic span:nth-child(3) { background: var(--codex-window-traffic-green); } + + .ulw-window-tabs { + display: flex; + gap: 8px; + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--codex-window-text-soft); + min-width: 0; + overflow: hidden; + } + + .ulw-window-tab { + padding: 4px 10px; + border-radius: 7px; + white-space: nowrap; + } + + .ulw-window-tab[data-current="true"] { + background: var(--codex-window-bg); + border: 1px solid var(--codex-window-border); + color: var(--codex-window-text); + } + + /* ---- Scene transcript content (strings from lib/ulw-demo-scenes.ts). */ + + .ulw-mode-flag { + font-family: var(--font-mono); + font-size: 11px; + color: var(--codex-window-accent); + font-weight: 600; + letter-spacing: 0.04em; + } + + .ulw-scene-status { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.02em; + color: var(--codex-window-text-soft); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .ulw-scene-copy { + display: flex; + flex-direction: column; + gap: 6px; + } + + .ulw-scene-copy h3 { + font-size: 19px; + font-weight: 650; + letter-spacing: -0.02em; + line-height: 1.2; + text-wrap: balance; + } + + .ulw-scene-copy p { + font-size: 13px; + line-height: 1.55; + color: var(--codex-window-text-soft); + max-width: 62ch; + } +} diff --git a/packages/web/components/design-system/actions.tsx b/packages/web/components/design-system/actions.tsx index 1ef56d3..a355db1 100644 --- a/packages/web/components/design-system/actions.tsx +++ b/packages/web/components/design-system/actions.tsx @@ -6,7 +6,6 @@ interface LinkActionProps { readonly children: ReactNode readonly className?: string readonly href: string - readonly prefetch?: false readonly variant?: "primary" | "secondary" } @@ -14,20 +13,19 @@ const actionClassName = { primary: "relative block rounded-md bg-[color:var(--text-primary)] px-6 py-3 font-medium text-[color:var(--surface-base)] transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--surface-panel)]", secondary: - "rounded-md border border-white/20 bg-transparent px-6 py-3 font-medium text-[color:var(--text-primary)] transition-colors hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--surface-panel)]", + "rounded-md border border-[color:var(--border-default)] bg-transparent px-6 py-3 font-medium text-[color:var(--text-primary)] transition-colors hover:bg-[color:var(--surface-1)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--surface-panel)]", } as const export function LinkAction({ children, className, href, - prefetch = false, variant = "secondary", }: LinkActionProps): JSX.Element { return ( <Link href={href} - prefetch={prefetch} + prefetch={false} className={cx(actionClassName[variant], className)} > {children} @@ -38,7 +36,7 @@ export function LinkAction({ export function GlowActionFrame({ children }: { readonly children: ReactNode }): JSX.Element { return ( <div className="relative group"> - <div className="absolute -inset-1 rounded-lg bg-[color:var(--accent-mint)] opacity-20 blur-xl transition-opacity group-hover:opacity-30" /> + <div className="absolute -inset-1 rounded-lg bg-[color:var(--accent-mint)] opacity-25 blur-xl transition-opacity group-hover:opacity-35" /> {children} </div> ) diff --git a/packages/web/components/design-system/brand-mark.tsx b/packages/web/components/design-system/brand-mark.tsx index 02851b2..46062f9 100644 --- a/packages/web/components/design-system/brand-mark.tsx +++ b/packages/web/components/design-system/brand-mark.tsx @@ -1,5 +1,4 @@ import type { JSX } from "react" -import { cx } from "./utils" interface BrandMarkProps { readonly className?: string @@ -75,14 +74,14 @@ export function HeroBrandMark(): JSX.Element { className="absolute inset-0 rounded-[28px] opacity-60 blur-2xl" style={{ background: - "radial-gradient(circle at 50% 50%, rgba(74,222,128,0.45) 0%, transparent 70%)", + "radial-gradient(circle at 50% 50%, rgba(74,222,128,0.16) 0%, transparent 70%)", }} aria-hidden="true" /> <BrandMark size="hero" dotFill="var(--accent-primary)" - className={cx("relative h-[140px] w-[140px] md:h-[160px] md:w-[160px]")} + className="relative h-[140px] w-[140px] md:h-[160px] md:w-[160px]" /> </div> ) diff --git a/packages/web/components/design-system/layout.tsx b/packages/web/components/design-system/layout.tsx index 86e4d77..0358606 100644 --- a/packages/web/components/design-system/layout.tsx +++ b/packages/web/components/design-system/layout.tsx @@ -10,23 +10,24 @@ interface ClassNameProps extends ChildrenProps { } interface SkipLinkProps { - readonly children?: ReactNode readonly className?: string - readonly href?: string } export function PageShell({ children }: ChildrenProps): JSX.Element { - return <div className="flex min-h-[100dvh] flex-col">{children}</div> + return ( + <div className="relative flex min-h-[100dvh] flex-col"> + {/* Atmosphere backdrop: static low-alpha green glows behind everything. + Absolute + pointer-events-none — zero layout cost, no CLS. */} + <div aria-hidden className="glow-backdrop pointer-events-none absolute inset-0" /> + <div className="relative flex min-h-[100dvh] flex-col">{children}</div> + </div> + ) } -export function SkipLink({ - children = "Skip to main content", - className = "skip-link", - href = "#content", -}: SkipLinkProps): JSX.Element { +export function SkipLink({ className = "skip-link" }: SkipLinkProps): JSX.Element { return ( - <a href={href} className={className}> - {children} + <a href="#content" className={className}> + Skip to main content </a> ) } @@ -61,9 +62,21 @@ export function MarketingSection({ ) } -export function MarketingRuleGrid({ children }: ChildrenProps): JSX.Element { +interface MarketingRuleGridProps extends ChildrenProps { + readonly ruleStyle?: "solid" | "dotted" +} + +export function MarketingRuleGrid({ + children, + ruleStyle = "solid", +}: MarketingRuleGridProps): JSX.Element { return ( - <div className="grid gap-8 border-y border-white/10 py-12 md:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] md:py-16"> + <div + className={cx( + "grid gap-8 border-y border-[color:var(--border-subtle)] py-12 md:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] md:py-16", + ruleStyle === "dotted" && "rule-grid-dotted", + )} + > {children} </div> ) diff --git a/packages/web/components/design-system/surfaces.tsx b/packages/web/components/design-system/surfaces.tsx index 00939ae..0df6ab8 100644 --- a/packages/web/components/design-system/surfaces.tsx +++ b/packages/web/components/design-system/surfaces.tsx @@ -6,6 +6,15 @@ interface ChildrenProps { readonly className?: string } +interface ChildrenOnlyProps { + readonly children: ReactNode +} + +interface AccentSurfaceProps extends ChildrenProps { + readonly as?: "div" | "li" + readonly padding?: string +} + interface FactListProps { readonly items: readonly string[] readonly dotClassName?: string @@ -19,22 +28,33 @@ interface NumberedPointProps { export function SurfaceCard({ children, className }: ChildrenProps): JSX.Element { return ( - <div className={cx("rounded-lg border border-white/10 bg-white/[0.03] p-5", className)}> + <div + className={cx( + "rounded-lg border border-[color:var(--border-subtle)] bg-[color:var(--card-base)] p-5 shadow-[0_1px_2px_rgba(0,0,0,0.4)]", + className, + )} + > {children} </div> ) } -export function AccentSurface({ children, className }: ChildrenProps): JSX.Element { +export function AccentSurface({ + children, + className, + as: Tag = "div", + padding = "p-5", +}: AccentSurfaceProps): JSX.Element { return ( - <div + <Tag className={cx( - "rounded-lg border border-[color:var(--accent-primary)]/20 bg-[color:var(--accent-primary)]/5 p-5", + "rounded-lg border border-[color:var(--accent-primary)]/20 bg-[color:var(--accent-primary)]/5", + padding, className, )} > {children} - </div> + </Tag> ) } @@ -42,7 +62,7 @@ export function ShowcaseSurface({ children, className }: ChildrenProps): JSX.Ele return ( <div className={cx( - "relative flex w-full flex-col items-center rounded-3xl bg-black px-4 py-16 shadow-2xl ring-1 ring-white/5", + "relative flex w-full flex-col items-center rounded-3xl bg-[#16191e] px-4 py-16 shadow-2xl ring-1 ring-white/10", className, )} > @@ -52,11 +72,11 @@ export function ShowcaseSurface({ children, className }: ChildrenProps): JSX.Ele ) } -export function CommandCodeSurface({ children }: ChildrenProps): JSX.Element { - return <div className="rounded-md bg-black/40 p-3">{children}</div> +export function CommandCodeSurface({ children }: ChildrenOnlyProps): JSX.Element { + return <div className="rounded-md bg-[#1b1f24] p-3 text-[#dcfce7] ring-1 ring-white/10">{children}</div> } -export function IconWell({ children }: ChildrenProps): JSX.Element { +export function IconWell({ children }: ChildrenOnlyProps): JSX.Element { return ( <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-[color:var(--accent-primary)]/10 text-[color:var(--accent-primary)]"> {children} @@ -88,7 +108,7 @@ export function FactList({ export function CompactDotList({ items, - dotClassName = "bg-white/25", + dotClassName = "bg-[color:var(--border-default)]", }: FactListProps): JSX.Element { return ( <ul className="mt-3 flex flex-col gap-2"> @@ -105,9 +125,22 @@ export function CompactDotList({ ) } +export function MonoTag({ children, className }: ChildrenProps): JSX.Element { + return ( + <li + className={cx( + "rounded-md border border-[color:var(--border-subtle)] bg-[color:var(--surface-2)] px-3 py-2 font-mono text-xs text-[color:var(--text-secondary)]", + className, + )} + > + {children} + </li> + ) +} + export function NumberedPoint({ index, label, text }: NumberedPointProps): JSX.Element { return ( - <div className="grid gap-4 rounded-lg border border-white/10 bg-white/[0.03] p-5 md:grid-cols-[72px_1fr]"> + <div className="grid gap-4 rounded-lg border border-[color:var(--border-subtle)] bg-[color:var(--card-base)] p-5 shadow-[0_1px_2px_rgba(0,0,0,0.4)] md:grid-cols-[72px_1fr]"> <span className="font-mono text-xs uppercase text-[color:var(--text-tertiary)]"> {String(index + 1).padStart(2, "0")} </span> diff --git a/packages/web/components/design-system/typography.tsx b/packages/web/components/design-system/typography.tsx index 246c1ae..66bee46 100644 --- a/packages/web/components/design-system/typography.tsx +++ b/packages/web/components/design-system/typography.tsx @@ -2,7 +2,7 @@ import type { CSSProperties, JSX, ReactNode } from "react" import { cx } from "./utils" const gradientTextStyle = { - background: "linear-gradient(180deg, #86efac 0%, #4ade80 50%, #16a34a 100%)", + background: "linear-gradient(180deg, #86efac 0%, #4ade80 50%, #22c55e 100%)", WebkitBackgroundClip: "text", backgroundClip: "text", color: "transparent", @@ -26,11 +26,20 @@ export function Kicker({ children, className }: TextProps): JSX.Element { ) } -export function SectionHeading({ children, className }: TextProps): JSX.Element { +interface SectionHeadingProps extends TextProps { + readonly serif?: boolean +} + +export function SectionHeading({ + children, + className, + serif = false, +}: SectionHeadingProps): JSX.Element { return ( <h2 className={cx( "mt-4 text-balance font-medium leading-tight text-[color:var(--text-primary)]", + serif && "font-[family-name:var(--font-serif)]", className, )} > @@ -73,6 +82,20 @@ export function AccentBadge({ children, className }: TextProps): JSX.Element { ) } +const cardLabelTone = { + default: "text-[color:var(--text-tertiary)]", + accent: "text-[color:var(--accent-primary)]", +} as const + +interface CardLabelProps { + readonly children: ReactNode + readonly tone?: keyof typeof cardLabelTone +} + +export function CardLabel({ children, tone = "default" }: CardLabelProps): JSX.Element { + return <h3 className={cx("font-mono text-xs uppercase", cardLabelTone[tone])}>{children}</h3> +} + export function InlineCode({ children, className }: TextProps): JSX.Element { return ( <code className={cx("font-mono font-medium", className)}> diff --git a/packages/web/components/site/command-card.tsx b/packages/web/components/site/command-card.tsx index 773b1ec..6d49f5f 100644 --- a/packages/web/components/site/command-card.tsx +++ b/packages/web/components/site/command-card.tsx @@ -74,7 +74,7 @@ function GlyphIcon({ type }: { readonly type: LazyCommand["glyph"] }): JSX.Eleme export function CommandCard({ command }: CommandCardProps): JSX.Element { return ( - <article className="flex flex-col gap-4 rounded-xl border border-white/5 bg-[color:var(--surface-panel)] p-6 shadow-sm transition-colors hover:border-white/10"> + <article className="flex flex-col gap-4 rounded-xl border border-[color:var(--border-subtle)] bg-[color:var(--card-base)] p-6 shadow-sm"> <header className="flex items-center gap-3"> <IconWell> <GlyphIcon type={command.glyph} /> @@ -85,7 +85,7 @@ export function CommandCard({ command }: CommandCardProps): JSX.Element { </header> <CommandCodeSurface> - <code className="block overflow-x-auto whitespace-nowrap font-mono text-sm text-[color:var(--text-secondary)]"> + <code className="block overflow-x-auto whitespace-nowrap font-mono text-sm"> {command.syntax} </code> </CommandCodeSurface> diff --git a/packages/web/components/site/copy-button.tsx b/packages/web/components/site/copy-button.tsx index 711c5b9..c68aa86 100644 --- a/packages/web/components/site/copy-button.tsx +++ b/packages/web/components/site/copy-button.tsx @@ -22,7 +22,7 @@ export function CopyButton({ value, className = "" }: CopyButtonProps): JSX.Elem <button type="button" onClick={handleCopy} - className={`group relative flex h-8 w-8 items-center justify-center rounded-md transition-colors hover:bg-[color:var(--surface-panel)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-primary)] ${className}`} + className={`group relative flex h-8 w-8 items-center justify-center rounded-md transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-mint)] ${className}`} aria-label="Copy install command" > <span className="sr-only" aria-live="polite"> @@ -39,7 +39,7 @@ export function CopyButton({ value, className = "" }: CopyButtonProps): JSX.Elem strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" - className="text-[color:var(--accent-primary)]" + className="text-[color:var(--accent-mint)]" aria-hidden="true" > <polyline points="20 6 9 17 4 12" /> @@ -55,7 +55,7 @@ export function CopyButton({ value, className = "" }: CopyButtonProps): JSX.Elem strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" - className="text-[color:var(--text-muted)] transition-colors group-hover:text-[color:var(--text-primary)]" + className="text-[color:var(--accent-mint)] opacity-80 transition-opacity group-hover:opacity-100" aria-hidden="true" > <rect width="14" height="14" x="8" y="8" rx="2" ry="2" /> diff --git a/packages/web/components/site/docs-cta.tsx b/packages/web/components/site/docs-cta.tsx index 12d6eab..c9f1bfc 100644 --- a/packages/web/components/site/docs-cta.tsx +++ b/packages/web/components/site/docs-cta.tsx @@ -4,8 +4,8 @@ import { SITE_CONFIG } from "../../lib/site-config" export function DocsCta(): JSX.Element { return ( - <section className="mx-auto mt-32 mb-24 flex w-full max-w-[800px] flex-col items-center rounded-2xl border border-white/10 bg-[color:var(--surface-panel)] px-6 py-16 text-center md:mt-40 md:px-12 relative overflow-hidden"> - <div className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 h-[600px] w-[600px] rounded-full border-[1.5px] border-[rgba(74,222,128,0.12)]" /> + <section className="mx-auto mt-32 mb-24 flex w-full max-w-[800px] flex-col items-center rounded-2xl border border-[color:var(--border-subtle)] bg-[color:var(--surface-panel)] px-6 py-16 text-center md:mt-40 md:px-12 relative overflow-hidden"> + <div className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 h-[600px] w-[600px] rounded-full border-[1.5px] border-[color:var(--accent-primary-border)]" /> <h2 className="relative z-10 text-3xl font-medium tracking-tight text-[color:var(--text-primary)]"> Ready to wire the harness? diff --git a/packages/web/components/site/feature-workflows-section.tsx b/packages/web/components/site/feature-workflows-section.tsx index 518dee7..58a411d 100644 --- a/packages/web/components/site/feature-workflows-section.tsx +++ b/packages/web/components/site/feature-workflows-section.tsx @@ -3,17 +3,17 @@ import { MarketingRuleGrid, MarketingSection, } from "../design-system/layout" -import { AccentSurface, NumberedPoint } from "../design-system/surfaces" +import { AccentSurface, MonoTag, NumberedPoint } from "../design-system/surfaces" import { BodyText, Kicker, SectionHeading } from "../design-system/typography" import { SITE_CONFIG } from "../../lib/site-config" export function FeatureWorkflowsSection(): JSX.Element { return ( <MarketingSection className="mt-24 md:mt-32"> - <MarketingRuleGrid> + <MarketingRuleGrid ruleStyle="dotted"> <div> <Kicker>{SITE_CONFIG.featureWorkflows.kicker}</Kicker> - <SectionHeading className="text-[clamp(32px,5vw,56px)]"> + <SectionHeading serif className="text-[clamp(32px,5vw,56px)]"> {SITE_CONFIG.featureWorkflows.title} </SectionHeading> <BodyText>{SITE_CONFIG.featureWorkflows.intro}</BodyText> @@ -42,12 +42,7 @@ export function FeatureWorkflowsSection(): JSX.Element { </div> <ul className="grid grid-cols-2 gap-2 sm:grid-cols-4"> {SITE_CONFIG.builtInSkills.skills.map((skill) => ( - <li - key={skill} - className="rounded-md border border-white/10 bg-black/20 px-3 py-2 font-mono text-xs text-[color:var(--text-secondary)]" - > - {skill} - </li> + <MonoTag key={skill}>{skill}</MonoTag> ))} </ul> </AccentSurface> diff --git a/packages/web/components/site/hephaestus-section.tsx b/packages/web/components/site/hephaestus-section.tsx index e677ff6..c19edbc 100644 --- a/packages/web/components/site/hephaestus-section.tsx +++ b/packages/web/components/site/hephaestus-section.tsx @@ -12,6 +12,7 @@ import { import { AccentBadge, BodyText, + CardLabel, GradientTitle, Kicker, SectionHeading, @@ -35,15 +36,11 @@ export function HephaestusSection(): JSX.Element { <div className="grid gap-3 sm:grid-cols-2"> <SurfaceCard> - <h3 className="font-mono text-xs uppercase text-[color:var(--text-tertiary)]"> - {omoIntro.omoLabel} - </h3> + <CardLabel>{omoIntro.omoLabel}</CardLabel> <CompactDotList items={omoIntro.omoPoints} /> </SurfaceCard> <AccentSurface> - <h3 className="font-mono text-xs uppercase text-[color:var(--accent-primary)]"> - {omoIntro.lazyLabel} - </h3> + <CardLabel tone="accent">{omoIntro.lazyLabel}</CardLabel> <CompactDotList items={omoIntro.lazyPoints} dotClassName="bg-[color:var(--accent-primary)]" @@ -52,7 +49,11 @@ export function HephaestusSection(): JSX.Element { </div> </MarketingRuleGrid> - {/* Hephaestus — the ported agent */} + {/* Hephaestus — the ported agent. This ShowcaseSurface is an elevated + showcase band (#16191e + hairline ring) on the dark canvas. The + global dark ink tokens already read AA here: #f7f8f8/#b4bcc8 are + ~16:1/~8.8:1 and #4ade80 ~9.5:1 against the #16191e band, so no + per-band token re-scope is needed. */} <ShowcaseSurface className="mt-16 overflow-hidden text-center md:px-8"> <div className="flex items-center gap-3"> <AccentBadge>{hephaestus.badge}</AccentBadge> @@ -70,16 +71,13 @@ export function HephaestusSection(): JSX.Element { <ol className="mt-12 grid w-full max-w-[960px] grid-cols-2 gap-3 md:grid-cols-5"> {hephaestus.loop.map((phase, i) => ( - <li - key={phase.step} - className="rounded-lg border border-[color:var(--accent-primary)]/20 bg-[color:var(--accent-primary)]/5 p-4 text-center" - > + <AccentSurface as="li" key={phase.step} padding="p-4" className="text-center"> <div className="mb-2 font-mono text-xs text-[color:var(--accent-primary)]"> {String(i + 1).padStart(2, "0")} </div> <p className="text-sm font-medium text-[color:var(--text-primary)]">{phase.step}</p> <p className="mt-1 text-xs leading-snug text-[color:var(--text-muted)]">{phase.text}</p> - </li> + </AccentSurface> ))} </ol> diff --git a/packages/web/components/site/hero.tsx b/packages/web/components/site/hero.tsx index a6d10a0..30675f9 100644 --- a/packages/web/components/site/hero.tsx +++ b/packages/web/components/site/hero.tsx @@ -4,41 +4,30 @@ import { SITE_CONFIG } from "../../lib/site-config" export function Hero(): JSX.Element { return ( - <section className="relative isolate flex w-full flex-col justify-end overflow-hidden rounded-[20px] bg-[color:var(--card-base)] px-[28px] pb-[44px] pt-[88px] shadow-[0_40px_120px_rgba(0,0,0,0.6)] md:px-[64px] md:pb-[56px] md:pt-[120px]"> - {/* Card background — pure CSS gradient layers. A left-weighted emerald - glow rather than a flat centered slab, so the composition reads as - a crafted product moment instead of a generic gradient card. - Image-free so the hero text is the LCP element and paints at FCP. */} - <div className="card-gradient-pools absolute inset-0 -z-10" /> - <div className="card-gradient-beam absolute inset-0 -z-10" /> - <div className="card-gradient-sheen absolute -inset-[10%] -z-10" /> - <div - className="absolute inset-0 -z-10" - style={{ - background: - "radial-gradient(90% 70% at 18% 110%, rgba(74,222,128,0.42) 0%, rgba(34,197,94,0.18) 28%, rgba(10,12,11,0) 60%), linear-gradient(180deg, rgba(10,12,11,0.86) 0%, rgba(14,20,17,0.5) 45%, rgba(14,20,17,0) 100%)", - }} - /> - - {/* Content — left-aligned editorial with a right-anchored mark panel - so the hero composes as a scene, not a blank gradient field. */} + <section className="relative flex w-full flex-col justify-end pb-[36px] pt-[36px] md:pb-[24px] md:pt-[40px]"> + {/* Open dark canvas — no card, no gradient washes. The declarative + hero text sits directly on the graphite ground so it stays the LCP + element and paints at FCP; the demo window below carries the + visual weight. Compact by design: the block stays under ~60vh at + 1440x900 so the demo window's top edge is visible in the first + viewport. */} <div className="flex items-end justify-between gap-8"> - <div className="flex max-w-[820px] flex-col gap-[20px] text-left"> - <p className="font-mono text-[13px] font-medium uppercase tracking-[0.28em] text-[color:var(--accent-mint)]"> + <div className="flex max-w-[820px] flex-col gap-[18px] text-left"> + <p className="font-mono text-[13px] font-medium uppercase tracking-[0.28em] text-[color:var(--text-soft)]"> {SITE_CONFIG.eyebrow} </p> - <h1 className="wordmark m-0 text-balance text-[clamp(44px,7vw,104px)] font-semibold leading-[0.95] tracking-[-0.03em] text-[color:var(--text-primary)]"> + <h1 className="wordmark m-0 text-balance text-[clamp(44px,6vw,88px)] font-semibold leading-[0.95] tracking-[-0.03em] text-[color:var(--text-primary)]"> {SITE_CONFIG.wordmark} </h1> - <p className="m-0 max-w-[640px] text-balance text-[clamp(18px,2.2vw,26px)] font-normal leading-[1.4] tracking-[-0.005em] text-[color:var(--text-secondary)]"> + <p className="m-0 max-w-[640px] text-balance text-[clamp(18px,2vw,24px)] font-normal leading-[1.4] tracking-[-0.005em] text-[color:var(--text-secondary)]"> {SITE_CONFIG.heroLineA} </p> - <p className="m-0 max-w-[620px] text-balance text-[clamp(15px,1.6vw,19px)] font-normal leading-[1.5] text-[color:var(--text-muted)]"> + <p className="m-0 max-w-[620px] text-balance text-[clamp(15px,1.5vw,18px)] font-normal leading-[1.5] text-[color:var(--text-muted)]"> {SITE_CONFIG.heroLineB.prefix} - <span className="font-mono text-[color:var(--accent-mint)]"> + <span className="font-mono font-medium text-[color:var(--accent-primary)]"> {SITE_CONFIG.heroLineB.slot} </span> {SITE_CONFIG.heroLineB.suffix} @@ -47,9 +36,25 @@ export function Hero(): JSX.Element { </span> {SITE_CONFIG.heroLineB.period} </p> + + {/* Declarative pillar row — sisyphus-style stacked statements + rendered as a compact inline list (plain text, no links). */} + <ul className="m-0 flex list-none flex-wrap items-center gap-x-[18px] gap-y-[8px] p-0 pt-[4px]"> + {SITE_CONFIG.harnessPillars.map((pillar) => ( + <li + key={pillar} + className="flex items-center gap-[8px] font-mono text-[12px] uppercase tracking-[0.14em] text-[color:var(--text-tertiary)]" + > + <span + aria-hidden="true" + className="h-[5px] w-[5px] rounded-full bg-[color:var(--accent-primary)]" + /> + {pillar} + </li> + ))} + </ul> </div> - {/* Right-anchored brand mark — a composed visual anchor. */} <div className="hidden shrink-0 items-center justify-end md:flex"> <HeroBrandMark /> </div> diff --git a/packages/web/components/site/install-block.tsx b/packages/web/components/site/install-block.tsx index f0bb275..bd0cd97 100644 --- a/packages/web/components/site/install-block.tsx +++ b/packages/web/components/site/install-block.tsx @@ -1,4 +1,5 @@ import type { JSX } from "react" +import { CommandCodeSurface } from "../design-system/surfaces" import { InlineCode } from "../design-system/typography" import { SITE_CONFIG } from "../../lib/site-config" import { CopyButton } from "./copy-button" @@ -6,20 +7,29 @@ import { CopyButton } from "./copy-button" export function InstallBlock(): JSX.Element { return ( <section className="mx-auto mt-8 flex w-full max-w-2xl flex-col items-center gap-4 px-4 md:mt-10"> - <div className="flex w-full items-center justify-between rounded-lg border border-white/10 bg-[color:var(--surface-panel)] p-2 pl-4 shadow-lg"> - <div className="flex items-center gap-3 overflow-x-auto"> - <span className="select-none font-mono text-[color:var(--text-tertiary)]" aria-hidden="true"> - $ - </span> - <InlineCode className="whitespace-nowrap text-sm text-[color:var(--text-primary)] md:text-base"> - {SITE_CONFIG.installCommand} - </InlineCode> - </div> - <CopyButton value={SITE_CONFIG.installCommand} className="ml-4 shrink-0" /> + <div className="w-full rounded-xl border border-[color:var(--border-subtle)] bg-[color:var(--card-base)] p-2 shadow-sm"> + <CommandCodeSurface> + <div className="flex w-full items-center justify-between"> + <div className="flex items-center gap-3 overflow-x-auto pl-1"> + <span + className="select-none font-mono text-[color:var(--accent-mint)]" + aria-hidden="true" + > + $ + </span> + <InlineCode className="whitespace-nowrap text-sm md:text-base"> + {SITE_CONFIG.installCommand} + </InlineCode> + </div> + <CopyButton value={SITE_CONFIG.installCommand} className="ml-4 shrink-0" /> + </div> + </CommandCodeSurface> </div> - <div className="flex flex-col items-center gap-1 text-center text-sm text-[color:var(--text-muted)]"> - <p className="font-mono text-xs opacity-70">= {SITE_CONFIG.installEquivalent}</p> + <div className="flex flex-col items-center gap-1 text-center text-sm"> + <p className="font-mono text-xs text-[color:var(--text-tertiary)]"> + = {SITE_CONFIG.installEquivalent} + </p> </div> </section> ) diff --git a/packages/web/components/site/site-footer.tsx b/packages/web/components/site/site-footer.tsx index 4c5d8f8..56b56f4 100644 --- a/packages/web/components/site/site-footer.tsx +++ b/packages/web/components/site/site-footer.tsx @@ -1,12 +1,18 @@ import type { JSX } from "react" +import Link from "next/link" import { MarketingContainer } from "../design-system/layout" import { SITE_CONFIG } from "../../lib/site-config" +const footerLinkClassName = + "transition-colors hover:text-[color:var(--accent-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-primary)]" + export function SiteFooter(): JSX.Element { return ( - <footer className="w-full border-t border-white/5 bg-[color:var(--surface-night)] py-8"> - <MarketingContainer className="flex flex-col items-center justify-between gap-4 text-sm text-[color:var(--text-tertiary)] md:flex-row"> - <div className="flex items-center gap-2"> + <footer className="w-full border-t border-[color:var(--border-subtle)] bg-[color:var(--surface-night)] py-12"> + {/* Modest column footer (ampcode-style rule): EXISTING destinations + only — brand line, Docs, GitHub, OmO. No invented links. */} + <MarketingContainer className="grid gap-8 text-sm text-[color:var(--text-tertiary)] sm:grid-cols-3"> + <div className="flex items-start gap-2"> <span className="font-mono font-medium text-[color:var(--text-secondary)]"> lazycodex.ai </span> @@ -14,22 +20,32 @@ export function SiteFooter(): JSX.Element { <span>MIT License</span> </div> - <div className="flex items-center gap-6"> + <div className="flex flex-col items-start gap-2.5"> + <Link + href={SITE_CONFIG.docsPath} + prefetch={false} + className={footerLinkClassName} + > + Docs + </Link> <a - href={SITE_CONFIG.omoUrl} + href={SITE_CONFIG.githubUrl} target="_blank" rel="noopener noreferrer" - className="transition-colors hover:text-[color:var(--accent-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-primary)]" + className={footerLinkClassName} > - OmO + GitHub </a> + </div> + + <div className="flex flex-col items-start gap-2.5"> <a - href={SITE_CONFIG.githubUrl} + href={SITE_CONFIG.omoUrl} target="_blank" rel="noopener noreferrer" - className="transition-colors hover:text-[color:var(--accent-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-primary)]" + className={footerLinkClassName} > - GitHub + OmO </a> </div> </MarketingContainer> diff --git a/packages/web/components/site/site-header.tsx b/packages/web/components/site/site-header.tsx index 0bd8d61..63806ea 100644 --- a/packages/web/components/site/site-header.tsx +++ b/packages/web/components/site/site-header.tsx @@ -7,7 +7,7 @@ import { GithubStarsPill } from "./github-stars-pill" export function SiteHeader(): JSX.Element { return ( - <header className="sticky top-0 z-40 w-full border-b border-white/5 bg-[color:var(--surface-base)]/80 backdrop-blur-md"> + <header className="sticky top-0 z-40 w-full border-b border-[color:var(--border-subtle)] bg-[color:var(--surface-base)]/85 backdrop-blur-md"> <MarketingContainer className="flex h-14 items-center justify-between"> <div className="flex items-center gap-3"> <Link diff --git a/packages/web/components/site/team-mode-section.tsx b/packages/web/components/site/team-mode-section.tsx new file mode 100644 index 0000000..faa5d8f --- /dev/null +++ b/packages/web/components/site/team-mode-section.tsx @@ -0,0 +1,119 @@ +import type { JSX } from "react" +import { MarketingRuleGrid, MarketingSection } from "../design-system/layout" +import { SurfaceCard } from "../design-system/surfaces" +import { BodyText, CardLabel, Kicker, SectionHeading } from "../design-system/typography" +import { SITE_CONFIG } from "../../lib/site-config" + +/** + * Team Mode — copy grounded in plugins/omo/skills/teammode/SKILL.md and the + * recorded Codex Desktop team session (see .omo/evidence/copy-ledger.md). + * The thread mock reuses the Codex window adapter tokens; rows are not + * interactive, so they carry no hover states. + */ +export function TeamModeSection(): JSX.Element { + const { teamMode } = SITE_CONFIG + + return ( + <MarketingSection className="mt-24 md:mt-32"> + <MarketingRuleGrid> + <div> + <Kicker>{teamMode.kicker}</Kicker> + <SectionHeading className="text-[clamp(28px,4vw,44px)]"> + {teamMode.title} + </SectionHeading> + <BodyText>{teamMode.body}</BodyText> + <p className="mt-4 text-sm leading-relaxed text-[color:var(--text-tertiary)]"> + {teamMode.compositionRule} + </p> + <p className="mt-2 font-mono text-xs text-[color:var(--text-tertiary)]"> + {teamMode.stateNote} + </p> + </div> + + <div className="flex flex-col gap-3"> + {/* Dark window, matching the demo default: creating a team spawns + ONE chat session per member — the left pane lists them exactly + like the app sidebar lists sessions. */} + <div + className="ulw-window" + data-window-theme="dark" + aria-label="Team mode member sessions" + > + <div className="ulw-titlebar"> + <span className="ulw-traffic" aria-hidden="true"> + <span /> + <span /> + <span /> + </span> + <div className="ulw-window-tabs" aria-hidden="true"> + <span className="ulw-window-tab" data-current="true"> + Leader — main session + </span> + </div> + </div> + <div className="grid gap-0 sm:grid-cols-[minmax(0,11rem)_minmax(0,1fr)]"> + <div className="flex flex-col gap-1 border-b border-[color:var(--codex-window-border)] p-3 sm:border-b-0 sm:border-r"> + {teamMode.memberThreads.map((member) => ( + <div + key={member.name} + className="flex min-w-0 items-center gap-2 rounded-md bg-[color:var(--codex-window-chip)] px-2.5 py-1.5" + > + <span + className="h-1.5 w-1.5 shrink-0 rounded-full bg-[color:var(--codex-window-accent)]" + aria-hidden="true" + /> + <span className="min-w-0 truncate text-[12px] font-medium text-[color:var(--codex-window-text)]"> + {member.name} + </span> + </div> + ))} + </div> + <div className="flex flex-col gap-2 p-4"> + {teamMode.memberThreads.map((member) => ( + <div + key={member.name} + className="flex items-center justify-between gap-3 rounded-lg border border-[color:var(--codex-window-border)] px-3 py-2" + > + <span className="min-w-0 truncate text-[12.5px] font-medium text-[color:var(--codex-window-text)]"> + {member.name} + </span> + <span className="whitespace-nowrap font-mono text-[10px] text-[color:var(--codex-window-text-soft)]"> + {member.status} + </span> + </div> + ))} + <div className="mt-1 rounded-lg bg-[color:var(--codex-window-chip)] px-3 py-2"> + <p className="text-right font-mono text-[10px] text-[color:var(--codex-window-text-soft)]"> + {teamMode.threadNote} + </p> + <p className="mt-1 text-[11.5px] leading-snug text-[color:var(--codex-window-text)]"> + Member A COMPLETE verification note: report exists, pinned-link + check passed, and no GitHub mutations/repo edits. + </p> + </div> + </div> + </div> + </div> + + <SurfaceCard> + <CardLabel>{teamMode.whenTitle}</CardLabel> + <ul className="mt-3 flex flex-col gap-2"> + {teamMode.whenPoints.map((point) => ( + <li + key={point} + className="flex gap-2 text-sm leading-relaxed text-[color:var(--text-muted)]" + > + <span + className="mt-[9px] h-1 w-1 shrink-0 rounded-full bg-[color:var(--accent-primary)]" + aria-hidden="true" + /> + {point} + </li> + ))} + </ul> + </SurfaceCard> + </div> + </MarketingRuleGrid> + </MarketingSection> + ) +} diff --git a/packages/web/components/site/ultrawork-section.tsx b/packages/web/components/site/ultrawork-section.tsx index 97475a8..a79a6ae 100644 --- a/packages/web/components/site/ultrawork-section.tsx +++ b/packages/web/components/site/ultrawork-section.tsx @@ -1,24 +1,18 @@ import type { JSX } from "react" import { MarketingSection } from "../design-system/layout" import { ShowcaseSurface } from "../design-system/surfaces" -import { GradientTitle, InlineCode } from "../design-system/typography" -import { SITE_CONFIG } from "../../lib/site-config" +import { GradientTitle } from "../design-system/typography" import { BrandImage } from "./brand-image" +/** + * The glassy Ultrawork badge showcase — the original identity moment, + * restored per user direction. The live demo owns the tagline/example + * copy now, so this section is purely the brand artifact. + */ export function UltraworkSection(): JSX.Element { return ( <MarketingSection className="mt-32 flex flex-col items-center text-center md:mt-40"> - <h2 className="text-balance text-[clamp(32px,5vw,48px)] font-medium tracking-tight text-[color:var(--text-primary)]"> - {SITE_CONFIG.ultraworkTagline} - </h2> - - <div className="mt-8 rounded-lg border border-[color:var(--accent-primary)]/20 bg-[color:var(--accent-primary)]/5 px-6 py-3 shadow-[0_0_30px_rgba(74,222,128,0.1)]"> - <InlineCode className="text-lg text-[color:var(--accent-mint)]"> - {SITE_CONFIG.ultraworkExample} - </InlineCode> - </div> - - <ShowcaseSurface className="mt-24 max-w-[960px]"> + <ShowcaseSurface className="max-w-[960px]"> <GradientTitle className="mb-12 text-center text-[clamp(28px,4vw,48px)] font-semibold tracking-tight opacity-90" > diff --git a/packages/web/components/site/ulw-demo/codex-window.tsx b/packages/web/components/site/ulw-demo/codex-window.tsx new file mode 100644 index 0000000..4c139d9 --- /dev/null +++ b/packages/web/components/site/ulw-demo/codex-window.tsx @@ -0,0 +1,87 @@ +"use client" + +import { useEffect, useRef, useState, type JSX } from "react" +import { + ULW_DEMO_ENTRY_MS, + ULW_DEMO_INITIAL_ENTRIES, + ULW_DEMO_SCENES, + ULW_DEMO_TIMELINE, +} from "../../../lib/ulw-demo-scenes" +import { WindowSidebar, WindowTitlebar } from "./window-chrome" +import { SidePanel, TranscriptPane, WindowFooter } from "./window-panes" + +const LOOP_REST_MS = 4000 + +/** + * Chat-replay machine for the Codex-desktop window (DESIGN.md § CodexWindow). + * The demo is ONE session: the user's ask opens the transcript and the run + * appends beneath it — tool rows, prose, code — like the real app following + * a live session. Non-playable: no controls anywhere. Arms on + * scroll-into-view, appends on a fast tick, rests briefly at the checkpoint, + * then loops. prefers-reduced-motion renders the COMPLETED transcript + * statically. The opening entries are server-rendered; the window theme is + * fixed dark. + */ +export function CodexWindow(): JSX.Element { + const [visibleCount, setVisibleCount] = useState(ULW_DEMO_INITIAL_ENTRIES) + const [playing, setPlaying] = useState(false) + const rootRef = useRef<HTMLDivElement | null>(null) + + const entries = ULW_DEMO_TIMELINE.slice(0, visibleCount) + const phase = entries[entries.length - 1]?.phase ?? 0 + const scene = ULW_DEMO_SCENES[phase] ?? ULW_DEMO_SCENES[0] + + useEffect(() => { + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + // Static completed run: everything readable, nothing moving. + setVisibleCount(ULW_DEMO_TIMELINE.length) + return + } + const node = rootRef.current + if (!node) return + const observer = new IntersectionObserver( + (observed) => { + if (observed.some((entry) => entry.isIntersecting)) { + setPlaying(true) + observer.disconnect() + } + }, + { threshold: 0.2 }, + ) + observer.observe(node) + return () => observer.disconnect() + }, []) + + useEffect(() => { + if (!playing) return + if (visibleCount >= ULW_DEMO_TIMELINE.length) { + // Rest on the finished checkpoint, then replay from the opening ask. + const rest = window.setTimeout( + () => setVisibleCount(ULW_DEMO_INITIAL_ENTRIES), + LOOP_REST_MS, + ) + return () => window.clearTimeout(rest) + } + const tick = window.setTimeout( + () => setVisibleCount((count) => count + 1), + ULW_DEMO_ENTRY_MS, + ) + return () => window.clearTimeout(tick) + }, [playing, visibleCount]) + + return ( + <div ref={rootRef} className="flex w-full flex-col items-center"> + <div className="ulw-window ulw-app" data-window-theme="dark"> + <div className="ulw-app-frame"> + <WindowSidebar /> + <section className="ulw-app-main" aria-label="Ultrawork root orchestration surface"> + <WindowTitlebar sceneTab={scene.tab} /> + <TranscriptPane entries={entries} /> + <WindowFooter scene={scene} sceneIndex={phase} /> + </section> + <SidePanel key={scene.key} scene={scene} /> + </div> + </div> + </div> + ) +} diff --git a/packages/web/components/site/ulw-demo/ulw-demo-section.tsx b/packages/web/components/site/ulw-demo/ulw-demo-section.tsx new file mode 100644 index 0000000..254b44a --- /dev/null +++ b/packages/web/components/site/ulw-demo/ulw-demo-section.tsx @@ -0,0 +1,43 @@ +import type { JSX } from "react" +import { MarketingSection } from "../../design-system/layout" +import { AccentSurface } from "../../design-system/surfaces" +import { InlineCode, Kicker } from "../../design-system/typography" +import { SITE_CONFIG } from "../../../lib/site-config" +import { CodexWindow } from "./codex-window" + +/** + * Interactive Ultrawork demo — the landing's product anchor. Intro copy is + * grounded in content/docs/ultrawork.md (see .omo/evidence/copy-ledger.md); + * the window itself replays a real ulw run as a live-DOM scene machine. + */ +export function UlwDemoSection(): JSX.Element { + return ( + /* This worktree's MarketingSection exposes no id prop, so the #ulw-demo + anchor lives on a wrapper div (landing-sections.spec locates it). */ + <div id="ulw-demo" className="mt-24 scroll-mt-20 md:mt-32"> + <MarketingSection className="flex flex-col items-center text-center"> + <Kicker>{SITE_CONFIG.ulwDemo.kicker}</Kicker> + <h2 className="text-balance text-[clamp(32px,5vw,48px)] font-medium tracking-tight text-[color:var(--text-primary)]"> + {SITE_CONFIG.ulwDemo.title} + </h2> + <p className="mt-4 max-w-[70ch] text-balance text-base leading-relaxed text-[color:var(--text-muted)] md:text-lg"> + {SITE_CONFIG.ulwDemo.intro} + </p> + + <AccentSurface className="mt-6" padding="px-6 py-3"> + <InlineCode className="text-lg text-[color:var(--accent-glow)]"> + {SITE_CONFIG.ultraworkExample} + </InlineCode> + </AccentSurface> + + <blockquote className="mt-3 font-mono text-xs uppercase tracking-[0.18em] text-[color:var(--text-tertiary)]"> + “{SITE_CONFIG.ulwDemo.quote}” + </blockquote> + + <div className="mt-12 w-full"> + <CodexWindow /> + </div> + </MarketingSection> + </div> + ) +} diff --git a/packages/web/components/site/ulw-demo/window-chrome.tsx b/packages/web/components/site/ulw-demo/window-chrome.tsx new file mode 100644 index 0000000..8b9527b --- /dev/null +++ b/packages/web/components/site/ulw-demo/window-chrome.tsx @@ -0,0 +1,74 @@ +import type { JSX } from "react" +import { SITE_CONFIG } from "../../../lib/site-config" +import { UlwIcon, type UlwIconName } from "./window-icons" + +/** + * Window chrome for the Codex-desktop demo: left session sidebar plus the + * per-session title bar. Anatomy mirrors the real app frames + * (.omo/reference/app-frames/creation-03.png): traffic lights above a nav + * list, then the Projects group. The sidebar is static — it shows the run's + * single long-lived session (a span with aria-current, nothing clickable): + * the demo is a staged recording, and nothing in the window is interactive. + */ + +const SIDEBAR_NAV: readonly { icon: UlwIconName; label: string }[] = [ + { icon: "compose", label: "New chat" }, + { icon: "search", label: "Search" }, + { icon: "plugins", label: "Plugins" }, + { icon: "clock", label: "Automations" }, +] + +export function WindowSidebar(): JSX.Element { + return ( + <aside className="ulw-app-sidebar"> + <span className="ulw-traffic" aria-hidden="true"> + <span /> + <span /> + <span /> + </span> + + <div className="ulw-app-nav" aria-hidden="true"> + {SIDEBAR_NAV.map((item) => ( + <span className="ulw-app-row" key={item.label}> + <UlwIcon name={item.icon} /> + {item.label} + </span> + ))} + </div> + + {/* ONE session pursuing one goal — constant for the whole replay, + like a real 30h+ run. */} + <nav className="ulw-app-group" aria-label="Sessions"> + <span className="ulw-app-group-label" aria-hidden="true"> + Projects + </span> + <span className="ulw-app-row" aria-hidden="true"> + <UlwIcon name="folder" /> + {SITE_CONFIG.wordmark} + </span> + <span className="ulw-app-session ulw-app-session-active" aria-current="true"> + <span className="ulw-spinner" aria-hidden="true" /> + {SITE_CONFIG.ultraworkExample} + </span> + <span className="ulw-app-row ulw-app-showmore" aria-hidden="true"> + Show more + </span> + </nav> + </aside> + ) +} + +export function WindowTitlebar({ + sceneTab, +}: { + readonly sceneTab: string +}): JSX.Element { + return ( + <header className="ulw-app-titlebar"> + <span className="ulw-app-title">{sceneTab}</span> + <span className="ulw-app-title-dots" aria-hidden="true"> + ⋯ + </span> + </header> + ) +} diff --git a/packages/web/components/site/ulw-demo/window-icons.tsx b/packages/web/components/site/ulw-demo/window-icons.tsx new file mode 100644 index 0000000..20ecd86 --- /dev/null +++ b/packages/web/components/site/ulw-demo/window-icons.tsx @@ -0,0 +1,48 @@ +import type { JSX } from "react" + +/** + * Minimal stroke-only SVG glyphs for the Codex-window demo. Decorative + * (aria-hidden), currentColor, sized by the surrounding chrome — anatomy + * mirrors the small nav/tool glyphs in .omo/reference/app-frames/creation-03.png. + */ + +const ICON_PATHS = { + compose: + "M9 3H4a1.5 1.5 0 0 0-1.5 1.5V12A1.5 1.5 0 0 0 4 13.5h7.5A1.5 1.5 0 0 0 13 12V7M13.2 2.8a1.4 1.4 0 0 0-2 0L6.5 7.5 6 10l2.5-.5 4.7-4.7a1.4 1.4 0 0 0 0-2Z", + search: "M12.5 12.5 10 10M11 6.75a4.25 4.25 0 1 1-8.5 0 4.25 4.25 0 0 1 8.5 0Z", + plugins: "M3 3h4v4H3ZM9 3h4v4H9ZM3 9h4v4H3ZM9 9h4v4H9Z", + clock: "M8 4.5V8l2.3 1.4M14 8A6 6 0 1 1 2 8a6 6 0 0 1 12 0Z", + folder: + "M2.5 4A1.5 1.5 0 0 1 4 2.5h2.6l1.5 1.7H12A1.5 1.5 0 0 1 13.5 5.7V11A1.5 1.5 0 0 1 12 12.5H4A1.5 1.5 0 0 1 2.5 11Z", + terminal: + "M3 2.5h10A1.5 1.5 0 0 1 14.5 4v8a1.5 1.5 0 0 1-1.5 1.5H3A1.5 1.5 0 0 1 1.5 12V4A1.5 1.5 0 0 1 3 2.5ZM4.5 6l2 2-2 2M8.5 10.5h3", + check: "M2.5 8.5 6 12l7.5-8", + target: "M8 10.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12Z", + alert: + "M8 6v3.2M8 11.4v.2M7 2.8 1.8 12a1.2 1.2 0 0 0 1 1.8h10.4a1.2 1.2 0 0 0 1-1.8L9 2.8a1.2 1.2 0 0 0-2 0Z", + plus: "M8 3.5v9M3.5 8h9", + mic: "M8 10.5A2.5 2.5 0 0 0 10.5 8V4.5a2.5 2.5 0 0 0-5 0V8A2.5 2.5 0 0 0 8 10.5ZM12.5 8a4.5 4.5 0 0 1-9 0M8 12.5V14", + "arrow-up": "M8 12.5v-9M4.5 7 8 3.5 11.5 7", +} as const + +export type UlwIconName = keyof typeof ICON_PATHS + +export function UlwIcon({ name }: { readonly name: UlwIconName }): JSX.Element { + return ( + <svg + className="ulw-icon" + viewBox="0 0 16 16" + width="14" + height="14" + fill="none" + stroke="currentColor" + strokeWidth="1.4" + strokeLinecap="round" + strokeLinejoin="round" + aria-hidden="true" + focusable="false" + > + <path d={ICON_PATHS[name]} /> + </svg> + ) +} diff --git a/packages/web/components/site/ulw-demo/window-panes.tsx b/packages/web/components/site/ulw-demo/window-panes.tsx new file mode 100644 index 0000000..2ead3be --- /dev/null +++ b/packages/web/components/site/ulw-demo/window-panes.tsx @@ -0,0 +1,193 @@ +import { useEffect, useRef, type JSX } from "react" +import { SITE_CONFIG } from "../../../lib/site-config" +import { + ULW_DEMO_ENVIRONMENT, + ULW_DEMO_SCENES, + ULW_DEMO_WORKERS, + type UlwEntry, + type UlwScene, +} from "../../../lib/ulw-demo-scenes" +import { UlwIcon, type UlwIconName } from "./window-icons" + +/** + * Presentational panes for the Codex window: the transcript renders the + * appended replay entries; the footer and side panel derive from the + * current phase's scene. Every visible string comes from + * `lib/ulw-demo-scenes.ts` or the + * generic chrome labels visible in our own app frames + * (.omo/reference/app-frames/creation-03.png, subagents-03.png). + * Transcript anatomy follows the real app: command bubble, prose, then + * tool-activity rows (the run-ledger lines) with small inline glyphs. + */ + +function ledgerIcon(line: string): UlwIconName { + if (line.includes("fail")) return "alert" + if (line.includes("evidence_captured") || line.includes("checkpoint") || line.includes("status=pass")) { + return "check" + } + if (line.startsWith("goal_") || line.includes("activeGoalId")) return "target" + return "terminal" +} + +export function TranscriptPane({ + entries, +}: { + readonly entries: readonly UlwEntry[] +}): JSX.Element { + const paneRef = useRef<HTMLElement | null>(null) + + // Follow the replay like the real app follows a live session: keep the + // newest entry in view via INNER scroll only (the window box never moves). + useEffect(() => { + const node = paneRef.current + if (!node) return + const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches + node.scrollTo({ top: node.scrollHeight, behavior: reduced ? "auto" : "smooth" }) + }, [entries.length]) + + return ( + <section ref={paneRef} className="ulw-app-transcript" aria-label="Ultrawork run transcript"> + {/* The user's opening ask — the whole run answers this one message. */} + <div className="ulw-entry ulw-app-user"> + <code>{SITE_CONFIG.ultraworkExample}</code> + </div> + + {entries.map((entry) => { + if (entry.kind === "mode") { + return ( + <p className="ulw-entry ulw-mode-flag" key={entry.id}> + {entry.text} + </p> + ) + } + if (entry.kind === "status") { + return ( + <small className="ulw-entry ulw-scene-status" key={entry.id}> + {entry.text} + </small> + ) + } + if (entry.kind === "prose") { + return ( + <div className="ulw-entry ulw-scene-copy" key={entry.id}> + <h3>{entry.heading}</h3> + <p>{entry.text}</p> + </div> + ) + } + if (entry.kind === "tool") { + return ( + <div className="ulw-entry ulw-app-tool" key={entry.id}> + <UlwIcon name={ledgerIcon(entry.text)} /> + <span>{entry.text}</span> + </div> + ) + } + return ( + <div className="ulw-entry ulw-app-code" key={entry.id}> + <code>{entry.text}</code> + </div> + ) + })} + </section> + ) +} + +export function WindowFooter({ + scene, + sceneIndex, +}: { + readonly scene: UlwScene + readonly sceneIndex: number +}): JSX.Element { + return ( + <div className="ulw-app-footer"> + {/* The app's running line ("Working for 4m 8s" in desktop app.png), + with a run-progress track filling as the goal advances. */} + <div className="ulw-app-working"> + <span className="ulw-app-step"> + <span className="ulw-spinner" aria-hidden="true" /> + Working for {scene.elapsed} + </span> + <span className="ulw-run-progress" aria-hidden="true"> + <span + style={{ + transform: `scaleX(${(sceneIndex + 1) / ULW_DEMO_SCENES.length})`, + }} + /> + </span> + </div> + + <div className="ulw-app-goal"> + <UlwIcon name="target" /> + <strong>Pursuing goal</strong> + <span>{scene.composer}</span> + <span className="ulw-app-goal-elapsed">{scene.elapsed}</span> + </div> + + {/* Static, decorative composer — faithful to the app frame but never a + real input, so the whole block is hidden from assistive tech. */} + <div className="ulw-app-composer" aria-hidden="true"> + <span className="ulw-app-composer-placeholder">Ask for follow-up changes</span> + <div className="ulw-app-composer-row"> + <span className="ulw-app-composer-chip"> + <UlwIcon name="plus" /> + </span> + <span className="ulw-app-composer-chip">Full access</span> + <span className="ulw-app-composer-chip"> + <UlwIcon name="target" /> + Goal + </span> + <span className="ulw-app-composer-grow" /> + <span className="ulw-app-composer-chip">5.5 High</span> + <span className="ulw-app-composer-chip"> + <UlwIcon name="mic" /> + </span> + <span className="ulw-app-composer-send"> + <UlwIcon name="arrow-up" /> + </span> + </div> + </div> + </div> + ) +} + +export function SidePanel({ scene }: { readonly scene: UlwScene }): JSX.Element { + return ( + <aside className="ulw-side" aria-label="Environment and subagents panel"> + <div className="ulw-side-card"> + <span className="ulw-side-heading">Environment</span> + {ULW_DEMO_ENVIRONMENT.map(([label, value]) => ( + <div className="ulw-side-row" key={label}> + <span>{label}</span> + <span>{value}</span> + </div> + ))} + </div> + + <div className="ulw-side-card"> + <span className="ulw-side-heading">Subagents</span> + <div className="ulw-workers" aria-label="Subagent status list"> + {ULW_DEMO_WORKERS.map((worker) => ( + <div + className="ulw-worker" + data-live={scene.lanes.includes(worker.lane)} + key={worker.name} + > + <span className="ulw-worker-glyph" data-lane={worker.lane} aria-hidden="true"> + {worker.glyph} + </span> + <span className="ulw-worker-name">{worker.name}</span> + <small>{worker.role}</small> + </div> + ))} + </div> + </div> + + <div className="ulw-side-card ulw-app-side-note"> + <strong>{scene.sideTitle}</strong> + <span>{scene.sideBody}</span> + </div> + </aside> + ) +} diff --git a/packages/web/components/site/ulw-research-section.tsx b/packages/web/components/site/ulw-research-section.tsx new file mode 100644 index 0000000..b1323d7 --- /dev/null +++ b/packages/web/components/site/ulw-research-section.tsx @@ -0,0 +1,41 @@ +import type { JSX } from "react" +import { MarketingSection } from "../design-system/layout" +import { AccentSurface, MonoTag } from "../design-system/surfaces" +import { BodyText, Kicker, SectionHeading } from "../design-system/typography" +import { SITE_CONFIG } from "../../lib/site-config" + +/** + * ulw-research — copy grounded in plugins/omo/skills/ulw-research/SKILL.md + * (see .omo/evidence/copy-ledger.md). Lane chips are informational, not + * interactive, so they carry no hover states. + */ +export function UlwResearchSection(): JSX.Element { + const { ulwResearch } = SITE_CONFIG + + return ( + <MarketingSection className="mt-24 md:mt-32"> + <AccentSurface className="grid gap-6 md:grid-cols-[1.1fr_0.9fr] md:p-8"> + <div> + <Kicker>{ulwResearch.kicker}</Kicker> + <SectionHeading className="text-[clamp(26px,3.5vw,40px)]"> + {ulwResearch.title} + </SectionHeading> + <BodyText>{ulwResearch.body}</BodyText> + </div> + + <div className="flex flex-col justify-center gap-4"> + <ul className="grid grid-cols-2 gap-2"> + {ulwResearch.lanes.map((lane) => ( + <MonoTag key={lane} className="text-center"> + {lane} + </MonoTag> + ))} + </ul> + <p className="text-sm leading-relaxed text-[color:var(--text-muted)]"> + {ulwResearch.activation} + </p> + </div> + </AccentSurface> + </MarketingSection> + ) +} diff --git a/packages/web/e2e/landing-sections.spec.ts b/packages/web/e2e/landing-sections.spec.ts new file mode 100644 index 0000000..c0a8531 --- /dev/null +++ b/packages/web/e2e/landing-sections.spec.ts @@ -0,0 +1,99 @@ +import { expect, test } from "@playwright/test" +import { SITE_CONFIG } from "../lib/site-config" + +/** + * New landing sections contract (TDD target state). + * + * Team Mode and ulw-research sections render grounded copy only, and the + * information architecture keeps: hero → demo → install → commands → + * workflows → team mode → ulw-research → Hephaestus. + */ + +async function topOf(page: import("@playwright/test").Page, text: string): Promise<number> { + return page + .getByText(text, { exact: false }) + .first() + .evaluate((node) => node.getBoundingClientRect().top + window.scrollY) +} + +test.describe("team mode section", () => { + test("renders the grounded team mode copy", async ({ page }) => { + await page.goto("/") + await expect( + page.getByRole("heading", { name: SITE_CONFIG.teamMode.title }), + ).toBeVisible() + await expect( + page.getByText(SITE_CONFIG.teamMode.compositionRule, { exact: false }).first(), + ).toBeVisible() + await expect( + page.getByText(SITE_CONFIG.teamMode.whenTitle, { exact: false }).first(), + ).toBeVisible() + await expect( + page.getByText(SITE_CONFIG.teamMode.threadNote, { exact: false }).first(), + ).toBeVisible() + for (const member of SITE_CONFIG.teamMode.memberThreads) { + await expect(page.getByText(member.name, { exact: false }).first()).toBeVisible() + } + }) +}) + +test.describe("ulw-research section", () => { + test("renders the grounded ulw-research copy", async ({ page }) => { + await page.goto("/") + await expect( + page.getByRole("heading", { name: SITE_CONFIG.ulwResearch.title }), + ).toBeVisible() + await expect( + page.getByText(SITE_CONFIG.ulwResearch.body, { exact: false }).first(), + ).toBeVisible() + await expect( + page.getByText("Activates only on an explicit demand", { exact: false }).first(), + ).toBeVisible() + }) +}) + +test.describe("information architecture", () => { + test("keeps the planned section order", async ({ page }) => { + await page.goto("/") + + const hero = await page + .locator("h1") + .evaluate((node) => node.getBoundingClientRect().top + window.scrollY) + const install = await topOf(page, SITE_CONFIG.installCommand) + const demo = await page + .locator("#ulw-demo") + .evaluate((node) => node.getBoundingClientRect().top + window.scrollY) + const commands = await topOf(page, '$ulw-loop "task"') + const workflows = await topOf(page, SITE_CONFIG.featureWorkflows.title) + const teamMode = await topOf(page, SITE_CONFIG.teamMode.title) + const research = await topOf(page, SITE_CONFIG.ulwResearch.title) + // The hero eyebrow contains the omoIntro title case-insensitively, so + // anchor on the section heading role instead of raw text. + const hephaestus = await page + .getByRole("heading", { name: SITE_CONFIG.omoIntro.title }) + .evaluate((node) => node.getBoundingClientRect().top + window.scrollY) + + expect(hero).toBeLessThan(demo) + expect(demo).toBeLessThan(install) + expect(install).toBeLessThan(commands) + expect(commands).toBeLessThan(workflows) + expect(workflows).toBeLessThan(teamMode) + expect(teamMode).toBeLessThan(research) + expect(research).toBeLessThan(hephaestus) + + // The glassy Ultrawork badge showcase is back below Hephaestus. + // (Lazy-loaded: bring it into view so it acquires its box first.) + const badge = page.locator('img[src*="badge-ultrawork"]') + await badge.scrollIntoViewIfNeeded() + await expect(badge).toBeVisible() + const badgeTop = await badge.evaluate( + (node) => node.getBoundingClientRect().top + window.scrollY, + ) + expect(hephaestus).toBeLessThan(badgeTop) + + await page.screenshot({ + path: "../../.omo/evidence/g3-c1/landing-1280-full.png", + fullPage: true, + }) + }) +}) diff --git a/packages/web/e2e/ulw-demo.spec.ts b/packages/web/e2e/ulw-demo.spec.ts new file mode 100644 index 0000000..b178791 --- /dev/null +++ b/packages/web/e2e/ulw-demo.spec.ts @@ -0,0 +1,143 @@ +import { expect, type Page, test } from "@playwright/test" +import { SITE_CONFIG } from "../lib/site-config" +import { ULW_DEMO_SCENES } from "../lib/ulw-demo-scenes" + +// The one ask the whole demo answers (the user's opening message). +const SESSION_TITLE = SITE_CONFIG.ultraworkExample + +/** + * Ultrawork demo contract — v10 (appending chat replay). + * + * CONTRACT CHANGE (v5 → v10): the demo is no longer scene slides. It is ONE + * chat session replay: the user's opening ask renders as a message bubble, + * and the run then unfolds BENEATH it — tool-call rows, prose, code chips + * appended one after another (earlier entries persist; the transcript + * scrolls internally). The replay walks the whole grounded run to the + * checkpoint, then loops. Still non-playable: zero interactive controls, + * no slide progress bar; the footer keeps the running line ("Working + * for <elapsed>") plus the run-progress track that fills by phase. + * Reduced motion renders the COMPLETED transcript statically. The window + * is fixed dark; the sidebar shows the single constant session; the + * window's outer box never changes (inner scroll only). + */ + +const RESEARCH = ULW_DEMO_SCENES[0] +const CHECKPOINT = ULW_DEMO_SCENES[7] + +function activeSession(page: Page) { + return page + .locator("#ulw-demo") + .getByRole("navigation", { name: "Sessions" }) + .locator('[aria-current="true"]') +} + +test.describe("ulw demo — chat replay @happy", () => { + test("one ask, then the run appends beneath it", async ({ page }) => { + await page.goto("/") + const demo = page.locator("#ulw-demo") + await demo.scrollIntoViewIfNeeded() + + // The user's ask opens the session as a chat bubble; the mode flag and + // the first run entries are server-rendered beneath it. + await expect(demo.locator(".ulw-app-user")).toContainText(SESSION_TITLE) + await expect(page.getByText("ULTRAWORK MODE ENABLED!", { exact: true })).toBeVisible() + await expect(demo.locator(".ulw-window")).toHaveAttribute("data-window-theme", "dark") + await expect(activeSession(page)).toContainText(SESSION_TITLE) + + // NOT playable, no slide-timer chrome — just the alive markers. + await expect(demo.locator(".ulw-window button")).toHaveCount(0) + await expect(demo.locator(".ulw-app-progress")).toHaveCount(0) + await expect(demo.locator(".ulw-spinner")).toHaveCount(2) + await expect(demo.locator(".ulw-run-progress")).toHaveCount(1) + + // The transcript APPENDS: entry count grows while earlier entries stay. + const entries = demo.locator(".ulw-entry") + const before = await entries.count() + expect(before).toBeGreaterThan(0) + await expect + .poll(async () => entries.count(), { timeout: 12_000 }) + .toBeGreaterThan(before) + await expect(page.getByText(RESEARCH.title, { exact: true })).toBeVisible() + await expect(demo.locator(".ulw-app-user")).toContainText(SESSION_TITLE) + + // Inner scroll only: the window's outer box is unchanged by growth. + const ulwWindow = demo.locator(".ulw-window") + const boxA = await ulwWindow.boundingBox() + await page.waitForTimeout(4_000) + const boxB = await ulwWindow.boundingBox() + expect(boxA).not.toBeNull() + expect(boxB).not.toBeNull() + expect(Math.abs((boxA?.height ?? 0) - (boxB?.height ?? 0))).toBeLessThanOrEqual(1) + + await page.screenshot({ + path: "../../.omo/evidence/v10-replay-playing.png", + fullPage: false, + }) + }) + + test("walks the whole run to the checkpoint, then loops", async ({ page }) => { + await page.goto("/") + const demo = page.locator("#ulw-demo") + await demo.locator(".ulw-window").scrollIntoViewIfNeeded() + + // The replay reaches the final phase (multi-day elapsed on the goal) … + await expect(page.getByText(CHECKPOINT.title, { exact: true })).toBeVisible({ + timeout: 75_000, + }) + await expect(demo.getByText(`Working for ${CHECKPOINT.elapsed}`, { exact: true })).toBeVisible() + + // … and loops: the transcript resets to the opening state. + const entries = demo.locator(".ulw-entry") + await expect + .poll(async () => entries.count(), { timeout: 20_000 }) + .toBeLessThan(10) + await expect(demo.locator(".ulw-app-user")).toContainText(SESSION_TITLE) + }) +}) + +test.describe("ulw demo — reduced motion + mobile @edge", () => { + test("reduced motion shows the completed run statically", async ({ page }) => { + await page.emulateMedia({ reducedMotion: "reduce" }) + await page.goto("/") + const demo = page.locator("#ulw-demo") + await demo.scrollIntoViewIfNeeded() + + // The full transcript is there at once — nothing to wait for, nothing + // to click, and nothing appends afterwards. + await expect(demo.locator(".ulw-app-user")).toContainText(SESSION_TITLE) + await expect(page.getByText(CHECKPOINT.title, { exact: true })).toBeVisible() + await expect(demo.locator(".ulw-window button")).toHaveCount(0) + + const entries = demo.locator(".ulw-entry") + const settled = await entries.count() + await page.waitForTimeout(3_000) + await expect(entries).toHaveCount(settled) + }) + + test("no horizontal overflow at 390x844 and the sidebar collapses", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }) + await page.goto("/") + const demo = page.locator("#ulw-demo") + await demo.locator(".ulw-window").scrollIntoViewIfNeeded() + + // The session sidebar is hidden at mobile widths, like the real app. + await expect(page.getByRole("navigation", { name: "Sessions" })).toBeHidden() + + // The replay keeps appending; dynamic entries must not overflow. + const entries = demo.locator(".ulw-entry") + const before = await entries.count() + await expect + .poll(async () => entries.count(), { timeout: 12_000 }) + .toBeGreaterThan(before) + + const overflow = await page.evaluate( + () => document.documentElement.scrollWidth - document.documentElement.clientWidth, + ) + expect(overflow).toBeLessThanOrEqual(0) + + await page.screenshot({ + path: "../../.omo/evidence/v10-replay-mobile.png", + fullPage: false, + }) + }) +}) diff --git a/packages/web/lib/site-config.ts b/packages/web/lib/site-config.ts index 449ea68..5b851de 100644 --- a/packages/web/lib/site-config.ts +++ b/packages/web/lib/site-config.ts @@ -79,6 +79,45 @@ export const SITE_CONFIG = { "Skills auto-activate when a task matches their domain, so you do not need to study every one first. Add a skill name to your prompt when you want to call it explicitly; ulw-research is the maximum-saturation mode for deep codebase, web, official-docs, and OSS-repo research.", skills: ["ulw-research", "review-work", "remove-ai-slops", "frontend", "programming", "visual-qa", "LSP", "AST-grep"], }, + // Copy grounded in content/docs/ultrawork.md and ulw-loop.md — see .omo/evidence/copy-ledger.md. + ulwDemo: { + kicker: "Ultrawork, live", + title: "Watch an ultrawork run close the loop", + intro: + "Include ultrawork (or the short alias ulw) anywhere in your prompt and the harness switches to maximum-precision, outcome-first, evidence-driven orchestration. An agent saying it is done does not mean the work is done — the work is done when observable evidence verifies it.", + quote: "Plan, execute, verify, and keep the evidence attached.", + }, + // Copy grounded in plugins/omo/skills/teammode/SKILL.md — see .omo/evidence/copy-ledger.md. + teamMode: { + kicker: "Team Mode", + title: "Run a named team of cooperating Codex threads", + body: "One leader, durable state on disk. The main session is always the team leader: it splits the work and assigns each slice, holds live situational awareness of every member, verifies and QAs what they deliver, relays findings between members, and synthesizes the result.", + compositionRule: + "Members are defined by a concrete part, ownership area, or perspective — never a vague job role.", + whenTitle: "When a team beats plain subagents", + whenPoints: [ + "The work does not split into perfectly isolated pieces, but doing it in parallel is clearly more convenient — members need to see and react to each other's findings.", + "One task still needs exploration, yet its goal is already clear — parallel investigation under a fixed objective.", + ], + stateNote: + "A bundled cross-platform script writes the .omo/teams state plus an auto-generated member field manual.", + threadNote: "Sent by Codex from another thread", + memberThreads: [ + { name: "Triage feature and question issues", status: "running" }, + { name: "Review PR readiness", status: "running" }, + { name: "Triage LazyCodex issues", status: "reported" }, + { name: "Triage runtime bug reports", status: "reported" }, + ], + }, + // Copy grounded in plugins/omo/skills/ulw-research/SKILL.md — see .omo/evidence/copy-ledger.md. + ulwResearch: { + kicker: "$ulw-research", + title: "Maximum-saturation research orchestration", + body: "Parallel explore and librarian swarms across the codebase, web, official docs, and OSS repos; a recursive expand loop driven by the leads workers return; empirical verification by running code; cited synthesis and optional reports.", + activation: + "Activates only on an explicit demand for research — say ulw-research or any ulw research wording in your prompt. While active, exhaustive coverage is the goal.", + lanes: ["codebase", "web", "official docs", "OSS repos"], + }, } as const; export type SiteConfig = typeof SITE_CONFIG; diff --git a/packages/web/lib/ulw-demo-scenes.ts b/packages/web/lib/ulw-demo-scenes.ts new file mode 100644 index 0000000..204bad4 --- /dev/null +++ b/packages/web/lib/ulw-demo-scenes.ts @@ -0,0 +1,240 @@ +/** + * Source-grounded scene data for the interactive Ultrawork demo. + * + * Every visible string traces to OMO/LazyCodex source truth via + * `.omo/reference/source-ledger.md` (workflow beats → omo source lines) and + * `content/docs/{ultrawork,ulw-loop,manual-qa}.md`. Do not invent beats, + * metrics, or command flags here — extend the ledger first. + */ + +export type UlwLane = + | "root" + | "explore" + | "library" + | "plan" + | "todo" + | "execute" + | "test" + | "qa" + | "review" + | "continuation"; + +export type UlwScene = { + readonly key: string; + readonly tab: string; + readonly command: string; + readonly status: string; + readonly title: string; + readonly body: string; + readonly composer: string; + readonly sideTitle: string; + readonly sideBody: string; + readonly ledger: string; + readonly json: string; + readonly lanes: readonly UlwLane[]; + readonly proof: number; + readonly elapsed: string; +}; + +export type UlwWorker = { + readonly name: string; + readonly role: string; + readonly lane: UlwLane; + readonly glyph: string; +}; + +export const ULW_DEMO_WORKERS = [ + { name: "Root Orchestrator", role: "holding goal", lane: "root", glyph: "R" }, + { name: "Explorer the 53rd", role: "repo scan", lane: "explore", glyph: "X" }, + { name: "Explorer the 54th", role: "tests", lane: "explore", glyph: "X" }, + { name: "Librarian the 24th", role: "docs", lane: "library", glyph: "L" }, + { name: "Librarian the 25th", role: "contracts", lane: "library", glyph: "L" }, + { name: "Plan agent / Prometheus", role: "waiting", lane: "plan", glyph: "P" }, + { name: "TodoWrite adapter", role: "tasks", lane: "todo", glyph: "T" }, + { name: "Executor the 23rd", role: "implementation", lane: "execute", glyph: "E" }, + { name: "TDD Executor the 12th", role: "red/green", lane: "test", glyph: "T" }, + { name: "QA Executor the 23rd", role: "Manual QA", lane: "qa", glyph: "Q" }, + { name: "lazycodex-code-reviewer", role: "codeReview", lane: "review", glyph: "C" }, + { name: "lazycodex-gate-reviewer", role: "gateReview", lane: "review", glyph: "G" }, + { name: "Stop/SubagentStop hook", role: "continue", lane: "continuation", glyph: "S" }, +] as const satisfies readonly UlwWorker[]; + +export const ULW_DEMO_SCENES = [ + { + key: "research", + tab: "01 Research", + command: 'task(subagent_type="explorer") + task(subagent_type="librarian")', + status: "Research wave · Explorer and Librarian gather source truth", + title: "Ultrawork starts by fanning out research.", + body: "Root does not guess. Explorer reads the local implementation, Librarian checks docs and contracts, and their findings become the input to the Plan agent.", + composer: "Root is waiting for Explorer and Librarian findings before the Plan agent writes anything.", + sideTitle: "Research lanes are live.", + sideBody: "Explorer reads the local implementation while Librarian checks docs and skill contracts. Root waits for both before planning.", + ledger: "plan_created pending research facts", + json: '{ "activeGoalId": null, "criteria": "pending" }', + lanes: ["root", "explore", "library"], + proof: 0, + elapsed: "3d 2h 14m", + }, + { + key: "plan", + tab: "02 Plan", + command: "$ulw-plan .omo/plans/ultrawork-demo.md", + status: "Planning · Plan agent / Prometheus synthesizes the lanes", + title: "Plan first, then execute with evidence.", + body: "The Plan agent merges Explorer and Librarian findings into a decision-complete plan: references, acceptance criteria, QA channel, evidence path, and workers.", + composer: "Plan agent is writing the handoff surface; no product code is touched here.", + sideTitle: "Plan agent is planner-only.", + sideBody: "The safe claim is planning, not execution. Implementation begins only after start-work or ulw-loop picks up the plan.", + ledger: "plan_created .omo/plans/ultrawork-demo.md\nsteering_accepted research findings", + json: '{ "briefPath": ".omo/ulw-loop/brief.md", "goalsPath": ".omo/ulw-loop/goals.json" }', + lanes: ["root", "explore", "library", "plan"], + proof: 1, + elapsed: "3d 4h 51m", + }, + { + key: "todo", + tab: "03 Todo", + command: "TodoWrite -> task_create + omo ulw-loop create-goals", + status: "Todo registration · file-backed tasks and success criteria", + title: "The todo list becomes durable state.", + body: "TodoWrite commits atomic work before generation. In OMO, ulw-loop persists goals, criteria, and the append-only ledger under `.omo/ulw-loop/`.", + composer: "Registering G001 with C001 happy, C002 edge, and C003 regression criteria.", + sideTitle: "TodoWrite is not decoration.", + sideBody: "Tasks are marked complete only after their matching artifact lands; multi-agent work uses dependencies instead of loose memory.", + ledger: "plan_created 1 goal(s) created\nG001 status=pending\nC001/C002/C003 status=pending", + json: '{ "activeGoalId": null, "goals": [{ "id": "G001-ultrawork-demo", "status": "pending" }] }', + lanes: ["root", "plan", "todo"], + proof: 2, + elapsed: "3d 7h 33m", + }, + { + key: "assign", + tab: "04 Assign", + command: "$start-work .omo/plans/ultrawork-demo.md", + status: "Assignment · subagents receive bounded tasks", + title: "Root assigns work instead of doing it all directly.", + body: "Executor owns implementation, TDD Executor owns the failing-first proof, QA Executor owns the real browser scenario, and reviewers own the final gate.", + composer: "Executor, TDD, QA, and review lanes are active; root watches dependencies and drift.", + sideTitle: "Subagents are visible work lanes.", + sideBody: "The roster makes delegation observable: each worker owns a narrow deliverable and returns artifact-backed evidence.", + ledger: "goal_started G001-ultrawork-demo Attempt 1\nactiveGoalId=G001-ultrawork-demo", + json: '{ "activeGoalId": "G001-ultrawork-demo", "attempt": 1, "status": "in_progress" }', + lanes: ["root", "todo", "execute", "test", "qa", "review", "continuation"], + proof: 3, + elapsed: "3d 11h 06m", + }, + { + key: "red", + tab: "05 TDD RED", + command: "bun test ultrawork-demo.test.ts # TDD red", + status: "TDD red · failing-first proof captured", + title: "The first proof is allowed to fail.", + body: "A behavior change gets a RED proof before the fix. ULW treats a hollow test as non-evidence, so the failure has to match the user-facing contract.", + composer: "TDD Executor captured RED for the right reason; Executor can now make the smallest GREEN change.", + sideTitle: "TDD red is a gate, not theater.", + sideBody: "The run records the failing proof before production changes, then routes the fix to the right worker.", + ledger: "criterion_failed G001 C001\nmessage=TDD red captured before fix", + json: '{ "C001": { "status": "fail", "capturedEvidence": "TDD red" } }', + lanes: ["root", "execute", "test"], + proof: 4, + elapsed: "3d 15h 48m", + }, + { + key: "green", + tab: "06 GREEN", + command: 'omo ulw-loop record-evidence --goal-id G001 --criterion-id C001 --status pass --evidence "GREEN unit proof + cleanup receipt"', + status: "GREEN · criterion-scoped evidence is recorded", + title: "GREEN lands only with recorded evidence.", + body: "After the smallest fix, ULW records non-empty evidence against the exact criterion. The ledger entry is `evidence_captured`, not a vague done message.", + composer: "Executor returned GREEN; root is recording C001 evidence before moving to real-surface QA.", + sideTitle: "record-evidence is append-only proof.", + sideBody: "The criterion now has status pass, capturedEvidence, capturedAt, and a ledger entry.", + ledger: "evidence_captured G001 C001 status=pass\ncapturedEvidence=GREEN unit proof", + json: '{ "C001": { "status": "pass", "capturedEvidence": "GREEN unit proof" } }', + lanes: ["root", "execute", "test", "todo"], + proof: 5, + elapsed: "3d 19h 22m", + }, + { + key: "qa-retry", + tab: "07 QA retry", + command: "browser QA -> QA fail -> omo ulw-loop complete-goals --retry-failed", + status: "QA fail · retry the same criterion until the surface passes", + title: "Manual QA can send the work back.", + body: "Tests prove the unit contract; browser/manual QA proves the real surface. If QA fails, ULW records failure evidence, retries the goal, and keeps the loop moving.", + composer: "QA Executor found a real-surface mismatch; root records goal_failed and resumes with --retry-failed.", + sideTitle: "QA fail is part of the loop.", + sideBody: "The demo should show retry, not pretend the first pass is final.", + ledger: "goal_failed G001 evidence=QA fail\ncriterion_failed C002 real surface mismatch\ngoal_retried G001 Attempt 2", + json: '{ "status": "in_progress", "attempt": 2, "failureReason": "QA fail" }', + lanes: ["root", "execute", "test", "qa", "continuation"], + proof: 6, + elapsed: "3d 22h 57m", + }, + { + key: "checkpoint", + tab: "08 Checkpoint", + command: 'omo ulw-loop checkpoint --goal-id G001 --status complete --evidence "manual QA + review + criteria evidence" --codex-goal-json .omo/evidence/get-goal-complete.json --quality-gate-json .omo/evidence/quality-gate.json', + status: "Checkpoint · quality gate closes the story", + title: "Done means the quality gate passes.", + body: "The final story needs codeReview, manualQa, gateReview, iteration, criteriaCoverage, and artifact-backed evidence before checkpointing complete.", + composer: "Root has code review, Manual QA screenshots, gate review, iteration proof, and criteria coverage.", + sideTitle: "checkpoint --status complete is the close.", + sideBody: "Only after the quality gate is clean does the run checkpoint the story and write aggregate completion evidence.", + ledger: "evidence_captured C002/C003 status=pass\naggregate_completed G001\ncheckpoint --status complete", + json: '{ "aggregateCompletion": { "status": "complete" }, "qualityGate": "clean" }', + lanes: ["root", "qa", "review", "todo"], + proof: 7, + elapsed: "4d 2h 41m", + }, +] as const satisfies readonly UlwScene[]; + +export const ULW_DEMO_ENVIRONMENT: readonly (readonly [string, string])[] = [ + ["Changes", "scoped"], + [".omo/ulw-loop", "ledger"], + ["Mode", "ulw ulw ulw"], +] as const; + +/* ---- v10 chat-replay timeline (derived — every string above is reused + verbatim; the only new strings are the user ask, sourced from + SITE_CONFIG.ultraworkExample at the consumer, and the mode flag that + already ships in the window chrome). ---- */ + +export type UlwEntryKind = "mode" | "status" | "prose" | "tool" | "code"; + +export type UlwEntry = { + readonly id: string; + readonly kind: UlwEntryKind; + readonly heading?: string; + readonly text: string; + readonly phase: number; +}; + +export const ULW_DEMO_TIMELINE: readonly UlwEntry[] = [ + { id: "mode", kind: "mode", text: "ULTRAWORK MODE ENABLED!", phase: 0 }, + ...ULW_DEMO_SCENES.flatMap((scene, phase) => [ + { id: `${scene.key}-status`, kind: "status" as const, text: scene.status, phase }, + { id: `${scene.key}-cmd`, kind: "code" as const, text: scene.command, phase }, + { + id: `${scene.key}-prose`, + kind: "prose" as const, + heading: scene.title, + text: scene.body, + phase, + }, + ...scene.ledger.split("\n").map((line, i) => ({ + id: `${scene.key}-tool-${i}`, + kind: "tool" as const, + text: line, + phase, + })), + { id: `${scene.key}-json`, kind: "code" as const, text: scene.json, phase }, + ]), +]; + +/** Cadence of the replay: one appended entry per tick. */ +export const ULW_DEMO_ENTRY_MS = 900; + +/** Entries visible in the server-rendered opening state (ask + mode + first activity). */ +export const ULW_DEMO_INITIAL_ENTRIES = 4;