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:
33
ir/README.md
Normal file
33
ir/README.md
Normal file
@@ -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/<Name>.json` — one contract per component.
|
||||
- `exemplars/<Name>.{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.
|
||||
141
ir/SCHEMA.md
Normal file
141
ir/SCHEMA.md
Normal file
@@ -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/<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.
|
||||
|
||||
```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/<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:
|
||||
|
||||
```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 `<wn-button>` (`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 <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 `enum` values for `variant` are illustrative of the contract shape; the
|
||||
> authoritative values come from the React source at extraction time (T05).
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
63
ir/components/Button.json
Normal file
63
ir/components/Button.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "Button",
|
||||
"tag": "wn-button",
|
||||
"group": "atoms",
|
||||
"description": "Button — extracted from designbook ui_kits/whynot-control/Atoms.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "variant",
|
||||
"attribute": "variant",
|
||||
"type": "enum",
|
||||
"enum": [
|
||||
"secondary",
|
||||
"primary",
|
||||
"ghost"
|
||||
],
|
||||
"default": "secondary"
|
||||
},
|
||||
{
|
||||
"name": "onClick",
|
||||
"type": "function",
|
||||
"attribute": false,
|
||||
"portable": false,
|
||||
"description": "React callback prop — surface as an event on attribute-driven stacks."
|
||||
},
|
||||
{
|
||||
"name": "style",
|
||||
"type": "object",
|
||||
"attribute": false,
|
||||
"portable": false,
|
||||
"description": "React inline style override — not portable to an attribute."
|
||||
},
|
||||
{
|
||||
"name": "icon",
|
||||
"attribute": "icon",
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"slots": [
|
||||
{
|
||||
"name": "default",
|
||||
"description": "Default content."
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "wn-click",
|
||||
"description": "Emitted for onClick."
|
||||
}
|
||||
],
|
||||
"variants": [
|
||||
{
|
||||
"axis": "variant",
|
||||
"values": [
|
||||
"secondary",
|
||||
"primary",
|
||||
"ghost"
|
||||
],
|
||||
"default": "secondary"
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Atoms.jsx",
|
||||
"exemplarRef": "ir/exemplars/Button.html"
|
||||
}
|
||||
23
ir/components/Eyebrow.json
Normal file
23
ir/components/Eyebrow.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "Eyebrow",
|
||||
"tag": "wn-eyebrow",
|
||||
"group": "atoms",
|
||||
"description": "Eyebrow — extracted from designbook ui_kits/whynot-control/Atoms.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "style",
|
||||
"type": "object",
|
||||
"attribute": false,
|
||||
"portable": false,
|
||||
"description": "React inline style override — not portable to an attribute."
|
||||
}
|
||||
],
|
||||
"slots": [
|
||||
{
|
||||
"name": "default",
|
||||
"description": "Default content."
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Atoms.jsx",
|
||||
"exemplarRef": "ir/exemplars/Eyebrow.html"
|
||||
}
|
||||
27
ir/components/Icon.json
Normal file
27
ir/components/Icon.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Icon",
|
||||
"tag": "wn-icon",
|
||||
"group": "atoms",
|
||||
"description": "Icon — extracted from designbook ui_kits/whynot-control/Atoms.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "name",
|
||||
"attribute": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"attribute": "size",
|
||||
"type": "number",
|
||||
"default": 16
|
||||
},
|
||||
{
|
||||
"name": "style",
|
||||
"type": "object",
|
||||
"attribute": false,
|
||||
"portable": false,
|
||||
"description": "React inline style override — not portable to an attribute."
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Atoms.jsx"
|
||||
}
|
||||
30
ir/components/PageHeader.json
Normal file
30
ir/components/PageHeader.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "PageHeader",
|
||||
"tag": "wn-page-header",
|
||||
"group": "chrome",
|
||||
"description": "PageHeader — extracted from designbook ui_kits/whynot-control/Chrome.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "eyebrow",
|
||||
"attribute": "eyebrow",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"attribute": "title",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "lede",
|
||||
"attribute": "lede",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "actions",
|
||||
"attribute": "actions",
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Chrome.jsx",
|
||||
"exemplarRef": "ir/exemplars/PageHeader.html"
|
||||
}
|
||||
16
ir/components/PipelineStrip.json
Normal file
16
ir/components/PipelineStrip.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "PipelineStrip",
|
||||
"tag": "wn-pipeline-strip",
|
||||
"group": "chrome",
|
||||
"description": "PipelineStrip — extracted from designbook ui_kits/whynot-control/Chrome.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "activeIdx",
|
||||
"attribute": "active-idx",
|
||||
"type": "number",
|
||||
"default": 3
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Chrome.jsx",
|
||||
"exemplarRef": "ir/exemplars/PipelineStrip.html"
|
||||
}
|
||||
39
ir/components/Sidebar.json
Normal file
39
ir/components/Sidebar.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "Sidebar",
|
||||
"tag": "wn-sidebar",
|
||||
"group": "chrome",
|
||||
"description": "Sidebar — extracted from designbook ui_kits/whynot-control/Chrome.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "current",
|
||||
"attribute": "current",
|
||||
"type": "enum",
|
||||
"enum": [
|
||||
"doc:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onNav",
|
||||
"type": "function",
|
||||
"attribute": false,
|
||||
"portable": false,
|
||||
"description": "React callback prop — surface as an event on attribute-driven stacks."
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "wn-nav",
|
||||
"description": "Emitted for onNav."
|
||||
}
|
||||
],
|
||||
"variants": [
|
||||
{
|
||||
"axis": "current",
|
||||
"values": [
|
||||
"doc:"
|
||||
]
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Chrome.jsx",
|
||||
"exemplarRef": "ir/exemplars/Sidebar.html"
|
||||
}
|
||||
28
ir/components/StageDot.json
Normal file
28
ir/components/StageDot.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "StageDot",
|
||||
"tag": "wn-stage-dot",
|
||||
"group": "atoms",
|
||||
"description": "StageDot — extracted from designbook ui_kits/whynot-control/Atoms.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "level",
|
||||
"attribute": "level",
|
||||
"type": "string",
|
||||
"default": "S2"
|
||||
},
|
||||
{
|
||||
"name": "label",
|
||||
"attribute": "label",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "style",
|
||||
"type": "object",
|
||||
"attribute": false,
|
||||
"portable": false,
|
||||
"description": "React inline style override — not portable to an attribute."
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Atoms.jsx",
|
||||
"exemplarRef": "ir/exemplars/StageDot.html"
|
||||
}
|
||||
22
ir/components/Stamp.json
Normal file
22
ir/components/Stamp.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Stamp",
|
||||
"tag": "wn-stamp",
|
||||
"group": "atoms",
|
||||
"description": "Stamp — extracted from designbook ui_kits/whynot-control/Atoms.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "style",
|
||||
"type": "object",
|
||||
"attribute": false,
|
||||
"portable": false,
|
||||
"description": "React inline style override — not portable to an attribute."
|
||||
}
|
||||
],
|
||||
"slots": [
|
||||
{
|
||||
"name": "default",
|
||||
"description": "Default content."
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Atoms.jsx"
|
||||
}
|
||||
33
ir/components/Tag.json
Normal file
33
ir/components/Tag.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "Tag",
|
||||
"tag": "wn-tag",
|
||||
"group": "atoms",
|
||||
"description": "Tag — extracted from designbook ui_kits/whynot-control/Atoms.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "active",
|
||||
"attribute": "active",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "draft",
|
||||
"attribute": "draft",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "style",
|
||||
"type": "object",
|
||||
"attribute": false,
|
||||
"portable": false,
|
||||
"description": "React inline style override — not portable to an attribute."
|
||||
}
|
||||
],
|
||||
"slots": [
|
||||
{
|
||||
"name": "default",
|
||||
"description": "Default content."
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Atoms.jsx",
|
||||
"exemplarRef": "ir/exemplars/Tag.html"
|
||||
}
|
||||
23
ir/components/TopNav.json
Normal file
23
ir/components/TopNav.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "TopNav",
|
||||
"tag": "wn-top-nav",
|
||||
"group": "chrome",
|
||||
"description": "TopNav — extracted from designbook ui_kits/whynot-control/Chrome.jsx.",
|
||||
"props": [
|
||||
{
|
||||
"name": "onNew",
|
||||
"type": "function",
|
||||
"attribute": false,
|
||||
"portable": false,
|
||||
"description": "React callback prop — surface as an event on attribute-driven stacks."
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "wn-new",
|
||||
"description": "Emitted for onNew."
|
||||
}
|
||||
],
|
||||
"docsRef": "designbook/ui_kits/whynot-control/Chrome.jsx",
|
||||
"exemplarRef": "ir/exemplars/TopNav.html"
|
||||
}
|
||||
40
ir/exemplars/Button.html
Normal file
40
ir/exemplars/Button.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!doctype html>
|
||||
<!-- @dsCard group="Components" name="Components · Buttons" subtitle="Primary · secondary · ghost" viewport="700x240" -->
|
||||
<html><head>
|
||||
<meta charset="utf-8"><title>Buttons</title>
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<style>
|
||||
body { padding: 28px 32px; background: var(--paper); }
|
||||
.row-label { font: 500 11px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 16px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(4, max-content); gap: 12px 14px; align-items: center; }
|
||||
.col-h { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-3); }
|
||||
.row-h { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-3); }
|
||||
.btn { font: 500 13px var(--ff-sans); letter-spacing: -0.005em; padding: 9px 16px; border-radius: var(--r-2); border: 1px solid transparent; cursor: pointer; transition: background 120ms ease, border-color 120ms ease, color 120ms ease; }
|
||||
.btn.primary { background: var(--ink); color: var(--paper); border-color: var(--ink); }
|
||||
.btn.primary.hover { background: var(--ink-2); border-color: var(--ink-2); }
|
||||
.btn.primary.disabled { background: var(--ink-5); border-color: var(--ink-5); color: var(--paper); cursor: not-allowed; }
|
||||
.btn.secondary { background: var(--paper); color: var(--ink); border-color: var(--border); }
|
||||
.btn.secondary.hover { border-color: var(--ink); }
|
||||
.btn.secondary.disabled { color: var(--ink-5); border-color: var(--border); cursor: not-allowed; }
|
||||
.btn.ghost { background: transparent; color: var(--ink); border-color: transparent; padding-left: 8px; padding-right: 8px; }
|
||||
.btn.ghost.hover { background: var(--paper-3); }
|
||||
.btn.ghost.disabled { color: var(--ink-5); cursor: not-allowed; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="row-label">Buttons — primary · secondary · ghost</div>
|
||||
<div class="grid">
|
||||
<div></div><div class="col-h">Default</div><div class="col-h">Hover</div><div class="col-h">Disabled</div>
|
||||
<div class="row-h">Primary</div>
|
||||
<button class="btn primary">Promote prototype</button>
|
||||
<button class="btn primary hover">Promote prototype</button>
|
||||
<button class="btn primary disabled">Promote prototype</button>
|
||||
<div class="row-h">Secondary</div>
|
||||
<button class="btn secondary">Park</button>
|
||||
<button class="btn secondary hover">Park</button>
|
||||
<button class="btn secondary disabled">Park</button>
|
||||
<div class="row-h">Ghost</div>
|
||||
<button class="btn ghost">View signal</button>
|
||||
<button class="btn ghost hover">View signal</button>
|
||||
<button class="btn ghost disabled">View signal</button>
|
||||
</div>
|
||||
</body></html>
|
||||
44
ir/exemplars/Eyebrow.html
Normal file
44
ir/exemplars/Eyebrow.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<!-- @dsCard group="Components" name="Components · Labels & Tags" subtitle="Stage tags · signal dots" viewport="700x200" -->
|
||||
<html><head>
|
||||
<meta charset="utf-8"><title>Labels & Tags</title>
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<style>
|
||||
body { padding: 24px 32px; background: var(--paper); }
|
||||
.row-label { font: 500 11px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 14px; }
|
||||
.stack { display: flex; flex-direction: column; gap: 18px; }
|
||||
.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
.tag { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 5px 10px; border-radius: var(--r-pill); border: 1px solid var(--border); color: var(--fg-2); background: var(--paper); }
|
||||
.tag.active { color: var(--paper); background: var(--ink); border-color: var(--ink); }
|
||||
.tag.draft { background: var(--hi); color: var(--hi-ink); border-color: transparent; }
|
||||
.stage { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 5px 10px; color: var(--fg-2); display: inline-flex; align-items: center; gap: 6px; }
|
||||
.stage .dot { width: 8px; height: 8px; border-radius: 999px; background: var(--ink); }
|
||||
.stage.s0 .dot { background: #B5B5B3 } .stage.s1 .dot { background: #8A8A8A }
|
||||
.stage.s2 .dot { background: #5C5C5C } .stage.s3 .dot { background: #0A0A0A }
|
||||
.stage.s4 .dot { background: #FFD400 }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="stack">
|
||||
<div>
|
||||
<div class="row-label">Tags — default · active · draft</div>
|
||||
<div class="row">
|
||||
<span class="tag">Raw Idea</span>
|
||||
<span class="tag">Prototype Candidate</span>
|
||||
<span class="tag active">Experiment</span>
|
||||
<span class="tag">Promotion Candidate</span>
|
||||
<span class="tag">Parked</span>
|
||||
<span class="tag draft">Draft</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="row-label">Signal dots — inline indicator</div>
|
||||
<div class="row">
|
||||
<span class="stage s0"><span class="dot"></span>S0 · No signal</span>
|
||||
<span class="stage s1"><span class="dot"></span>S1 · Weak</span>
|
||||
<span class="stage s2"><span class="dot"></span>S2 · Medium</span>
|
||||
<span class="stage s3"><span class="dot"></span>S3 · Strong</span>
|
||||
<span class="stage s4"><span class="dot"></span>S4 · Commercial</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
37
ir/exemplars/PageHeader.html
Normal file
37
ir/exemplars/PageHeader.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<!-- @dsCard group="Components" name="Components · Top Navigation" subtitle="56px · 1px hairline · ⌘K search" viewport="900x160" -->
|
||||
<html><head>
|
||||
<meta charset="utf-8"><title>Top Navigation</title>
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<style>
|
||||
body { margin: 0; background: var(--paper-2); font-family: var(--ff-sans); min-height: 200px; }
|
||||
.nav { height: 56px; background: rgba(255,255,255,0.92); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 32px; padding: 0 24px; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; font: 500 14px var(--ff-sans); }
|
||||
.brand img { width: 22px; height: 22px; }
|
||||
.brand .org { font-family: var(--ff-mono); font-size: 12px; color: var(--fg-3); letter-spacing: 0.04em; }
|
||||
.links { display: flex; gap: 22px; }
|
||||
.links a { font: 500 13px var(--ff-sans); color: var(--fg-2); text-decoration: none; padding: 6px 0; border-bottom: 1px solid transparent; }
|
||||
.links a.active { color: var(--fg-1); border-bottom-color: var(--ink); }
|
||||
.right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
|
||||
.right .search { font: 400 12px var(--ff-mono); color: var(--fg-3); border: 1px solid var(--border); padding: 6px 10px; border-radius: var(--r-1); display: flex; align-items: center; gap: 8px; min-width: 200px; }
|
||||
.right .kbd { margin-left: auto; padding: 1px 5px; border: 1px solid var(--border); border-radius: 2px; font-size: 10px; }
|
||||
.right .btn { font: 500 12px var(--ff-sans); padding: 7px 12px; border-radius: var(--r-2); border: 1px solid var(--ink); background: var(--ink); color: var(--paper); cursor: pointer; }
|
||||
.body-preview { padding: 32px 24px; color: var(--fg-3); font: 400 13px var(--ff-mono); }
|
||||
</style></head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<div class="brand"><img src="../assets/whynot-logo.png" alt=""><span>whynot</span><span class="org">/ control</span></div>
|
||||
<div class="links">
|
||||
<a class="active" href="#">Inbox</a>
|
||||
<a href="#">Prototypes</a>
|
||||
<a href="#">Signals</a>
|
||||
<a href="#">Betas</a>
|
||||
<a href="#">Decisions</a>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="search"><span>Search…</span><span class="kbd">⌘ K</span></div>
|
||||
<button class="btn">+ New idea</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="body-preview">// 56px height · 1px hairline · rgba(255,255,255,0.92) when scrolled</div>
|
||||
</body></html>
|
||||
30
ir/exemplars/PipelineStrip.html
Normal file
30
ir/exemplars/PipelineStrip.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<!-- @dsCard group="Components" name="Components · Pipeline" subtitle="Lifecycle stage tracker" viewport="700x180" -->
|
||||
<html><head>
|
||||
<meta charset="utf-8"><title>Pipeline / Lifecycle</title>
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<style>
|
||||
body { padding: 28px 32px; background: var(--paper); }
|
||||
.row-label { font: 500 11px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 22px; }
|
||||
.pipeline { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0; position: relative; }
|
||||
.stage { padding: 10px 12px 14px; border-top: 2px solid var(--border); display: flex; flex-direction: column; gap: 4px; position: relative; }
|
||||
.stage.done { border-top-color: var(--ink); }
|
||||
.stage.active { border-top-color: var(--hi-2); }
|
||||
.stage .num { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-3); }
|
||||
.stage.done .num, .stage.active .num { color: var(--fg-1); }
|
||||
.stage .name { font: 500 14px/1.25 var(--ff-sans); color: var(--fg-1); }
|
||||
.stage.pending .name { color: var(--fg-3); }
|
||||
.stage .meta { font: 400 11px/1.35 var(--ff-mono); color: var(--fg-3); }
|
||||
.arrow { position: absolute; top: -8px; right: -7px; font: 400 14px var(--ff-mono); color: var(--ink-5); }
|
||||
.stage.active .arrow, .stage.done .arrow { color: var(--ink); }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="row-label">Pipeline — Raw → Candidate → Experiment → Signal → Decision</div>
|
||||
<div class="pipeline">
|
||||
<div class="stage done"><span class="num">Stage 0</span><span class="name">Raw idea</span><span class="meta">inbox/</span></div>
|
||||
<div class="stage done"><span class="num">Stage 1</span><span class="name">Triage</span><span class="meta">2026-02-12</span><span class="arrow">→</span></div>
|
||||
<div class="stage done"><span class="num">Stage 2</span><span class="name">Prototype card</span><span class="meta">prototypes/</span><span class="arrow">→</span></div>
|
||||
<div class="stage active"><span class="num">Stage 3</span><span class="name">Experiment</span><span class="meta">ends 2026-04-01</span><span class="arrow">→</span></div>
|
||||
<div class="stage pending"><span class="num">Stage 4</span><span class="name">Signal review</span><span class="meta">— pending</span><span class="arrow">→</span></div>
|
||||
</div>
|
||||
</body></html>
|
||||
85
ir/exemplars/Sidebar.html
Normal file
85
ir/exemplars/Sidebar.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<!doctype html>
|
||||
<!-- @dsCard group="Components" name="Components · Left Navigation" subtitle="Grouped sidebar · active state · minimal variant" viewport="700x420" -->
|
||||
<html><head>
|
||||
<meta charset="utf-8"><title>Left Navigation</title>
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; background: var(--paper); display: flex; gap: 28px; align-items: stretch; }
|
||||
.frame { display: flex; flex-direction: column; gap: 8px; }
|
||||
.frame > .cap { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-3); padding-left: 12px; }
|
||||
.leftnav {
|
||||
width: 220px; box-sizing: border-box;
|
||||
display: flex; flex-direction: column; gap: 28px;
|
||||
padding: 24px 8px 24px 12px;
|
||||
border-right: 1px solid var(--line-soft);
|
||||
min-height: 360px;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 8px; padding: 0 10px; }
|
||||
.brand img { width: 20px; height: 20px; }
|
||||
.brand .nm { font: 500 14px var(--ff-sans); color: var(--fg-1); }
|
||||
.brand .slug { font: 400 12px var(--ff-mono); color: var(--fg-3); }
|
||||
.body { display: flex; flex-direction: column; gap: 28px; flex: 1; }
|
||||
.section { display: flex; flex-direction: column; gap: 8px; }
|
||||
.section .lbl { font: 500 11px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-3); padding-left: 12px; opacity: 0.7; }
|
||||
.items { display: flex; flex-direction: column; gap: 1px; }
|
||||
.item {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 6px 10px; border-left: 2px solid transparent;
|
||||
color: var(--fg-3); font: 400 13px var(--ff-sans); cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.item .ic { width: 16px; height: 16px; stroke: currentColor; stroke-width: 1.5; fill: none; flex: none; }
|
||||
.item .t { flex: 1; }
|
||||
.item .n { margin-left: auto; font: 400 11px var(--ff-mono); color: var(--ink-5); }
|
||||
.item.active { color: var(--fg-1); font-weight: 500; border-left-color: var(--ink); }
|
||||
.item.active .n { color: var(--fg-3); }
|
||||
.item.doc { font: 400 12px var(--ff-mono); }
|
||||
.footer { margin-top: auto; display: flex; align-items: center; gap: 8px; padding: 0 12px; font: 400 11px var(--ff-mono); letter-spacing: 0.06em; text-transform: uppercase; color: var(--fg-3); }
|
||||
.footer .dot { width: 5px; height: 5px; border-radius: 999px; background: var(--ink-4); }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<span class="cap">Default · grouped, with active state</span>
|
||||
<nav class="leftnav">
|
||||
<div class="brand"><img src="../assets/whynot-logo.png" alt=""><span class="nm">whynot</span><span class="slug">/ control</span></div>
|
||||
<div class="body">
|
||||
<div class="section">
|
||||
<span class="lbl">Work</span>
|
||||
<div class="items">
|
||||
<a class="item"><svg class="ic" viewBox="0 0 24 24"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg><span class="t">Inbox</span><span class="n">7</span></a>
|
||||
<a class="item active"><svg class="ic" viewBox="0 0 24 24"><path d="M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2"/><path d="M6.453 15h11.094"/><path d="M8.5 2h7"/></svg><span class="t">Prototypes</span><span class="n">4</span></a>
|
||||
<a class="item"><svg class="ic" viewBox="0 0 24 24"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.5.5 0 0 1-.96 0L9.24 2.18a.5.5 0 0 0-.96 0l-2.35 8.36A2 2 0 0 1 4 12H2"/></svg><span class="t">Signals</span><span class="n">12</span></a>
|
||||
<a class="item"><svg class="ic" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/><path d="M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/></svg><span class="t">Betas</span><span class="n">1</span></a>
|
||||
<a class="item"><svg class="ic" viewBox="0 0 24 24"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg><span class="t">Decisions</span><span class="n">3</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<span class="lbl">Control docs</span>
|
||||
<div class="items">
|
||||
<a class="item doc"><span class="t">INTENT.md</span></a>
|
||||
<a class="item doc"><span class="t">SCOPE.md</span></a>
|
||||
<a class="item doc"><span class="t">OPERATING_MODEL.md</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer"><span class="dot"></span><span>A1 · Incubating</span></div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="frame">
|
||||
<span class="cap">Minimal · no brand, no icons</span>
|
||||
<nav class="leftnav" style="min-height: 360px;">
|
||||
<div class="body">
|
||||
<div class="section">
|
||||
<span class="lbl">Navigate</span>
|
||||
<div class="items">
|
||||
<a class="item active"><span class="t">Overview</span></a>
|
||||
<a class="item"><span class="t">Prototypes</span><span class="n">4</span></a>
|
||||
<a class="item"><span class="t">Signals</span><span class="n">12</span></a>
|
||||
<a class="item"><span class="t">Settings</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</body></html>
|
||||
44
ir/exemplars/StageDot.html
Normal file
44
ir/exemplars/StageDot.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<!-- @dsCard group="Components" name="Components · Labels & Tags" subtitle="Stage tags · signal dots" viewport="700x200" -->
|
||||
<html><head>
|
||||
<meta charset="utf-8"><title>Labels & Tags</title>
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<style>
|
||||
body { padding: 24px 32px; background: var(--paper); }
|
||||
.row-label { font: 500 11px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 14px; }
|
||||
.stack { display: flex; flex-direction: column; gap: 18px; }
|
||||
.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
.tag { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 5px 10px; border-radius: var(--r-pill); border: 1px solid var(--border); color: var(--fg-2); background: var(--paper); }
|
||||
.tag.active { color: var(--paper); background: var(--ink); border-color: var(--ink); }
|
||||
.tag.draft { background: var(--hi); color: var(--hi-ink); border-color: transparent; }
|
||||
.stage { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 5px 10px; color: var(--fg-2); display: inline-flex; align-items: center; gap: 6px; }
|
||||
.stage .dot { width: 8px; height: 8px; border-radius: 999px; background: var(--ink); }
|
||||
.stage.s0 .dot { background: #B5B5B3 } .stage.s1 .dot { background: #8A8A8A }
|
||||
.stage.s2 .dot { background: #5C5C5C } .stage.s3 .dot { background: #0A0A0A }
|
||||
.stage.s4 .dot { background: #FFD400 }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="stack">
|
||||
<div>
|
||||
<div class="row-label">Tags — default · active · draft</div>
|
||||
<div class="row">
|
||||
<span class="tag">Raw Idea</span>
|
||||
<span class="tag">Prototype Candidate</span>
|
||||
<span class="tag active">Experiment</span>
|
||||
<span class="tag">Promotion Candidate</span>
|
||||
<span class="tag">Parked</span>
|
||||
<span class="tag draft">Draft</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="row-label">Signal dots — inline indicator</div>
|
||||
<div class="row">
|
||||
<span class="stage s0"><span class="dot"></span>S0 · No signal</span>
|
||||
<span class="stage s1"><span class="dot"></span>S1 · Weak</span>
|
||||
<span class="stage s2"><span class="dot"></span>S2 · Medium</span>
|
||||
<span class="stage s3"><span class="dot"></span>S3 · Strong</span>
|
||||
<span class="stage s4"><span class="dot"></span>S4 · Commercial</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
44
ir/exemplars/Tag.html
Normal file
44
ir/exemplars/Tag.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<!-- @dsCard group="Components" name="Components · Labels & Tags" subtitle="Stage tags · signal dots" viewport="700x200" -->
|
||||
<html><head>
|
||||
<meta charset="utf-8"><title>Labels & Tags</title>
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<style>
|
||||
body { padding: 24px 32px; background: var(--paper); }
|
||||
.row-label { font: 500 11px/1 var(--ff-mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 14px; }
|
||||
.stack { display: flex; flex-direction: column; gap: 18px; }
|
||||
.row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
.tag { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 5px 10px; border-radius: var(--r-pill); border: 1px solid var(--border); color: var(--fg-2); background: var(--paper); }
|
||||
.tag.active { color: var(--paper); background: var(--ink); border-color: var(--ink); }
|
||||
.tag.draft { background: var(--hi); color: var(--hi-ink); border-color: transparent; }
|
||||
.stage { font: 500 10px/1 var(--ff-mono); letter-spacing: 0.1em; text-transform: uppercase; padding: 5px 10px; color: var(--fg-2); display: inline-flex; align-items: center; gap: 6px; }
|
||||
.stage .dot { width: 8px; height: 8px; border-radius: 999px; background: var(--ink); }
|
||||
.stage.s0 .dot { background: #B5B5B3 } .stage.s1 .dot { background: #8A8A8A }
|
||||
.stage.s2 .dot { background: #5C5C5C } .stage.s3 .dot { background: #0A0A0A }
|
||||
.stage.s4 .dot { background: #FFD400 }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="stack">
|
||||
<div>
|
||||
<div class="row-label">Tags — default · active · draft</div>
|
||||
<div class="row">
|
||||
<span class="tag">Raw Idea</span>
|
||||
<span class="tag">Prototype Candidate</span>
|
||||
<span class="tag active">Experiment</span>
|
||||
<span class="tag">Promotion Candidate</span>
|
||||
<span class="tag">Parked</span>
|
||||
<span class="tag draft">Draft</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="row-label">Signal dots — inline indicator</div>
|
||||
<div class="row">
|
||||
<span class="stage s0"><span class="dot"></span>S0 · No signal</span>
|
||||
<span class="stage s1"><span class="dot"></span>S1 · Weak</span>
|
||||
<span class="stage s2"><span class="dot"></span>S2 · Medium</span>
|
||||
<span class="stage s3"><span class="dot"></span>S3 · Strong</span>
|
||||
<span class="stage s4"><span class="dot"></span>S4 · Commercial</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
37
ir/exemplars/TopNav.html
Normal file
37
ir/exemplars/TopNav.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<!-- @dsCard group="Components" name="Components · Top Navigation" subtitle="56px · 1px hairline · ⌘K search" viewport="900x160" -->
|
||||
<html><head>
|
||||
<meta charset="utf-8"><title>Top Navigation</title>
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<style>
|
||||
body { margin: 0; background: var(--paper-2); font-family: var(--ff-sans); min-height: 200px; }
|
||||
.nav { height: 56px; background: rgba(255,255,255,0.92); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 32px; padding: 0 24px; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; font: 500 14px var(--ff-sans); }
|
||||
.brand img { width: 22px; height: 22px; }
|
||||
.brand .org { font-family: var(--ff-mono); font-size: 12px; color: var(--fg-3); letter-spacing: 0.04em; }
|
||||
.links { display: flex; gap: 22px; }
|
||||
.links a { font: 500 13px var(--ff-sans); color: var(--fg-2); text-decoration: none; padding: 6px 0; border-bottom: 1px solid transparent; }
|
||||
.links a.active { color: var(--fg-1); border-bottom-color: var(--ink); }
|
||||
.right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
|
||||
.right .search { font: 400 12px var(--ff-mono); color: var(--fg-3); border: 1px solid var(--border); padding: 6px 10px; border-radius: var(--r-1); display: flex; align-items: center; gap: 8px; min-width: 200px; }
|
||||
.right .kbd { margin-left: auto; padding: 1px 5px; border: 1px solid var(--border); border-radius: 2px; font-size: 10px; }
|
||||
.right .btn { font: 500 12px var(--ff-sans); padding: 7px 12px; border-radius: var(--r-2); border: 1px solid var(--ink); background: var(--ink); color: var(--paper); cursor: pointer; }
|
||||
.body-preview { padding: 32px 24px; color: var(--fg-3); font: 400 13px var(--ff-mono); }
|
||||
</style></head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<div class="brand"><img src="../assets/whynot-logo.png" alt=""><span>whynot</span><span class="org">/ control</span></div>
|
||||
<div class="links">
|
||||
<a class="active" href="#">Inbox</a>
|
||||
<a href="#">Prototypes</a>
|
||||
<a href="#">Signals</a>
|
||||
<a href="#">Betas</a>
|
||||
<a href="#">Decisions</a>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="search"><span>Search…</span><span class="kbd">⌘ K</span></div>
|
||||
<button class="btn">+ New idea</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="body-preview">// 56px height · 1px hairline · rgba(255,255,255,0.92) when scrolled</div>
|
||||
</body></html>
|
||||
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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
ir/schema/tokens.schema.json
Normal file
60
ir/schema/tokens.schema.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
266
ir/tokens.json
Normal file
266
ir/tokens.json
Normal file
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user