From 0d688ca94a6adf5500f7c99ecd7a894e6f97e139 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 24 Jun 2026 12:36:24 +0200 Subject: [PATCH] feat(designbook): technology-neutral IR + stack-adapter pipeline (WHYNOT-WP-0002 T01-T06) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Author the design language once in the canonical React designbook and project it one-way onto each stack: React -> designbook/ -> ir/ -> adapters//. Phase 0 — contracts & governance (T01-T03): - ir/SCHEMA.md + ir/schema/{component,tokens}.schema.json — neutral IR contract (W3C DTCG tokens; React prop -> HTML attribute mapping; non-portable props flagged). - adapters/ADAPTER_CONTRACT.md — inputs, drift-report + parity-result shapes, idempotency rules, CI exit codes (0 ok / 2 usage / 3 drift / 4 parity / 5 internal). - .claude/rules/designbook-propagation.md + DesignSystemIntroduction.md §5.1 — one-way directionality + drift-resolution workflow. T04 — canonical React designbook + the missing pull tool: - The bundled /design-sync skill only PUSHES repo->cloud; it cannot populate designbook/. Added scripts/designbook_pull.py + `make designbook-pull`, which drives the local claude binary headless (acceptEdits) so DesignSync fetch+write runs in a subprocess (contents never hit the orchestrator's context). Pulled 44 files; excludes the _whynot-design-seed/ self-copy. Corrected the docs that wrongly called /design-sync the pull. T05 — IR extractor (scripts/ir-extract.mjs + `make ir`): - ir/tokens.json (80 tokens, DTCG, var() -> {ref} alias resolution); ir/components/*.json (10 contracts parsed from .jsx signatures: enum/boolean/number inference, prop->attr map, style/callback marked non-portable); ir/exemplars/*. T06 — Lit token adapter (adapters/lit/ + `make adapt-lit`): - Full-gen tokens into src/styles/colors_and_type.css :root (marker-bounded, idempotent no-op on re-run; hand-authored type CSS preserved). NOTE: token regen synced Lit to canonical React — fonts IBM Plex -> system stacks and 8 status tokens added. This is a VISUAL change: review and run `pnpm test:visual:update` before merge. Remaining: T07 scaffold+drift, T08 parity, T09 runbook, T10 2nd-adapter. Co-Authored-By: Claude Opus 4.8 --- .claude/rules/designbook-propagation.md | 59 + .claude/rules/stack-and-commands.md | 2 +- CHANGELOG.md | 29 +- CLAUDE.md | 1 + DesignSystemIntroduction.md | 53 + Makefile | 13 +- RecentChanges.md | 51 +- adapters/ADAPTER_CONTRACT.md | 137 ++ adapters/lit/README.md | 45 + adapters/lit/adapt.mjs | 94 ++ designbook/.design-pull.json | 18 + designbook/.design-sync.json | 6 + designbook/REACT_CANONICAL_DECISION.md | 63 + designbook/_ds_bundle.js | 1396 +++++++++++++++++ designbook/_ds_manifest.json | 1 + designbook/colors_and_type.css | 293 ++++ designbook/preview/brand-iconography.html | 36 + designbook/preview/brand-lockups.html | 26 + designbook/preview/brand-logo.html | 21 + designbook/preview/brand-wireframe-motif.html | 43 + designbook/preview/colors-accent.html | 33 + designbook/preview/colors-borders.html | 25 + designbook/preview/colors-neutrals.html | 36 + designbook/preview/colors-signal.html | 25 + .../preview/colors-status-functional.html | 31 + designbook/preview/comp-buttons.html | 40 + .../preview/comp-empty-placeholder.html | 38 + designbook/preview/comp-inputs.html | 30 + designbook/preview/comp-labels-tags.html | 44 + designbook/preview/comp-left-nav.html | 85 + designbook/preview/comp-pipeline.html | 30 + designbook/preview/comp-prototype-card.html | 46 + designbook/preview/comp-topnav.html | 37 + designbook/preview/page-beta-invitation.html | 104 ++ designbook/preview/page-landing-auth.html | 224 +++ designbook/preview/page-prototype-detail.html | 158 ++ .../preview/page-signals-dashboard.html | 135 ++ designbook/preview/spacing-elevation.html | 26 + designbook/preview/spacing-radii.html | 24 + designbook/preview/spacing-scale.html | 28 + designbook/preview/type-body.html | 23 + designbook/preview/type-display.html | 23 + designbook/preview/type-headings.html | 20 + designbook/preview/type-mono-eyebrows.html | 42 + designbook/preview/type-serif-quote.html | 17 + designbook/styles.css | 9 + designbook/ui_kits/whynot-control/Atoms.jsx | 102 ++ designbook/ui_kits/whynot-control/Chrome.jsx | 163 ++ designbook/ui_kits/whynot-control/DocView.jsx | 102 ++ designbook/ui_kits/whynot-control/README.md | 31 + designbook/ui_kits/whynot-control/Screens.jsx | 274 ++++ designbook/ui_kits/whynot-control/data.jsx | 71 + designbook/ui_kits/whynot-control/index.html | 77 + ir/README.md | 33 + ir/SCHEMA.md | 141 ++ ir/components/Button.json | 63 + ir/components/Eyebrow.json | 23 + ir/components/Icon.json | 27 + ir/components/PageHeader.json | 30 + ir/components/PipelineStrip.json | 16 + ir/components/Sidebar.json | 39 + ir/components/StageDot.json | 28 + ir/components/Stamp.json | 22 + ir/components/Tag.json | 33 + ir/components/TopNav.json | 23 + ir/exemplars/Button.html | 40 + ir/exemplars/Eyebrow.html | 44 + ir/exemplars/PageHeader.html | 37 + ir/exemplars/PipelineStrip.html | 30 + ir/exemplars/Sidebar.html | 85 + ir/exemplars/StageDot.html | 44 + ir/exemplars/Tag.html | 44 + ir/exemplars/TopNav.html | 37 + ir/schema/component.schema.json | 159 ++ ir/schema/tokens.schema.json | 60 + ir/tokens.json | 266 ++++ scripts/designbook_pull.py | 226 +++ scripts/ir-extract.mjs | 271 ++++ src/styles/colors_and_type.css | 172 +- ...HYNOT-WP-0002-designbook-stack-adapters.md | 12 +- 80 files changed, 6439 insertions(+), 106 deletions(-) create mode 100644 .claude/rules/designbook-propagation.md create mode 100644 adapters/ADAPTER_CONTRACT.md create mode 100644 adapters/lit/README.md create mode 100644 adapters/lit/adapt.mjs create mode 100644 designbook/.design-pull.json create mode 100644 designbook/.design-sync.json create mode 100644 designbook/REACT_CANONICAL_DECISION.md create mode 100644 designbook/_ds_bundle.js create mode 100644 designbook/_ds_manifest.json create mode 100644 designbook/colors_and_type.css create mode 100644 designbook/preview/brand-iconography.html create mode 100644 designbook/preview/brand-lockups.html create mode 100644 designbook/preview/brand-logo.html create mode 100644 designbook/preview/brand-wireframe-motif.html create mode 100644 designbook/preview/colors-accent.html create mode 100644 designbook/preview/colors-borders.html create mode 100644 designbook/preview/colors-neutrals.html create mode 100644 designbook/preview/colors-signal.html create mode 100644 designbook/preview/colors-status-functional.html create mode 100644 designbook/preview/comp-buttons.html create mode 100644 designbook/preview/comp-empty-placeholder.html create mode 100644 designbook/preview/comp-inputs.html create mode 100644 designbook/preview/comp-labels-tags.html create mode 100644 designbook/preview/comp-left-nav.html create mode 100644 designbook/preview/comp-pipeline.html create mode 100644 designbook/preview/comp-prototype-card.html create mode 100644 designbook/preview/comp-topnav.html create mode 100644 designbook/preview/page-beta-invitation.html create mode 100644 designbook/preview/page-landing-auth.html create mode 100644 designbook/preview/page-prototype-detail.html create mode 100644 designbook/preview/page-signals-dashboard.html create mode 100644 designbook/preview/spacing-elevation.html create mode 100644 designbook/preview/spacing-radii.html create mode 100644 designbook/preview/spacing-scale.html create mode 100644 designbook/preview/type-body.html create mode 100644 designbook/preview/type-display.html create mode 100644 designbook/preview/type-headings.html create mode 100644 designbook/preview/type-mono-eyebrows.html create mode 100644 designbook/preview/type-serif-quote.html create mode 100644 designbook/styles.css create mode 100644 designbook/ui_kits/whynot-control/Atoms.jsx create mode 100644 designbook/ui_kits/whynot-control/Chrome.jsx create mode 100644 designbook/ui_kits/whynot-control/DocView.jsx create mode 100644 designbook/ui_kits/whynot-control/README.md create mode 100644 designbook/ui_kits/whynot-control/Screens.jsx create mode 100644 designbook/ui_kits/whynot-control/data.jsx create mode 100644 designbook/ui_kits/whynot-control/index.html create mode 100644 ir/README.md create mode 100644 ir/SCHEMA.md create mode 100644 ir/components/Button.json create mode 100644 ir/components/Eyebrow.json create mode 100644 ir/components/Icon.json create mode 100644 ir/components/PageHeader.json create mode 100644 ir/components/PipelineStrip.json create mode 100644 ir/components/Sidebar.json create mode 100644 ir/components/StageDot.json create mode 100644 ir/components/Stamp.json create mode 100644 ir/components/Tag.json create mode 100644 ir/components/TopNav.json create mode 100644 ir/exemplars/Button.html create mode 100644 ir/exemplars/Eyebrow.html create mode 100644 ir/exemplars/PageHeader.html create mode 100644 ir/exemplars/PipelineStrip.html create mode 100644 ir/exemplars/Sidebar.html create mode 100644 ir/exemplars/StageDot.html create mode 100644 ir/exemplars/Tag.html create mode 100644 ir/exemplars/TopNav.html create mode 100644 ir/schema/component.schema.json create mode 100644 ir/schema/tokens.schema.json create mode 100644 ir/tokens.json create mode 100644 scripts/designbook_pull.py create mode 100644 scripts/ir-extract.mjs diff --git a/.claude/rules/designbook-propagation.md b/.claude/rules/designbook-propagation.md new file mode 100644 index 0000000..3a0bf8d --- /dev/null +++ b/.claude/rules/designbook-propagation.md @@ -0,0 +1,59 @@ +## Designbook propagation & adapter governance (WHYNOT-WP-0002) + +The whynot design language is **technology-neutral**. It is authored once and +projected onto each UI stack through an intermediate representation. These rules +keep that flow one-way and the stacks in sync. + +### Directionality — one way only + +``` +Claude Design (React, canonical) → designbook/ → ir/ → adapters// → stack source +``` + +- **Claude Design (the React designbook) is the source of truth** for the *language*. +- **Never hand-edit `ir/`** — `ir/tokens.json`, `ir/components/*.json`, and + `ir/exemplars/*` are written only by the extractor (`scripts/ir-extract.mjs`, + `make ir`). They are committed so blueprint changes show up as a git diff. + Authored-by-hand exceptions: `ir/schema/`, `ir/SCHEMA.md`, `ir/README.md`. +- **Never back-edit React/Claude Design from a stack.** A Lit (or any stack) change + that should alter the shared language must be made in Claude Design and + re-propagated. A direct stack→React edit that bypasses Claude Design is a + governance violation — it desyncs the canonical source from the implementation. +- **Adapters never overwrite hand-authored behaviour.** Tokens regenerate fully; + new components get stubs; changed components get a **drift report**, never a + rewrite. See `adapters/ADAPTER_CONTRACT.md`. + +### When the cloud designbook moves + +Run the refresh sequence (orchestrated by `make designbook-refresh`, WHYNOT-WP-0002 +Phase 5); do not shortcut it: + +1. `make designbook-check` — detect the cloud designbook moved ahead. +2. `make designbook-pull` — pull the latest React designbook into `designbook/` + (drives the local `claude` binary headless via `DesignSync`; stamps freshness + itself). The bundled `/design-sync` skill *pushes* repo→cloud and does **not** + populate `designbook/` — use `make designbook-pull` for the pull. +3. `make designbook-sync` — record the diff in `RecentChanges.md`. +4. `make ir` — re-extract the IR; **review the `ir/` git diff** (the blueprint change). +5. `make adapt-lit` — regenerate tokens, scaffold new components, emit drift reports. +6. **Resolve drift** (human) — fill/adjust Lit behaviour per `adapters/lit/drift/*.md`. +7. `make parity-lit` — confirm appearance + contract parity (gate). + +### Drift triage + +A drift report (`adapters/lit/drift/.md`, command exit code `3`) is resolved +by a human, not the adapter: + +- Decide direction: stale stack → fix the stack to match IR; language should change + → edit Claude Design and re-propagate (never patch only the stack). +- **Non-portable props** (React objects, render props, callbacks) are surfaced as + drift on purpose and must be handled explicitly — never silently dropped. +- A report is closed when a fresh `make ir && make adapt-lit` produces no issues + for that component and `make parity-lit` passes (exit `0`). + +### Exit codes (CI) + +`0` ok · `2` usage/config error · `3` drift detected (stop for human triage) · +`4` parity failure (fail) · `5` internal error. See `adapters/ADAPTER_CONTRACT.md`. + +Full narrative: `DesignSystemIntroduction.md` §5.1. diff --git a/.claude/rules/stack-and-commands.md b/.claude/rules/stack-and-commands.md index db590a5..0250439 100644 --- a/.claude/rules/stack-and-commands.md +++ b/.claude/rules/stack-and-commands.md @@ -28,7 +28,7 @@ There is no unit-test suite — correctness is verified by full-page Playwright The **designbook** is the upstream atelier — the **Claude Design project** (cloud, `claude.ai/design`), source of truth for the *language*. Its local mirror lives in-repo at `designbook/` (see `designbook/README.md`). Sync is **agent-driven and incremental** (one component at a time, never a wholesale replace): -1. **Pull** — run `/design-sync` in Claude Code (the `DesignSync` tool over the claude.ai login); it writes into `designbook/`. Immediately stamp the time: `node scripts/designbook-sync.mjs --mark-synced`. A Makefile cannot invoke the MCP tool, so the pull is always an agent step. +1. **Pull** — run `make designbook-pull` (`scripts/designbook_pull.py`). It drives the local `claude` binary headless (`claude --print --permission-mode acceptEdits`), which has the `DesignSync` tool over the claude.ai login, to fetch the React designbook and write it into `designbook/`; the file contents stay in that subprocess, so the pull is cheap regardless of size. Selection is governed by `designbook/.design-pull.json` (it excludes `_whynot-design-seed/**`, the cloud project's copy of this repo). The script stamps freshness itself on success. **Note:** the bundled `/design-sync` skill goes the *other* way — it *pushes* a repo up to Claude Design — so it does **not** populate `designbook/`; use `make designbook-pull` for the pull (see `designbook-propagation.md`). 2. **Record** — `make designbook-sync` runs `scripts/designbook-sync.mjs`, writing `RecentChanges.md`: a **snapshot** (not a log) of what changed across `designbook/` + the derived surfaces (`tokens/`, `src/styles/`, `src/elements/`, `examples/`), grouped by layer. **Freshness** is tracked in `designbook/.design-sync.json` (`lastSyncAt`, `remoteUpdatedAt`, `projectId`, `projectName`). Every report states **"Last /design-sync: "** so it's clear whether the snapshot reflects the latest design. The cloud-ahead check is backed by **llm-connect** (`make designbook-check` → `scripts/check_designbook_staleness.py`): it uses the `claude-code` adapter to ask the local `claude` binary for the project's current `updatedAt` via `DesignSync.list_projects`, then records it with `node scripts/designbook-sync.mjs --remote-updated `. Only the `claude-code` adapter can reach the user's Claude Design project (Gemini/OpenRouter/OpenAI cannot), and no secret goes in the prompt — DesignSync uses the claude.ai login (see `credential-routing.md`). The check needs `llm_connect` importable; the Makefile auto-selects `~/llm-connect/.venv/bin/python`. Use `--remote-updated ` to run the comparison offline/manually, or `--fail-if-stale` (exit 3) in automation. When `remoteUpdatedAt` is newer than `lastSyncAt`, the report and stdout **warn that the local mirror is OUTDATED** until the next `/design-sync`. If no sync was ever recorded, it warns that `/design-sync` has not run. The reporter is deterministic (built from `git status`/`git diff`), only writes the working tree, never commits, and never edits `designbook/` content. Fold notable entries into `CHANGELOG.md` under `## [Unreleased]` before releasing — `RecentChanges.md` is overwritten every run and is **not** the CI-enforced artifact. diff --git a/CHANGELOG.md b/CHANGELOG.md index b41cdc3..3c07624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,34 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version ## [Unreleased] -_Nothing yet. Add entries above the next `[vX.Y.Z]` block as PRs land._ +### Added + +- **Technology-neutral IR + stack-adapter pipeline** (WHYNOT-WP-0002, Phase 0–3). The + design language is now authored once in the canonical React designbook and projected + one-way onto each stack: `React → designbook/ → ir/ → adapters//`. + - `ir/` — committed, diffable blueprint: `ir/SCHEMA.md`, JSON Schemas + (`ir/schema/{component,tokens}.schema.json`), and the extractor's output + (`ir/tokens.json` in W3C DTCG format, `ir/components/*.json`, `ir/exemplars/*`). + - `adapters/ADAPTER_CONTRACT.md` — the contract every stack adapter implements + (inputs, drift report + parity result shapes, idempotency, CI exit codes). + - `scripts/designbook_pull.py` + `make designbook-pull` — pulls the React designbook + from Claude Design into `designbook/` (the bundled `/design-sync` skill only + *pushes*, so it cannot populate `designbook/`). + - `scripts/ir-extract.mjs` + `make ir` — extracts the IR from the `.jsx` ui-kit, + manifest, and previews. + - `adapters/lit/` + `make adapt-lit` — Lit reference adapter; tokens fully generated + into `src/styles/colors_and_type.css` (marker-bounded, idempotent). + - `.claude/rules/designbook-propagation.md` + `DesignSystemIntroduction.md` §5.1 — + one-way governance and drift-resolution workflow. + +### Changed + +- `src/styles/colors_and_type.css` — token `:root` block is now **generated** by + `make adapt-lit` from `ir/tokens.json` (between `@generated tokens` markers). This + synced the Lit token layer to the canonical React designbook: font stacks switched + from IBM Plex to system-font stacks, and the functional-status tokens + (`--status-error/warn/success/info` + `-bg`) were added. **Visual change — Playwright + baselines need review + `pnpm test:visual:update`.** ## [0.2.0] — 2026-05-25 diff --git a/CLAUDE.md b/CLAUDE.md index e384c56..a1f48b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,7 @@ @.claude/rules/first-session.md @.claude/rules/workplan-convention.md @.claude/rules/stack-and-commands.md +@.claude/rules/designbook-propagation.md @.claude/rules/architecture.md @.claude/rules/repo-boundary.md @.claude/rules/credential-routing.md diff --git a/DesignSystemIntroduction.md b/DesignSystemIntroduction.md index e65548e..9dfb40f 100644 --- a/DesignSystemIntroduction.md +++ b/DesignSystemIntroduction.md @@ -217,6 +217,59 @@ The end-to-end flow for a single design change: The whole loop, warm, takes minutes. **Automation works only because every step has a deterministic check** — visual regression on both sides, semver, changelogs. Skip those and the pipeline is a slow manual process with extra tools. +### 5.1 The IR pivot — technology-neutral propagation (WHYNOT-WP-0002) + +Claude Design's `/design-sync` produces a **React-bound** designbook. To keep the +language portable across UI stacks (Lit today, others later) without forking it +per stack, an **intermediate representation (IR)** sits between the atelier and +each stack's source: + +``` +Claude Design (React) ──/design-sync──▶ designbook/ ──make ir──▶ ir/ ──make adapt-lit──▶ src/ (Lit) + canonical authoring React mirror neutral blueprint per-stack adapter +``` + +**Directionality is one-way: React → IR → stacks.** + +- The **React designbook in Claude Design is canonical.** The shared language is + authored there; nothing downstream is the source of truth. +- `ir/` is the **committed, diffable blueprint** (tokens in W3C DTCG format, + per-component contracts, reference exemplars). Only the extractor writes it — + never hand-edit `ir/tokens.json`, `ir/components/`, or `ir/exemplars/`. See + `ir/SCHEMA.md`. +- Each stack has an **adapter** (`adapters//`, contract in + `adapters/ADAPTER_CONTRACT.md`). Adapters are **scaffold + drift-detect**: + tokens fully generated, new components stubbed, changed components reported as + **drift** — the hand-authored source is never overwritten. Lit is the reference + adapter. +- **Lit-side changes do not flow back automatically.** A change to the shared + language must be made in Claude Design (React) and re-propagated through the IR. + A Lit→React back-edit that bypasses Claude Design is a governance violation: it + silently desyncs the canonical source from the implementation. + +**Drift resolution workflow.** When `make adapt-lit` reports drift +(`adapters/lit/drift/.md`, exit code `3`): + +1. **Triage** — read the drift report. Each issue is one of: prop missing, + attribute mismatch, variant added/removed, prop removed, or a **non-portable + prop** (a React object/render-prop/callback that has no clean attribute). +2. **Decide the direction.** If the IR is right and Lit is stale, a human adjusts + the Lit element's contract/behaviour to match. If the *language itself* should + change, that edit goes to Claude Design (React) and re-propagates — not into Lit. +3. **Behaviour is filled by a human**, guided by the stub's `TODO` and the drift + report. The adapter never authors behaviour. +4. **Close** — re-run `make adapt-lit` until drift clears, then `make parity-lit` + (exit `0`) confirms contract + visual parity. A drift report is "closed" when + the next extract+adapt produces no issues for that component. + +This **extends** the five-hop pipeline above rather than replacing it: hops 3–5 +(tag → publish → consumer) are unchanged; the IR pivot is inserted between the +atelier and the Lit source so the same change can later be projected onto a second +stack without re-authoring it. The full operational sequence is the +`make designbook-refresh` runbook (WHYNOT-WP-0002 Phase 5). Governance for this +flow is restated as an enforceable rule in +`.claude/rules/designbook-propagation.md`. + --- ## 6. Versioning discipline diff --git a/Makefile b/Makefile index 5301e37..19cf0c3 100644 --- a/Makefile +++ b/Makefile @@ -10,13 +10,16 @@ NODE ?= node PYTHON ?= $(shell [ -x $(HOME)/llm-connect/.venv/bin/python ] && echo $(HOME)/llm-connect/.venv/bin/python || echo python3) .DEFAULT_GOAL := help -.PHONY: help designbook-sync designbook-check recent-changes sync-styles test +.PHONY: help designbook-pull designbook-sync designbook-check ir adapt-lit recent-changes sync-styles test help: ## Show this help. @grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) \ | awk 'BEGIN{FS=":.*?## "}{printf " \033[1m%-16s\033[0m %s\n", $$1, $$2}' -designbook-sync: ## After a /design-sync pull, record what changed + last-sync time into RecentChanges.md. +designbook-pull: ## Pull the React designbook from Claude Design into designbook/ (cloud -> local mirror). + $(PYTHON) scripts/designbook_pull.py $(ARGS) + +designbook-sync: ## After a designbook pull, record what changed + last-sync time into RecentChanges.md. @echo "Pull the designbook first (in Claude Code): /design-sync" @echo " then record the pull time: node scripts/designbook-sync.mjs --mark-synced" @echo "This reports the diff + last /design-sync time (and warns if the cloud is newer):" @@ -25,6 +28,12 @@ designbook-sync: ## After a /design-sync pull, record what changed + last-sync t designbook-check: ## Ask Claude Design (via llm-connect) if the cloud is newer; warn if the mirror is stale. $(PYTHON) scripts/check_designbook_staleness.py $(ARGS) +ir: ## Extract the technology-neutral IR (ir/) from the designbook mirror. One-way: React -> IR. + $(NODE) scripts/ir-extract.mjs + +adapt-lit: ## Project the IR onto the Lit stack: regen tokens (full gen), scaffold + drift (T07). + $(NODE) adapters/lit/adapt.mjs + recent-changes: ## Regenerate RecentChanges.md (alias of the reporter; --range supported). $(NODE) scripts/designbook-sync.mjs $(ARGS) diff --git a/RecentChanges.md b/RecentChanges.md index 71f60b7..25c512a 100644 --- a/RecentChanges.md +++ b/RecentChanges.md @@ -2,13 +2,9 @@ Snapshot of the last designbook integration. Regenerated by `make designbook-sync`. -- Generated: 2026-06-22T20:02:58Z +- Generated: 2026-06-23T19:25:28Z - Compared: working tree (uncommitted) -- Last /design-sync: never recorded - -> WARNING — no /design-sync has been recorded, so the local designbook/ may not -> reflect the Claude Design project. Run `/design-sync` in Claude Code, then -> `node scripts/designbook-sync.mjs --mark-synced`. +- Last /design-sync: 2026-06-23T19:25:28Z > This file is overwritten on every run — a snapshot, not a log. Fold notable entries > into `CHANGELOG.md` under `## [Unreleased]` before releasing; that is the file CI @@ -17,4 +13,45 @@ Snapshot of the last designbook integration. Regenerated by `make designbook-syn ## Changed files ### Designbook -- `ADDED ` designbook/README.md (+70 / -0) +- `ADDED ` designbook/_ds_bundle.js (+1396 / -0) +- `ADDED ` designbook/_ds_manifest.json (+1 / -0) +- `ADDED ` designbook/.design-pull.json (+18 / -0) +- `ADDED ` designbook/colors_and_type.css (+293 / -0) +- `ADDED ` designbook/preview/brand-iconography.html (+36 / -0) +- `ADDED ` designbook/preview/brand-lockups.html (+26 / -0) +- `ADDED ` designbook/preview/brand-logo.html (+21 / -0) +- `ADDED ` designbook/preview/brand-wireframe-motif.html (+43 / -0) +- `ADDED ` designbook/preview/colors-accent.html (+33 / -0) +- `ADDED ` designbook/preview/colors-borders.html (+25 / -0) +- `ADDED ` designbook/preview/colors-neutrals.html (+36 / -0) +- `ADDED ` designbook/preview/colors-signal.html (+25 / -0) +- `ADDED ` designbook/preview/colors-status-functional.html (+31 / -0) +- `ADDED ` designbook/preview/comp-buttons.html (+40 / -0) +- `ADDED ` designbook/preview/comp-empty-placeholder.html (+38 / -0) +- `ADDED ` designbook/preview/comp-inputs.html (+30 / -0) +- `ADDED ` designbook/preview/comp-labels-tags.html (+44 / -0) +- `ADDED ` designbook/preview/comp-left-nav.html (+85 / -0) +- `ADDED ` designbook/preview/comp-pipeline.html (+30 / -0) +- `ADDED ` designbook/preview/comp-prototype-card.html (+46 / -0) +- `ADDED ` designbook/preview/comp-topnav.html (+37 / -0) +- `ADDED ` designbook/preview/page-beta-invitation.html (+104 / -0) +- `ADDED ` designbook/preview/page-landing-auth.html (+224 / -0) +- `ADDED ` designbook/preview/page-prototype-detail.html (+158 / -0) +- `ADDED ` designbook/preview/page-signals-dashboard.html (+135 / -0) +- `ADDED ` designbook/preview/spacing-elevation.html (+26 / -0) +- `ADDED ` designbook/preview/spacing-radii.html (+24 / -0) +- `ADDED ` designbook/preview/spacing-scale.html (+28 / -0) +- `ADDED ` designbook/preview/type-body.html (+23 / -0) +- `ADDED ` designbook/preview/type-display.html (+23 / -0) +- `ADDED ` designbook/preview/type-headings.html (+20 / -0) +- `ADDED ` designbook/preview/type-mono-eyebrows.html (+42 / -0) +- `ADDED ` designbook/preview/type-serif-quote.html (+17 / -0) +- `ADDED ` designbook/REACT_CANONICAL_DECISION.md (+60 / -0) +- `ADDED ` designbook/styles.css (+9 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/Atoms.jsx (+102 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/Chrome.jsx (+163 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/data.jsx (+71 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/DocView.jsx (+102 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/index.html (+77 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/README.md (+31 / -0) +- `ADDED ` designbook/ui_kits/whynot-control/Screens.jsx (+274 / -0) diff --git a/adapters/ADAPTER_CONTRACT.md b/adapters/ADAPTER_CONTRACT.md new file mode 100644 index 0000000..c71913a --- /dev/null +++ b/adapters/ADAPTER_CONTRACT.md @@ -0,0 +1,137 @@ +# Adapter Contract + +> The contract **every** whynot stack adapter implements, so a new stack +> (Vue, Angular, Svelte, plain-CSS, …) is a drop-in. Part of WHYNOT-WP-0002. +> Lit (`adapters/lit/`) is the reference implementation. Django +> (`adapters/django/`) predates the IR and is a hand-authored Layer-3 partial +> set, not an IR adapter — it is exempt until/unless migrated. + +An adapter is **scaffold + drift-detect**, not full behavioural codegen: + +- **tokens** → fully generated (deterministic, re-run is a no-op when unchanged), +- **new component** → a stub is generated (skeleton + typed inputs + a behaviour TODO), +- **changed component** → a **drift report** is emitted; the hand-authored source is + **never overwritten**, +- **appearance** → verified against the designbook's own exemplars (parity). + +Behaviour stays hand-authored per stack. The adapter keeps each component's +*contract and appearance* aligned; it does not own its behaviour. + +--- + +## Inputs + +The sole input is the committed IR (`ir/`, produced one-way from the React +designbook — see `ir/SCHEMA.md`): + +| Input | What the adapter reads it for | +|---|---| +| `ir/tokens.json` | Token generation (W3C DTCG → stack-native token form). | +| `ir/components/.json` | Per-component contract: props, prop→attribute map, slots, events, variants. | +| `ir/exemplars/.{html,png}` | Reference render for visual parity. | +| `ir/schema/` | The schemas the adapter may re-validate inputs against. | + +An adapter **MUST NOT** write to `ir/`. Flow is one-way: React → IR → stacks. + +## Outputs + +An adapter produces exactly three kinds of output: + +1. **Generated artifacts** into the stack's own source tree. + - Tokens: fully generated, deterministic (e.g. Lit → `src/styles/colors_and_type.css`). + - New-component stubs: written to the stack's component tree, marked generated, + with a behaviour `TODO`. A stub is created **only** when no hand-authored + counterpart exists. + +2. **A machine-readable drift report** — one file per drifted component plus a + roll-up. Lit writes these to `adapters/lit/drift/.md` (human view) backed + by a machine-readable summary. The report enumerates, per component: + - props present in IR but missing on the element (and vice-versa), + - prop→attribute mismatches, + - missing / extra / renamed variants, + - removed props, + - **non-portable props** (`portable:false`) surfaced explicitly — never dropped. + +3. **A parity result** — a single structured outcome per `make parity-` + covering (a) **contract parity** (observed attributes/properties vs IR) and + (b) **visual parity** (rendered component diffed against `ir/exemplars/`). + +### Drift report — minimal machine shape + +```json +{ + "stack": "lit", + "generatedAt": "", + "irRef": "", + "components": [ + { + "name": "Button", + "status": "drift", // "ok" | "new" | "drift" | "removed" + "issues": [ + { "kind": "prop-missing", "prop": "tone", "detail": "in IR, absent on " }, + { "kind": "attribute-mismatch","prop": "iconEnd", "expected": "icon-end", "actual": "iconend" }, + { "kind": "non-portable", "prop": "renderLabel", "detail": "type=function; cannot map to attribute" } + ] + } + ] +} +``` + +### Parity result — minimal machine shape + +```json +{ + "stack": "lit", + "generatedAt": "", + "components": [ + { "name": "Button", "contract": "pass", "visual": "pass", "diffRatio": 0.0009 } + ], + "summary": { "total": 1, "contractFail": 0, "visualFail": 0 } +} +``` + +## Idempotency rules + +1. **Tokens regenerate fully.** Running token generation twice on an unchanged + `ir/tokens.json` yields a byte-identical file (no-op diff). +2. **Stubs are write-once.** A stub is generated only when no hand-authored source + exists. Once a human has touched a component, the adapter never re-writes it — + it emits drift instead. +3. **Behaviour is never overwritten.** No adapter output replaces hand-authored + behaviour. The strongest action against an existing component is a drift report. +4. **Reports are overwritten, not appended.** Drift and parity outputs are + snapshots of the current IR-vs-source state; each run replaces the previous. +5. **No network, no `ir/` writes.** Adapters are pure functions of `ir/` + the + stack source tree. + +## Exit codes (for CI) + +Every adapter command (`make adapt-`, `make parity-`) follows the +same convention so pipelines can branch uniformly: + +| Code | Meaning | +|---|---| +| `0` | Success. Tokens/stubs generated; no drift; parity passed. | +| `2` | Usage / configuration error (bad args, missing `ir/`, malformed input). | +| `3` | **Drift detected.** Generation succeeded but one or more components drifted from the IR. Non-fatal by default; use to gate a review. | +| `4` | **Parity failure.** A contract or visual parity check failed. | +| `5` | Internal adapter error (unexpected exception). | + +Codes are additive in spirit but a command returns the **highest applicable** +code (e.g. drift + parity failure → `4`). `make designbook-refresh` (T09) treats +`3` as "stop for human drift triage" and `4` as "fail". + +## Implementing a new adapter — checklist + +1. Create `adapters//` with a `README.md` pointing back to this contract. +2. Implement token generation from `ir/tokens.json` → the stack's token form + (full gen, deterministic). +3. Implement stub generation from `ir/components/.json` using the + **prop→attribute map** (respect `attribute:false` and `portable:false`). +4. Implement drift detection against existing hand-authored sources → the drift + report shape above. +5. Implement `make parity-` → the parity result shape above, reusing the + Playwright harness where the stack renders to HTML. +6. Wire `make adapt-` and `make parity-`; honour the exit codes. + +See `adapters/lit/` for the reference implementation. diff --git a/adapters/lit/README.md b/adapters/lit/README.md new file mode 100644 index 0000000..828a2dd --- /dev/null +++ b/adapters/lit/README.md @@ -0,0 +1,45 @@ +# Lit reference adapter + +Projects the technology-neutral IR (`ir/`) onto the Lit stack. This is the +**reference** adapter — the contract every stack adapter implements lives in +[`adapters/ADAPTER_CONTRACT.md`](../ADAPTER_CONTRACT.md). + +Run it with **`make adapt-lit`** (`adapters/lit/adapt.mjs`). + +## What it does + +Per the contract, an adapter is **scaffold + drift-detect**, never a rewrite: + +| Concern | Behaviour | Status | +|---|---|---| +| **Tokens** | **Fully generated** from `ir/tokens.json` into the `:root` block of `src/styles/colors_and_type.css`, between `@generated tokens` markers. Deterministic — re-running with an unchanged IR is a byte-identical no-op. The hand-authored type/utility CSS after the block is preserved. | **done (T06)** | +| **New component** | Generate a `` Lit stub from the IR contract's prop→attribute map + a behaviour `TODO`. | T07 | +| **Changed component** | Emit a **drift report** (`adapters/lit/drift/.md`) — never overwrite the hand-authored element. | T07 | + +## Directionality + +One-way: **React → `ir/` → Lit**. This adapter is downstream of the IR; it never +writes back to `ir/` or to the React designbook. A change to the shared language is +made in Claude Design and re-propagated (`make designbook-pull && make ir && +make adapt-lit`). See `.claude/rules/designbook-propagation.md`. + +## Token regeneration is a visual change + +Because tokens are fully generated, regenerating them can change rendered +appearance when the canonical React designbook has moved (e.g. a font-stack or +colour change). That makes the Playwright baselines diverge **by design** — it is a +human review point, not an error: + +``` +make adapt-lit # regenerates tokens +pnpm test:visual # will fail where appearance changed +# review the change, then if correct: +pnpm test:visual:update # accept new baselines +``` + +Never run `test:visual:update` to silence a token change without reviewing it — that +defeats the parity gate (T08). + +## Exit codes + +`0` ok · `2` usage/config · `3` drift detected · `4` parity failure · `5` internal. diff --git a/adapters/lit/adapt.mjs b/adapters/lit/adapt.mjs new file mode 100644 index 0000000..c696d36 --- /dev/null +++ b/adapters/lit/adapt.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node +// ============================================================= +// adapters/lit/adapt.mjs — the Lit reference adapter (WHYNOT-WP-0002) +// +// Projects the technology-neutral IR (ir/) onto the Lit stack. Per +// adapters/ADAPTER_CONTRACT.md this is scaffold + drift-detect, never a rewrite: +// +// • tokens (T06) → FULLY generated, deterministic (this file, today) +// • new component → stub (T07) +// • changed component → drift report (T07), never an overwrite +// +// Exit codes: 0 ok · 2 usage/config · 3 drift · 4 parity · 5 internal. +// +// Run via `make adapt-lit`. Tokens regenerate the :root block of +// src/styles/colors_and_type.css between generated markers; the hand-authored +// type/utility CSS after it is preserved untouched. Re-running with an unchanged +// ir/tokens.json is a no-op (byte-identical output). +// ============================================================= +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const TOKENS_JSON = join(REPO, "ir", "tokens.json"); +const TOKEN_CSS = join(REPO, "src", "styles", "colors_and_type.css"); + +const BEGIN = "/* @generated tokens — regenerated by `make adapt-lit` from ir/tokens.json. DO NOT EDIT. */"; +const END = "/* @end generated tokens */"; + +// ---------- tokens: ir/tokens.json (DTCG) → CSS custom properties ---------- +function refToVar(value) { + // {group.key} alias → var(--key). Literals pass through unchanged. + const m = /^\{[A-Za-z0-9]+\.([A-Za-z0-9-]+)\}$/.exec(String(value).trim()); + return m ? `var(--${m[1]})` : value; +} + +function renderRootBlock(tokens) { + const lines = [BEGIN, ":root {"]; + for (const [group, entries] of Object.entries(tokens)) { + lines.push(` /* ${group} */`); + for (const [key, tok] of Object.entries(entries)) { + if (key === "$type") continue; + lines.push(` --${key}: ${refToVar(tok.$value)};`); + } + } + lines.push("}", END); + return lines.join("\n"); +} + +// Replace the current :root token region with the freshly generated block. +// First run (no markers): replace the first `:root { … }` via brace matching. +// Later runs: replace strictly between the @generated markers. +function spliceTokenBlock(css, block) { + const b = css.indexOf(BEGIN); + if (b !== -1) { + const e = css.indexOf(END, b); + if (e === -1) throw new Error("Found @generated marker without its @end — refusing to guess."); + return css.slice(0, b) + block + css.slice(e + END.length); + } + const rootAt = css.indexOf(":root"); + if (rootAt === -1) throw new Error("No :root block in colors_and_type.css to replace."); + let i = css.indexOf("{", rootAt), depth = 0; + for (; i < css.length; i++) { + if (css[i] === "{") depth++; + else if (css[i] === "}" && --depth === 0) break; + } + return css.slice(0, rootAt) + block + css.slice(i + 1); +} + +function generateTokens() { + if (!existsSync(TOKENS_JSON)) { + console.error("No ir/tokens.json — run `make ir` first."); + process.exit(2); + } + const tokens = JSON.parse(readFileSync(TOKENS_JSON, "utf8")); + const block = renderRootBlock(tokens); + const before = existsSync(TOKEN_CSS) ? readFileSync(TOKEN_CSS, "utf8") : `${block}\n`; + const after = existsSync(TOKEN_CSS) ? spliceTokenBlock(before, block) : `${block}\n`; + const count = block.split("\n").filter((l) => l.trim().startsWith("--")).length; + if (after === before) { + console.log(`tokens: up to date (${count} custom properties, no change).`); + return; + } + writeFileSync(TOKEN_CSS, after); + console.log(`tokens: regenerated ${count} custom properties → src/styles/colors_and_type.css`); +} + +function main() { + generateTokens(); + // T07 will add: component stub scaffolding + drift reports here. + console.log("adapt-lit: tokens done. (component scaffold + drift: T07)"); +} + +main(); diff --git a/designbook/.design-pull.json b/designbook/.design-pull.json new file mode 100644 index 0000000..865d6f0 --- /dev/null +++ b/designbook/.design-pull.json @@ -0,0 +1,18 @@ +{ + "comment": "Globs (over Claude Design project paths) that designbook_pull.py mirrors into designbook/. Exclude _whynot-design-seed/** \u2014 it is a copy of THIS repo living in the cloud project and must not shadow the real source.", + "include": [ + "ui_kits/**", + "preview/**", + "_ds_manifest.json", + "_ds_bundle.js", + "styles.css", + "colors_and_type.css" + ], + "exclude": [ + "_whynot-design-seed/**", + "uploads/**", + "_check/**", + ".thumbnail", + "assets/**" + ] +} diff --git a/designbook/.design-sync.json b/designbook/.design-sync.json new file mode 100644 index 0000000..4c97e6e --- /dev/null +++ b/designbook/.design-sync.json @@ -0,0 +1,6 @@ +{ + "lastSyncAt": "2026-06-23T19:25:28Z", + "remoteUpdatedAt": "2026-06-23T19:25:28Z", + "projectId": "fb2eef8c-c1fc-4c75-bff4-3782552e5511", + "projectName": "WhyNot Design System" +} diff --git a/designbook/REACT_CANONICAL_DECISION.md b/designbook/REACT_CANONICAL_DECISION.md new file mode 100644 index 0000000..08aa9df --- /dev/null +++ b/designbook/REACT_CANONICAL_DECISION.md @@ -0,0 +1,63 @@ +# Decision: the canonical React designbook (WHYNOT-WP-0002 · T04) + +> **Status: RESOLVED.** The canonical React designbook **already exists** — it is +> the **"WhyNot Design System"** project in Claude Design +> (`fb2eef8c-c1fc-4c75-bff4-3782552e5511`, owner Bernd). The remaining concrete +> action is the first `/design-sync` pull into `designbook/`. + +## Correction to the earlier premise + +An earlier draft of this doc assumed the Claude Design project held only the +hand-authored **Lit** experiment, and therefore proposed *authoring* a new React +designbook. That premise was wrong. Inspecting the project +(`DesignSync.list_files` / `get_file`) shows it already contains a real React +source: + +- `ui_kits/whynot-control/*.jsx` — **React function components**: `Atoms.jsx` + (`Eyebrow`, `Tag`, `Button`, `StageDot`, `Stamp`, `Icon`), `Chrome.jsx`, + `Screens.jsx`, `DocView.jsx`, `data.jsx`. Props are expressed as JSX function + parameters with defaults (e.g. `Button({ variant = 'secondary', icon, … })`). +- `preview/comp-*.html` — per-component preview cards (the exemplar renders). +- `styles.css`, `colors_and_type.css`, `_ds_manifest.json`, `_ds_bundle.js` — + token/style layers + the grouping manifest. +- `_whynot-design-seed/` — a full copy of this Lit repo that seeded the project. + +So Claude Design is genuinely canonical today, and `/design-sync` provides the +React origin from which Lit (and any future stack) is generated. This is the +directionality the workplan already assumes: **React → IR → stacks.** + +## Resolution + +- **Canonical source:** the existing "WhyNot Design System" Claude Design project. + No new designbook is authored; nothing is adopted from a foreign kit. +- **How it reaches `designbook/`:** run **`make designbook-pull`** + (`scripts/designbook_pull.py`) — it drives the local `claude` binary headless + (`claude --print --permission-mode acceptEdits`) so the `DesignSync` fetch+write + happens in a subprocess, and stamps freshness on success. (The bundled + `/design-sync` skill goes the other way — it *pushes* repo→cloud — so it does not + populate `designbook/`.) **Done 2026-06-23:** 44 files pulled (the `.jsx` ui-kit, + `_ds_manifest.json`, style layers, and `preview/*.html` exemplars); + `_whynot-design-seed/**` excluded. + +## Consequence for the extractor (T05) + +The React source is a **bundled `.jsx` ui-kit** (several components per file), +**not** the per-component `.d.ts` + `.prompt.md` layout the T01 schema notes +assumed. The neutral IR **contract schema is unaffected** (it describes the +*output* shape), but `scripts/ir-extract.mjs` (T05) must: + +- read component **props/defaults from the JSX function signatures** in + `ui_kits/whynot-control/*.jsx` (not `.d.ts`), +- take **grouping** from `_ds_manifest.json`, +- take **exemplars** from `preview/comp-*.html`, +- take **tokens** from the project's `styles.css` / `colors_and_type.css` (and/or + the seed `tokens/*.json`), normalising to W3C DTCG. + +This is recorded so T05 is designed against the real source layout. + +## What unblocks the rest of the pipeline + +1. Run `/design-sync` → `designbook/` receives the React mirror (more than README). +2. Stamp + `make designbook-sync`. +3. `make ir` (T05) extracts the IR from the `.jsx` ui-kit + previews + manifest. +4. T06–T08 (Lit adapter + parity) then run against real data. diff --git a/designbook/_ds_bundle.js b/designbook/_ds_bundle.js new file mode 100644 index 0000000..fc6d340 --- /dev/null +++ b/designbook/_ds_bundle.js @@ -0,0 +1,1396 @@ +/* @ds-bundle: {"format":3,"namespace":"WhyNotDesignSystem_fb2eef","components":[],"sourceHashes":{"ui_kits/whynot-control/Atoms.jsx":"79a33f57ef80","ui_kits/whynot-control/Chrome.jsx":"31e0ba69d22d","ui_kits/whynot-control/DocView.jsx":"636cbc6e3422","ui_kits/whynot-control/Screens.jsx":"c21f0206c6c7","ui_kits/whynot-control/data.jsx":"16b3caf0bced"},"inlinedExternals":[],"unexposedExports":[]} */ + +(() => { + +const __ds_ns = (window.WhyNotDesignSystem_fb2eef = window.WhyNotDesignSystem_fb2eef || {}); + +const __ds_scope = {}; + +(__ds_ns.__errors = __ds_ns.__errors || []); + +// ui_kits/whynot-control/Atoms.jsx +try { (() => { +// ============================================================= +// Atoms — Eyebrow, Tag, Button, StageDot, Stamp, IconBtn +// ============================================================= + +function Eyebrow({ + children, + style +}) { + return /*#__PURE__*/React.createElement("span", { + style: { + font: '500 11px/1.2 var(--ff-mono)', + letterSpacing: '0.08em', + textTransform: 'uppercase', + color: 'var(--fg-3)', + ...style + } + }, children); +} +function Tag({ + children, + active, + draft, + style +}) { + const base = { + font: '500 10px/1 var(--ff-mono)', + letterSpacing: '0.1em', + textTransform: 'uppercase', + padding: '5px 10px', + borderRadius: 'var(--r-pill)', + border: '1px solid var(--border)', + color: 'var(--fg-2)', + background: 'var(--paper)', + display: 'inline-block' + }; + if (active) Object.assign(base, { + background: 'var(--ink)', + color: 'var(--paper)', + borderColor: 'var(--ink)' + }); + if (draft) Object.assign(base, { + background: 'var(--hi)', + color: 'var(--hi-ink)', + borderColor: 'transparent' + }); + return /*#__PURE__*/React.createElement("span", { + style: { + ...base, + ...style + } + }, children); +} +function Button({ + children, + variant = 'secondary', + onClick, + style, + icon +}) { + const base = { + font: '500 13px var(--ff-sans)', + letterSpacing: '-0.005em', + padding: '9px 14px', + borderRadius: 'var(--r-2)', + border: '1px solid var(--border)', + background: 'var(--paper)', + color: 'var(--ink)', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: 8, + whiteSpace: 'nowrap', + transition: 'background 120ms ease, border-color 120ms ease' + }; + if (variant === 'primary') Object.assign(base, { + background: 'var(--ink)', + color: 'var(--paper)', + borderColor: 'var(--ink)' + }); + if (variant === 'ghost') Object.assign(base, { + background: 'transparent', + borderColor: 'transparent', + padding: '7px 10px' + }); + return /*#__PURE__*/React.createElement("button", { + onClick: onClick, + style: { + ...base, + ...style + } + }, icon && /*#__PURE__*/React.createElement("i", { + "data-lucide": icon, + style: { + width: 14, + height: 14, + strokeWidth: 1.5 + } + }), children); +} +const STAGE_COLORS = { + S0: '#B5B5B3', + S1: '#8A8A8A', + S2: '#5C5C5C', + S3: '#0A0A0A', + S4: '#FFD400' +}; +function StageDot({ + level = 'S2', + label, + style +}) { + return /*#__PURE__*/React.createElement("span", { + style: { + font: '500 10px/1 var(--ff-mono)', + letterSpacing: '0.1em', + textTransform: 'uppercase', + color: 'var(--fg-2)', + display: 'inline-flex', + alignItems: 'center', + gap: 6, + ...style + } + }, /*#__PURE__*/React.createElement("span", { + style: { + width: 8, + height: 8, + borderRadius: 999, + background: STAGE_COLORS[level] + } + }), label || level); +} +function Stamp({ + children, + style +}) { + return /*#__PURE__*/React.createElement("span", { + style: { + display: 'inline-block', + background: 'var(--hi)', + color: 'var(--hi-ink)', + padding: '5px 10px 3px', + font: '500 10px/1 var(--ff-mono)', + letterSpacing: '0.12em', + textTransform: 'uppercase', + transform: 'rotate(-1.5deg)', + ...style + } + }, children); +} +function Icon({ + name, + size = 16, + style +}) { + return /*#__PURE__*/React.createElement("i", { + "data-lucide": name, + style: { + width: size, + height: size, + strokeWidth: 1.5, + ...style + } + }); +} +Object.assign(window, { + Eyebrow, + Tag, + Button, + StageDot, + Stamp, + Icon, + STAGE_COLORS +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/whynot-control/Atoms.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/whynot-control/Chrome.jsx +try { (() => { +// ============================================================= +// Chrome — TopNav, Sidebar, PageHeader, PipelineStrip +// ============================================================= + +function TopNav({ + onNew +}) { + return /*#__PURE__*/React.createElement("nav", { + style: { + height: 56, + background: 'rgba(255,255,255,0.92)', + borderBottom: '1px solid var(--border)', + display: 'flex', + alignItems: 'center', + gap: 28, + padding: '0 24px', + position: 'sticky', + top: 0, + zIndex: 10 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 10 + } + }, /*#__PURE__*/React.createElement("img", { + src: "../../assets/whynot-logo.png", + alt: "", + style: { + width: 22, + height: 22 + } + }), /*#__PURE__*/React.createElement("span", { + style: { + font: '500 14px var(--ff-sans)' + } + }, "whynot"), /*#__PURE__*/React.createElement("span", { + style: { + font: '400 12px var(--ff-mono)', + color: 'var(--fg-3)', + letterSpacing: '0.04em' + } + }, "/ control")), /*#__PURE__*/React.createElement("div", { + style: { + marginLeft: 'auto', + display: 'flex', + alignItems: 'center', + gap: 12 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + font: '400 12px var(--ff-mono)', + color: 'var(--fg-3)', + border: '1px solid var(--border)', + padding: '6px 10px', + borderRadius: 'var(--r-1)', + display: 'flex', + alignItems: 'center', + gap: 10, + minWidth: 240 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "search", + size: 14 + }), /*#__PURE__*/React.createElement("span", null, "Search ideas, prototypes, signals…"), /*#__PURE__*/React.createElement("span", { + style: { + marginLeft: 'auto', + padding: '1px 5px', + border: '1px solid var(--border)', + borderRadius: 2, + fontSize: 10 + } + }, "⌘ K")), /*#__PURE__*/React.createElement(Button, { + variant: "primary", + icon: "plus", + onClick: onNew + }, "New idea"))); +} +const NAV_ITEMS = [{ + key: 'inbox', + label: 'Inbox', + icon: 'inbox', + count: 7 +}, { + key: 'prototypes', + label: 'Prototypes', + icon: 'flask-conical', + count: 4 +}, { + key: 'signals', + label: 'Signals', + icon: 'activity', + count: 12 +}, { + key: 'betas', + label: 'Betas', + icon: 'users', + count: 1 +}, { + key: 'decisions', + label: 'Decisions', + icon: 'check-square', + count: 3 +}]; +const DOC_ITEMS = [{ + key: 'intent', + label: 'INTENT.md' +}, { + key: 'scope', + label: 'SCOPE.md' +}, { + key: 'operating', + label: 'OPERATING_MODEL.md' +}, { + key: 'pipeline', + label: 'PROTOTYPE_PIPELINE.md' +}, { + key: 'agent', + label: 'AGENT_RULES.md' +}]; +function Sidebar({ + current, + onNav +}) { + const itemStyle = active => ({ + display: 'flex', + alignItems: 'center', + gap: 10, + padding: '6px 10px', + color: active ? 'var(--fg-1)' : 'var(--fg-3)', + background: 'transparent', + borderLeft: active ? '2px solid var(--ink)' : '2px solid transparent', + paddingLeft: active ? 10 : 12, + font: active ? '500 13px var(--ff-sans)' : '400 13px var(--ff-sans)', + cursor: 'pointer', + textDecoration: 'none', + transition: 'color 120ms ease, border-color 120ms ease' + }); + return /*#__PURE__*/React.createElement("aside", { + style: { + width: 200, + flex: 'none', + background: 'transparent', + borderRight: 'none', + padding: '32px 0 32px 8px', + display: 'flex', + flexDirection: 'column', + gap: 32, + height: 'calc(100vh - 56px)', + position: 'sticky', + top: 56, + overflowY: 'auto' + } + }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Eyebrow, { + style: { + paddingLeft: 12, + marginBottom: 10, + display: 'block', + opacity: 0.7 + } + }, "Work"), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 1 + } + }, NAV_ITEMS.map(item => /*#__PURE__*/React.createElement("a", { + key: item.key, + onClick: () => onNav(item.key), + style: itemStyle(current === item.key) + }, /*#__PURE__*/React.createElement("span", null, item.label), /*#__PURE__*/React.createElement("span", { + style: { + marginLeft: 'auto', + font: '400 11px var(--ff-mono)', + color: 'var(--ink-5)' + } + }, item.count))))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Eyebrow, { + style: { + paddingLeft: 12, + marginBottom: 10, + display: 'block', + opacity: 0.7 + } + }, "Control docs"), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 1 + } + }, DOC_ITEMS.map(item => /*#__PURE__*/React.createElement("a", { + key: item.key, + onClick: () => onNav('doc:' + item.key), + style: { + ...itemStyle(current === 'doc:' + item.key), + font: current === 'doc:' + item.key ? '500 12px var(--ff-mono)' : '400 12px var(--ff-mono)' + } + }, /*#__PURE__*/React.createElement("span", null, item.label))))), /*#__PURE__*/React.createElement("div", { + style: { + marginTop: 'auto', + padding: '0 12px' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + width: 5, + height: 5, + borderRadius: 999, + background: 'var(--ink-4)' + } + }), /*#__PURE__*/React.createElement("span", { + style: { + font: '400 11px var(--ff-mono)', + letterSpacing: '0.06em', + textTransform: 'uppercase', + color: 'var(--fg-3)' + } + }, "A1 \xB7 Incubating")))); +} +function PageHeader({ + eyebrow, + title, + lede, + actions +}) { + return /*#__PURE__*/React.createElement("header", { + style: { + marginBottom: 48, + display: 'flex', + flexDirection: 'column', + gap: 10 + } + }, eyebrow && /*#__PURE__*/React.createElement(Eyebrow, null, eyebrow), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'flex-end', + gap: 24 + } + }, /*#__PURE__*/React.createElement("h1", { + style: { + font: '400 36px/1.1 var(--ff-sans)', + letterSpacing: '-0.02em', + margin: 0, + flex: 1 + } + }, title), actions && /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 8 + } + }, actions)), lede && /*#__PURE__*/React.createElement("p", { + style: { + font: '400 16px/1.6 var(--ff-sans)', + color: 'var(--fg-2)', + margin: '4px 0 0', + maxWidth: '56ch' + } + }, lede)); +} +function PipelineStrip({ + activeIdx = 3 +}) { + const stages = [{ + num: 'Stage 0', + name: 'Raw idea', + meta: 'inbox/' + }, { + num: 'Stage 1', + name: 'Triage', + meta: '2026-02-12' + }, { + num: 'Stage 2', + name: 'Prototype card', + meta: 'prototypes/' + }, { + num: 'Stage 3', + name: 'Experiment', + meta: 'ends 2026-04-01' + }, { + num: 'Stage 4', + name: 'Signal review', + meta: '— pending' + }]; + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: 'repeat(5, 1fr)', + gap: 0, + position: 'relative', + margin: '0 0 32px' + } + }, stages.map((s, i) => { + const state = i < activeIdx ? 'done' : i === activeIdx ? 'active' : 'pending'; + const topColor = state === 'done' ? 'var(--ink)' : state === 'active' ? 'var(--hi-2)' : 'var(--border)'; + return /*#__PURE__*/React.createElement("div", { + key: i, + style: { + padding: '10px 12px 14px', + borderTop: `2px solid ${topColor}`, + display: 'flex', + flexDirection: 'column', + gap: 4, + position: 'relative' + } + }, /*#__PURE__*/React.createElement("span", { + style: { + font: '500 10px/1 var(--ff-mono)', + letterSpacing: '0.1em', + textTransform: 'uppercase', + color: state === 'pending' ? 'var(--fg-3)' : 'var(--fg-1)' + } + }, s.num), /*#__PURE__*/React.createElement("span", { + style: { + font: '500 14px/1.25 var(--ff-sans)', + color: state === 'pending' ? 'var(--fg-3)' : 'var(--fg-1)' + } + }, s.name), /*#__PURE__*/React.createElement("span", { + style: { + font: '400 11px/1.35 var(--ff-mono)', + color: 'var(--fg-3)' + } + }, s.meta), i > 0 && /*#__PURE__*/React.createElement("span", { + style: { + position: 'absolute', + top: -8, + right: -7, + font: '400 14px var(--ff-mono)', + color: state === 'pending' ? 'var(--ink-5)' : 'var(--ink)' + } + }, "→")); + })); +} +Object.assign(window, { + TopNav, + Sidebar, + PageHeader, + PipelineStrip, + NAV_ITEMS, + DOC_ITEMS +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/whynot-control/Chrome.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/whynot-control/DocView.jsx +try { (() => { +// ============================================================= +// Document viewer — renders one of the control docs +// ============================================================= + +const DOC_CONTENT = { + intent: { + title: 'INTENT.md', + eyebrow: 'whynot-control · control document', + sections: [{ + h: 'Purpose', + p: 'whynot-control exists to serve as the control repository for the whynot organisation: a prototype, feedback, and market-signal space for discovering the weird and the useful.' + }, { + h: 'Primary utility', + list: ['capture unusual but potentially useful ideas;', 'distinguish curiosity from commitment;', 'shape rough ideas into testable prototypes;', 'collect early feedback and market signals;', 'run closed beta concepts in a controlled way;', 'identify which ideas should move toward Helix, Coulomb, Sloppers, Plenitude, Binky, or Tegwick;', 'prevent premature productisation.'] + }, { + h: 'Operating principle', + quote: 'A prototype is a question made tangible. The purpose of a prototype is not to prove that an idea is brilliant. The purpose is to learn what is actually useful, desirable, feasible, or irrelevant.' + }] + }, + scope: { + title: 'SCOPE.md', + eyebrow: 'whynot-control · control document', + sections: [{ + h: 'Current reality', + p: 'whynot-control is the control repository for organising prototype exploration and early market-signal capture.' + }, { + h: 'In scope', + list: ['Prototype idea capture.', 'Prototype classification.', 'Early user feedback notes.', 'Market-signal tracking.', 'Closed beta planning.', 'Experiment records.', 'Promotion recommendations.', 'Agent-assisted drafting and analysis.'] + }, { + h: 'Out of scope', + list: ['Production implementation.', 'Long-term product maintenance.', 'Payment processing.', 'Legal investment documentation.', 'Public launch operations.', 'Binding financial, legal, or tax conclusions.'] + }, { + h: 'Scope guardrail', + quote: 'whynot-control explores and validates. It does not absorb all product development.' + }] + }, + operating: { + title: 'OPERATING_MODEL.md', + eyebrow: 'whynot-control · control document', + sections: [{ + h: 'Core rules', + list: ['Prototypes are questions. Each prototype should express a question about usefulness, desirability, feasibility, or willingness to pay.', 'Signal beats enthusiasm. An idea should not be promoted only because it is exciting.', 'Low-cost learning first. Prefer sketches, mockups, demos, landing pages, conversations.', 'Closed beta before broad launch.', 'Promotion requires criteria.'] + }, { + h: 'Burnout guardrail', + quote: 'A prototype can be interesting and still be parked. whynot exists to reduce uncertainty, not to create more obligations.' + }] + }, + pipeline: { + title: 'PROTOTYPE_PIPELINE.md', + eyebrow: 'whynot-control · control document', + sections: [{ + h: 'Stage 0 — Raw capture', + p: 'Capture ideas without judging them immediately. Located in inbox/. Done when the idea is saved and no longer needs to be held in memory.' + }, { + h: 'Stage 1 — Triage', + p: 'Decide whether an idea deserves a prototype card. Outcomes: create card, park, merge, reject.' + }, { + h: 'Stage 2 — Prototype card', + p: 'Turn the idea into a structured prototype candidate. Located in prototypes/.' + }, { + h: 'Stage 3 — Experiment', + p: 'Test the idea with minimal cost: concept note, landing page, clickable mockup, CLI/demo script, Wizard-of-Oz, manual concierge test, closed conversation, private beta.' + }, { + h: 'Stage 4 — Signal review', + p: 'Evaluate what was learned. Interest, usefulness, retention, referral, payment, contribution, strategic fit.' + }, { + h: 'Stage 5 — Decision', + p: 'Park, iterate, promote, reject, or merge. Promotion requires an explicit record in DECISIONS.md.' + }] + }, + agent: { + title: 'AGENT_RULES.md', + eyebrow: 'whynot-control · control document', + sections: [{ + h: 'General principle', + p: 'Agents may help clarify, structure, draft, compare, and analyse prototype ideas. They must not silently turn experiments into product commitments.' + }, { + h: 'Allowed', + list: ['draft prototype cards', 'classify ideas by lifecycle stage', 'propose smallest useful tests', 'summarise feedback', 'compare prototype candidates', 'improve wording and structure'] + }, { + h: 'Forbidden', + list: ['create artificial urgency', 'treat all prototype ideas as products', 'infer willingness to pay without evidence', 'present weak signals as strong validation', 'create legal, financial, or investment commitments'] + }, { + h: 'Preferred output style', + quote: 'Agent outputs should be concise, evidence-oriented, explicit about uncertainty, and careful to separate idea, hypothesis, signal, and decision.' + }] + } +}; +function DocView({ + docKey +}) { + const doc = DOC_CONTENT[docKey]; + if (!doc) return /*#__PURE__*/React.createElement("div", null, "Doc not found."); + return /*#__PURE__*/React.createElement("article", { + style: { + maxWidth: 680 + } + }, /*#__PURE__*/React.createElement(Eyebrow, null, doc.eyebrow), /*#__PURE__*/React.createElement("h1", { + style: { + font: '600 36px/1.1 var(--ff-mono)', + letterSpacing: '-0.01em', + margin: '12px 0 28px' + } + }, doc.title), doc.sections.map((s, i) => /*#__PURE__*/React.createElement("section", { + key: i, + style: { + marginBottom: 36 + } + }, /*#__PURE__*/React.createElement("h2", { + style: { + font: '500 22px/1.25 var(--ff-sans)', + letterSpacing: '-0.005em', + margin: '0 0 14px' + } + }, s.h), s.p && /*#__PURE__*/React.createElement("p", { + style: { + margin: 0, + font: '400 15px/1.65 var(--ff-sans)', + color: 'var(--fg-1)' + } + }, s.p), s.list && /*#__PURE__*/React.createElement("ul", { + style: { + margin: 0, + paddingLeft: 18, + color: 'var(--fg-1)', + font: '400 15px/1.7 var(--ff-sans)' + } + }, s.list.map((li, j) => /*#__PURE__*/React.createElement("li", { + key: j, + style: { + marginBottom: 6 + } + }, li))), s.quote && /*#__PURE__*/React.createElement("blockquote", { + style: { + margin: 0, + paddingLeft: 16, + borderLeft: '1px solid var(--border-strong)' + } + }, /*#__PURE__*/React.createElement("p", { + style: { + margin: 0, + font: '400 italic 17px/1.55 var(--ff-serif)', + color: 'var(--fg-2)' + } + }, s.quote)))), /*#__PURE__*/React.createElement("div", { + style: { + marginTop: 48, + padding: '14px 0', + borderTop: '1px solid var(--border)', + display: 'flex', + justifyContent: 'space-between', + font: '400 11px var(--ff-mono)', + color: 'var(--fg-3)' + } + }, /*#__PURE__*/React.createElement("span", null, "whynot-control / ", doc.title), /*#__PURE__*/React.createElement("span", null, "A1 \xB7 Incubating \xB7 2026"))); +} +Object.assign(window, { + DocView, + DOC_CONTENT +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/whynot-control/DocView.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/whynot-control/Screens.jsx +try { (() => { +// ============================================================= +// Screens — Inbox, PrototypesIndex, PrototypeDetail, SignalsIndex, DocView, BetasIndex, DecisionsIndex +// ============================================================= + +function Inbox({ + onCapture +}) { + const [draft, setDraft] = React.useState(''); + return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(PageHeader, { + eyebrow: "whynot-control / inbox", + title: "Inbox", + lede: "Temporary capture for rough ideas, weird observations, user comments, market hints, and product fragments. Capture is not commitment." + }), /*#__PURE__*/React.createElement("div", { + style: { + border: '1px solid var(--border)', + borderRadius: 'var(--r-2)', + padding: 16, + background: 'var(--paper)', + marginBottom: 28, + display: 'flex', + flexDirection: 'column', + gap: 10 + } + }, /*#__PURE__*/React.createElement(Eyebrow, null, "Capture"), /*#__PURE__*/React.createElement("textarea", { + value: draft, + onChange: e => setDraft(e.target.value), + placeholder: "An idea, an observation, a fragment. No filter, no judgement, no commitment.", + style: { + font: '400 14px/1.5 var(--ff-sans)', + border: 'none', + outline: 'none', + resize: 'none', + minHeight: 64, + padding: 0, + background: 'transparent', + color: 'var(--fg-1)' + } + }), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 8 + } + }, /*#__PURE__*/React.createElement("span", { + style: { + font: '400 11px var(--ff-mono)', + color: 'var(--fg-3)', + marginRight: 'auto' + } + }, "⌘ ↵ to capture \xB7 stored in ", /*#__PURE__*/React.createElement("code", { + className: "mono" + }, "inbox/")), /*#__PURE__*/React.createElement(Button, { + variant: "ghost", + onClick: () => setDraft('') + }, "Discard"), /*#__PURE__*/React.createElement(Button, { + variant: "primary", + icon: "inbox", + onClick: () => { + onCapture && onCapture(draft); + setDraft(''); + } + }, "Capture"))), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + alignItems: 'center', + gap: 12, + marginBottom: 14 + } + }, /*#__PURE__*/React.createElement(Eyebrow, null, "Recent \xB7 7"), /*#__PURE__*/React.createElement("div", { + style: { + flex: 1, + borderTop: '1px solid var(--border-soft)' + } + }), /*#__PURE__*/React.createElement("span", { + style: { + font: '400 11px var(--ff-mono)', + color: 'var(--fg-3)' + } + }, "↓ newest first")), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column' + } + }, INBOX.map(item => /*#__PURE__*/React.createElement("div", { + key: item.id, + style: { + display: 'grid', + gridTemplateColumns: '120px 1fr', + gap: '4px 24px', + padding: '20px 0', + borderBottom: '1px solid var(--border-soft)', + alignItems: 'baseline' + } + }, /*#__PURE__*/React.createElement("span", { + style: { + font: '400 11px var(--ff-mono)', + color: 'var(--fg-3)' + } + }, item.ts), /*#__PURE__*/React.createElement("span", { + style: { + font: '500 10px/1 var(--ff-mono)', + letterSpacing: '0.08em', + textTransform: 'uppercase', + color: 'var(--fg-3)' + } + }, item.from), /*#__PURE__*/React.createElement("p", { + style: { + gridColumn: '1 / -1', + margin: '4px 0 0', + font: '400 15px/1.6 var(--ff-sans)', + color: 'var(--fg-1)', + maxWidth: '60ch' + } + }, item.text))))); +} +function PrototypeListCard({ + p, + onOpen +}) { + const [hover, setHover] = React.useState(false); + return /*#__PURE__*/React.createElement("article", { + onClick: () => onOpen(p.id), + onMouseEnter: () => setHover(true), + onMouseLeave: () => setHover(false), + style: { + background: 'var(--paper)', + borderTop: '1px solid var(--border-soft)', + padding: '24px 0 28px', + display: 'flex', + flexDirection: 'column', + gap: 12, + position: 'relative', + cursor: 'pointer', + transition: 'background 120ms ease' + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'baseline' + } + }, /*#__PURE__*/React.createElement(Eyebrow, { + style: { + color: hover ? 'var(--fg-1)' : 'var(--fg-3)' + } + }, p.id, " \xB7 Prototype"), /*#__PURE__*/React.createElement(StageDot, { + level: p.signal, + label: p.stageLabel + })), /*#__PURE__*/React.createElement("h3", { + style: { + font: '400 22px/1.3 var(--ff-sans)', + letterSpacing: '-0.01em', + margin: '2px 0 8px', + color: 'var(--fg-1)', + maxWidth: '52ch' + } + }, p.pitch), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: '140px 1fr', + gap: '10px 16px', + fontSize: 14, + color: 'var(--fg-2)', + maxWidth: '60ch' + } + }, /*#__PURE__*/React.createElement("span", { + style: { + font: '500 11px/1.7 var(--ff-mono)', + letterSpacing: '0.06em', + textTransform: 'uppercase', + color: 'var(--fg-3)' + } + }, "Learning q."), /*#__PURE__*/React.createElement("span", { + style: { + lineHeight: 1.55 + } + }, p.learning), /*#__PURE__*/React.createElement("span", { + style: { + font: '500 11px/1.7 var(--ff-mono)', + letterSpacing: '0.06em', + textTransform: 'uppercase', + color: 'var(--fg-3)' + } + }, "Smallest test"), /*#__PURE__*/React.createElement("span", { + style: { + lineHeight: 1.55 + } + }, p.test)), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 24, + marginTop: 4, + font: '500 11px var(--ff-mono)', + letterSpacing: '0.06em', + textTransform: 'uppercase', + color: 'var(--fg-3)' + } + }, /*#__PURE__*/React.createElement("span", null, "→ ", p.target), /*#__PURE__*/React.createElement("span", null, p.signal, " signal"), /*#__PURE__*/React.createElement("span", { + style: { + marginLeft: 'auto', + color: hover ? 'var(--fg-1)' : 'var(--fg-3)' + } + }, "Open →"))); +} +function PrototypesIndex({ + onOpen +}) { + const [filter, setFilter] = React.useState('All'); + const filters = ['All', 'Experiment', 'Signal review', 'Parked']; + const list = filter === 'All' ? PROTOTYPES : PROTOTYPES.filter(p => p.stageLabel === filter); + return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(PageHeader, { + eyebrow: "whynot-control / prototypes", + title: "Prototypes", + lede: "Structured prototype cards. A prototype card defines a learning question and the smallest useful test.", + actions: /*#__PURE__*/React.createElement(Button, { + variant: "primary", + icon: "plus" + }, "New prototype") + }), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + gap: 10, + marginBottom: 8, + alignItems: 'center' + } + }, filters.map(f => /*#__PURE__*/React.createElement(Tag, { + key: f, + active: filter === f, + style: { + cursor: 'pointer' + } + }, /*#__PURE__*/React.createElement("span", { + onClick: () => setFilter(f) + }, f))), /*#__PURE__*/React.createElement("span", { + style: { + marginLeft: 'auto', + font: '400 11px var(--ff-mono)', + color: 'var(--fg-3)' + } + }, list.length, " of ", PROTOTYPES.length)), /*#__PURE__*/React.createElement("div", null, list.map(p => /*#__PURE__*/React.createElement(PrototypeListCard, { + key: p.id, + p: p, + onOpen: onOpen + })))); +} +function PrototypeDetail({ + id, + onBack +}) { + const p = PROTOTYPES.find(p => p.id === id) || PROTOTYPES[0]; + const stageIdx = { + 'parked': 0, + 'experiment': 3, + 'signal': 4, + 'experiment-active': 3 + }[p.stage] ?? 3; + return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("a", { + onClick: onBack, + style: { + display: 'inline-flex', + alignItems: 'center', + gap: 6, + font: '400 12px var(--ff-mono)', + color: 'var(--fg-2)', + textDecoration: 'none', + marginBottom: 18, + cursor: 'pointer' + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "arrow-left", + size: 14 + }), " Back to prototypes"), /*#__PURE__*/React.createElement(PageHeader, { + eyebrow: `${p.id} · Prototype`, + title: p.pitch, + actions: /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Button, { + variant: "secondary", + icon: "archive" + }, "Park"), /*#__PURE__*/React.createElement(Button, { + variant: "primary", + icon: "arrow-right" + }, "Promote → ", p.target)) + }), /*#__PURE__*/React.createElement(PipelineStrip, { + activeIdx: stageIdx + }), /*#__PURE__*/React.createElement("div", { + style: { + display: 'grid', + gridTemplateColumns: '1.4fr 1fr', + gap: 32 + } + }, /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 22 + } + }, /*#__PURE__*/React.createElement(Field, { + label: "Learning question", + value: p.learning + }), /*#__PURE__*/React.createElement(Field, { + label: "Smallest useful test", + value: p.test + }), /*#__PURE__*/React.createElement(Field, { + label: "Expected signal", + value: "At least one person asks for a concrete next step, gives specific use-case feedback, or identifies a realistic context where the idea would matter." + }), /*#__PURE__*/React.createElement(Field, { + label: "Risks", + value: p.risks + })), /*#__PURE__*/React.createElement("aside", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 18 + } + }, /*#__PURE__*/React.createElement(SidebarField, { + label: "Stage", + value: /*#__PURE__*/React.createElement(Tag, { + active: true + }, p.stageLabel) + }), /*#__PURE__*/React.createElement(SidebarField, { + label: "Signal", + value: /*#__PURE__*/React.createElement(StageDot, { + level: p.signal + }) + }), /*#__PURE__*/React.createElement(SidebarField, { + label: "Target", + value: /*#__PURE__*/React.createElement("code", { + className: "mono" + }, "→ ", p.target) + }), /*#__PURE__*/React.createElement(SidebarField, { + label: "Audience", + value: "Potential early users, collaborators, or customers." + }), /*#__PURE__*/React.createElement(SidebarField, { + label: "Agentic suitability", + value: "Agents may help turn rough notes into a sharper prototype card." + }), /*#__PURE__*/React.createElement("div", { + style: { + marginTop: 6, + border: '1px dashed var(--border-strong)', + borderRadius: 4, + padding: 14 + } + }, /*#__PURE__*/React.createElement(Eyebrow, { + style: { + display: 'block', + marginBottom: 8 + } + }, "Caveat"), /*#__PURE__*/React.createElement("p", { + style: { + margin: 0, + font: '400 13px/1.55 var(--ff-sans)', + color: 'var(--fg-2)' + } + }, "A prototype can be interesting and still be parked. ", /*#__PURE__*/React.createElement("code", { + className: "mono" + }, "whynot"), " exists to reduce uncertainty, not create more obligations."))))); +} +function Field({ + label, + value +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 6 + } + }, /*#__PURE__*/React.createElement(Eyebrow, null, label), /*#__PURE__*/React.createElement("p", { + style: { + margin: 0, + font: '400 15px/1.55 var(--ff-sans)', + color: 'var(--fg-1)' + } + }, value)); +} +function SidebarField({ + label, + value +}) { + return /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column', + gap: 6 + } + }, /*#__PURE__*/React.createElement(Eyebrow, null, label), /*#__PURE__*/React.createElement("div", { + style: { + font: '400 13px/1.5 var(--ff-sans)', + color: 'var(--fg-1)' + } + }, value)); +} +function SignalsIndex() { + return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(PageHeader, { + eyebrow: "whynot-control / signals", + title: "Signals", + lede: "Market-signal and feedback records. A signal is evidence. Record what happened, who did it, and how strong the evidence is.", + actions: /*#__PURE__*/React.createElement(Button, { + variant: "primary", + icon: "plus" + }, "Record signal") + }), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column' + } + }, SIGNALS.map(s => /*#__PURE__*/React.createElement("div", { + key: s.id, + style: { + display: 'grid', + gridTemplateColumns: '90px 90px 1fr', + gap: '6px 24px', + padding: '20px 0', + borderBottom: '1px solid var(--border-soft)', + alignItems: 'baseline' + } + }, /*#__PURE__*/React.createElement("code", { + className: "mono", + style: { + background: 'none', + padding: 0, + color: 'var(--fg-3)', + font: '400 11px var(--ff-mono)' + } + }, s.id), /*#__PURE__*/React.createElement("code", { + className: "mono", + style: { + background: 'none', + padding: 0, + color: 'var(--fg-2)', + font: '400 11px var(--ff-mono)' + } + }, s.proto), /*#__PURE__*/React.createElement(StageDot, { + level: s.level + }), /*#__PURE__*/React.createElement("p", { + style: { + gridColumn: '1 / -1', + margin: '4px 0 0', + font: '400 14px/1.55 var(--ff-sans)', + color: 'var(--fg-1)', + maxWidth: '60ch' + } + }, s.what), /*#__PURE__*/React.createElement("span", { + style: { + gridColumn: '1 / -1', + font: '400 11px var(--ff-mono)', + color: 'var(--fg-3)' + } + }, s.source, " \xB7 ", s.date))))); +} +function BetasIndex() { + return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(PageHeader, { + eyebrow: "whynot-control / betas", + title: "Betas", + lede: "Closed beta plans and beta review notes. A beta should have a clear learning question, entry criteria, and exit outcome." + }), /*#__PURE__*/React.createElement("div", { + style: { + border: '1px dashed var(--border-strong)', + padding: 32, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 8, + textAlign: 'center', + color: 'var(--fg-3)', + borderRadius: 4 + } + }, /*#__PURE__*/React.createElement(Icon, { + name: "users", + size: 20 + }), /*#__PURE__*/React.createElement("div", { + style: { + font: '500 14px var(--ff-sans)', + color: 'var(--fg-2)' + } + }, "One beta plan in draft."), /*#__PURE__*/React.createElement("div", { + style: { + font: '400 12px var(--ff-mono)', + color: 'var(--fg-3)' + } + }, "WNO-021 \xB7 Concierge triage \xB7 pending Binky approval"), /*#__PURE__*/React.createElement("a", { + href: "#", + style: { + font: '500 12px var(--ff-mono)', + color: 'var(--fg-1)', + marginTop: 4 + } + }, "Open draft →"))); +} +function DecisionsIndex() { + const decisions = [{ + id: 'DEC-001', + title: 'Shorten organisation name from whywhynot to whynot.', + status: 'Accepted', + date: '2026-01-08' + }, { + id: 'DEC-002', + title: 'Maintain A1 Incubating until first prototype candidates review.', + status: 'Open', + date: '—' + }, { + id: 'DEC-003', + title: 'Initial promotion targets: Helix, Coulomb, Sloppers, Plenitude, Binky, Tegwick.', + status: 'Open', + date: '—' + }]; + return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(PageHeader, { + eyebrow: "whynot-control / decisions", + title: "Decisions", + lede: "A promotion record is required before any prototype moves to Helix, Coulomb, Sloppers, Plenitude, Binky, or Tegwick." + }), /*#__PURE__*/React.createElement("div", { + style: { + display: 'flex', + flexDirection: 'column' + } + }, decisions.map(d => /*#__PURE__*/React.createElement("div", { + key: d.id, + style: { + display: 'grid', + gridTemplateColumns: '90px 1fr 130px 100px', + gap: 20, + alignItems: 'baseline', + padding: '16px 4px', + borderBottom: '1px solid var(--border-soft)' + } + }, /*#__PURE__*/React.createElement("code", { + className: "mono", + style: { + background: 'none', + padding: 0, + color: 'var(--fg-1)' + } + }, d.id), /*#__PURE__*/React.createElement("span", { + style: { + font: '500 15px var(--ff-sans)', + color: 'var(--fg-1)' + } + }, d.title), /*#__PURE__*/React.createElement(Tag, { + active: d.status === 'Accepted', + draft: d.status === 'Open' + }, d.status), /*#__PURE__*/React.createElement("span", { + style: { + font: '400 12px var(--ff-mono)', + color: 'var(--fg-3)', + textAlign: 'right' + } + }, d.date))))); +} +Object.assign(window, { + Inbox, + PrototypesIndex, + PrototypeDetail, + SignalsIndex, + BetasIndex, + DecisionsIndex, + Field, + SidebarField, + PrototypeListCard +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/whynot-control/Screens.jsx", error: String((e && e.message) || e) }); } + +// ui_kits/whynot-control/data.jsx +try { (() => { +// ============================================================= +// Sample data — prototypes, signals, inbox items +// ============================================================= + +const PROTOTYPES = [{ + id: 'WNO-014', + pitch: 'A field-notebook for catching weird ideas before they evaporate.', + learning: 'Do people return to capture more than once?', + test: 'One-page landing + email capture, 14 days.', + target: 'Coulomb', + stage: 'experiment', + stageLabel: 'Experiment', + signal: 'S1', + risks: 'Confused with note-taking apps.' +}, { + id: 'WNO-017', + pitch: 'A LEGO-brick mood board for engineers who don’t think in mood boards.', + learning: 'Will engineers attach metaphors to their tickets?', + test: 'Slack bot, three teams, two weeks.', + target: 'Helix', + stage: 'signal', + stageLabel: 'Signal review', + signal: 'S3', + risks: 'Cute but unused after a week.' +}, { + id: 'WNO-021', + pitch: 'Concierge-style “prototype triage” for indie hackers.', + learning: 'Will three founders pay for a one-hour triage call?', + test: 'Offer beta · 3 calls · listed price.', + target: 'Plenitude', + stage: 'experiment', + stageLabel: 'Experiment', + signal: 'S2', + risks: 'Time-cost outruns signal value.' +}, { + id: 'WNO-024', + pitch: 'A relevant-#CoronaPolitics timeline, re-released with one editor.', + learning: 'Is there residual demand five years on?', + test: 'Static preview page, 30 days, count returns.', + target: 'None yet', + stage: 'parked', + stageLabel: 'Parked', + signal: 'S0', + risks: 'Topical relevance has clearly faded.' +}]; +const INBOX = [{ + id: 1, + ts: '2026-03-02 14:21', + text: 'Idea: “subway map” view of the prototype pipeline. People understand transit maps; they don’t understand kanban boards.', + from: 'Tegwick' +}, { + id: 2, + ts: '2026-03-01 09:08', + text: 'Weird observation from yesterday’s call: three founders independently asked for “something to capture the half-formed stuff”.', + from: 'note-to-self' +}, { + id: 3, + ts: '2026-02-28 23:55', + text: 'Could the LEGO-brick metaphor extend to a public “build log” format? One brick = one decision.', + from: 'Tegwick' +}, { + id: 4, + ts: '2026-02-27 11:34', + text: 'Park idea: realtime sentiment dashboard for prototype landing pages. Probably worse than reading the comments.', + from: 'note-to-self' +}, { + id: 5, + ts: '2026-02-26 17:02', + text: 'Conversation with R. about closed-beta etiquette. Useful: pre-write the exit email before the beta opens.', + from: 'note-to-self' +}, { + id: 6, + ts: '2026-02-25 08:12', + text: 'fuerindifferenz shirts: residual interest from old whywhynot.de page. Could a yearly drop work?', + from: 'Tegwick' +}, { + id: 7, + ts: '2026-02-24 15:40', + text: 'Tiny idea: a “reject log” that publishes the ideas you said no to, with one-sentence reasons.', + from: 'note-to-self' +}]; +const SIGNALS = [{ + id: 'SIG-031', + proto: 'WNO-017', + level: 'S3', + what: 'Two teams shipped public README sections labelled “brick: scope” after using the bot for a week.', + source: 'usage log', + date: '2026-03-04' +}, { + id: 'SIG-030', + proto: 'WNO-017', + level: 'S2', + what: 'Three engineers DM’d asking for an export-to-Notion option.', + source: 'Slack', + date: '2026-03-03' +}, { + id: 'SIG-029', + proto: 'WNO-014', + level: 'S1', + what: 'Landing page: 34 visits, 7 emails, 0 returns in week 1.', + source: 'Plausible', + date: '2026-03-01' +}, { + id: 'SIG-028', + proto: 'WNO-021', + level: 'S2', + what: 'First triage call booked at listed price; second declined on price.', + source: 'Stripe / email', + date: '2026-02-28' +}, { + id: 'SIG-027', + proto: 'WNO-021', + level: 'S1', + what: '“Interesting but I’d want a free first one” ×2.', + source: 'interview', + date: '2026-02-26' +}, { + id: 'SIG-026', + proto: 'WNO-024', + level: 'S0', + what: 'Static preview: 12 visits in 30 days, 0 returns.', + source: 'Plausible', + date: '2026-02-24' +}]; +Object.assign(window, { + PROTOTYPES, + INBOX, + SIGNALS +}); +})(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/whynot-control/data.jsx", error: String((e && e.message) || e) }); } + +})(); diff --git a/designbook/_ds_manifest.json b/designbook/_ds_manifest.json new file mode 100644 index 0000000..8e5d372 --- /dev/null +++ b/designbook/_ds_manifest.json @@ -0,0 +1 @@ +{"namespace":"WhyNotDesignSystem_fb2eef","components":[],"startingPoints":[],"cards":[{"path":"preview/brand-iconography.html","group":"Brand","viewport":"700x240","subtitle":"Lucide · 1.5px stroke · 16 of set","name":"Brand · Iconography"},{"path":"preview/brand-lockups.html","group":"Brand","viewport":"700x260","subtitle":"Mark + slug · 3 sizes","name":"Brand · Lockups"},{"path":"preview/brand-logo.html","group":"Brand","viewport":"700x220","subtitle":"Primary · inverted · ?! wordmark","name":"Brand · Logo"},{"path":"preview/brand-wireframe-motif.html","group":"Brand","viewport":"700x240","subtitle":"Graph paper + draft stamp","name":"Brand · Wireframe Motif"},{"path":"preview/colors-accent.html","group":"Colors","viewport":"700x200","subtitle":"Annotation yellow — highlighter only","name":"Colors · Accent"},{"path":"preview/colors-borders.html","group":"Colors","viewport":"700x180","subtitle":"Hairline · default · strong","name":"Colors · Borders"},{"path":"preview/colors-neutrals.html","group":"Colors","viewport":"700x290","subtitle":"Ink scale + paper surfaces","name":"Colors · Neutrals"},{"path":"preview/colors-signal.html","group":"Colors","viewport":"700x220","subtitle":"S0–S4 ramp, S4 uses accent","name":"Colors · Signal Strength"},{"path":"preview/colors-status-functional.html","group":"Colors","viewport":"700x280","subtitle":"Error / warn / success / info — borders & dots only","name":"Colors · Functional Status"},{"path":"preview/comp-buttons.html","group":"Components","viewport":"700x240","subtitle":"Primary · secondary · ghost","name":"Components · Buttons"},{"path":"preview/comp-empty-placeholder.html","group":"Components","viewport":"700x220","subtitle":"Dashed border · wireframe lines","name":"Components · Empty State"},{"path":"preview/comp-inputs.html","group":"Components","viewport":"700x280","subtitle":"Default · focus · error","name":"Components · Inputs"},{"path":"preview/comp-labels-tags.html","group":"Components","viewport":"700x200","subtitle":"Stage tags · signal dots","name":"Components · Labels & Tags"},{"path":"preview/comp-left-nav.html","group":"Components","viewport":"700x420","subtitle":"Grouped sidebar · active state · minimal variant","name":"Components · Left Navigation"},{"path":"preview/comp-pipeline.html","group":"Components","viewport":"700x180","subtitle":"Lifecycle stage tracker","name":"Components · Pipeline"},{"path":"preview/comp-prototype-card.html","group":"Components","viewport":"700x290","subtitle":"Default + hover (black left bar)","name":"Components · Prototype Card"},{"path":"preview/comp-topnav.html","group":"Components","viewport":"900x160","subtitle":"56px · 1px hairline · ⌘K search","name":"Components · Top Navigation"},{"path":"preview/page-beta-invitation.html","group":"Pages","viewport":"1280x860","subtitle":"Invitation-only · seats, dates, accept / decline","name":"Pages · Closed-beta invitation"},{"path":"preview/page-landing-auth.html","group":"Pages","viewport":"1280x820","subtitle":"Public landing · log in / request access toggle","name":"Pages · Landing — Login & Registration"},{"path":"preview/page-prototype-detail.html","group":"Pages","viewport":"1280x860","subtitle":"Single prototype · pipeline, learning question, signal sidebar","name":"Pages · Prototype detail"},{"path":"preview/page-signals-dashboard.html","group":"Pages","viewport":"1280x860","subtitle":"Market-signal log · filter by strength · evidence rows","name":"Pages · Signals dashboard"},{"path":"ui_kits/whynot-control/index.html","group":"Pages","viewport":"1200x740","subtitle":"Authenticated app · inbox, prototypes, signals, betas, decisions, docs","name":"Pages · User dashboard"},{"path":"preview/spacing-elevation.html","group":"Spacing","viewport":"700x220","subtitle":"Mostly none · wireframe system","name":"Spacing · Elevation"},{"path":"preview/spacing-radii.html","group":"Spacing","viewport":"700x200","subtitle":"0 / 2 / 4 / 8 / pill","name":"Spacing · Radii"},{"path":"preview/spacing-scale.html","group":"Spacing","viewport":"700x280","subtitle":"4px base · 10 steps","name":"Spacing · Scale"},{"path":"preview/type-body.html","group":"Type","viewport":"700x220","subtitle":"Lead 17/1.55 · Body 15/1.5","name":"Type · Body & Lead"},{"path":"preview/type-display.html","group":"Type","viewport":"700x200","subtitle":"PlexSans 300/400 · -.035em tracking","name":"Type · Display"},{"path":"preview/type-headings.html","group":"Type","viewport":"700x320","subtitle":"H1–H5 · PlexSans 500 · tight tracking","name":"Type · Headings"},{"path":"preview/type-mono-eyebrows.html","group":"Type","viewport":"700x220","subtitle":"PlexMono · uppercase · .08em tracking","name":"Type · Mono & Eyebrows"},{"path":"preview/type-serif-quote.html","group":"Type","viewport":"700x210","subtitle":"PlexSerif italic · editorial moments","name":"Type · Serif Quote"}],"templates":[],"globalCssPaths":["colors_and_type.css","styles.css"],"tokens":[{"name":"--ink","value":"#0A0A0A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--ink-2","value":"#1F1F1F","kind":"color","definedIn":"colors_and_type.css"},{"name":"--ink-3","value":"#5C5C5C","kind":"color","definedIn":"colors_and_type.css"},{"name":"--ink-4","value":"#8A8A8A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--ink-5","value":"#B5B5B3","kind":"color","definedIn":"colors_and_type.css"},{"name":"--line","value":"#E5E5E2","kind":"color","definedIn":"colors_and_type.css"},{"name":"--line-strong","value":"#C9C9C5","kind":"color","definedIn":"colors_and_type.css"},{"name":"--line-soft","value":"#F0F0EC","kind":"color","definedIn":"colors_and_type.css"},{"name":"--paper","value":"#FFFFFF","kind":"color","definedIn":"colors_and_type.css"},{"name":"--paper-2","value":"#FAFAF7","kind":"color","definedIn":"colors_and_type.css"},{"name":"--paper-3","value":"#F4F4EF","kind":"color","definedIn":"colors_and_type.css"},{"name":"--fg-1","value":"var(--ink)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--fg-2","value":"var(--ink-3)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--fg-3","value":"var(--ink-4)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--fg-mute","value":"var(--ink-5)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--fg-on-dark","value":"#FAFAF7","kind":"color","definedIn":"colors_and_type.css"},{"name":"--bg-1","value":"var(--paper)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--bg-2","value":"var(--paper-2)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--bg-3","value":"var(--paper-3)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--bg-invert","value":"var(--ink)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--border","value":"var(--line)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--border-strong","value":"var(--line-strong)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--border-soft","value":"var(--line-soft)","kind":"color","definedIn":"colors_and_type.css"},{"name":"--hi","value":"#FFE14A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--hi-2","value":"#FFD400","kind":"color","definedIn":"colors_and_type.css"},{"name":"--hi-ink","value":"#1A1500","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-raw","value":"#B5B5B3","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-weak","value":"#8A8A8A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-medium","value":"#5C5C5C","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-strong","value":"#0A0A0A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-commercial","value":"#FFD400","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-error","value":"#B33A2E","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-error-bg","value":"#FCF3F1","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-warn","value":"#C28000","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-warn-bg","value":"#FFFCEB","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-success","value":"#2F6B3A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-success-bg","value":"#F2F7F2","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-info","value":"#2E5C8A","kind":"color","definedIn":"colors_and_type.css"},{"name":"--status-info-bg","value":"#F2F5FA","kind":"color","definedIn":"colors_and_type.css"},{"name":"--ff-sans","value":"ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,\n \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif","kind":"font","definedIn":"colors_and_type.css","annotation":"font"},{"name":"--ff-mono","value":"ui-monospace, \"SF Mono\", Menlo, Consolas, monospace","kind":"font","definedIn":"colors_and_type.css","annotation":"font"},{"name":"--ff-serif","value":"ui-serif, Georgia, \"Times New Roman\", serif","kind":"font","definedIn":"colors_and_type.css","annotation":"font"},{"name":"--fs-xs","value":"11px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--fs-sm","value":"13px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--fs-base","value":"15px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--fs-md","value":"17px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--fs-lg","value":"20px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--fs-xl","value":"24px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--fs-2xl","value":"32px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--fs-3xl","value":"44px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--fs-4xl","value":"64px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--fs-5xl","value":"96px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--lh-tight","value":"1.05","kind":"font","definedIn":"colors_and_type.css","annotation":"font"},{"name":"--lh-snug","value":"1.25","kind":"font","definedIn":"colors_and_type.css","annotation":"font"},{"name":"--lh-base","value":"1.5","kind":"font","definedIn":"colors_and_type.css","annotation":"font"},{"name":"--lh-loose","value":"1.7","kind":"font","definedIn":"colors_and_type.css","annotation":"font"},{"name":"--tr-tight","value":"-0.02em","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--tr-snug","value":"-0.01em","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--tr-base","value":"0em","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--tr-mono","value":"0.02em","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--tr-label","value":"0.08em","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-1","value":"4px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-2","value":"8px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-3","value":"12px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-4","value":"16px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-5","value":"24px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-6","value":"32px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-7","value":"48px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-8","value":"64px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-9","value":"96px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--sp-10","value":"128px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--r-0","value":"0px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--r-1","value":"2px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--r-2","value":"4px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--r-3","value":"8px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--r-pill","value":"999px","kind":"spacing","definedIn":"colors_and_type.css"},{"name":"--shadow-0","value":"none","kind":"shadow","definedIn":"colors_and_type.css"},{"name":"--shadow-1","value":"0 1px 0 var(--line)","kind":"shadow","definedIn":"colors_and_type.css"},{"name":"--shadow-2","value":"0 1px 0 var(--line-strong)","kind":"shadow","definedIn":"colors_and_type.css"},{"name":"--shadow-3","value":"0 4px 12px -6px rgba(10,10,10,0.10)","kind":"shadow","definedIn":"colors_and_type.css"}],"themes":[],"fonts":[],"brandFonts":[],"source":"spa"} \ No newline at end of file diff --git a/designbook/colors_and_type.css b/designbook/colors_and_type.css new file mode 100644 index 0000000..1d7aeac --- /dev/null +++ b/designbook/colors_and_type.css @@ -0,0 +1,293 @@ +/* ============================================================ + WhyNot Design System — Colors & Type + ------------------------------------------------------------ + Neutral, mostly black/white. Color is used SPARINGLY — only + one warm accent (annotation yellow) borrowed from the LEGO + brick in the logo. The system favours light grey wireframe + artefacts over heavy fills. + ============================================================ */ + +/* ---------- Fonts ---------- + System-font stacks: zero CDN dependency, zero offline issues, no CSP + headaches, no FOUC. macOS gets SF Pro / SF Mono; Windows gets Segoe UI + / Cascadia; Linux falls through to its own ui-* alias. All three stacks + ship as "quiet, document-quality" out of the box, which matches the + system's intent (wireframe-leaning, not branded display type). */ + +:root { + /* ---------- Base palette: neutrals ---------- */ + --ink: #0A0A0A; /* near-black, the only "fill" most of the time */ + --ink-2: #1F1F1F; + --ink-3: #5C5C5C; + --ink-4: #8A8A8A; + --ink-5: #B5B5B3; /* placeholder text, wireframe labels */ + --line: #E5E5E2; /* default 1px wireframe rule */ + --line-strong: #C9C9C5; /* dividers between sections */ + --line-soft: #F0F0EC; /* hairline within a card */ + --paper: #FFFFFF; /* canvas */ + --paper-2: #FAFAF7; /* sheet, dim canvas */ + --paper-3: #F4F4EF; /* recessed surface, code block bg */ + + /* ---------- Foreground / background semantic ---------- */ + --fg-1: var(--ink); + --fg-2: var(--ink-3); + --fg-3: var(--ink-4); + --fg-mute: var(--ink-5); + --fg-on-dark: #FAFAF7; + + --bg-1: var(--paper); + --bg-2: var(--paper-2); + --bg-3: var(--paper-3); + --bg-invert: var(--ink); + + --border: var(--line); + --border-strong: var(--line-strong); + --border-soft: var(--line-soft); + + /* ---------- The single accent: annotation yellow ---------- */ + /* Lifted from the LEGO brick. Used as highlighter, "draft" + stamp, signal-marker. Never as a button fill. */ + --hi: #FFE14A; + --hi-2: #FFD400; + --hi-ink: #1A1500; /* text on yellow */ + + /* ---------- Status (for prototype lifecycle, signal strength) ---------- */ + /* Kept deliberately desaturated so they read as labels, not UI. */ + --status-raw: #B5B5B3; /* S0 — no signal */ + --status-weak: #8A8A8A; /* S1 — weak signal */ + --status-medium: #5C5C5C; /* S2 — medium signal */ + --status-strong: #0A0A0A; /* S3 — strong signal */ + --status-commercial: #FFD400; /* S4 — commercial */ + + /* ---------- Functional status (for UI feedback: errors, warnings, success, info) ---------- + Distinct from signal strength above. Used as 2px borders, small dots, and icon tints — + NEVER as fills or button backgrounds. Tints (e.g. --status-error-bg) are barely-saturated + paper tones for banner backgrounds when the message must really attract the eye. + If even this feels too colourful, set the *-fg tokens to var(--ink) — the system still + reads correctly with the dots/borders alone. */ + --status-error: #B33A2E; /* muted brick red */ + --status-error-bg: #FCF3F1; + --status-warn: #C28000; /* deep mustard — keeps lineage with --hi */ + --status-warn-bg: #FFFCEB; + --status-success: #2F6B3A; /* muted forest */ + --status-success-bg: #F2F7F2; + --status-info: #2E5C8A; /* muted ink-blue */ + --status-info-bg: #F2F5FA; + + /* ---------- Type families ---------- */ + --ff-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; /* @kind font */ + --ff-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace; /* @kind font */ + --ff-serif: ui-serif, Georgia, "Times New Roman", serif; /* @kind font */ + + /* ---------- Type scale (modular, ~1.2) ---------- */ + --fs-xs: 11px; + --fs-sm: 13px; + --fs-base: 15px; + --fs-md: 17px; + --fs-lg: 20px; + --fs-xl: 24px; + --fs-2xl: 32px; + --fs-3xl: 44px; + --fs-4xl: 64px; + --fs-5xl: 96px; + + --lh-tight: 1.05; /* @kind font */ + --lh-snug: 1.25; /* @kind font */ + --lh-base: 1.5; /* @kind font */ + --lh-loose: 1.7; /* @kind font */ + + --tr-tight: -0.02em; + --tr-snug: -0.01em; + --tr-base: 0em; + --tr-mono: 0.02em; + --tr-label: 0.08em; /* uppercase eyebrow labels */ + + /* ---------- Spacing (4px base) ---------- */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 24px; + --sp-6: 32px; + --sp-7: 48px; + --sp-8: 64px; + --sp-9: 96px; + --sp-10: 128px; + + /* ---------- Radii — small, mostly square ---------- */ + --r-0: 0px; + --r-1: 2px; + --r-2: 4px; + --r-3: 8px; + --r-pill: 999px; + + /* ---------- Elevation — almost none. This is a wireframe system. ---------- */ + --shadow-0: none; + --shadow-1: 0 1px 0 var(--line); + --shadow-2: 0 1px 0 var(--line-strong); + --shadow-3: 0 4px 12px -6px rgba(10,10,10,0.10); +} + +/* ============================================================ + Semantic element styles + ============================================================ */ + +html { + font-family: var(--ff-sans); + font-size: var(--fs-base); + line-height: var(--lh-base); + color: var(--fg-1); + background: var(--bg-1); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +body { + margin: 0; + font-feature-settings: "ss01", "cv11"; + text-wrap: pretty; +} + +/* ---------- Headings ---------- */ +h1, .h1 { + font: 600 var(--fs-3xl)/var(--lh-tight) var(--ff-sans); + letter-spacing: var(--tr-tight); + margin: 0 0 var(--sp-5); + color: var(--fg-1); +} +h2, .h2 { + font: 500 var(--fs-2xl)/var(--lh-snug) var(--ff-sans); + letter-spacing: var(--tr-snug); + margin: 0 0 var(--sp-4); +} +h3, .h3 { + font: 500 var(--fs-xl)/var(--lh-snug) var(--ff-sans); + letter-spacing: var(--tr-snug); + margin: 0 0 var(--sp-3); +} +h4, .h4 { + font: 500 var(--fs-lg)/var(--lh-snug) var(--ff-sans); + margin: 0 0 var(--sp-2); +} +h5, .h5 { + font: 500 var(--fs-md)/var(--lh-snug) var(--ff-sans); + margin: 0 0 var(--sp-2); +} + +/* ---------- Display (for hero / title slides) ---------- */ +.display-1 { + font: 300 var(--fs-5xl)/0.95 var(--ff-sans); + letter-spacing: -0.035em; + color: var(--fg-1); +} +.display-2 { + font: 400 var(--fs-4xl)/1.0 var(--ff-sans); + letter-spacing: var(--tr-tight); +} + +/* ---------- Body ---------- */ +p { + margin: 0 0 var(--sp-4); + line-height: var(--lh-base); + color: var(--fg-1); +} +.lead { + font-size: var(--fs-md); + line-height: 1.55; + color: var(--fg-2); +} +small, .small { + font-size: var(--fs-sm); + color: var(--fg-2); +} + +/* ---------- Eyebrow / uppercase labels (very common in this system) ---------- */ +.eyebrow, +.label { + font: 500 var(--fs-xs)/1.2 var(--ff-mono); + letter-spacing: var(--tr-label); + text-transform: uppercase; + color: var(--fg-3); +} + +/* ---------- Code / mono ---------- */ +code, kbd, samp, pre, .mono { + font-family: var(--ff-mono); + font-size: 0.92em; + letter-spacing: var(--tr-mono); +} +code { + background: var(--bg-3); + padding: 1px 6px; + border-radius: var(--r-1); + color: var(--ink-2); +} +pre { + background: var(--bg-3); + border: 1px solid var(--border); + padding: var(--sp-4); + overflow-x: auto; + border-radius: var(--r-2); + font-size: var(--fs-sm); + line-height: var(--lh-snug); +} +pre code { background: none; padding: 0; } + +/* ---------- Editorial serif moments ---------- */ +.serif { font-family: var(--ff-serif); } +.serif-quote { + font: 400 italic var(--fs-xl)/1.4 var(--ff-serif); + color: var(--fg-2); +} + +/* ---------- Links ---------- */ +a { + color: var(--fg-1); + text-decoration: underline; + text-decoration-color: var(--border-strong); + text-underline-offset: 3px; + text-decoration-thickness: 1px; + transition: text-decoration-color 120ms ease, color 120ms ease; +} +a:hover { + text-decoration-color: var(--fg-1); +} + +/* ---------- HR ---------- */ +hr { + border: 0; + border-top: 1px solid var(--border); + margin: var(--sp-5) 0; +} + +/* ---------- Highlighter (the one place yellow appears in body copy) ---------- */ +mark, .mark { + background: var(--hi); + color: var(--hi-ink); + padding: 0 2px; +} + +/* ---------- Tables (used in templates) ---------- */ +table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-sm); +} +th, td { + text-align: left; + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--border); +} +th { + font-weight: 500; + color: var(--fg-2); + font-family: var(--ff-mono); + font-size: var(--fs-xs); + letter-spacing: var(--tr-label); + text-transform: uppercase; +} + +/* ---------- Selection ---------- */ +::selection { background: var(--hi); color: var(--hi-ink); } diff --git a/designbook/preview/brand-iconography.html b/designbook/preview/brand-iconography.html new file mode 100644 index 0000000..4863f14 --- /dev/null +++ b/designbook/preview/brand-iconography.html @@ -0,0 +1,36 @@ + + + +Iconography — Lucide @ 1.5px + + + +
Lucide · stroke-width 1.5 · currentColor
+
+
inbox
+
lightbulb
+
flask
+
signal
+
beta
+
branch
+
decision
+
park
+
promote
+
reject
+
search
+
filter
+
doc
+
folder
+
question
+
caveat
+
+ + + diff --git a/designbook/preview/brand-lockups.html b/designbook/preview/brand-lockups.html new file mode 100644 index 0000000..0430fc6 --- /dev/null +++ b/designbook/preview/brand-lockups.html @@ -0,0 +1,26 @@ + + + +Logo Lockups + + + +
Lockups — mark + wordmark + organisation slug
+
whynot/control
+
whynot/prototypes
+
whynot·2026 · A1 incubating
+ diff --git a/designbook/preview/brand-logo.html b/designbook/preview/brand-logo.html new file mode 100644 index 0000000..83481e3 --- /dev/null +++ b/designbook/preview/brand-logo.html @@ -0,0 +1,21 @@ + + + +Logo + + + +
whynot logoPrimary · black on white
+
whynot logoInverted · white on black
+
?!
Mini · ?! wordmark (favicon size)
+ diff --git a/designbook/preview/brand-wireframe-motif.html b/designbook/preview/brand-wireframe-motif.html new file mode 100644 index 0000000..ec11e7f --- /dev/null +++ b/designbook/preview/brand-wireframe-motif.html @@ -0,0 +1,43 @@ + + + +Wireframe Motif + + + +
+
+ Draft · WNO-014 + Stage 2 · Prototype +
+
+

