Files
whynot-design/DesignSystemIntroduction.md
tegwick 0d688ca94a 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>
2026-06-24 12:36:24 +02:00

339 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design System Introduction
> How `whynot-design` fits into the broader `whynot` workflow — from atelier exploration to production deploys, across React, Django, plain HTML, or any future consumer.
>
> Audience: anyone (human or agent) about to add `whynot-design` as a dependency, or contribute changes back to it.
---
## 1. Mental model — three places, three jobs
`whynot-design` is one of three surfaces. Each has a different job. Don't confuse them.
| Place | Job | What lives there |
|---|---|---|
| **Claude atelier project** (`WhyNot Design System` template) | Explore, decide, mock | HTML cards, prototypes of new components, the README that defines the rules. Source of truth for the *language* of the system. |
| **`whynot-design` repo** (this one) | Distribute | A versioned, publishable package: tokens, CSS, web components, the logo bundle. Source of truth for the *artefact*. |
| **`whynot-*` consuming repos** (apps, prototypes, marketing sites) | Use | Add `@whynot/design` as a dependency. Use it from React, Django, Vue, or plain HTML — see [`MultiFrameworkSupport.md`](./MultiFrameworkSupport.md). |
This mirrors the existing organisational logic. `whynot-control` is the *control* surface (intent, scope, decisions, governance). `whynot-design` is the *implementation* surface for the visual language.
> The Claude-Design template is the atelier for the *next* design exploration. It is **not** what production code consumes. Production consumes this repo.
---
## 2. The three-layer architecture
`whynot-design` is **deliberately framework-agnostic**. It ships in three stacked layers, and consumers pick how deep they go.
```
┌─────────────────────────────────────────────────────────────┐
│ Layer 3 — Framework adapters (optional) │
│ Django partials · React thin wrappers · Vue, Svelte, … │
│ All thin. All delegate to Layer 2. │
├─────────────────────────────────────────────────────────────┤
│ Layer 2 — Web components (canonical) │
│ <wn-button>, <wn-card>, <wn-modal>, <wn-prototype-card>… │
│ Lit-based. Light DOM. SSR-friendly. Work in any framework. │
├─────────────────────────────────────────────────────────────┤
│ Layer 1 — Tokens + CSS │
│ colors_and_type.css · components.css · tokens/*.json │
│ Plain CSS variables and utility classes. Works in any HTML.│
└─────────────────────────────────────────────────────────────┘
```
### Why this shape
- **Layer 1 is the bedrock.** Tokens are framework-agnostic by definition. `colors_and_type.css` is just CSS variables — Django, Rails, Phoenix, plain HTML, any framework can `<link>` to it and inherit the system's typography and palette tomorrow. The component utility classes in `components.css` (e.g. `.wn-btn`, `.wn-card`) compose those tokens into the canonical recipes. **Anything in this repo can be consumed with no JS at all.**
- **Layer 2 adds behaviour without lock-in.** Web Components are a stable platform feature — they're not a framework dependency that ages out. Lit is the implementation detail; the *contract* is the HTML custom-element API. A `<wn-button variant="primary">` works in a React JSX file, a Django template, a Vue SFC, and `index.html` identically. **Layer 2 components render to light DOM**, which means Layer 1's CSS styles them — no shadow-DOM style isolation, no FOUC, no SSR friction.
- **Layer 3 is convenience, not commitment.** If a consumer wants typed React props or Django `{% include %}` shorthand, the repo ships *thin* wrappers in `adapters/`. They delegate to Layer 2. Removing or refactoring them does not break Layer 2 consumers.
### Which layer should *you* use?
| If your consumer is… | Use this layer | Example |
|---|---|---|
| Django (server-rendered HTML + HTMX) | Layer 1 + 2 directly, or Layer 3 partials | `<wn-button variant="primary">{{ label }}</wn-button>` |
| React (any meta-framework) | Layer 2 directly | `<wn-button variant="primary">Promote</wn-button>` — React handles custom elements natively |
| Plain HTML page / a prototype landing page | Layer 2 directly | `<wn-button variant="primary">Sign up</wn-button>` |
| Email, PDF, or no-JS context | Layer 1 only | `<button class="wn-btn wn-btn--primary">Open</button>` |
| You only need typography & colours | Layer 1 only | Just `<link>` the CSS. |
The discipline: **do not subclass, fork, or restyle Layer 2 components in your repo**. If a component doesn't fit a use case, add a variant *upstream* in `whynot-design`. The thing that makes a design system valuable is the discipline of not forking it locally.
For the full how-to per framework — including SSR, hydration, the `noscript` story, form participation, and HTMX integration — see [`MultiFrameworkSupport.md`](./MultiFrameworkSupport.md).
---
## 3. What this repo contains
```
whynot-design/
├── README.md Full design language.
├── DesignSystemIntroduction.md This file.
├── MultiFrameworkSupport.md How to consume from React, Django, Vue, plain HTML.
├── SKILL.md Agent Skill manifest, cross-compatible with Claude Code.
├── CONTRIBUTING.md How to propose, review, and ship a change.
├── CHANGELOG.md Hand-edited, one entry per release.
├── BOOTSTRAP.md First-push instructions. Delete after use.
├── package.json @whynot/design — Lit is the one runtime dependency.
├── tokens/ Source-of-truth design tokens (JSON).
├── src/
│ ├── styles/
│ │ ├── colors_and_type.css Layer 1 — tokens + semantic element styles.
│ │ └── components.css Layer 1 — utility classes for all components.
│ ├── elements/ Layer 2 — Lit web components, light DOM.
│ │ ├── atoms.js button, tag, eyebrow, stamp, stage-dot, phase-dot, icon
│ │ ├── form.js input, textarea, select, search-input, field-row
│ │ ├── layout.js card, modal, table, toast, empty-state, breadcrumb
│ │ └── chrome.js top-nav, sidebar, page-header, pipeline, prototype-card
│ └── index.js Side-effect import: registers all custom elements.
├── adapters/ Layer 3 — optional wrappers.
│ └── django/templates/whynot/ Django {% include %} partials over the web components.
├── assets/ Logo, mark, future imagery.
├── examples/
│ ├── showcase/ Plain-HTML demo of every component.
│ └── whynot-control/ React + custom-element click-through UI kit.
└── .gitea/workflows/ Lint + Playwright visual regression on PR.
```
---
## 4. Integrating with a consuming codebase
### 4.1 Distribution channels at A1
In order of effort:
**a) pnpm workspaces (recommended for now)** — put `whynot-design` and your consuming app in the same monorepo (or use `file:` / `link:` references). Zero registry, zero auth, instant updates.
**b) Install directly from Gitea** — no registry needed.
```sh
pnpm add git+ssh://git@gitea.example.com/whynot/whynot-design.git#v0.2.0
```
Pin to a tag, not `main`. Tag-pinning is the entire versioning discipline at A1.
When you outgrow either (second team needs read access without cloning, semver resolution becomes valuable), publish to **Gitea Packages** (native npm protocol) or a private Verdaccio.
### 4.2 What a consumer imports
The smallest viable consumption is:
```html
<link rel="stylesheet" href="/static/whynot/colors_and_type.css">
<link rel="stylesheet" href="/static/whynot/components.css">
<script type="module" src="/static/whynot/index.js"></script>
```
That's it. Now `<wn-button>`, `<wn-card>`, `<wn-modal>` etc. work everywhere on the page, in any framework, server-rendered or client-rendered.
For a Node-tooled consumer:
```jsx
// At app root, once.
import "@whynot/design/styles/colors_and_type.css";
import "@whynot/design/styles/components.css";
import "@whynot/design"; // side-effect: registers all custom elements
// In any component file — React, Vue, Svelte, all behave the same.
function NewBetaPage() {
return (
<article>
<wn-eyebrow>whynot · closed beta</wn-eyebrow>
<h1>Concierge prototype triage</h1>
<wn-button variant="primary">Request an invite</wn-button>
</article>
);
}
```
Three rules of consumption:
1. **Import the CSS once at the app's root.** Both stylesheets.
2. **Use tokens for any colour, type, or spacing decision** that components don't cover. If the token doesn't exist, that's a signal to extend the system upstream — not to invent.
3. **Don't restyle components by overriding their CSS.** If a component doesn't fit, contribute a variant.
### 4.3 Bootstrapping a new consuming repo
```sh
mkdir whynot-prototype-WNO-022
cd whynot-prototype-WNO-022
pnpm init
pnpm add @whynot/design # or the git+ssh URL
# Then in your entry point:
echo 'import "@whynot/design/styles/colors_and_type.css"' >> src/main.js
echo 'import "@whynot/design/styles/components.css"' >> src/main.js
echo 'import "@whynot/design"' >> src/main.js
```
If it takes longer than this, the design system is fighting you.
---
## 5. Propagation pipeline — five hops
The end-to-end flow for a single design change:
```
[Claude atelier] [whynot-design] [Consuming repo] [Deploy]
explore variants ──► PR with token / ──► Renovate / pnpm ──► staging
in the template component change up opens PR bumping
user approves + Playwright diff @whynot/design │
+ CHANGELOG entry ▼
│ │ prod
tag v0.3.1 CI runs visual
CI publishes / regression on the
attaches release consuming app
asset │
│ merge if green
└──────────────────────────────┘
```
### Hop-by-hop
1. **Atelier → `whynot-design` PR.** Someone (you, a designer, or an agent) takes a change agreed in the Claude project and opens a PR against `whynot-design`. The PR description quotes the atelier decision.
2. **`whynot-design` CI** runs:
- Lint + (optional) typecheck.
- **Visual regression** — Playwright screenshots `examples/showcase/index.html` and `examples/whynot-control/index.html`, diffs against baselines. This catches both styling regressions and behavioural ones.
- A `CHANGELOG.md` entry is required.
3. **Merge → tag → publish.** On merge to `main`:
- Bump `package.json` (patch for token tweaks, minor for new components, major for renames/removals).
- Tag `v0.3.1`.
- Push tag. CI uploads release notes from the CHANGELOG.
4. **Consumer auto-PR.** Renovate or Dependabot watches `@whynot/design` and opens a PR in every consuming repo bumping the version.
5. **Consumer CI + deploy.** The consuming repo's *own* CI runs *its* visual regression. If unchanged, auto-merge. If changed, a human reviews. Merge triggers existing deploys to staging → prod.
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/<stack>/`, 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/<Name>.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 35
(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
Strict semver, even at A1.
| Change | Bump |
|---|---|
| Token value tweak that doesn't visibly break any existing example | **patch**`0.2.0 → 0.2.1` |
| New component, new variant, new token | **minor**`0.2.0 → 0.3.0` |
| Removing / renaming a component, prop, or token; changing default behaviour | **major**`0.3.0 → 1.0.0` |
Stay in `0.x.x` until something built with this system is in production. While in `0.x.x`, **minor bumps are allowed to break things** — that's the convention. This gives you permission to iterate without ceremony.
Promotion past `1.0.0` should appear in `whynot-control/DECISIONS.md`. Same rule as promotion to Helix or Coulomb: it's a deliberate act, not a release-script side-effect.
---
## 7. Where Claude fits
Two distinct roles, both useful:
- **The Claude atelier template** — used at hop 1. Designer (or you) opens a new project, mocks variations, decides, hands off a PR description + diff to whoever writes the `whynot-design` PR. The atelier never publishes anything to production directly.
- **Claude Code with `SKILL.md`** — used at hop 1 *and* hop 5. The same SKILL file works in both contexts:
- Pointed at `whynot-design`, Claude Code can write component PRs.
- Pointed at a consuming repo, Claude Code can build screens that respect the rules.
That's why `SKILL.md` ships with this repo. Drop it into `.claude/skills/` of any consuming repo and any agent operating in that repo will know the visual language.
---
## 8. Pragmatic A1 staging — don't build the whole pipeline yet
Right now, `whynot` is at A1 Incubating. Build the smallest pipeline that still has the right *shape*. Promote each piece only when a real signal demands it.
| Hop | A1 version | Promote to full when… |
|---|---|---|
| Atelier | Claude template, as-is. | Never — stays here. |
| `whynot-design` repo | This seed. Tokens + CSS + Lit web components. | Never — this is the canonical shape. |
| Distribution | pnpm workspace, or `git+ssh` install from tags. | An external collaborator needs read access without cloning. |
| Visual regression | Playwright over `examples/showcase/` and `examples/whynot-control/`. | The system has >40 components or >3 consuming apps. |
| Dependency updates | Manual `pnpm up` once a week. | More than two consuming repos. |
| Release notes | Hand-edited `CHANGELOG.md`. | More than two contributors. |
| Per-framework adapters | Django partials only, for the 45 most-used components. | Another non-React, non-Django consumer appears. |
This staging is exactly the *"low-cost learning first"* posture in `whynot-control/OPERATING_MODEL.md`. A design system with one consumer and one author does not need Chromatic. A design system with five consumers and three authors absolutely does.
---
## 9. First-week checklist
For whoever is bootstrapping this repo right now:
- [ ] Push the seed contents to `gitea.example.com/whynot/whynot-design`.
- [ ] Tag `v0.2.0` immediately so consumers can pin.
- [ ] Add the repo as a remote dependency in **one** consuming app (the Django one) and verify imports work end-to-end. Follow [`MultiFrameworkSupport.md` §Django](./MultiFrameworkSupport.md#django-server-rendered-templates--htmx).
- [ ] Open one trivial PR against `whynot-design` (e.g. a CHANGELOG typo) to confirm CI passes end-to-end.
- [ ] Record this bootstrap in `whynot-control/DECISIONS.md` as DEC-004 — *"Established whynot-design as the implementation surface, three-layer architecture, Lit web components as the canonical component layer."*
- [ ] Update `whynot-control/SCOPE.md` to mention `whynot-design` as a sibling.
That's it. Anything more is over-engineering for the current stage.
---
> A design system can be interesting and still be parked. `whynot-design` exists to reduce visual uncertainty across prototypes, not to create more obligations.