diff --git a/packages/web/DESIGN.md b/packages/web/DESIGN.md index c8f9920..0b977d3 100644 --- a/packages/web/DESIGN.md +++ b/packages/web/DESIGN.md @@ -4,11 +4,17 @@ 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 light sage-paper +canvas, editorial structure with dotted column rules, and green as the single brand signal. The +signature composition is a white card sitting on the pale sage ground with the geometric +rounded-square `L` mark. Dark surfaces still exist, but only as deliberate accents — code blocks, +command surfaces, the Hephaestus showcase band, and the demo window's dark theme — small dark +windows on light ground, never the page itself. The brand color is green, not teal, cyan, purple, +or blue. ## 2. Color @@ -16,36 +22,103 @@ LazyCodex feels like a serious command surface for complex codebases: near-black | 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` | `#f4f6ee` | Page canvas | +| Surface/night | `--surface-night` | `#e9ede0` | Footer and deeper page bands | +| Surface/subtle | `--surface-1` | `rgba(16,25,20,0.03)` | Hover and quiet fills | +| Surface/raised | `--surface-2` | `rgba(16,25,20,0.05)` | Secondary tonal layer | +| Surface/strong | `--surface-3` | `rgba(16,25,20,0.08)` | Stronger tonal layer | +| Surface/card | `--card-base` | `#ffffff` | Hero card, white content cards | +| Surface/panel | `--surface-panel` | `#fbfcf7` | Panels, install bar | +| Surface/alt | `--surface-panel-alt` | `#f7faf2` | Alternate panel | +| Surface/deep | `--surface-panel-deep` | `#f3f7ec` | Deep panel | +| 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` | `#166534` | CTAs, focus, active docs links (AA on light) | +| Accent/soft | `--accent-primary-soft` | `rgba(21,128,61,0.08)` | Soft green fills | +| Accent/border | `--accent-primary-border` | `rgba(21,128,61,0.28)` | Soft green outlines | +| Accent/mint | `--accent-mint` | `#86efac` | Fills and decoration ONLY — never text on light | +| Accent/glow | `--accent-glow` | `#14532d` | Deep green emphasis | +| Text/primary | `--text-primary` | `#101914` | Main text and headings | +| Text/secondary | `--text-secondary` | `#3f4b43` | Supporting text | +| Text/tertiary | `--text-tertiary` | `#55645b` | Labels, metadata | +| Text/muted | `--text-muted` | `rgba(16,25,20,0.75)` | Body copy | +| Text/soft | `--text-soft` | `#14532d` | Deep-green tinted text | +| Border/subtle | `--border-subtle` | `rgba(16,25,20,0.10)` | Dividers, dotted rules, quiet controls | +| Border/default | `--border-default` | `rgba(16,25,20,0.16)` | Panels and cards | +| Status/success | `--status-success` | `#15803d` | Positive status | +| Status/warning | `--status-warning` | `#a16207` | Warnings | +| Status/error | `--status-error` | `#b91c1c` | Errors | + +`::selection` uses a `#bbf7d0` background with `#14532d` text. `:focus-visible` outlines use +`--accent-primary`. The `html` element declares `color-scheme: light`; the site identity is a +FIXED light canvas — there is no site-wide `prefers-color-scheme` flip. Dark appears only inside +the sanctioned dark surfaces below. + +### Codex window adapter tokens (ulw-demo / team-mode mocks only) + +The interactive Ultrawork demo and the Team Mode thread mock reproduce the Codex Desktop surface +on the light canvas. The window carries its own isolated adapter palette with two themes selected +by `data-window-theme="light|dark"` on `.ulw-window` — light is the default and the +server-rendered state; a `role="group"` toggle switches it (see § CodexWindow). Adapter tokens +never leak into ordinary landing/docs UI, and ordinary tokens never restyle the window interior. + +Light theme (default block on `.ulw-window`): + +| 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): + +| Role | Token | Value | +| --- | --- | --- | +| Window/canvas | `--codex-window-bg` | `#101613` | +| Window/text | `--codex-window-text` | `#e8f0ea` | +| Window/chrome, border, chip, text-soft, active(+border), accent, glyph-text, traffic | same names | tuned values derived from the retired `#0E1411` dark-panel family — `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-`) 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-cyan` and `--accent-teal` remain green aliases only for compatibility. +- `--accent-mint` (`#86efac`) is a fill/decoration color only. It fails AA as text on the light canvas and must never be used for copy. - 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. +- Dark surfaces are allowed ONLY in four places: docs/code blocks (`pre`), command surfaces (`CommandCodeSurface`), the Hephaestus showcase band (`ShowcaseSurface`), and the demo window's dark theme. Everything else sits on the light canvas. +- 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: `#101613` (dark accent surface), `#dcfce7` (text on dark code chips), gradient stops `#15803d`/`#16a34a`/`#22c55e`, brand glow `rgba(21,128,61,0.25)`, and card shadow `rgba(16,25,20,0.04)`. ## 3. Typography @@ -67,7 +140,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 +161,98 @@ 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 light identity. +- 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 `
` — `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; tile fill `var(--card-base)`, stroke `var(--accent-primary)`. +- **Variants**: `nav` 24px geometry, `hero` 160px geometry with `HeroBrandMark` glow wrapper (soft `rgba(21,128,61,0.25)` glow tuned for the light 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`, `InlineCode`. +- **Usage**: marketing sections, showcase titles, badges, and command/code snippets. `GradientTitle` uses the light-legible green gradient (`#15803d → #16a34a → #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. +- **Usage**: white cards (`--card-base` + `--border-subtle` + soft shadow) for command cards, comparison cards, and numbered workflow rows. `ShowcaseSurface` stays an intentional dark accent band (`#101613`) for the Hephaestus showcase; `CommandCodeSurface` stays a dark code chip (`#101613` with `#dcfce7` text) — code surfaces are deliberately dark on the light canvas. - **Depth**: border plus tonal shift, 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 data in `lib/ulw-demo-scenes.ts`. +- **Structure**: Codex Desktop window (adapter tokens above) on the light canvas: title bar with traffic lights and + `ULTRAWORK MODE ENABLED!` badge, transcript pane (command chip → status line → scene headline → + scene body → 8 numbered workflow steps), right rail (Environment card, Subagents roster, + narrative card, `goals.json / ledger.jsonl` card), composer bar, scene tab strip with play/pause. +- **Variants**: 8 scenes (`research → plan → todo → assign → red → green → qa-retry → checkpoint`), + each atomically updating command, status, headline, body, active step, roster lanes, proof chips, + ledger, and JSON card. +- **Window themes**: light (default) and dark, driven by `data-window-theme="light|dark"` on + `.ulw-window`. Light is the server-rendered default (faithful to the real Codex app and the state + Lighthouse audits); dark re-themes only the window interior through the + `[data-window-theme="dark"]` token block — the page canvas never changes. +- **Window theme toggle**: a `role="group"` container labeled `aria-label="Demo window theme"` + holding two ` + {/* display:contents keeps the shared flex-wrap layout while giving the + tablist only tab children (aria-required-children). */} +
+ {ULW_DEMO_SCENES.map((entry, index) => ( + + ))} +
+
+ + +
+ + + ) +} 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..632b0ae --- /dev/null +++ b/packages/web/components/site/ulw-demo/ulw-demo-section.tsx @@ -0,0 +1,42 @@ +import type { JSX } from "react" +import { MarketingSection } from "../../design-system/layout" +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). */ +
+ + {SITE_CONFIG.ulwDemo.kicker} +