A field-notebook for catching weird ideas before they evaporate.

+
+
+
+
+
+
+ diff --git a/designbook/preview/colors-accent.html b/designbook/preview/colors-accent.html new file mode 100644 index 0000000..4d432f2 --- /dev/null +++ b/designbook/preview/colors-accent.html @@ -0,0 +1,33 @@ + + + +Accent — Annotation Yellow + + + +
+
Accent — used as highlighter, never as button fill
+
+
--hi#FFE14A
+
--hi-2#FFD400
+
+
+
+
Usage
+
+
Signals are evidence, not vibes.
+
Draft · S2
+
— only for marker / annotation / status
+
+
+ diff --git a/designbook/preview/colors-borders.html b/designbook/preview/colors-borders.html new file mode 100644 index 0000000..eb026ed --- /dev/null +++ b/designbook/preview/colors-borders.html @@ -0,0 +1,25 @@ + + + +Borders & Lines + + + +
Lines — hairline · default · strong
+
+
soft hairline
--border-soft#F0F0EC · within cards
+
default border
--border#E5E5E2 · cards, inputs
+
strong divider
--border-strong#C9C9C5 · sections
+
+ diff --git a/designbook/preview/colors-neutrals.html b/designbook/preview/colors-neutrals.html new file mode 100644 index 0000000..356ad0e --- /dev/null +++ b/designbook/preview/colors-neutrals.html @@ -0,0 +1,36 @@ + + + +Neutrals — Paper & Ink + + + +
+
+
Ink — text & fills
+
+
--ink#0A0A0A
+
--ink-2#1F1F1F
+
--ink-3#5C5C5C
+
--ink-4#8A8A8A
+
--ink-5#B5B5B3
+
+
+
+
Paper — surfaces
+
+
--paper#FFFFFF
+
--paper-2#FAFAF7
+
--paper-3#F4F4EF
+
+
+
+ diff --git a/designbook/preview/colors-signal.html b/designbook/preview/colors-signal.html new file mode 100644 index 0000000..cd34a97 --- /dev/null +++ b/designbook/preview/colors-signal.html @@ -0,0 +1,25 @@ + + + +Signal Strength Ramp + + + +
Signal strength — desaturated, S4 only uses the accent
+
+
S0No signalNo observable interest or usefulness.
+
S1WeakSome curiosity or informal interest.
+
S2MediumRepeated interest, specific feedback.
+
S3StrongAction, return, referral, contribution.
+
S4CommercialPayment, pre-order, budget commit.
+
+ diff --git a/designbook/preview/colors-status-functional.html b/designbook/preview/colors-status-functional.html new file mode 100644 index 0000000..808005c --- /dev/null +++ b/designbook/preview/colors-status-functional.html @@ -0,0 +1,31 @@ + + + +Functional Status Colours + + + +
Functional status — borders + dots only, never fills
+
+
Error--status-error#B33A2E · brick red
+
Warning--status-warn#C28000 · deep mustard
+
Success--status-success#2F6B3A · muted forest
+
Info--status-info#2E5C8A · muted ink-blue
+
+

