Author the design language once in the canonical React designbook and project it
one-way onto each stack: React -> designbook/ -> ir/ -> adapters/<stack>/.
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 <noreply@anthropic.com>
5.8 KiB
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/<Name>.json
tokens.json ← all design tokens, W3C DTCG format (emitted by T05)
components/<Name>.json ← one contract per component (emitted by T05)
exemplars/<Name>.{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.
{
"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/*.jsonuse the older draft shape (value/type, no$prefix). The extractor (T05) normalises them to the$-prefixed DTCG shape on the way intoir/tokens.json. Validate withschema/tokens.schema.json.
Component contract — ir/components/<Name>.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:
{
"name": "iconEnd", // React prop, camelCase
"type": "string",
"attribute": "icon-end", // HTML attribute an attribute-driven adapter binds
"portable": true
}
attribute: falsemeans the prop is deliberately not an attribute (property-only, or non-portable).portable: falsemarks 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 withtype∈ {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 <wn-button> (src/elements/atoms.js) to show the
target shape the React extractor must produce:
{
"name": "Button",
"tag": "wn-button",
"group": "atoms",
"description": "Primary action control. Renders a <button>, or an <a> when href is set.",
"props": [
{ "name": "variant", "type": "enum", "attribute": "variant", "enum": ["primary", "secondary", "ghost"], "default": "secondary" },
{ "name": "size", "type": "enum", "attribute": "size", "enum": ["sm", "md", "lg"], "default": "md" },
{ "name": "icon", "type": "string", "attribute": "icon" },
{ "name": "iconEnd", "type": "string", "attribute": "icon-end" },
{ "name": "type", "type": "string", "attribute": "type", "default": "button" },
{ "name": "disabled", "type": "boolean", "attribute": "disabled", "default": false },
{ "name": "href", "type": "string", "attribute": "href" }
],
"slots": [
{ "name": "default", "description": "Button label." }
],
"events": [],
"variants": [
{ "axis": "variant", "values": ["primary", "secondary", "ghost"], "default": "secondary" },
{ "axis": "size", "values": ["sm", "md", "lg"], "default": "md" }
],
"docsRef": "designbook/components/atoms/Button/Button.prompt.md",
"exemplarRef": "ir/exemplars/Button.html"
}
The
enumvalues forvariantare illustrative of the contract shape; the authoritative values come from the React source at extraction time (T05).
Validation
# once an extractor exists (T05):
node scripts/ir-validate.mjs # validates tokens.json + components/*.json against schema/
Until then, the schemas are usable with any draft-2020-12 validator (ajv, etc.). The extractor (T05) MUST validate its output against these schemas before writing.