diff --git a/Makefile b/Makefile index eb2343f..0aca222 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,9 @@ parity-lit: ## Confirm Lit elements honour the IR contract + render (browser). E designbook-refresh: ## Refresh routine: check->pull->sync->ir->adapt-lit->(drift triage)->parity. ARGS=--no-pull etc. $(NODE) scripts/designbook-refresh.mjs $(ARGS) +adapt-plain-css: ## Second-adapter SMOKE: prove a non-Lit adapter consumes the same ir/ (WHYNOT-WP-0002 T10). + $(NODE) adapters/plain-css/adapt.mjs + recent-changes: ## Regenerate RecentChanges.md (alias of the reporter; --range supported). $(NODE) scripts/designbook-sync.mjs $(ARGS) diff --git a/adapters/plain-css/README.md b/adapters/plain-css/README.md new file mode 100644 index 0000000..f982e7a --- /dev/null +++ b/adapters/plain-css/README.md @@ -0,0 +1,31 @@ +# plain-css adapter — second-adapter smoke (WHYNOT-WP-0002 · T10) + +> **This is not a finished stack.** It exists to prove the **IR/adapter boundary**: +> that a second, non-Lit adapter can consume the *same* `ir/` and honour the *same* +> [`adapters/ADAPTER_CONTRACT.md`](../ADAPTER_CONTRACT.md). The deliverable is +> confidence in the seam, so the architecture can later be lifted into a +> coulomb-level tool — **not** a usable plain-CSS component kit. + +Run with **`make adapt-plain-css`** (`adapters/plain-css/adapt.mjs`). + +## What it proves + +| Contract concern | This adapter | Same as Lit? | +|---|---|---| +| Input is `ir/` only | reads `ir/tokens.json` + `ir/components/*.json` | ✓ identical inputs | +| Tokens fully generated | → `adapters/plain-css/tokens.css` (CSS custom properties, deterministic no-op re-run) | ✓ same discipline, different target | +| New component → stub | write-once class stub `adapters/plain-css/stubs/.css` (base + variant modifier classes from the contract) | ✓ write-once, into a staging dir | +| Drift roll-up | `adapters/plain-css/_report.json` in the contract's report shape (`stack`, `generatedAt`, `components[]`) | ✓ portable shape | +| Exit codes | `0` ok · `2` usage · `3` new/drift · `5` internal | ✓ shared convention | + +Because plain-CSS has no hand-authored source, **every** IR component reports +`status: "new"` and gets a stub — exactly the contract's new-component path. That a +totally different stack reuses the same IR, the same report shape, and the same exit +codes — with **zero changes to `ir/`** — is the proof the boundary holds. + +## What it deliberately does NOT do + +No real CSS appearance, no parity, no full component set, no integration into the +repo's build. Finishing a plain-CSS (or Vue/Svelte/…) stack is future work; the seam +is what T10 validates. See `DesignSystemIntroduction.md` §5.1 and the Lit reference +adapter (`adapters/lit/`) for the full implementation. diff --git a/adapters/plain-css/_report.json b/adapters/plain-css/_report.json new file mode 100644 index 0000000..c7ba13a --- /dev/null +++ b/adapters/plain-css/_report.json @@ -0,0 +1,106 @@ +{ + "stack": "plain-css", + "generatedAt": "2026-06-30T07:20:50.544Z", + "components": [ + { + "name": "Button", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-button class — stub generated." + } + ] + }, + { + "name": "Eyebrow", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-eyebrow class — stub generated." + } + ] + }, + { + "name": "Icon", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-icon class — stub generated." + } + ] + }, + { + "name": "PageHeader", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-page-header class — stub generated." + } + ] + }, + { + "name": "PipelineStrip", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-pipeline-strip class — stub generated." + } + ] + }, + { + "name": "Sidebar", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-sidebar class — stub generated." + } + ] + }, + { + "name": "StageDot", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-stage-dot class — stub generated." + } + ] + }, + { + "name": "Stamp", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-stamp class — stub generated." + } + ] + }, + { + "name": "Tag", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-tag class — stub generated." + } + ] + }, + { + "name": "TopNav", + "status": "new", + "issues": [ + { + "kind": "no-counterpart", + "detail": "no .wn-top-nav class — stub generated." + } + ] + } + ] +} diff --git a/adapters/plain-css/adapt.mjs b/adapters/plain-css/adapt.mjs new file mode 100644 index 0000000..940e07e --- /dev/null +++ b/adapters/plain-css/adapt.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +// ============================================================= +// adapters/plain-css/adapt.mjs — second-adapter SMOKE (WHYNOT-WP-0002 · T10) +// +// NOT a finished stack. This exists only to prove the IR/adapter *boundary*: +// a non-Lit adapter consuming the very same ir/ and honouring the same +// adapters/ADAPTER_CONTRACT.md (token full-gen + stub/drift + exit codes). +// The deliverable is confidence in the seam, not a usable plain-CSS kit. +// +// It does two contract things from ir/ and nothing Lit-specific: +// • tokens → fully generated CSS custom properties (deterministic no-op re-run) +// • components → write-once class stubs + a drift roll-up in the contract shape +// (every component is "new" here — plain-CSS has no existing source to drift). +// +// Exit codes (shared contract): 0 ok · 2 usage · 3 drift/new · 5 internal. +// Run: `make adapt-plain-css`. +// ============================================================= +import { readFileSync, readdirSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const IR = join(REPO, "ir"); +const HERE = join(REPO, "adapters", "plain-css"); +const STUBS = join(HERE, "stubs"); + +const refToVar = (v) => { + const m = /^\{[A-Za-z0-9]+\.([A-Za-z0-9-]+)\}$/.exec(String(v).trim()); + return m ? `var(--${m[1]})` : v; +}; + +function generateTokens() { + const tokens = JSON.parse(readFileSync(join(IR, "tokens.json"), "utf8")); + const lines = ["/* @generated by adapters/plain-css (WHYNOT-WP-0002 T10) from ir/tokens.json — DO NOT EDIT. */", ":root {"]; + let n = 0; + for (const [group, entries] of Object.entries(tokens)) { + lines.push(` /* ${group} */`); + for (const [key, tok] of Object.entries(entries)) { + if (key === "$type") continue; + lines.push(` --${key}: ${refToVar(tok.$value)};`); n++; + } + } + lines.push("}", ""); + const out = join(HERE, "tokens.css"); + const body = lines.join("\n"); + const before = existsSync(out) ? readFileSync(out, "utf8") : null; + if (before === body) { console.log(`tokens: up to date (${n} custom properties, no change).`); return; } + writeFileSync(out, body); + console.log(`tokens: regenerated ${n} custom properties → adapters/plain-css/tokens.css`); +} + +function renderClassStub(c) { + const base = c.tag; // reuse the contract tag as the base class name + const lines = [ + `/* @generated STUB by adapters/plain-css (T10) from ir/components/${c.name}.json — write-once. */`, + `/* ${c.name}: ${c.description} */`, + `.${base} { /* TODO: base appearance per ir/exemplars/${c.name}.html */ }`, + ]; + for (const v of c.variants || []) { + for (const val of v.values) lines.push(`.${base}--${val} { /* ${v.axis}=${val} */ }`); + } + return lines.join("\n") + "\n"; +} + +function scaffold() { + const contracts = readdirSync(join(IR, "components")).filter((f) => f.endsWith(".json")) + .map((f) => JSON.parse(readFileSync(join(IR, "components", f), "utf8"))) + .sort((a, b) => a.name.localeCompare(b.name)); + + // Plain-CSS has no hand-authored source, so every IR component is "new". + const components = contracts.map((c) => ({ name: c.name, status: "new", + issues: [{ kind: "no-counterpart", detail: `no .${c.tag} class — stub generated.` }] })); + + mkdirSync(STUBS, { recursive: true }); + let stubbed = 0; + for (const c of contracts) { + const out = join(STUBS, `${c.tag}.css`); + if (!existsSync(out)) { writeFileSync(out, renderClassStub(c)); stubbed++; } + } + // Drift roll-up — same shape as the Lit adapter's, proving the contract is portable. + const reportPath = join(HERE, "_report.json"); + let generatedAt = new Date().toISOString(); + if (existsSync(reportPath)) { + try { const prev = JSON.parse(readFileSync(reportPath, "utf8")); + if (JSON.stringify(prev.components) === JSON.stringify(components) && prev.generatedAt) generatedAt = prev.generatedAt; + } catch { /* fresh */ } + } + writeFileSync(reportPath, JSON.stringify({ stack: "plain-css", generatedAt, components }, null, 2) + "\n"); + console.log(`scaffold: ${components.length} components, all new (${stubbed} stub${stubbed === 1 ? "" : "s"} written) → adapters/plain-css/stubs/`); + return components.length > 0; +} + +function main() { + if (!existsSync(join(IR, "tokens.json"))) { console.error("No ir/ — run `make ir` first."); process.exit(2); } + generateTokens(); + const isNew = scaffold(); + console.log("\nadapt-plain-css: SMOKE only — proves a non-Lit adapter consumes the same ir/ and"); + console.log("emits the same contract shapes. Not a finished stack (see adapters/plain-css/README.md)."); + if (isNew) process.exit(3); // new components present (contract: 3) — expected for a fresh stack +} + +main(); diff --git a/adapters/plain-css/stubs/wn-button.css b/adapters/plain-css/stubs/wn-button.css new file mode 100644 index 0000000..9c6f492 --- /dev/null +++ b/adapters/plain-css/stubs/wn-button.css @@ -0,0 +1,6 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/Button.json — write-once. */ +/* Button: Button — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ +.wn-button { /* TODO: base appearance per ir/exemplars/Button.html */ } +.wn-button--secondary { /* variant=secondary */ } +.wn-button--primary { /* variant=primary */ } +.wn-button--ghost { /* variant=ghost */ } diff --git a/adapters/plain-css/stubs/wn-eyebrow.css b/adapters/plain-css/stubs/wn-eyebrow.css new file mode 100644 index 0000000..f9a13ed --- /dev/null +++ b/adapters/plain-css/stubs/wn-eyebrow.css @@ -0,0 +1,3 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/Eyebrow.json — write-once. */ +/* Eyebrow: Eyebrow — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ +.wn-eyebrow { /* TODO: base appearance per ir/exemplars/Eyebrow.html */ } diff --git a/adapters/plain-css/stubs/wn-icon.css b/adapters/plain-css/stubs/wn-icon.css new file mode 100644 index 0000000..13a448e --- /dev/null +++ b/adapters/plain-css/stubs/wn-icon.css @@ -0,0 +1,3 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/Icon.json — write-once. */ +/* Icon: Icon — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ +.wn-icon { /* TODO: base appearance per ir/exemplars/Icon.html */ } diff --git a/adapters/plain-css/stubs/wn-page-header.css b/adapters/plain-css/stubs/wn-page-header.css new file mode 100644 index 0000000..15c8c07 --- /dev/null +++ b/adapters/plain-css/stubs/wn-page-header.css @@ -0,0 +1,3 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/PageHeader.json — write-once. */ +/* PageHeader: PageHeader — extracted from designbook ui_kits/whynot-control/Chrome.jsx. */ +.wn-page-header { /* TODO: base appearance per ir/exemplars/PageHeader.html */ } diff --git a/adapters/plain-css/stubs/wn-pipeline-strip.css b/adapters/plain-css/stubs/wn-pipeline-strip.css new file mode 100644 index 0000000..e187451 --- /dev/null +++ b/adapters/plain-css/stubs/wn-pipeline-strip.css @@ -0,0 +1,3 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/PipelineStrip.json — write-once. */ +/* PipelineStrip: PipelineStrip — extracted from designbook ui_kits/whynot-control/Chrome.jsx. */ +.wn-pipeline-strip { /* TODO: base appearance per ir/exemplars/PipelineStrip.html */ } diff --git a/adapters/plain-css/stubs/wn-sidebar.css b/adapters/plain-css/stubs/wn-sidebar.css new file mode 100644 index 0000000..ea51343 --- /dev/null +++ b/adapters/plain-css/stubs/wn-sidebar.css @@ -0,0 +1,4 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/Sidebar.json — write-once. */ +/* Sidebar: Sidebar — extracted from designbook ui_kits/whynot-control/Chrome.jsx. */ +.wn-sidebar { /* TODO: base appearance per ir/exemplars/Sidebar.html */ } +.wn-sidebar--doc: { /* current=doc: */ } diff --git a/adapters/plain-css/stubs/wn-stage-dot.css b/adapters/plain-css/stubs/wn-stage-dot.css new file mode 100644 index 0000000..e481bac --- /dev/null +++ b/adapters/plain-css/stubs/wn-stage-dot.css @@ -0,0 +1,3 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/StageDot.json — write-once. */ +/* StageDot: StageDot — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ +.wn-stage-dot { /* TODO: base appearance per ir/exemplars/StageDot.html */ } diff --git a/adapters/plain-css/stubs/wn-stamp.css b/adapters/plain-css/stubs/wn-stamp.css new file mode 100644 index 0000000..2f5592c --- /dev/null +++ b/adapters/plain-css/stubs/wn-stamp.css @@ -0,0 +1,3 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/Stamp.json — write-once. */ +/* Stamp: Stamp — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ +.wn-stamp { /* TODO: base appearance per ir/exemplars/Stamp.html */ } diff --git a/adapters/plain-css/stubs/wn-tag.css b/adapters/plain-css/stubs/wn-tag.css new file mode 100644 index 0000000..958ad3a --- /dev/null +++ b/adapters/plain-css/stubs/wn-tag.css @@ -0,0 +1,3 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/Tag.json — write-once. */ +/* Tag: Tag — extracted from designbook ui_kits/whynot-control/Atoms.jsx. */ +.wn-tag { /* TODO: base appearance per ir/exemplars/Tag.html */ } diff --git a/adapters/plain-css/stubs/wn-top-nav.css b/adapters/plain-css/stubs/wn-top-nav.css new file mode 100644 index 0000000..cc2a9e4 --- /dev/null +++ b/adapters/plain-css/stubs/wn-top-nav.css @@ -0,0 +1,3 @@ +/* @generated STUB by adapters/plain-css (T10) from ir/components/TopNav.json — write-once. */ +/* TopNav: TopNav — extracted from designbook ui_kits/whynot-control/Chrome.jsx. */ +.wn-top-nav { /* TODO: base appearance per ir/exemplars/TopNav.html */ } diff --git a/adapters/plain-css/tokens.css b/adapters/plain-css/tokens.css new file mode 100644 index 0000000..9d08898 --- /dev/null +++ b/adapters/plain-css/tokens.css @@ -0,0 +1,91 @@ +/* @generated by adapters/plain-css (WHYNOT-WP-0002 T10) from ir/tokens.json — DO NOT EDIT. */ +:root { + /* color */ + --ink: #0A0A0A; + --ink-2: #1F1F1F; + --ink-3: #5C5C5C; + --ink-4: #8A8A8A; + --ink-5: #B5B5B3; + --line: #E5E5E2; + --line-strong: #C9C9C5; + --line-soft: #F0F0EC; + --paper: #FFFFFF; + --paper-2: #FAFAF7; + --paper-3: #F4F4EF; + --fg-1: var(--ink); + --fg-2: var(--ink-3); + --fg-3: var(--ink-4); + --fg-mute: var(--ink-5); + --fg-on-dark: #FAFAF7; + --bg-1: var(--paper); + --bg-2: var(--paper-2); + --bg-3: var(--paper-3); + --bg-invert: var(--ink); + --border: var(--line); + --border-strong: var(--line-strong); + --border-soft: var(--line-soft); + --hi: #FFE14A; + --hi-2: #FFD400; + --hi-ink: #1A1500; + --status-raw: #B5B5B3; + --status-weak: #8A8A8A; + --status-medium: #5C5C5C; + --status-strong: #0A0A0A; + --status-commercial: #FFD400; + --status-error: #B33A2E; + --status-error-bg: #FCF3F1; + --status-warn: #C28000; + --status-warn-bg: #FFFCEB; + --status-success: #2F6B3A; + --status-success-bg: #F2F7F2; + --status-info: #2E5C8A; + --status-info-bg: #F2F5FA; + /* fontFamily */ + --ff-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --ff-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace; + --ff-serif: ui-serif, Georgia, "Times New Roman", serif; + /* fontSize */ + --fs-xs: 11px; + --fs-sm: 13px; + --fs-base: 15px; + --fs-md: 17px; + --fs-lg: 20px; + --fs-xl: 24px; + --fs-2xl: 32px; + --fs-3xl: 44px; + --fs-4xl: 64px; + --fs-5xl: 96px; + /* lineHeight */ + --lh-tight: 1.05; + --lh-snug: 1.25; + --lh-base: 1.5; + --lh-loose: 1.7; + /* letterSpacing */ + --tr-tight: -0.02em; + --tr-snug: -0.01em; + --tr-base: 0em; + --tr-mono: 0.02em; + --tr-label: 0.08em; + /* space */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 24px; + --sp-6: 32px; + --sp-7: 48px; + --sp-8: 64px; + --sp-9: 96px; + --sp-10: 128px; + /* radius */ + --r-0: 0px; + --r-1: 2px; + --r-2: 4px; + --r-3: 8px; + --r-pill: 999px; + /* shadow */ + --shadow-0: none; + --shadow-1: 0 1px 0 var(--line); + --shadow-2: 0 1px 0 var(--line-strong); + --shadow-3: 0 4px 12px -6px rgba(10,10,10,0.10); +}