Distinct from S0–S4 signal strength. Use sparingly: a 2px left-border on banners, a small dot next to status text, or as currentColor on an icon. If even this feels too colourful for a context, fall back to ink and the existing yellow accent — the system still parses without these.

+ diff --git a/designbook/preview/comp-buttons.html b/designbook/preview/comp-buttons.html new file mode 100644 index 0000000..ca227a3 --- /dev/null +++ b/designbook/preview/comp-buttons.html @@ -0,0 +1,40 @@ + + + +Buttons + + + +
Buttons — primary · secondary · ghost
+
+
Default
Hover
Disabled
+
Primary
+ + + +
Secondary
+ + + +
Ghost
+ + + +
+ diff --git a/designbook/preview/comp-empty-placeholder.html b/designbook/preview/comp-empty-placeholder.html new file mode 100644 index 0000000..c9950bd --- /dev/null +++ b/designbook/preview/comp-empty-placeholder.html @@ -0,0 +1,38 @@ + + + +Empty / Placeholder State + + + +
+
Empty — dashed border + caption
+
+
No signals yet.
+
Lack of signal is also information.
+ Record a signal → +
+
+
+
Wireframe — placeholder content
+
+
+
+
+
+
+
+
+ diff --git a/designbook/preview/comp-inputs.html b/designbook/preview/comp-inputs.html new file mode 100644 index 0000000..3ef0362 --- /dev/null +++ b/designbook/preview/comp-inputs.html @@ -0,0 +1,30 @@ + + + +Inputs & Form Fields + + + +
+
Text · Default / Focus
+
+
120 char limit · plain sentence
+
+
+
Textarea · Error
+
+
Required — describe in one sentence.
+
+ diff --git a/designbook/preview/comp-labels-tags.html b/designbook/preview/comp-labels-tags.html new file mode 100644 index 0000000..24729dd --- /dev/null +++ b/designbook/preview/comp-labels-tags.html @@ -0,0 +1,44 @@ + + + +Labels & Tags + + + +
+
+
Tags — default · active · draft
+
+ Raw Idea + Prototype Candidate + Experiment + Promotion Candidate + Parked + Draft +
+
+
+
Signal dots — inline indicator
+
+ S0 · No signal + S1 · Weak + S2 · Medium + S3 · Strong + S4 · Commercial +
+
+
+ diff --git a/designbook/preview/comp-left-nav.html b/designbook/preview/comp-left-nav.html new file mode 100644 index 0000000..53986a6 --- /dev/null +++ b/designbook/preview/comp-left-nav.html @@ -0,0 +1,85 @@ + + + +Left Navigation + + + +
+ Default · grouped, with active state + +
+ +
+ Minimal · no brand, no icons + +
+ diff --git a/designbook/preview/comp-pipeline.html b/designbook/preview/comp-pipeline.html new file mode 100644 index 0000000..4d8d865 --- /dev/null +++ b/designbook/preview/comp-pipeline.html @@ -0,0 +1,30 @@ + + + +Pipeline / Lifecycle + + + +
Pipeline — Raw → Candidate → Experiment → Signal → Decision
+
+
Stage 0Raw ideainbox/
+
Stage 1Triage2026-02-12
+
Stage 2Prototype cardprototypes/
+
Stage 3Experimentends 2026-04-01
+
Stage 4Signal review— pending
+
+ diff --git a/designbook/preview/comp-prototype-card.html b/designbook/preview/comp-prototype-card.html new file mode 100644 index 0000000..6ed4161 --- /dev/null +++ b/designbook/preview/comp-prototype-card.html @@ -0,0 +1,46 @@ + + + +Prototype Card + + + +
Prototype card — default · hover
+
+
+
+ WNO-014 · Prototype + Experiment +
+

