feat(designbook): technology-neutral IR + stack-adapter pipeline (WHYNOT-WP-0002 T01-T06)
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>
This commit is contained in:
159
ir/schema/component.schema.json
Normal file
159
ir/schema/component.schema.json
Normal file
@@ -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 <wn-button> 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/<group>.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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user