+ {SITE_CONFIG.ulwDemo.title} +

+

+ {SITE_CONFIG.ulwDemo.intro} +

+ +
+ + {SITE_CONFIG.ultraworkExample} + +
+ +
+ “{SITE_CONFIG.ulwDemo.quote}” +
+ +
+ +
+
+
+ ) +} 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..d4e376c --- /dev/null +++ b/packages/web/components/site/ulw-demo/window-panes.tsx @@ -0,0 +1,157 @@ +import type { JSX } from "react" +import { + ULW_DEMO_ENVIRONMENT, + ULW_DEMO_PROOFS, + ULW_DEMO_STEPS, + ULW_DEMO_WORKERS, + type UlwScene, +} from "../../../lib/ulw-demo-scenes" + +/** + * Presentational panes for the Codex window. Pure functions of the active + * scene — every visible string comes from `lib/ulw-demo-scenes.ts` + * (source-grounded, see .omo/evidence/copy-ledger.md). + */ + +export function WindowChrome(): JSX.Element { + return ( + <> + +
+
+ + ) +} + +export function TranscriptPane({ + scene, + sceneIndex, +}: { + readonly scene: UlwScene + readonly sceneIndex: number +}): JSX.Element { + return ( +
+
+ + + {scene.command} + +
+ + {/* The live region stays OUTSIDE the keyed swap subtree: React must + mutate its text in place for screen readers to announce scenes. */} + + {scene.status} + +
+

{scene.title}

+

{scene.body}

+
+ +
+ {ULW_DEMO_STEPS.map((step, index) => ( +
+ {String(index + 1).padStart(2, "0")} +
+ {step.heading} +

{step.detail}

+
+
+ ))} +
+ +
+ {ULW_DEMO_PROOFS.map((proof, index) => ( + + {proof} + + ))} +
+
+ ) +} + +export function ComposerBar({ scene }: { readonly scene: UlwScene }): JSX.Element { + return ( +
+ + {scene.composer} + Full access + 5.5 High +
+ ) +} + +export function SideRail({ scene }: { readonly scene: UlwScene }): JSX.Element { + return ( + + ) +} 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..4fdd34c --- /dev/null +++ b/packages/web/components/site/ulw-research-section.tsx @@ -0,0 +1,44 @@ +import type { JSX } from "react" +import { MarketingSection } from "../design-system/layout" +import { AccentSurface } 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 ( + + +
+ {ulwResearch.kicker} + + {ulwResearch.title} + + {ulwResearch.body} +
+ +
+
    + {ulwResearch.lanes.map((lane) => ( +
  • + {lane} +
  • + ))} +
+

+ {ulwResearch.activation} +

+
+
+
+ ) +} diff --git a/packages/web/e2e/landing-sections.spec.ts b/packages/web/e2e/landing-sections.spec.ts new file mode 100644 index 0000000..9147e2f --- /dev/null +++ b/packages/web/e2e/landing-sections.spec.ts @@ -0,0 +1,89 @@ +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 { + 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) + + 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..86bf35a --- /dev/null +++ b/packages/web/e2e/ulw-demo.spec.ts @@ -0,0 +1,169 @@ +import { expect, test } from "@playwright/test" +import { ULW_DEMO_SCENES } from "../lib/ulw-demo-scenes" + +/** + * Interactive Ultrawork demo contract (TDD target state). + * + * The demo is a live-DOM Codex-desktop window driven by a typed scene machine: + * autoplay on scroll-into-view, scene tabs (role=tab), play/pause (aria-pressed), + * reduced-motion disables autoplay, and no horizontal overflow at 390px. + */ + +const RESEARCH = ULW_DEMO_SCENES[0] +const RED = ULW_DEMO_SCENES[4] +const CHECKPOINT = ULW_DEMO_SCENES[7] + +test.describe("ulw demo — happy path @happy", () => { + test("renders scene 0, autoplays forward, and tabs jump to checkpoint", async ({ page }) => { + await page.goto("/") + const demo = page.locator("#ulw-demo") + await demo.scrollIntoViewIfNeeded() + + // Scene 0 is the server-rendered initial state. + await expect(page.getByText(RESEARCH.title, { exact: true })).toBeVisible() + await expect(page.getByText("ULTRAWORK MODE ENABLED!", { exact: true })).toBeVisible() + + // Autoplay must advance beyond scene 0 once in view (interval ~7s). + await expect(page.getByRole("tab", { name: ULW_DEMO_SCENES[1].tab })).toHaveAttribute( + "aria-selected", + "true", + { timeout: 12_000 }, + ) + + // Direct scene selection: checkpoint updates every pane atomically. + await page.getByRole("tab", { name: CHECKPOINT.tab }).click() + await expect(page.getByText(CHECKPOINT.title, { exact: true })).toBeVisible() + await expect(demo.getByText("checkpoint --status complete", { exact: false }).first()).toBeVisible() + await expect(demo.getByText(CHECKPOINT.sideTitle, { exact: true })).toBeVisible() + + // Play/pause is a real control with observable state. + const playToggle = page.getByRole("button", { name: /pause|play/i }).first() + const before = await playToggle.getAttribute("aria-pressed") + await playToggle.click() + await expect(playToggle).not.toHaveAttribute("aria-pressed", String(before)) + + await page.screenshot({ + path: "../../.omo/evidence/g2-c1-demo-checkpoint.png", + fullPage: false, + }) + }) +}) + +test.describe("ulw demo — reduced motion + mobile @edge", () => { + test("reduced motion disables autoplay but tabs still switch scenes", async ({ page }) => { + await page.emulateMedia({ reducedMotion: "reduce" }) + await page.goto("/") + const demo = page.locator("#ulw-demo") + await demo.scrollIntoViewIfNeeded() + + await expect(page.getByText(RESEARCH.title, { exact: true })).toBeVisible() + + // No autoplay: after > one interval the first tab is still selected. + await page.waitForTimeout(9_000) + await expect(page.getByRole("tab", { name: RESEARCH.tab })).toHaveAttribute( + "aria-selected", + "true", + ) + + await page.getByRole("tab", { name: RED.tab }).click() + await expect(page.getByText(RED.title, { exact: true })).toBeVisible() + }) + + test("no horizontal overflow at 390x844 with the last scene open", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }) + await page.goto("/") + const demo = page.locator("#ulw-demo") + await demo.scrollIntoViewIfNeeded() + + await page.getByRole("tab", { name: CHECKPOINT.tab }).click() + await expect(page.getByText(CHECKPOINT.title, { exact: true })).toBeVisible() + + const overflow = await page.evaluate( + () => document.documentElement.scrollWidth - document.documentElement.clientWidth, + ) + expect(overflow).toBeLessThanOrEqual(0) + + await page.screenshot({ + path: "../../.omo/evidence/g2-c2-demo-mobile.png", + fullPage: false, + }) + }) +}) + +test.describe("ulw demo — window theme toggle", () => { + test("defaults to the light window theme with an accessible toggle group @happy", async ({ + page, + }) => { + await page.goto("/") + const ulwWindow = page.locator("#ulw-demo .ulw-window") + + // Light is the default (faithful to the real Codex app; Lighthouse audits it). + await expect(ulwWindow).toHaveAttribute("data-window-theme", "light") + + const group = page.getByRole("group", { name: "Demo window theme" }) + await expect(group.getByRole("button", { name: "Light" })).toHaveAttribute( + "aria-pressed", + "true", + ) + await expect(group.getByRole("button", { name: "Dark" })).toHaveAttribute( + "aria-pressed", + "false", + ) + }) + + test("clicking Dark flips data-window-theme and aria-pressed states @happy", async ({ + page, + }) => { + await page.goto("/") + const ulwWindow = page.locator("#ulw-demo .ulw-window") + const group = page.getByRole("group", { name: "Demo window theme" }) + const lightButton = group.getByRole("button", { name: "Light" }) + const darkButton = group.getByRole("button", { name: "Dark" }) + + await expect(darkButton).toBeVisible() + await darkButton.click() + + await expect(ulwWindow).toHaveAttribute("data-window-theme", "dark") + await expect(darkButton).toHaveAttribute("aria-pressed", "true") + await expect(lightButton).toHaveAttribute("aria-pressed", "false") + }) + + test("keyboard: Tab reaches Dark and Enter flips the window theme @edge", async ({ + page, + }) => { + await page.goto("/") + const ulwWindow = page.locator("#ulw-demo .ulw-window") + const group = page.getByRole("group", { name: "Demo window theme" }) + const lightButton = group.getByRole("button", { name: "Light" }) + const darkButton = group.getByRole("button", { name: "Dark" }) + + // Standard tab order (locked contract): Light and Dark are plain buttons, + // so Tab moves focus from Light to Dark and Enter activates it. + await expect(lightButton).toBeVisible() + await lightButton.focus() + await page.keyboard.press("Tab") + await expect(darkButton).toBeFocused() + await page.keyboard.press("Enter") + + await expect(ulwWindow).toHaveAttribute("data-window-theme", "dark") + await expect(darkButton).toHaveAttribute("aria-pressed", "true") + await expect(lightButton).toHaveAttribute("aria-pressed", "false") + }) + + test("dark window theme keeps the scene-0 transcript visible @edge", async ({ page }) => { + // Reduced motion pins the demo on scene 0 so the visibility check is stable. + await page.emulateMedia({ reducedMotion: "reduce" }) + await page.goto("/") + const ulwWindow = page.locator("#ulw-demo .ulw-window") + const darkButton = page + .getByRole("group", { name: "Demo window theme" }) + .getByRole("button", { name: "Dark" }) + + await expect(darkButton).toBeVisible() + await darkButton.click() + await expect(ulwWindow).toHaveAttribute("data-window-theme", "dark") + + await expect(page.getByText(RESEARCH.title, { exact: true })).toBeVisible() + await expect(page.getByText("ULTRAWORK MODE ENABLED!", { exact: true })).toBeVisible() + }) +}) 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..367f31e --- /dev/null +++ b/packages/web/lib/ulw-demo-scenes.ts @@ -0,0 +1,214 @@ +/** + * 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; +}; + +export type UlwStep = { readonly heading: string; readonly detail: string }; + +export type UlwWorker = { + readonly name: string; + readonly role: string; + readonly lane: UlwLane; + readonly glyph: string; +}; + +export const ULW_DEMO_STEPS = [ + { heading: "Explorer + Librarian swarm", detail: "Call parallel subagents to find repo truth, docs, commands, and inaccurate claims to avoid." }, + { heading: "Plan agent / Prometheus", detail: "Combine research into a decision-complete plan before implementation begins." }, + { heading: "TodoWrite + task system", detail: "Register atomic tasks and blockedBy edges; `.omo/ulw-loop/goals.json` stores criteria." }, + { heading: "$start-work assigns lanes", detail: "Executor, QA Executor, reviewer, and gate workers receive bounded deliverables." }, + { heading: "TDD red", detail: "Capture the failing-first proof before production code changes." }, + { heading: "GREEN + record-evidence", detail: "Make the smallest fix, pass the test, and record criterion-scoped evidence." }, + { heading: "QA fail -> goal_retried", detail: "Manual QA can fail the run; ULW records the failure and retries the same criterion." }, + { heading: "Quality gate + checkpoint", detail: "Only code review, manualQa, gateReview, iteration, and coverage evidence close the story." }, +] as const satisfies readonly UlwStep[]; + +export const ULW_DEMO_PROOFS = [ + "research facts", + "plan artifact", + "TodoWrite", + "worker lanes", + "TDD red", + "record-evidence", + "QA fail retry", + "checkpoint complete", +] as const satisfies readonly 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, + }, + { + 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, + }, + { + 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, + }, + { + 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, + }, + { + 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, + }, + { + 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, + }, + { + 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, + }, + { + 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, + }, +] 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; + +export const ULW_DEMO_AUTOPLAY_MS = 7000; diff --git a/packages/web/public/img/badge-ultrawork.avif b/packages/web/public/img/badge-ultrawork.avif deleted file mode 100644 index 2c8fa56..0000000 Binary files a/packages/web/public/img/badge-ultrawork.avif and /dev/null differ diff --git a/packages/web/public/img/badge-ultrawork.png b/packages/web/public/img/badge-ultrawork.png deleted file mode 100644 index 50cb940..0000000 Binary files a/packages/web/public/img/badge-ultrawork.png and /dev/null differ diff --git a/packages/web/public/img/badge-ultrawork.webp b/packages/web/public/img/badge-ultrawork.webp deleted file mode 100644 index d518df4..0000000 Binary files a/packages/web/public/img/badge-ultrawork.webp and /dev/null differ