A pocket field-notebook for catching weird ideas before they evaporate.

+
Learning q.Do people return to capture more than once?
+
Smallest testOne-page landing + email capture, 14 days.
+
→ CoulombS1 · weak
+
+
+
+ WNO-017 · Prototype + Signal review +
+

A LEGO-brick mood board for engineers who don't think in mood boards.

+
Learning q.Will engineers attach metaphors to their tickets?
+
Smallest testSlack bot, three teams, two weeks.
+
→ HelixS3 · strong
+
+
+ diff --git a/designbook/preview/comp-topnav.html b/designbook/preview/comp-topnav.html new file mode 100644 index 0000000..2d55f9c --- /dev/null +++ b/designbook/preview/comp-topnav.html @@ -0,0 +1,37 @@ + + + +Top Navigation + + + + +
// 56px height · 1px hairline · rgba(255,255,255,0.92) when scrolled
+ diff --git a/designbook/preview/page-beta-invitation.html b/designbook/preview/page-beta-invitation.html new file mode 100644 index 0000000..923c0ee --- /dev/null +++ b/designbook/preview/page-beta-invitation.html @@ -0,0 +1,104 @@ + + + + + +You're invited · whynot closed beta + + + + + +
+ + +
+
+ Invitation · WNO-021 + You’re invited +

Concierge prototype triage

+

A one-hour call where we take one of your half-formed ideas and turn it into a testable prototype card — learning question, smallest useful test, and all.

+ +
+
Learning questionWill three founders pay a listed price for a single triage call?
+
What you doBring one idea. Leave with a prototype card and a next step.
+
Seats5 · 2 remaining
+
Window2026-04-01 → 2026-04-14
+
CostListed price. No refunds, no obligations after.
+
+ +
+ + + Expires in 6 days +
+ +

Invitation only. This is a prototype, not a product — it may be parked after the beta regardless of how it goes. Accepting reserves a seat; it is not a commitment to continue.

+
+
+ +
+ whynot · betas + · + BETA_MODEL.md + try($idea) until success; +
+
+ + diff --git a/designbook/preview/page-landing-auth.html b/designbook/preview/page-landing-auth.html new file mode 100644 index 0000000..d1f034d --- /dev/null +++ b/designbook/preview/page-landing-auth.html @@ -0,0 +1,224 @@ + + + + + +whynot — landing + + + + + +
+ + + +
+ + +
+ Prototype & market-signal space +

why? why not!

+

A quiet workshop for discovering the weird and the useful — building, testing, and reviewing prototypes before they ever pretend to be products.

+
$ try($idea) until success;
+ +
+
01A prototype is a question made tangible. Not a promise.
+
02Signal beats enthusiasm. Evidence, not vibes.
+
03Capture is not commitment. A good idea can still be parked.
+
+
+ + +
+
+ + +
+ +
+
+ + +
+
+
+ + Forgot? +
+ +
+ +

Access is limited to current contributors and invited beta participants. No public sign-ups.

+
+ + +
+ +
+ +
+ + whynot · 2026 + · + Prereleases & prototypes only + try($idea) until success; +
+ +
+ + + + diff --git a/designbook/preview/page-prototype-detail.html b/designbook/preview/page-prototype-detail.html new file mode 100644 index 0000000..b2088aa --- /dev/null +++ b/designbook/preview/page-prototype-detail.html @@ -0,0 +1,158 @@ + + + + + +WNO-017 · Prototype + + + + + + + +
+ + +
+ + +
+ WNO-017 · Prototype +
+

A LEGO-brick mood board for engineers who don’t think in mood boards.

+
+ + +
+
+
+ +
+
Stage 0Raw ideainbox/
+
Stage 1Triage2026-02-15
+
Stage 2Prototype cardprototypes/
+
Stage 3Experimentclosed 2026-03-04
+
Stage 4Signal reviewin progress
+
+ +
+
+
Learning questionWill engineers attach metaphors to their tickets, and do those metaphors help anyone else read the work later?
+
Smallest useful testA Slack bot in three teams for two weeks. One command attaches a “brick” — a one-line metaphor — to any ticket.
+
Expected signalAt least one team voluntarily keeps using the bricks after the two weeks, or references a brick in a review without being prompted.
+
RisksCute but unused after a week. Or: engineers treat it as a chore rather than a shortcut.
+
+ +
+
+
+ + diff --git a/designbook/preview/page-signals-dashboard.html b/designbook/preview/page-signals-dashboard.html new file mode 100644 index 0000000..310e90f --- /dev/null +++ b/designbook/preview/page-signals-dashboard.html @@ -0,0 +1,135 @@ + + + + + +Signals · whynot-control + + + + + + + +
+ + +
+
+ whynot-control / signals +
+

Signals

+ +
+

A signal is evidence, not a vibe. Record what happened, who did it, and how strong the evidence is. Lack of signal is also information.

+
+ +
+
S01
+
S12
+
S22
+
S31
+
S40
+
+ +
+ All + S0 + S1 + S2 + S3 + S4 + 6 of 6 +
+ +
+
SIG-031WNO-017S3

Two teams shipped public README sections labelled “brick: scope” after using the bot for a week.

usage log · 2026-03-04
+
SIG-030WNO-017S2

Three engineers DM’d asking for an export-to-Notion option.

Slack · 2026-03-03
+
SIG-029WNO-014S1

Landing page: 34 visits, 7 emails, 0 returns in week 1.

Plausible · 2026-03-01
+
SIG-028WNO-021S2

First triage call booked at listed price; second declined on price.

Stripe / email · 2026-02-28
+
SIG-027WNO-021S1

“Interesting but I’d want a free first one” ×2.

interview · 2026-02-26
+
SIG-026WNO-024S0

Static preview: 12 visits in 30 days, 0 returns.

Plausible · 2026-02-24
+
+
+
+ + diff --git a/designbook/preview/spacing-elevation.html b/designbook/preview/spacing-elevation.html new file mode 100644 index 0000000..441e50e --- /dev/null +++ b/designbook/preview/spacing-elevation.html @@ -0,0 +1,26 @@ + + + +Elevation + + + +
Elevation — this is a wireframe system, prefer none
+
+
default
--shadow-0none · everywhere
+
+1
--shadow-11px hairline · sticky nav
+
+2
--shadow-21px strong · sticky strong
+
float
--shadow-3soft 4–12px · popover only
+
+ diff --git a/designbook/preview/spacing-radii.html b/designbook/preview/spacing-radii.html new file mode 100644 index 0000000..5dab92c --- /dev/null +++ b/designbook/preview/spacing-radii.html @@ -0,0 +1,24 @@ + + + +Radii + + + +
Radii — big things stay square
+
+
--r-0
0 · documents
+
--r-1
2 · inputs, tags
+
--r-2
4 · buttons
+
--r-3
8 · cards, modals
+
--r-pill
∞ · label caps only
+
+ diff --git a/designbook/preview/spacing-scale.html b/designbook/preview/spacing-scale.html new file mode 100644 index 0000000..5777676 --- /dev/null +++ b/designbook/preview/spacing-scale.html @@ -0,0 +1,28 @@ + + + +Spacing Scale + + + +
Spacing — 4px base unit
+
+ --sp-1
4px + --sp-2
8px + --sp-3
12px + --sp-4
16px + --sp-5
24px + --sp-6
32px + --sp-7
48px + --sp-8
64px + --sp-9
96px + --sp-10
128px +
+ diff --git a/designbook/preview/type-body.html b/designbook/preview/type-body.html new file mode 100644 index 0000000..f4b078a --- /dev/null +++ b/designbook/preview/type-body.html @@ -0,0 +1,23 @@ + + + +Body & Lead + + + +
+ Lead +

A prototype is a question made tangible. The purpose is not to prove an idea is brilliant — it is to learn what is useful, desirable, or irrelevant.

+ .lead · system-sans 400 · 17 / 1.55 · fg-2 +
+
+ Body +

Signals are evidence, not vibes. Weak signals are useful if clearly labelled. Contradictory signals should be preserved. A signal should be connected to a prototype, audience, or hypothesis. Lack of signal is also information.

+ p · system-sans 400 · 15 / 1.5 · fg-1 +
+ diff --git a/designbook/preview/type-display.html b/designbook/preview/type-display.html new file mode 100644 index 0000000..f9d530e --- /dev/null +++ b/designbook/preview/type-display.html @@ -0,0 +1,23 @@ + + + +Display Type + + + +
+ Display 1 + try($idea) + system-sans 300 · 96 / .95 · -.035em +
+
+ Display 2 + why? why not! + system-sans 400 · 64 / 1.0 · -.02em +
+ diff --git a/designbook/preview/type-headings.html b/designbook/preview/type-headings.html new file mode 100644 index 0000000..73e9c9d --- /dev/null +++ b/designbook/preview/type-headings.html @@ -0,0 +1,20 @@ + + + +Headings + + + +
H1

Prototype pipeline

500 · 44 / 1.05
+
H2

Stage 2 — Prototype card

500 · 32 / 1.25
+
H3

Learning question

500 · 24 / 1.25
+
H4

Smallest useful test

500 · 20 / 1.25
+
H5
Expected signal
500 · 17 / 1.25
+ diff --git a/designbook/preview/type-mono-eyebrows.html b/designbook/preview/type-mono-eyebrows.html new file mode 100644 index 0000000..103af3a --- /dev/null +++ b/designbook/preview/type-mono-eyebrows.html @@ -0,0 +1,42 @@ + + + +Mono & Eyebrows + + + +
+
+
Eyebrow labels
+
+ PROTOTYPE + STAGE + SIGNAL · S2 + IN BETA + PROMOTION TARGET +
+
+
+
Mono inline
+
whynot-control/INTENT.md
+
stage: prototype-candidate
+
→ Helix
+
+
+
+ Raw Idea + Prototype Candidate + Experiment + Promotion Candidate + Parked +
+ diff --git a/designbook/preview/type-serif-quote.html b/designbook/preview/type-serif-quote.html new file mode 100644 index 0000000..a9ff386 --- /dev/null +++ b/designbook/preview/type-serif-quote.html @@ -0,0 +1,17 @@ + + + +Serif Quote + + + +
+

A prototype is a question made tangible. The purpose is not to prove an idea is brilliant. The purpose is to learn what is actually useful, desirable, feasible, or irrelevant.

+ — whynot-control / INTENT.md +
+ diff --git a/designbook/styles.css b/designbook/styles.css new file mode 100644 index 0000000..659f057 --- /dev/null +++ b/designbook/styles.css @@ -0,0 +1,9 @@ +/* ============================================================ + WhyNot Design System — canonical stylesheet entry point + ------------------------------------------------------------ + Consumers link ONE file: + It pulls in the token + semantic layer. The component utility + layer (components.css) ships with the distributable package + (see the whynot-design repo seed) and is imported there. + ============================================================ */ +@import "colors_and_type.css"; diff --git a/designbook/ui_kits/whynot-control/Atoms.jsx b/designbook/ui_kits/whynot-control/Atoms.jsx new file mode 100644 index 0000000..868b24f --- /dev/null +++ b/designbook/ui_kits/whynot-control/Atoms.jsx @@ -0,0 +1,102 @@ +// ============================================================= +// Atoms — Eyebrow, Tag, Button, StageDot, Stamp, IconBtn +// ============================================================= + +function Eyebrow({ children, style }) { + return ( + {children} + ); +} + +function Tag({ children, active, draft, style }) { + const base = { + font: '500 10px/1 var(--ff-mono)', + letterSpacing: '0.1em', + textTransform: 'uppercase', + padding: '5px 10px', + borderRadius: 'var(--r-pill)', + border: '1px solid var(--border)', + color: 'var(--fg-2)', + background: 'var(--paper)', + display: 'inline-block', + }; + if (active) Object.assign(base, { background: 'var(--ink)', color: 'var(--paper)', borderColor: 'var(--ink)' }); + if (draft) Object.assign(base, { background: 'var(--hi)', color: 'var(--hi-ink)', borderColor: 'transparent' }); + return {children}; +} + +function Button({ children, variant = 'secondary', onClick, style, icon }) { + const base = { + font: '500 13px var(--ff-sans)', + letterSpacing: '-0.005em', + padding: '9px 14px', + borderRadius: 'var(--r-2)', + border: '1px solid var(--border)', + background: 'var(--paper)', + color: 'var(--ink)', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: 8, + whiteSpace: 'nowrap', + transition: 'background 120ms ease, border-color 120ms ease', + }; + if (variant === 'primary') Object.assign(base, { background: 'var(--ink)', color: 'var(--paper)', borderColor: 'var(--ink)' }); + if (variant === 'ghost') Object.assign(base, { background: 'transparent', borderColor: 'transparent', padding: '7px 10px' }); + return ( + + ); +} + +const STAGE_COLORS = { + S0: '#B5B5B3', S1: '#8A8A8A', S2: '#5C5C5C', S3: '#0A0A0A', S4: '#FFD400', +}; + +function StageDot({ level = 'S2', label, style }) { + return ( + + + {label || level} + + ); +} + +function Stamp({ children, style }) { + return ( + {children} + ); +} + +function Icon({ name, size = 16, style }) { + return ; +} + +Object.assign(window, { Eyebrow, Tag, Button, StageDot, Stamp, Icon, STAGE_COLORS }); diff --git a/designbook/ui_kits/whynot-control/Chrome.jsx b/designbook/ui_kits/whynot-control/Chrome.jsx new file mode 100644 index 0000000..147649c --- /dev/null +++ b/designbook/ui_kits/whynot-control/Chrome.jsx @@ -0,0 +1,163 @@ +// ============================================================= +// Chrome — TopNav, Sidebar, PageHeader, PipelineStrip +// ============================================================= + +function TopNav({ onNew }) { + return ( + + ); +} + +const NAV_ITEMS = [ + { key: 'inbox', label: 'Inbox', icon: 'inbox', count: 7 }, + { key: 'prototypes', label: 'Prototypes', icon: 'flask-conical', count: 4 }, + { key: 'signals', label: 'Signals', icon: 'activity', count: 12 }, + { key: 'betas', label: 'Betas', icon: 'users', count: 1 }, + { key: 'decisions', label: 'Decisions', icon: 'check-square', count: 3 }, +]; + +const DOC_ITEMS = [ + { key: 'intent', label: 'INTENT.md' }, + { key: 'scope', label: 'SCOPE.md' }, + { key: 'operating', label: 'OPERATING_MODEL.md' }, + { key: 'pipeline', label: 'PROTOTYPE_PIPELINE.md' }, + { key: 'agent', label: 'AGENT_RULES.md' }, +]; + +function Sidebar({ current, onNav }) { + const itemStyle = (active) => ({ + display: 'flex', + alignItems: 'center', + gap: 10, + padding: '6px 10px', + color: active ? 'var(--fg-1)' : 'var(--fg-3)', + background: 'transparent', + borderLeft: active ? '2px solid var(--ink)' : '2px solid transparent', + paddingLeft: active ? 10 : 12, + font: active ? '500 13px var(--ff-sans)' : '400 13px var(--ff-sans)', + cursor: 'pointer', + textDecoration: 'none', + transition: 'color 120ms ease, border-color 120ms ease', + }); + return ( + + ); +} + +function PageHeader({ eyebrow, title, lede, actions }) { + return ( +
+ {eyebrow && {eyebrow}} +
+

{title}

+ {actions &&
{actions}
} +
+ {lede &&

{lede}

} +
+ ); +} + +function PipelineStrip({ activeIdx = 3 }) { + const stages = [ + { num: 'Stage 0', name: 'Raw idea', meta: 'inbox/' }, + { num: 'Stage 1', name: 'Triage', meta: '2026-02-12' }, + { num: 'Stage 2', name: 'Prototype card', meta: 'prototypes/' }, + { num: 'Stage 3', name: 'Experiment', meta: 'ends 2026-04-01' }, + { num: 'Stage 4', name: 'Signal review', meta: '— pending' }, + ]; + return ( +
+ {stages.map((s, i) => { + const state = i < activeIdx ? 'done' : i === activeIdx ? 'active' : 'pending'; + const topColor = state === 'done' ? 'var(--ink)' : state === 'active' ? 'var(--hi-2)' : 'var(--border)'; + return ( +
+ {s.num} + {s.name} + {s.meta} + {i > 0 && ( + + )} +
+ ); + })} +
+ ); +} + +Object.assign(window, { TopNav, Sidebar, PageHeader, PipelineStrip, NAV_ITEMS, DOC_ITEMS }); diff --git a/designbook/ui_kits/whynot-control/DocView.jsx b/designbook/ui_kits/whynot-control/DocView.jsx new file mode 100644 index 0000000..8580e6c --- /dev/null +++ b/designbook/ui_kits/whynot-control/DocView.jsx @@ -0,0 +1,102 @@ +// ============================================================= +// Document viewer — renders one of the control docs +// ============================================================= + +const DOC_CONTENT = { + intent: { + title: 'INTENT.md', + eyebrow: 'whynot-control · control document', + sections: [ + { h: 'Purpose', p: 'whynot-control exists to serve as the control repository for the whynot organisation: a prototype, feedback, and market-signal space for discovering the weird and the useful.' }, + { h: 'Primary utility', list: [ + 'capture unusual but potentially useful ideas;', + 'distinguish curiosity from commitment;', + 'shape rough ideas into testable prototypes;', + 'collect early feedback and market signals;', + 'run closed beta concepts in a controlled way;', + 'identify which ideas should move toward Helix, Coulomb, Sloppers, Plenitude, Binky, or Tegwick;', + 'prevent premature productisation.', + ]}, + { h: 'Operating principle', quote: 'A prototype is a question made tangible. The purpose of a prototype is not to prove that an idea is brilliant. The purpose is to learn what is actually useful, desirable, feasible, or irrelevant.' }, + ], + }, + scope: { + title: 'SCOPE.md', + eyebrow: 'whynot-control · control document', + sections: [ + { h: 'Current reality', p: 'whynot-control is the control repository for organising prototype exploration and early market-signal capture.' }, + { h: 'In scope', list: ['Prototype idea capture.', 'Prototype classification.', 'Early user feedback notes.', 'Market-signal tracking.', 'Closed beta planning.', 'Experiment records.', 'Promotion recommendations.', 'Agent-assisted drafting and analysis.'] }, + { h: 'Out of scope', list: ['Production implementation.', 'Long-term product maintenance.', 'Payment processing.', 'Legal investment documentation.', 'Public launch operations.', 'Binding financial, legal, or tax conclusions.'] }, + { h: 'Scope guardrail', quote: 'whynot-control explores and validates. It does not absorb all product development.' }, + ], + }, + operating: { + title: 'OPERATING_MODEL.md', + eyebrow: 'whynot-control · control document', + sections: [ + { h: 'Core rules', list: [ + 'Prototypes are questions. Each prototype should express a question about usefulness, desirability, feasibility, or willingness to pay.', + 'Signal beats enthusiasm. An idea should not be promoted only because it is exciting.', + 'Low-cost learning first. Prefer sketches, mockups, demos, landing pages, conversations.', + 'Closed beta before broad launch.', + 'Promotion requires criteria.', + ]}, + { h: 'Burnout guardrail', quote: 'A prototype can be interesting and still be parked. whynot exists to reduce uncertainty, not to create more obligations.' }, + ], + }, + pipeline: { + title: 'PROTOTYPE_PIPELINE.md', + eyebrow: 'whynot-control · control document', + sections: [ + { h: 'Stage 0 — Raw capture', p: 'Capture ideas without judging them immediately. Located in inbox/. Done when the idea is saved and no longer needs to be held in memory.' }, + { h: 'Stage 1 — Triage', p: 'Decide whether an idea deserves a prototype card. Outcomes: create card, park, merge, reject.' }, + { h: 'Stage 2 — Prototype card', p: 'Turn the idea into a structured prototype candidate. Located in prototypes/.' }, + { h: 'Stage 3 — Experiment', p: 'Test the idea with minimal cost: concept note, landing page, clickable mockup, CLI/demo script, Wizard-of-Oz, manual concierge test, closed conversation, private beta.' }, + { h: 'Stage 4 — Signal review', p: 'Evaluate what was learned. Interest, usefulness, retention, referral, payment, contribution, strategic fit.' }, + { h: 'Stage 5 — Decision', p: 'Park, iterate, promote, reject, or merge. Promotion requires an explicit record in DECISIONS.md.' }, + ], + }, + agent: { + title: 'AGENT_RULES.md', + eyebrow: 'whynot-control · control document', + sections: [ + { h: 'General principle', p: 'Agents may help clarify, structure, draft, compare, and analyse prototype ideas. They must not silently turn experiments into product commitments.' }, + { h: 'Allowed', list: ['draft prototype cards', 'classify ideas by lifecycle stage', 'propose smallest useful tests', 'summarise feedback', 'compare prototype candidates', 'improve wording and structure'] }, + { h: 'Forbidden', list: ['create artificial urgency', 'treat all prototype ideas as products', 'infer willingness to pay without evidence', 'present weak signals as strong validation', 'create legal, financial, or investment commitments'] }, + { h: 'Preferred output style', quote: 'Agent outputs should be concise, evidence-oriented, explicit about uncertainty, and careful to separate idea, hypothesis, signal, and decision.' }, + ], + }, +}; + +function DocView({ docKey }) { + const doc = DOC_CONTENT[docKey]; + if (!doc) return
Doc not found.
; + return ( +
+ {doc.eyebrow} +

{doc.title}

+ {doc.sections.map((s, i) => ( +
+

{s.h}

+ {s.p &&

{s.p}

} + {s.list && ( +
    + {s.list.map((li, j) =>
  • {li}
  • )} +
+ )} + {s.quote && ( +
+

{s.quote}

+
+ )} +
+ ))} +
+ whynot-control / {doc.title} + A1 · Incubating · 2026 +
+
+ ); +} + +Object.assign(window, { DocView, DOC_CONTENT }); diff --git a/designbook/ui_kits/whynot-control/README.md b/designbook/ui_kits/whynot-control/README.md new file mode 100644 index 0000000..8021233 --- /dev/null +++ b/designbook/ui_kits/whynot-control/README.md @@ -0,0 +1,31 @@ +# whynot-control UI kit + +A click-through high-fidelity recreation of the `whynot-control` repository — rendered not as a folder of Markdown files, but as the lightweight web application it implies. + +This kit demonstrates the WhyNot Design System applied to its primary use case: a prototype-and-signal control surface. Everything here is **cosmetic** — there's no backend, no persistence, no real router. Each screen is a working visual artefact you can drop into a design review. + +## Screens + +| Screen | Source doc(s) | Component | +|---|---|---| +| Inbox | `inbox/` | `Inbox.jsx` | +| Prototypes (index) | `prototypes/` + `PROTOTYPE_PIPELINE.md` | `PrototypesIndex.jsx` | +| Prototype (detail) | `templates/prototype-card.md` + `example-prototype-card.md` | `PrototypeDetail.jsx` | +| Signals | `signals/` + `MARKET_SIGNAL.md` | `SignalsIndex.jsx` | +| Document viewer | `INTENT.md`, `OPERATING_MODEL.md` | `DocView.jsx` | + +## Components + +- `TopNav.jsx` — sticky 56px hairline top bar (search + new-idea action). +- `Sidebar.jsx` — left rail with org slug, repo nav, activation indicator. +- `PrototypeCard.jsx` — the card from `preview/comp-prototype-card.html`, factored. +- `PipelineStrip.jsx` — the 5-stage progress strip from `preview/comp-pipeline.html`. +- `SignalRow.jsx` — one row in the signals table. +- `Tag.jsx`, `Eyebrow.jsx`, `Button.jsx`, `StageDot.jsx`, `Stamp.jsx` — atoms used everywhere. + +## Conventions + +- All components are flat function components, no hooks beyond `useState` for screen routing. +- Components export themselves onto `window` so each ` + + + + + +
+ + + + + + + + + + diff --git a/ir/README.md b/ir/README.md new file mode 100644 index 0000000..3921e24 --- /dev/null +++ b/ir/README.md @@ -0,0 +1,33 @@ +# `ir/` — technology-neutral design blueprint + +This directory holds the **intermediate representation (IR)** of the whynot design +language: tokens, per-component contracts, and reference exemplars, in a form that +carries no framework assumptions. + +## Decision: `ir/` is committed + +`ir/` is **checked into git**, on purpose. The IR is the diffable blueprint of the +shared language — committing it means a re-extract (`make ir`) surfaces every +blueprint change as a reviewable git diff, and adapters have a stable, versioned +input. It is a build *input*, not a throwaway build *output*. + +What is committed: + +- `tokens.json` — all tokens, W3C DTCG format. +- `components/.json` — one contract per component. +- `exemplars/.{png,html}` — reference renders from the designbook preview. +- `schema/` + `SCHEMA.md` — the contract definitions (this is what T01 delivered). + +## Direction of flow + +``` +Claude Design (React) ──/design-sync──▶ designbook/ ──make ir──▶ ir/ ──make adapt-lit──▶ adapters/lit/ +``` + +One-way. The only writer of `tokens.json`, `components/`, and `exemplars/` is the +extractor (`scripts/ir-extract.mjs`, T05). **Do not hand-edit those** — change the +language in Claude Design and re-propagate. The `schema/` files and these docs are +the exception: they are authored here. + +See [`SCHEMA.md`](./SCHEMA.md) for the full contract spec and a worked `Button` +exemplar. diff --git a/ir/SCHEMA.md b/ir/SCHEMA.md new file mode 100644 index 0000000..83f4872 --- /dev/null +++ b/ir/SCHEMA.md @@ -0,0 +1,141 @@ +# whynot IR — Schema + +> The **technology-neutral blueprint** for the whynot design language. +> Part of WHYNOT-WP-0002. The IR is the pivot between the canonical React +> designbook and every per-stack adapter (Lit is the reference adapter). + +## Why an IR exists + +Claude Design's `/design-sync` produces a **React-bound** designbook. A non-React +design system "has nothing for the design agent to build with." The IR breaks that +binding: React stays the *authoring surface*, but the IR — committed, diffable, +framework-free — is the actual contract that adapters project onto each stack. + +**Directionality is one-way: React → IR → stacks.** Nothing writes back to `ir/` +except the extractor (`scripts/ir-extract.mjs`, T05). A change to the shared +language is made in Claude Design and re-propagated; the IR is never hand-edited. + +## Layout + +``` +ir/ + SCHEMA.md ← this file (narrative spec) + README.md ← the committed-blueprint decision + workflow + schema/ + tokens.schema.json ← JSON Schema for tokens.json (W3C DTCG) + component.schema.json ← JSON Schema for each components/.json + tokens.json ← all design tokens, W3C DTCG format (emitted by T05) + components/.json ← one contract per component (emitted by T05) + exemplars/.{png,html} ← reference render from the designbook (emitted by T05) +``` + +## Tokens — `ir/tokens.json` + +Adopts the **W3C Design Tokens Community Group** format: every token is an object +with `$value` and (optionally inherited) `$type`; groups nest; `$description` +carries documentation. This is a published standard rather than a bespoke shape, +so the token layer can feed Style Dictionary or any DTCG tool unchanged. + +```json +{ + "color": { + "$type": "color", + "ink": { "$value": "#0A0A0A", "$description": "Near-black. The only fill most of the time." }, + "line": { "$value": "#E5E5E2", "$description": "Default 1px wireframe rule." } + } +} +``` + +> **Migration note.** The repo's current `tokens/*.json` use the older draft shape +> (`value`/`type`, no `$` prefix). The extractor (T05) normalises them to the +> `$`-prefixed DTCG shape on the way into `ir/tokens.json`. Validate with +> `schema/tokens.schema.json`. + +## Component contract — `ir/components/.json` + +Captures everything an adapter needs to scaffold a stub and detect drift, with no +framework assumptions. Validate with `schema/component.schema.json`. Fields: + +| Field | Meaning | +|---|---| +| `name` | PascalCase canonical name (`Button`). | +| `tag` | Advisory custom-element tag (`wn-button`). | +| `group` | Designbook group (`atoms`, `chrome`, `form`). | +| `description` | Purpose, from the React `.prompt.md`. | +| `props[]` | Public inputs — see below. | +| `slots[]` | Named/default content slots. | +| `events[]` | Emitted events (e.g. `wn-dismiss`). | +| `variants[]` | Variant axes — named dimensions with discrete values. | +| `docsRef` | Path to source docs under `designbook/`. | +| `exemplarRef` | Path to the reference render under `ir/exemplars/`. | + +### The prop → attribute mapping (the crux) + +React props are camelCase properties; Lit/Vue/plain-HTML bind **attributes** +(kebab-case). Each prop therefore records **both** identities plus a portability +flag: + +```json +{ + "name": "iconEnd", // React prop, camelCase + "type": "string", + "attribute": "icon-end", // HTML attribute an attribute-driven adapter binds + "portable": true +} +``` + +- `attribute: false` means the prop is deliberately **not** an attribute + (property-only, or non-portable). +- `portable: false` marks props that don't map cleanly to an attribute — objects, + render props, callbacks. **Adapters MUST surface non-portable props as drift, + never silently drop them** (open risk in the workplan). Such props pair with + `type` ∈ {`object`, `function`, `node`}. + +This mapping is exactly what the Lit elements already encode, e.g. +`iconEnd: { type: String, attribute: "icon-end" }` in `src/elements/atoms.js`. + +## Worked exemplar — `Button` + +Derived from the existing `` (`src/elements/atoms.js`) to show the +target shape the React extractor must produce: + +```json +{ + "name": "Button", + "tag": "wn-button", + "group": "atoms", + "description": "Primary action control. Renders a + + +
Secondary
+ + + +
Ghost
+ + + + + diff --git a/ir/exemplars/Eyebrow.html b/ir/exemplars/Eyebrow.html new file mode 100644 index 0000000..24729dd --- /dev/null +++ b/ir/exemplars/Eyebrow.html @@ -0,0 +1,44 @@ + + + +Labels & Tags + + + +
+
+
Tags — default · active · draft
+
+ Raw Idea + Prototype Candidate + Experiment + Promotion Candidate + Parked + Draft +
+
+
+
Signal dots — inline indicator
+
+ S0 · No signal + S1 · Weak + S2 · Medium + S3 · Strong + S4 · Commercial +
+
+
+ diff --git a/ir/exemplars/PageHeader.html b/ir/exemplars/PageHeader.html new file mode 100644 index 0000000..2d55f9c --- /dev/null +++ b/ir/exemplars/PageHeader.html @@ -0,0 +1,37 @@ + + + +Top Navigation + + + +
+
// 56px height · 1px hairline · rgba(255,255,255,0.92) when scrolled
+ diff --git a/ir/exemplars/PipelineStrip.html b/ir/exemplars/PipelineStrip.html new file mode 100644 index 0000000..4d8d865 --- /dev/null +++ b/ir/exemplars/PipelineStrip.html @@ -0,0 +1,30 @@ + + + +Pipeline / Lifecycle + + + +
Pipeline — Raw → Candidate → Experiment → Signal → Decision
+
+
Stage 0Raw ideainbox/
+
Stage 1Triage2026-02-12
+
Stage 2Prototype cardprototypes/
+
Stage 3Experimentends 2026-04-01
+
Stage 4Signal review— pending
+
+ diff --git a/ir/exemplars/Sidebar.html b/ir/exemplars/Sidebar.html new file mode 100644 index 0000000..53986a6 --- /dev/null +++ b/ir/exemplars/Sidebar.html @@ -0,0 +1,85 @@ + + + +Left Navigation + + + +
+ Default · grouped, with active state + +
+ +
+ Minimal · no brand, no icons + +
+ diff --git a/ir/exemplars/StageDot.html b/ir/exemplars/StageDot.html new file mode 100644 index 0000000..24729dd --- /dev/null +++ b/ir/exemplars/StageDot.html @@ -0,0 +1,44 @@ + + + +Labels & Tags + + + +
+
+
Tags — default · active · draft
+
+ Raw Idea + Prototype Candidate + Experiment + Promotion Candidate + Parked + Draft +
+
+
+
Signal dots — inline indicator
+
+ S0 · No signal + S1 · Weak + S2 · Medium + S3 · Strong + S4 · Commercial +
+
+
+ diff --git a/ir/exemplars/Tag.html b/ir/exemplars/Tag.html new file mode 100644 index 0000000..24729dd --- /dev/null +++ b/ir/exemplars/Tag.html @@ -0,0 +1,44 @@ + + + +Labels & Tags + + + +
+
+
Tags — default · active · draft
+
+ Raw Idea + Prototype Candidate + Experiment + Promotion Candidate + Parked + Draft +
+
+
+
Signal dots — inline indicator
+
+ S0 · No signal + S1 · Weak + S2 · Medium + S3 · Strong + S4 · Commercial +
+
+
+ diff --git a/ir/exemplars/TopNav.html b/ir/exemplars/TopNav.html new file mode 100644 index 0000000..2d55f9c --- /dev/null +++ b/ir/exemplars/TopNav.html @@ -0,0 +1,37 @@ + + + +Top Navigation + + + + +
// 56px height · 1px hairline · rgba(255,255,255,0.92) when scrolled
+ diff --git a/ir/schema/component.schema.json b/ir/schema/component.schema.json new file mode 100644 index 0000000..e815440 --- /dev/null +++ b/ir/schema/component.schema.json @@ -0,0 +1,159 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://whynot.design/ir/schema/component.schema.json", + "title": "whynot IR — Component Contract", + "description": "Technology-neutral contract for one component in the whynot designbook. Extracted one-way from the canonical React designbook (WHYNOT-WP-0002). Adapters consume this to scaffold stubs and detect drift; they never write back to it.", + "type": "object", + "additionalProperties": false, + "required": ["name", "group", "description", "props"], + "properties": { + "name": { + "type": "string", + "description": "Canonical component name in PascalCase (e.g. \"Button\"). The Lit adapter maps this to the tag.", + "pattern": "^[A-Z][A-Za-z0-9]*$" + }, + "tag": { + "type": "string", + "description": "Suggested custom-element tag for web-component adapters (e.g. \"wn-button\"). Advisory; an adapter owns its own naming.", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "group": { + "type": "string", + "description": "Grouping from the designbook manifest (e.g. \"atoms\", \"chrome\", \"form\"). Mirrors src/elements/.js in the Lit adapter." + }, + "description": { + "type": "string", + "description": "One- or two-sentence purpose, sourced from the React component's .prompt.md docs." + }, + "props": { + "type": "array", + "description": "Public inputs. Each prop carries its React identity AND its projection onto an HTML attribute, so attribute-driven stacks (Lit, Vue, plain HTML) are first-class.", + "items": { "$ref": "#/$defs/prop" } + }, + "slots": { + "type": "array", + "description": "Named/default content slots.", + "items": { "$ref": "#/$defs/slot" } + }, + "events": { + "type": "array", + "description": "Events the component emits.", + "items": { "$ref": "#/$defs/event" } + }, + "variants": { + "type": "array", + "description": "Variant axes — each axis is a named dimension with discrete values (e.g. axis \"variant\" = [primary, secondary]). Usually derived from an enum prop.", + "items": { "$ref": "#/$defs/variantAxis" } + }, + "docsRef": { + "type": "string", + "description": "Path (relative to repo root) to the source docs in designbook/, e.g. designbook/components/atoms/Button/Button.prompt.md." + }, + "exemplarRef": { + "type": "string", + "description": "Path (relative to repo root) to the reference render under ir/exemplars/, e.g. ir/exemplars/Button.html. Parity (T08) diffs the adapter's render against this." + } + }, + "$defs": { + "prop": { + "type": "object", + "additionalProperties": false, + "required": ["name", "type", "attribute"], + "properties": { + "name": { + "type": "string", + "description": "React prop name, camelCase (e.g. \"iconEnd\")." + }, + "type": { + "type": "string", + "description": "Neutral type. \"enum\" pairs with `enum`. Non-attribute-portable shapes use \"object\", \"function\", or \"node\" and MUST set portable:false.", + "enum": ["string", "number", "boolean", "enum", "object", "function", "node"] + }, + "attribute": { + "description": "HTML attribute name an attribute-driven adapter binds this prop to (kebab-case), e.g. \"icon-end\". `false` means the prop is intentionally not exposed as an attribute (property-only or non-portable).", + "oneOf": [ + { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }, + { "type": "boolean", "const": false } + ] + }, + "enum": { + "type": "array", + "description": "Allowed values when type is \"enum\".", + "items": { "type": "string" } + }, + "default": { + "description": "Default value as authored in the React source. Type matches `type`." + }, + "required": { + "type": "boolean", + "default": false, + "description": "Whether the consumer must supply this prop." + }, + "portable": { + "type": "boolean", + "default": true, + "description": "False marks props that do not map cleanly to an HTML attribute (objects, render props, callbacks). Adapters MUST surface non-portable props as drift, never silently drop them (see open risks)." + }, + "description": { + "type": "string" + } + }, + "allOf": [ + { + "if": { "properties": { "type": { "const": "enum" } } }, + "then": { "required": ["enum"] } + } + ] + }, + "slot": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "Slot name; use \"default\" for the unnamed default slot." + }, + "description": { "type": "string" }, + "required": { "type": "boolean", "default": false } + } + }, + "event": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "Emitted event name as dispatched by the component, e.g. \"wn-dismiss\"." + }, + "description": { "type": "string" }, + "detail": { + "type": "string", + "description": "Free-text description of the event's detail payload shape." + } + } + }, + "variantAxis": { + "type": "object", + "additionalProperties": false, + "required": ["axis", "values"], + "properties": { + "axis": { + "type": "string", + "description": "Name of the variant dimension, usually the driving prop name (e.g. \"variant\", \"size\")." + }, + "values": { + "type": "array", + "minItems": 1, + "items": { "type": "string" }, + "description": "Discrete values along this axis." + }, + "default": { + "type": "string", + "description": "Default value for the axis." + } + } + } + } +} diff --git a/ir/schema/tokens.schema.json b/ir/schema/tokens.schema.json new file mode 100644 index 0000000..b9efb5f --- /dev/null +++ b/ir/schema/tokens.schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://whynot.design/ir/schema/tokens.schema.json", + "title": "whynot IR — Design Tokens (W3C DTCG)", + "description": "ir/tokens.json adopts the W3C Design Tokens Community Group format ($value/$type) so the token layer is a standard, not a bespoke shape. NOTE: the repo's existing tokens/*.json use the older draft shape (value/type, no $); the IR extractor (T05) normalises to $-prefixed DTCG.", + "type": "object", + "$ref": "#/$defs/group", + "$defs": { + "token": { + "type": "object", + "required": ["$value"], + "properties": { + "$value": { + "description": "The token value. For type=color this is a hex/colour string; for dimension a CSS length; aliases use {group.token} reference syntax." + }, + "$type": { + "type": "string", + "description": "DTCG type. May be inherited from an ancestor group.", + "enum": [ + "color", + "dimension", + "fontFamily", + "fontWeight", + "duration", + "cubicBezier", + "number", + "string", + "shadow", + "border", + "typography", + "transition", + "gradient" + ] + }, + "$description": { + "type": "string", + "description": "Human-readable note. Carries over the `comment` field from the legacy token files." + } + }, + "$comment": "A token is any object carrying $value. Properties beyond the $-prefixed ones are disallowed at token level via the group dispatch below." + }, + "group": { + "type": "object", + "description": "A DTCG group: a map of names to sub-groups or tokens. $-prefixed keys are group metadata; every other key is a child node.", + "properties": { + "$type": { "type": "string" }, + "$description": { "type": "string" } + }, + "patternProperties": { + "^[^$].*$": { + "oneOf": [ + { "$ref": "#/$defs/token" }, + { "$ref": "#/$defs/group" } + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/ir/tokens.json b/ir/tokens.json new file mode 100644 index 0000000..277b37d --- /dev/null +++ b/ir/tokens.json @@ -0,0 +1,266 @@ +{ + "color": { + "$type": "color", + "ink": { + "$value": "#0A0A0A" + }, + "ink-2": { + "$value": "#1F1F1F" + }, + "ink-3": { + "$value": "#5C5C5C" + }, + "ink-4": { + "$value": "#8A8A8A" + }, + "ink-5": { + "$value": "#B5B5B3" + }, + "line": { + "$value": "#E5E5E2" + }, + "line-strong": { + "$value": "#C9C9C5" + }, + "line-soft": { + "$value": "#F0F0EC" + }, + "paper": { + "$value": "#FFFFFF" + }, + "paper-2": { + "$value": "#FAFAF7" + }, + "paper-3": { + "$value": "#F4F4EF" + }, + "fg-1": { + "$value": "{color.ink}" + }, + "fg-2": { + "$value": "{color.ink-3}" + }, + "fg-3": { + "$value": "{color.ink-4}" + }, + "fg-mute": { + "$value": "{color.ink-5}" + }, + "fg-on-dark": { + "$value": "#FAFAF7" + }, + "bg-1": { + "$value": "{color.paper}" + }, + "bg-2": { + "$value": "{color.paper-2}" + }, + "bg-3": { + "$value": "{color.paper-3}" + }, + "bg-invert": { + "$value": "{color.ink}" + }, + "border": { + "$value": "{color.line}" + }, + "border-strong": { + "$value": "{color.line-strong}" + }, + "border-soft": { + "$value": "{color.line-soft}" + }, + "hi": { + "$value": "#FFE14A" + }, + "hi-2": { + "$value": "#FFD400" + }, + "hi-ink": { + "$value": "#1A1500" + }, + "status-raw": { + "$value": "#B5B5B3" + }, + "status-weak": { + "$value": "#8A8A8A" + }, + "status-medium": { + "$value": "#5C5C5C" + }, + "status-strong": { + "$value": "#0A0A0A" + }, + "status-commercial": { + "$value": "#FFD400" + }, + "status-error": { + "$value": "#B33A2E" + }, + "status-error-bg": { + "$value": "#FCF3F1" + }, + "status-warn": { + "$value": "#C28000" + }, + "status-warn-bg": { + "$value": "#FFFCEB" + }, + "status-success": { + "$value": "#2F6B3A" + }, + "status-success-bg": { + "$value": "#F2F7F2" + }, + "status-info": { + "$value": "#2E5C8A" + }, + "status-info-bg": { + "$value": "#F2F5FA" + } + }, + "fontFamily": { + "$type": "fontFamily", + "ff-sans": { + "$value": "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif" + }, + "ff-mono": { + "$value": "ui-monospace, \"SF Mono\", Menlo, Consolas, monospace" + }, + "ff-serif": { + "$value": "ui-serif, Georgia, \"Times New Roman\", serif" + } + }, + "fontSize": { + "$type": "dimension", + "fs-xs": { + "$value": "11px" + }, + "fs-sm": { + "$value": "13px" + }, + "fs-base": { + "$value": "15px" + }, + "fs-md": { + "$value": "17px" + }, + "fs-lg": { + "$value": "20px" + }, + "fs-xl": { + "$value": "24px" + }, + "fs-2xl": { + "$value": "32px" + }, + "fs-3xl": { + "$value": "44px" + }, + "fs-4xl": { + "$value": "64px" + }, + "fs-5xl": { + "$value": "96px" + } + }, + "lineHeight": { + "$type": "number", + "lh-tight": { + "$value": "1.05" + }, + "lh-snug": { + "$value": "1.25" + }, + "lh-base": { + "$value": "1.5" + }, + "lh-loose": { + "$value": "1.7" + } + }, + "letterSpacing": { + "$type": "dimension", + "tr-tight": { + "$value": "-0.02em" + }, + "tr-snug": { + "$value": "-0.01em" + }, + "tr-base": { + "$value": "0em" + }, + "tr-mono": { + "$value": "0.02em" + }, + "tr-label": { + "$value": "0.08em" + } + }, + "space": { + "$type": "dimension", + "sp-1": { + "$value": "4px" + }, + "sp-2": { + "$value": "8px" + }, + "sp-3": { + "$value": "12px" + }, + "sp-4": { + "$value": "16px" + }, + "sp-5": { + "$value": "24px" + }, + "sp-6": { + "$value": "32px" + }, + "sp-7": { + "$value": "48px" + }, + "sp-8": { + "$value": "64px" + }, + "sp-9": { + "$value": "96px" + }, + "sp-10": { + "$value": "128px" + } + }, + "radius": { + "$type": "dimension", + "r-0": { + "$value": "0px" + }, + "r-1": { + "$value": "2px" + }, + "r-2": { + "$value": "4px" + }, + "r-3": { + "$value": "8px" + }, + "r-pill": { + "$value": "999px" + } + }, + "shadow": { + "$type": "shadow", + "shadow-0": { + "$value": "none" + }, + "shadow-1": { + "$value": "0 1px 0 var(--line)" + }, + "shadow-2": { + "$value": "0 1px 0 var(--line-strong)" + }, + "shadow-3": { + "$value": "0 4px 12px -6px rgba(10,10,10,0.10)" + } + } +} diff --git a/scripts/designbook_pull.py b/scripts/designbook_pull.py new file mode 100644 index 0000000..ed9b1af --- /dev/null +++ b/scripts/designbook_pull.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""Pull the canonical React designbook from Claude Design into ``designbook/``. + +WHYNOT-WP-0002 needs a one-way flow: the React designbook in Claude Design is the +source of truth, and ``make ir`` extracts the technology-neutral blueprint from a +*local mirror* of it (``designbook/``). The bundled ``/design-sync`` skill only goes +the other way (it *pushes* a repo up to Claude Design), so it cannot populate +``designbook/``. This script is the missing **pull** half. + +The only thing that can read your Claude Design project is the local ``claude`` +binary, which has the DesignSync tool over your claude.ai login. This script drives +it directly in headless mode (``claude --print --permission-mode acceptEdits``): the +subprocess fetches AND writes the files itself, in *its* context, and returns only a +small JSON manifest. The (potentially large) file contents never pass through the +orchestrating agent's context, so the pull is cheap no matter how many files it +moves. ``acceptEdits`` is required because a plain ``claude --print`` (e.g. the +llm-connect claude-code adapter used by check_designbook_staleness.py) auto-denies +``Write`` in non-interactive mode — fine for that read-only check, but this pull must +write. No secret goes in the prompt — DesignSync authenticates through the local +login (see .claude/rules/credential-routing.md). + + python scripts/designbook_pull.py # pull, then stamp freshness + python scripts/designbook_pull.py --project # override the target project + python scripts/designbook_pull.py --dry-run # print the plan; fetch nothing + python scripts/designbook_pull.py --no-stamp # skip the --mark-synced step + +What is pulled is governed by ``designbook/.design-pull.json`` (created with sane +defaults on first run): ``include``/``exclude`` glob lists over the project's paths. +The defaults take the React ui-kit, the preview/exemplar cards, the manifest and the +style/token layers, and deliberately EXCLUDE ``_whynot-design-seed/**`` (a copy of +this very repo that lives in the cloud project and must not shadow the real repo). +""" +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +DESIGNBOOK = REPO / "designbook" +MARKER = DESIGNBOOK / ".design-sync.json" +PIN = REPO / ".design-sync" / "config.json" # the /design-sync skill's pin +PULL_CONFIG = DESIGNBOOK / ".design-pull.json" # what this script mirrors + +# Sane first-run defaults. Editable; committed so the pull is reproducible. +DEFAULT_PULL_CONFIG = { + "comment": "Globs (over Claude Design project paths) that designbook_pull.py mirrors " + "into designbook/. Exclude _whynot-design-seed/** — it is a copy of THIS " + "repo living in the cloud project and must not shadow the real source.", + "include": [ + "ui_kits/**", + "preview/**", + "_ds_manifest.json", + "_ds_bundle.js", + "styles.css", + "colors_and_type.css", + ], + "exclude": [ + "_whynot-design-seed/**", + "uploads/**", + "_check/**", + ".thumbnail", + "assets/**", + ], +} + +# Strict output contract for the headless claude call. The subprocess does ALL the +# fetching and writing; it returns only a manifest so file bytes never reach us. +PROMPT = """\ +You have the DesignSync tool (claude.ai/design) and Write/Bash tools. Mirror selected +files from a Claude Design project into a local directory. Do NOT modify the remote +project (no write_files/delete_files/create_project/finalize_plan). Do not request any +secret or API key. + +Target project: pick projectId {project_id!r} if non-null, else the writable project +whose name best matches {project_name!r} (use DesignSync "list_projects"). + +Local destination root (absolute): {dest_root!r} + +Selection — mirror every project path that matches ANY of these include globs: +{include} +…and matches NONE of these exclude globs: +{exclude} +(`**` matches any depth, `*` matches within one path segment.) + +Steps: +1. DesignSync "list_files" on the chosen project. +2. Compute the selected set per the include/exclude globs above. +3. For each selected path: DesignSync "get_file", then Write its content to + {dest_root!r} + "/" + path (create parent dirs; preserve the relative path exactly; + for base64/binary files decode before writing). +4. Output ONLY a single JSON object, no prose, no code fence: + {{"projectId": "", "name": "", "updatedAt": "", + "written": ["", ...], "skipped": ["", ...]}} + On failure output {{"error": ""}}. +""" + + +def load_json(path: Path) -> dict: + return json.loads(path.read_text()) if path.exists() else {} + + +def ensure_pull_config() -> dict: + if not PULL_CONFIG.exists(): + DESIGNBOOK.mkdir(parents=True, exist_ok=True) + PULL_CONFIG.write_text(json.dumps(DEFAULT_PULL_CONFIG, indent=2) + "\n") + print(f"Wrote default pull config: {PULL_CONFIG.relative_to(REPO)}") + return load_json(PULL_CONFIG) + + +def target_project(cli_project: str | None) -> tuple[str | None, str]: + """Resolve (projectId, projectName) from CLI > marker > skill pin.""" + marker, pin = load_json(MARKER), load_json(PIN) + project_id = cli_project or marker.get("projectId") or pin.get("projectId") + project_name = marker.get("projectName") or pin.get("projectName") or "WhyNot Design System" + return project_id, project_name + + +def extract_json(text: str) -> dict: + match = re.search(r"\{.*\}", text, re.DOTALL) + if not match: + sys.exit(f"Could not parse a JSON object from the model reply:\n{text[:600]}") + return json.loads(match.group(0)) + + +def resolve_claude_cli() -> str: + """Mirror the llm-connect adapter's resolution: env override, else ~/.local/bin.""" + configured = os.environ.get("CLAUDE_CLI_PATH") + if configured: + return configured + local_cli = Path.home() / ".local" / "bin" / "claude" + return str(local_cli) if local_cli.exists() else "claude" + + +def run_pull(project_id: str | None, project_name: str, cfg: dict) -> dict: + prompt = PROMPT.format( + project_id=project_id, + project_name=project_name, + dest_root=str(DESIGNBOOK), + include="\n".join(f" - {g}" for g in cfg.get("include", [])), + exclude="\n".join(f" - {g}" for g in cfg.get("exclude", [])), + ) + # acceptEdits auto-approves the subprocess's Write calls (it creates parent dirs); + # DesignSync reads are already permitted under default policy. cwd=REPO so any + # relative reasoning stays inside the repo, though dest paths are absolute. + cmd = [resolve_claude_cli(), "--print", "--permission-mode", "acceptEdits"] + try: + result = subprocess.run( + cmd, input=prompt, cwd=REPO, + capture_output=True, text=True, timeout=1800, + ) + except FileNotFoundError: + sys.exit("Could not find the `claude` CLI. Set CLAUDE_CLI_PATH or add it to PATH.") + except subprocess.TimeoutExpired: + sys.exit("claude CLI timed out after 1800s during the pull.") + if result.returncode != 0: + sys.exit(f"claude CLI exited {result.returncode}:\n{result.stderr[:600]}") + return extract_json(result.stdout) + + +def stamp(project_id: str | None, project_name: str, updated_at: str | None) -> None: + cmd = ["node", "scripts/designbook-sync.mjs", "--mark-synced"] + if project_id: + cmd += ["--project", project_id] + if project_name: + cmd += ["--project-name", project_name] + if updated_at: + cmd += ["--remote-updated", updated_at] + # Flags match designbook-sync.mjs --mark-synced [--remote-updated] [--project] [--project-name]. + subprocess.run(cmd, cwd=REPO, check=False) + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--project", metavar="UUID", help="Override the target project id.") + ap.add_argument("--dry-run", action="store_true", + help="Print the resolved target + selection plan; fetch nothing.") + ap.add_argument("--no-stamp", action="store_true", + help="Do not run designbook-sync.mjs --mark-synced afterwards.") + args = ap.parse_args() + + cfg = ensure_pull_config() + project_id, project_name = target_project(args.project) + + print(f"Target : {project_name} ({project_id or 'by name match'})") + print(f"Dest : {DESIGNBOOK.relative_to(REPO)}/") + print(f"Include: {', '.join(cfg.get('include', [])) or '(none)'}") + print(f"Exclude: {', '.join(cfg.get('exclude', [])) or '(none)'}") + + if args.dry_run: + print("\n(--dry-run: no DesignSync calls made, nothing written)") + return 0 + + result = run_pull(project_id, project_name, cfg) + if "error" in result: + print(f"\nPull failed: {result['error']}") + return 1 + + written = result.get("written", []) + skipped = result.get("skipped", []) + print(f"\nPulled {len(written)} file(s) into {DESIGNBOOK.relative_to(REPO)}/" + f" (skipped {len(skipped)}).") + for path in written[:40]: + print(f" + {path}") + if len(written) > 40: + print(f" … and {len(written) - 40} more") + + if not written: + print("Nothing was written — check the include/exclude globs or the project.") + return 1 + + if not args.no_stamp: + stamp(result.get("projectId") or project_id, + result.get("name") or project_name, + result.get("updatedAt")) + print("Stamped freshness. Next: review the designbook/ diff, then `make ir`.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ir-extract.mjs b/scripts/ir-extract.mjs new file mode 100644 index 0000000..5c7e22a --- /dev/null +++ b/scripts/ir-extract.mjs @@ -0,0 +1,271 @@ +#!/usr/bin/env node +// ============================================================= +// ir-extract.mjs — the pivot (WHYNOT-WP-0002 · T05) +// +// Reads the local React designbook mirror (designbook/) and emits the +// technology-neutral IR (ir/): tokens, per-component contracts, and exemplars. +// One-way only: React → IR. Never hand-edit ir/tokens.json, ir/components/*, +// or ir/exemplars/* — they are regenerated here (`make ir`) and committed so a +// blueprint change shows up as a git diff. See ir/SCHEMA.md and +// .claude/rules/designbook-propagation.md. +// +// Source layout (a bundled .jsx ui-kit, not per-component .d.ts — see +// designbook/REACT_CANONICAL_DECISION.md): +// designbook/_ds_manifest.json → structured tokens[] + preview cards[] +// designbook/ui_kits//Atoms.jsx → component fn signatures (props/defaults) +// designbook/ui_kits//Chrome.jsx → component fn signatures +// designbook/preview/comp-*.html → exemplar renders +// ============================================================= +import { readFileSync, writeFileSync, mkdirSync, copyFileSync, rmSync, existsSync, readdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO = join(dirname(fileURLToPath(import.meta.url)), ".."); +const DESIGNBOOK = join(REPO, "designbook"); +const IR = join(REPO, "ir"); +const KIT = "whynot-control"; + +// Which ui-kit files hold reusable design-system components (NOT app screens/demo data). +const COMPONENT_SOURCES = ["Atoms.jsx", "Chrome.jsx"]; + +const log = (...a) => console.log(...a); +const kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); + +// ---------- Tokens: _ds_manifest.json tokens[] → W3C DTCG ---------- +// Each manifest token: { name:"--ink", value:"#0A0A0A", kind:"color", definedIn }. +// Group + DTCG $type are decided by name prefix (kind is a fallback hint). +const GROUP_BY_PREFIX = [ + ["--ff-", "fontFamily", "fontFamily"], + ["--fs-", "fontSize", "dimension"], + ["--lh-", "lineHeight", "number"], + ["--tr-", "letterSpacing", "dimension"], + ["--sp-", "space", "dimension"], + ["--r-", "radius", "dimension"], + ["--shadow-", "shadow", "shadow"], +]; + +function classify(name) { + for (const [prefix, group, type] of GROUP_BY_PREFIX) { + if (name.startsWith(prefix)) return { group, type }; + } + return { group: "color", type: "color" }; // every remaining token is a colour +} + +function extractTokens() { + const manifest = JSON.parse(readFileSync(join(DESIGNBOOK, "_ds_manifest.json"), "utf8")); + const tokens = manifest.tokens || []; + // First pass: map each css var → its DTCG reference path {group.key}. + const refOf = new Map(); + for (const t of tokens) { + const { group } = classify(t.name); + refOf.set(t.name, `${group}.${t.name.slice(2)}`); + } + // Second pass: build the nested DTCG tree. + const out = {}; + for (const t of tokens) { + const { group, type } = classify(t.name); + const key = t.name.slice(2); + out[group] ||= { $type: type }; + // Resolve `var(--x)` aliases to DTCG references; literals pass through. + const aliasMatch = /^var\(\s*(--[A-Za-z0-9-]+)\s*\)$/.exec(t.value.trim()); + let value = t.value.replace(/\s+/g, " ").trim(); + if (aliasMatch && refOf.has(aliasMatch[1])) value = `{${refOf.get(aliasMatch[1])}}`; + out[group][key] = { $value: value }; + } + return out; +} + +// ---------- Components: parse .jsx function signatures ---------- +// Matches: function Name({ a, b = 'x', children, onClick, style }) { … } +const FN_RE = /function\s+([A-Z][A-Za-z0-9]*)\s*\(\s*\{([^}]*)\}\s*\)\s*\{/g; + +function parseParams(raw) { + // Split top-level commas (defaults here have no nested commas/objects). + return raw.split(",").map((s) => s.trim()).filter(Boolean).map((p) => { + const eq = p.indexOf("="); + if (eq === -1) return { name: p.trim(), default: undefined }; + return { name: p.slice(0, eq).trim(), default: p.slice(eq + 1).trim() }; + }); +} + +// Collect enum values from `name === 'x'` / `name !== 'x'` comparisons in the body. +function enumValuesFor(name, body) { + const re = new RegExp(`${name}\\s*[=!]==\\s*'([^']+)'`, "g"); + const vals = new Set(); + let m; + while ((m = re.exec(body))) vals.add(m[1]); + return [...vals]; +} + +function usedAsBoolean(name, body) { + return new RegExp(`(if\\s*\\(\\s*${name}\\b|\\b${name}\\s*\\?|\\b${name}\\s*&&)`).test(body) + && !new RegExp(`${name}\\s*===`).test(body); +} + +function defaultLiteral(raw) { + if (raw === undefined) return undefined; + if (/^'[^']*'$/.test(raw) || /^"[^"]*"$/.test(raw)) return raw.slice(1, -1); + if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw); + if (raw === "true" || raw === "false") return raw === "true"; + return raw; // expression default; keep verbatim +} + +function propFor(param, body) { + const { name, default: dflt } = param; + const def = defaultLiteral(dflt); + // children → slot, handled by caller. style/object & callbacks → non-portable. + if (name === "style") { + return { name, type: "object", attribute: false, portable: false, + description: "React inline style override — not portable to an attribute." }; + } + if (/^on[A-Z]/.test(name)) { + return { name, type: "function", attribute: false, portable: false, + description: "React callback prop — surface as an event on attribute-driven stacks." }; + } + const prop = { name, attribute: kebab(name) }; + const enums = enumValuesFor(name, body); + if (typeof def === "string" && enums.length) { + prop.type = "enum"; + prop.enum = [...new Set([def, ...enums])]; + } else if (enums.length) { + prop.type = "enum"; + prop.enum = enums; + } else if (typeof def === "number") { + prop.type = "number"; + } else if (typeof def === "boolean" || usedAsBoolean(name, body)) { + prop.type = "boolean"; + } else { + prop.type = "string"; + } + if (def !== undefined) prop.default = def; + return prop; +} + +function bodyOf(src, startIdx) { + // Return the balanced { … } body beginning at the first { at/after startIdx. + let depth = 0, i = src.indexOf("{", startIdx); + const begin = i; + for (; i < src.length; i++) { + if (src[i] === "{") depth++; + else if (src[i] === "}" && --depth === 0) return src.slice(begin, i + 1); + } + return src.slice(begin); +} + +// Map a component to a preview exemplar via the manifest cards (best-effort). +function buildExemplarIndex() { + const manifest = JSON.parse(readFileSync(join(DESIGNBOOK, "_ds_manifest.json"), "utf8")); + return (manifest.cards || []).filter((c) => c.group === "Components"); +} + +function matchExemplar(name, cards) { + const n = name.toLowerCase(); + // direct hints: Button→comp-buttons, Tag/StageDot→comp-labels-tags, etc. + const hint = { stagedot: "labels-tags", tag: "labels-tags", eyebrow: "labels-tags", + pipelinestrip: "pipeline", topnav: "topnav", sidebar: "left-nav", + pageheader: "topnav", button: "buttons" }[n]; + const want = hint || n; + return cards.find((c) => c.path.includes(want)) || null; +} + +function extractComponents() { + const cards = buildExemplarIndex(); + const components = []; + for (const file of COMPONENT_SOURCES) { + const path = join(DESIGNBOOK, "ui_kits", KIT, file); + if (!existsSync(path)) { log(` (skip ${file} — not present)`); continue; } + const src = readFileSync(path, "utf8"); + const group = file.replace(".jsx", "").toLowerCase(); + let m; + FN_RE.lastIndex = 0; + while ((m = FN_RE.exec(src))) { + const name = m[1]; + const params = parseParams(m[2]); + // FN_RE ends at the function body's opening brace; start the body scan there + // (not at m.index, whose first `{` is the param-destructuring brace). + const body = bodyOf(src, m.index + m[0].length - 1); + const props = []; + const slots = []; + for (const p of params) { + if (p.name === "children") { slots.push({ name: "default", description: "Default content." }); continue; } + props.push(propFor(p, body)); + } + const events = props.filter((p) => /^on[A-Z]/.test(p.name)) + .map((p) => ({ name: kebab(p.name.replace(/^on/, "wn-")), description: `Emitted for ${p.name}.` })); + const variants = props.filter((p) => p.type === "enum") + .map((p) => ({ axis: p.name, values: p.enum, ...(p.default ? { default: p.default } : {}) })); + const card = matchExemplar(name, cards); + const contract = { + name, + tag: `wn-${kebab(name)}`, + group, + description: `${name} — extracted from designbook ui_kits/${KIT}/${file}.`, + props, + ...(slots.length ? { slots } : {}), + ...(events.length ? { events } : {}), + ...(variants.length ? { variants } : {}), + docsRef: `designbook/ui_kits/${KIT}/${file}`, + ...(card ? { exemplarRef: `ir/exemplars/${name}.html` } : {}), + }; + components.push({ contract, card }); + } + } + return components; +} + +// ---------- Lightweight contract validation (invariants from ir/schema) ---------- +function validateContract(c) { + const errs = []; + if (!/^[A-Z][A-Za-z0-9]*$/.test(c.name)) errs.push(`bad name ${c.name}`); + if (!c.group || !c.description) errs.push(`${c.name}: missing group/description`); + for (const p of c.props) { + if (!p.name) errs.push(`${c.name}: prop without name`); + if (p.type === "enum" && !(p.enum && p.enum.length)) errs.push(`${c.name}.${p.name}: enum without values`); + if (!("attribute" in p)) errs.push(`${c.name}.${p.name}: missing attribute mapping`); + } + return errs; +} + +// ---------- Emit ---------- +function resetDir(dir) { + if (existsSync(dir)) for (const f of readdirSync(dir)) rmSync(join(dir, f), { recursive: true, force: true }); + else mkdirSync(dir, { recursive: true }); +} + +function main() { + if (!existsSync(join(DESIGNBOOK, "_ds_manifest.json"))) { + console.error("No designbook/_ds_manifest.json — run `make designbook-pull` first."); + process.exit(2); + } + mkdirSync(IR, { recursive: true }); + + log("Tokens → ir/tokens.json"); + const tokens = extractTokens(); + writeFileSync(join(IR, "tokens.json"), JSON.stringify(tokens, null, 2) + "\n"); + const tokenCount = Object.values(tokens).reduce((n, g) => n + Object.keys(g).filter((k) => k !== "$type").length, 0); + log(` ${tokenCount} tokens in ${Object.keys(tokens).length} groups`); + + log("Components → ir/components/.json"); + const comps = extractComponents(); + resetDir(join(IR, "components")); + resetDir(join(IR, "exemplars")); + const allErrs = []; + for (const { contract, card } of comps) { + allErrs.push(...validateContract(contract)); + writeFileSync(join(IR, "components", `${contract.name}.json`), JSON.stringify(contract, null, 2) + "\n"); + if (card) { + const srcHtml = join(DESIGNBOOK, card.path); + if (existsSync(srcHtml)) copyFileSync(srcHtml, join(IR, "exemplars", `${contract.name}.html`)); + } + } + log(` ${comps.length} components, ${comps.filter((c) => c.card).length} with exemplars`); + + if (allErrs.length) { + console.error("\nContract validation errors:"); + for (const e of allErrs) console.error(" - " + e); + process.exit(5); + } + log("\nIR extracted. Review the ir/ git diff (the blueprint change)."); +} + +main(); diff --git a/src/styles/colors_and_type.css b/src/styles/colors_and_type.css index d64a8ff..40e4114 100644 --- a/src/styles/colors_and_type.css +++ b/src/styles/colors_and_type.css @@ -10,104 +10,98 @@ /* ---------- Webfonts (Google Fonts, see /fonts for offline) ---------- */ @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600;700&family=IBM+Plex+Serif:ital,wght@0,400;0,500;1,400&display=swap"); +/* @generated tokens — regenerated by `make adapt-lit` from ir/tokens.json. DO NOT EDIT. */ :root { - /* ---------- Base palette: neutrals ---------- */ - --ink: #0A0A0A; /* near-black, the only "fill" most of the time */ - --ink-2: #1F1F1F; - --ink-3: #5C5C5C; - --ink-4: #8A8A8A; - --ink-5: #B5B5B3; /* placeholder text, wireframe labels */ - --line: #E5E5E2; /* default 1px wireframe rule */ - --line-strong: #C9C9C5; /* dividers between sections */ - --line-soft: #F0F0EC; /* hairline within a card */ - --paper: #FFFFFF; /* canvas */ - --paper-2: #FAFAF7; /* sheet, dim canvas */ - --paper-3: #F4F4EF; /* recessed surface, code block bg */ - - /* ---------- Foreground / background semantic ---------- */ - --fg-1: var(--ink); - --fg-2: var(--ink-3); - --fg-3: var(--ink-4); - --fg-mute: var(--ink-5); - --fg-on-dark: #FAFAF7; - - --bg-1: var(--paper); - --bg-2: var(--paper-2); - --bg-3: var(--paper-3); - --bg-invert: var(--ink); - - --border: var(--line); + /* color */ + --ink: #0A0A0A; + --ink-2: #1F1F1F; + --ink-3: #5C5C5C; + --ink-4: #8A8A8A; + --ink-5: #B5B5B3; + --line: #E5E5E2; + --line-strong: #C9C9C5; + --line-soft: #F0F0EC; + --paper: #FFFFFF; + --paper-2: #FAFAF7; + --paper-3: #F4F4EF; + --fg-1: var(--ink); + --fg-2: var(--ink-3); + --fg-3: var(--ink-4); + --fg-mute: var(--ink-5); + --fg-on-dark: #FAFAF7; + --bg-1: var(--paper); + --bg-2: var(--paper-2); + --bg-3: var(--paper-3); + --bg-invert: var(--ink); + --border: var(--line); --border-strong: var(--line-strong); - --border-soft: var(--line-soft); - - /* ---------- The single accent: annotation yellow ---------- */ - /* Lifted from the LEGO brick. Used as highlighter, "draft" - stamp, signal-marker. Never as a button fill. */ - --hi: #FFE14A; - --hi-2: #FFD400; - --hi-ink: #1A1500; /* text on yellow */ - - /* ---------- Status (for prototype lifecycle, signal strength) ---------- */ - /* Kept deliberately desaturated so they read as labels, not UI. */ - --status-raw: #B5B5B3; /* S0 — no signal */ - --status-weak: #8A8A8A; /* S1 — weak signal */ - --status-medium: #5C5C5C; /* S2 — medium signal */ - --status-strong: #0A0A0A; /* S3 — strong signal */ - --status-commercial: #FFD400; /* S4 — commercial */ - - /* ---------- Type families ---------- */ - --ff-sans: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif; - --ff-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace; - --ff-serif: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; - - /* ---------- Type scale (modular, ~1.2) ---------- */ - --fs-xs: 11px; - --fs-sm: 13px; - --fs-base: 15px; - --fs-md: 17px; - --fs-lg: 20px; - --fs-xl: 24px; - --fs-2xl: 32px; - --fs-3xl: 44px; - --fs-4xl: 64px; - --fs-5xl: 96px; - - --lh-tight: 1.05; - --lh-snug: 1.25; - --lh-base: 1.5; - --lh-loose: 1.7; - - --tr-tight: -0.02em; - --tr-snug: -0.01em; - --tr-base: 0em; - --tr-mono: 0.02em; - --tr-label: 0.08em; /* uppercase eyebrow labels */ - - /* ---------- Spacing (4px base) ---------- */ - --sp-1: 4px; - --sp-2: 8px; - --sp-3: 12px; - --sp-4: 16px; - --sp-5: 24px; - --sp-6: 32px; - --sp-7: 48px; - --sp-8: 64px; - --sp-9: 96px; + --border-soft: var(--line-soft); + --hi: #FFE14A; + --hi-2: #FFD400; + --hi-ink: #1A1500; + --status-raw: #B5B5B3; + --status-weak: #8A8A8A; + --status-medium: #5C5C5C; + --status-strong: #0A0A0A; + --status-commercial: #FFD400; + --status-error: #B33A2E; + --status-error-bg: #FCF3F1; + --status-warn: #C28000; + --status-warn-bg: #FFFCEB; + --status-success: #2F6B3A; + --status-success-bg: #F2F7F2; + --status-info: #2E5C8A; + --status-info-bg: #F2F5FA; + /* fontFamily */ + --ff-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --ff-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + --ff-serif: ui-serif, Georgia, "Times New Roman", serif; + /* fontSize */ + --fs-xs: 11px; + --fs-sm: 13px; + --fs-base: 15px; + --fs-md: 17px; + --fs-lg: 20px; + --fs-xl: 24px; + --fs-2xl: 32px; + --fs-3xl: 44px; + --fs-4xl: 64px; + --fs-5xl: 96px; + /* lineHeight */ + --lh-tight: 1.05; + --lh-snug: 1.25; + --lh-base: 1.5; + --lh-loose: 1.7; + /* letterSpacing */ + --tr-tight: -0.02em; + --tr-snug: -0.01em; + --tr-base: 0em; + --tr-mono: 0.02em; + --tr-label: 0.08em; + /* space */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 24px; + --sp-6: 32px; + --sp-7: 48px; + --sp-8: 64px; + --sp-9: 96px; --sp-10: 128px; - - /* ---------- Radii — small, mostly square ---------- */ - --r-0: 0px; - --r-1: 2px; - --r-2: 4px; - --r-3: 8px; + /* radius */ + --r-0: 0px; + --r-1: 2px; + --r-2: 4px; + --r-3: 8px; --r-pill: 999px; - - /* ---------- Elevation — almost none. This is a wireframe system. ---------- */ + /* shadow */ --shadow-0: none; --shadow-1: 0 1px 0 var(--line); --shadow-2: 0 1px 0 var(--line-strong); --shadow-3: 0 4px 12px -6px rgba(10,10,10,0.10); } +/* @end generated tokens */ /* ============================================================ Semantic element styles diff --git a/workplans/WHYNOT-WP-0002-designbook-stack-adapters.md b/workplans/WHYNOT-WP-0002-designbook-stack-adapters.md index 9797ac7..70c89b0 100644 --- a/workplans/WHYNOT-WP-0002-designbook-stack-adapters.md +++ b/workplans/WHYNOT-WP-0002-designbook-stack-adapters.md @@ -78,7 +78,7 @@ Establish the durable interfaces before any code, so future stacks slot in clean ```task id: WHYNOT-WP-0002-T01 -status: todo +status: done priority: high state_hub_task_id: "66187d76-3755-4204-ad71-d9fae8ed38ac" ``` @@ -95,7 +95,7 @@ name, group, description, props (name/type/enum/default/required), **prop→attr ```task id: WHYNOT-WP-0002-T02 -status: todo +status: done priority: high state_hub_task_id: "6fe8481c-02b3-407d-9b7b-c47f161c0dcd" ``` @@ -110,7 +110,7 @@ machine-readable **drift report**, a **parity result**), idempotency rules (rege ```task id: WHYNOT-WP-0002-T03 -status: todo +status: done priority: medium state_hub_task_id: "97aadf8a-4d56-47d0-b841-d664d0676a53" ``` @@ -128,7 +128,7 @@ triaged/closed), and how this extends the existing atelier→repo pipeline. Upda ```task id: WHYNOT-WP-0002-T04 -status: todo +status: done priority: high state_hub_task_id: "ed4dd1d4-f649-40f0-83f5-9cbd88622a7b" ``` @@ -149,7 +149,7 @@ a minimal token-plus-core-components React designbook is enough to prove the pip ```task id: WHYNOT-WP-0002-T05 -status: todo +status: done priority: high state_hub_task_id: "dcdc0b01-756f-4253-9599-e5d5dfbe1083" ``` @@ -168,7 +168,7 @@ T01. Add `make ir`. `ir/` is committed so a re-extract surfaces blueprint change ```task id: WHYNOT-WP-0002-T06 -status: todo +status: done priority: high state_hub_task_id: "a106c673-e849-4d06-91c9-3f7f63fec2ea" ```