@ponchia/ui 0.3.6 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +1063 -0
  2. package/README.md +31 -14
  3. package/behaviors/index.d.ts +3 -1
  4. package/behaviors/index.js +179 -106
  5. package/classes/index.d.ts +41 -0
  6. package/classes/index.js +42 -0
  7. package/classes/vscode.css-custom-data.json +18 -22
  8. package/css/dataviz.css +96 -0
  9. package/css/disclosure.css +29 -0
  10. package/css/dots.css +32 -2
  11. package/css/feedback.css +53 -0
  12. package/css/motion.css +88 -0
  13. package/css/overlay.css +52 -1
  14. package/css/report.css +382 -0
  15. package/css/skins.css +54 -0
  16. package/css/tokens.css +56 -32
  17. package/dist/bronto.css +1 -1
  18. package/dist/css/dataviz.css +1 -0
  19. package/dist/css/disclosure.css +1 -1
  20. package/dist/css/dots.css +1 -1
  21. package/dist/css/feedback.css +1 -1
  22. package/dist/css/motion.css +1 -1
  23. package/dist/css/overlay.css +1 -1
  24. package/dist/css/report.css +1 -0
  25. package/dist/css/skins.css +1 -0
  26. package/dist/css/tokens.css +1 -1
  27. package/docs/adr/0001-color-system.md +271 -0
  28. package/docs/adr/0002-scope-and-2026-baseline.md +104 -0
  29. package/docs/adr/0003-theme-model.md +94 -0
  30. package/docs/contrast.md +170 -51
  31. package/docs/reference.md +118 -19
  32. package/docs/reporting.md +270 -0
  33. package/docs/stability.md +37 -0
  34. package/docs/theming.md +84 -7
  35. package/docs/usage.md +67 -8
  36. package/glyphs/glyphs.d.ts +14 -1
  37. package/glyphs/glyphs.js +143 -2
  38. package/llms.txt +101 -5
  39. package/package.json +79 -5
  40. package/qwik/index.d.ts +55 -0
  41. package/qwik/index.js +129 -0
  42. package/react/index.d.ts +57 -0
  43. package/react/index.js +84 -0
  44. package/solid/index.d.ts +57 -0
  45. package/solid/index.js +85 -0
  46. package/tokens/charts.d.ts +37 -0
  47. package/tokens/charts.js +96 -0
  48. package/tokens/charts.json +61 -0
  49. package/tokens/index.d.ts +3 -3
  50. package/tokens/index.js +16 -18
  51. package/tokens/index.json +32 -36
  52. package/tokens/resolved.json +39 -41
  53. package/tokens/skins.d.ts +27 -0
  54. package/tokens/skins.js +62 -0
  55. package/tokens/tokens.dtcg.json +22 -34
@@ -0,0 +1,271 @@
1
+ # ADR-0001 — Color system: governed evolution beyond monochrome
2
+
3
+ Status: accepted (all steps 1–8 implemented in 0.4.0) · 2026-05-31 ·
4
+ supersedes the informal "monochrome + one accent" framing in `README.md` /
5
+ `package.json`
6
+
7
+ > This is the **color constitution** for `@ponchia/ui`. It is forward-looking:
8
+ > nothing here changes the default build. It defines the tiers color is
9
+ > allowed to occupy, the rules that keep restraint load-bearing, and the
10
+ > ordered, backward-compatible path to expand expression without diluting the
11
+ > brand. Companion to [`architecture.md`](../architecture.md).
12
+
13
+ ## Context
14
+
15
+ `@ponchia/ui` took its aesthetic from Nothing: monochrome surfaces, one
16
+ rationed red accent, dot-matrix motifs. The driving question for this ADR:
17
+ **is monochrome creatively restrictive, and should we evolve past Nothing's
18
+ literal one-hue constraint?**
19
+
20
+ Two facts frame the answer.
21
+
22
+ **1. We are already not monochrome — and that was the right call.** The README
23
+ says "monochrome, one rationed accent." The CSS says otherwise. We already
24
+ ship a full **functional status palette** (`--success`, `--warning`,
25
+ `--danger`, `--info`, each with light/dark/`-soft` variants, contrast-tuned
26
+ per theme, wired into `ui-alert` / `ui-toast` / `ui-meter`). It is rationed —
27
+ status-only, always paired with a leading dot/shape, never decorative — but it
28
+ exists. So the real model already has **three *active* tiers nobody wrote
29
+ down** (neutral + accent + status); the two new axes this ADR adds (display
30
+ expression, data-viz) are tiers 3–4. The question is not "should we abandon
31
+ monochrome" (we did, carefully, around 0.3.4) but **"what is our color theory
32
+ for the next axis."**
33
+
34
+ **2. Inspiration is not doctrine.** The thing worth inheriting from Nothing is
35
+ the *attitude* — restraint, intentionality, signal-over-noise — **not** the
36
+ literal one-hue limit. "We do not decorate with color; we signal with it" is a
37
+ stronger, more defensible principle than "we have no color," and we are most
38
+ of the way to it already. Monochrome *is* creatively restrictive, but only on
39
+ three specific axes: **categorical distinction** (tags/legends past ~3 items),
40
+ **multi-series data-viz** (one accent + grays cannot draw a 5-line chart — we
41
+ already felt this; `tokens/resolved.json` exists because charts needed real
42
+ color), and **brand/emotional range** (one hue at a time). What monochrome
43
+ *buys* and we must not casually spend: identity differentiation, forced
44
+ hierarchy discipline, accessibility-by-default, and the scarcity that makes the
45
+ one accent *mean* something.
46
+
47
+ ### What SOTA looks like (2025–2026)
48
+
49
+ | Practice | Who | Relevance to us |
50
+ | --- | --- | --- |
51
+ | **Perceptual color space** (equal numeric step = equal perceived step) | Tailwind v4 (OKLCH+P3), Radix 3 (P3), Material 3 (HCT) | `--accent-1..6` now derives via `color-mix(in oklch,…)`; OKLCH is the modern substrate and is **CSS-native** — no dependency cost |
52
+ | **Relative color syntax** `oklch(from var(--base) calc(l-.1) c h)` | CSS Color 5 | ~90% support, Baseline in 2026 — lets a single hue derive a whole tonal ramp natively |
53
+ | **WCAG 2.2 to pass, APCA to design** | Radix text steps spec'd in APCA Lc | WCAG 2.2 AA stays the legal/hard gate; APCA (Lc, accounts for size/weight, WCAG 3 candidate) as an **advisory** track |
54
+ | **Role-bound scale steps** (1–2 bg, 3–5 component, 6–8 border, 9–10 solid, 11–12 text) | Radix 12-step | Our tiers are coarser; the role-bound scale is where depth lives if we ever need it |
55
+ | **Contrast-first generation** (pick a target Lc, derive the swatch) | Adobe Leonardo, Material dynamic | Natural evolution of "one `--accent` derives everything" |
56
+ | **Categorical color is its own discipline** | Wong / IBM / ColorBrewer | Hard limit **6–8 hues**, ≥30 CIELAB lightness delta per pair, **redundant encoding** (shape/pattern/label) — our dot-matrix wheelhouse |
57
+
58
+ SOTA is **not "more hues by default."** It is CSS-native token systems,
59
+ perceptual color, role-based scales, contrast-aware generation, and *governed
60
+ escape hatches*. Our restraint is already SOTA-adjacent; the work is to
61
+ modernize the engine and add opt-in expression without spending the brand.
62
+
63
+ ## Decision
64
+
65
+ Adopt a **five-tier color model** with a fixed governance contract. Color must
66
+ *earn its place by encoding meaning*; the discipline moves from "absence of
67
+ color" to "color is tiered, token-only, contrast-gated, and off-by-default."
68
+
69
+ | Tier | Name | What it is | Default state |
70
+ | --- | --- | --- | --- |
71
+ | **0** | Neutral canvas | The grayscale ramp — surfaces, lines, text | Always on |
72
+ | **1** | Brand accent | One themeable `--accent`, derives `--accent-1..6` | Always on (red) |
73
+ | **2** | Functional status | `success` / `warning` / `danger` / `info` — signal only | Always on |
74
+ | **3** | Display expression | Luminance, dot density/intensity, scan cadence, pulse, reveal timing — the dot-matrix substrate | Opt-in tokens |
75
+ | **4** | Categorical / data-viz | A 6–8 color colorblind-safe scale + sequential/diverging ramps — **charts only** | Opt-in module, never bundled |
76
+
77
+ ### The six rules (the constitution)
78
+
79
+ Each rule notes how it is held — **[gate]** = mechanically enforced by
80
+ `check:color-policy`; **[other gate]** = enforced elsewhere in `npm run check`
81
+ / the e2e suite; **[norm]** = a review norm, not yet machine-checked. Don't
82
+ read "rule" as "all six are CI-enforced" — only what's tagged **[gate]** is.
83
+
84
+ 1. **Token-only.** No raw chromatic color in component CSS — every color is a
85
+ tiered token (grays / `color-mix()` endpoints excepted). **[gate]** —
86
+ `check:color-policy` invariants 1 (tier classification) + 3 (raw-color scan).
87
+ 2. **Contrast-gated.** Every UI text/bg pairing passes **WCAG 2.2 AA**; **APCA
88
+ Lc is advisory** beside the matrix, never the sole gate while WCAG 3 is a
89
+ Working Draft. **[other gate]** — `check:contrast` + the e2e axe suite, not
90
+ `check:color-policy`.
91
+ 3. **Off-by-default.** The default red accent, all token names, and all visual
92
+ baselines remain **unchanged**; the strict-mono purist keeps their exact
93
+ system; everything new is opt-in. **[other gate]** — `check:dist`
94
+ byte-equality + visual baselines.
95
+ 4. **Semantic tier names.** A token's name declares its tier. **Status is never
96
+ decorative**; **chart color is never UI chrome.** No `primary/secondary`.
97
+ **[gate]** for "every color token is tiered" + the reserved `--chart/--cat/
98
+ --data-*` namespace (invariants 1–2); **[norm]** for "status never
99
+ decorative" (needs usage context a static gate can't see).
100
+ 5. **One active UI accent per scope.** No second brand accent. "Duotone" lives
101
+ in Tier 3 (dots/display ornament), not as a second interactive accent.
102
+ **[norm]**.
103
+ 6. **Meaning never by color or motion alone.** Redundant encoding (shape, dot,
104
+ label) is mandatory — satisfies WCAG 1.4.1 / 2.2.2 and is on-brand for the
105
+ dot-matrix. Reduced-motion users must get equivalent information statically.
106
+ **[other gate]** — the e2e axe/`forced-colors`/reduced-motion suite.
107
+
108
+ ### The brand's answer to "decorative color": luminance + time
109
+
110
+ For a *dot-matrix display* system, the authentic expressive channel is not
111
+ hue — it is **brightness and motion**, exactly like a real LED/phosphor panel
112
+ (and we already shipped reveal/pulse in 0.3.6). Tier 3 is the strategic
113
+ substitute for decorative polychrome: it deepens identity instead of diluting
114
+ it, and no competitor in this niche owns it.
115
+
116
+ ### Resolving the `--orange` orphan — RESOLVED (removed, 0.4.0)
117
+
118
+ Audit finding: `--orange` / `--orange-soft` were defined in every token file
119
+ (`tokens/index.js`, `.d.ts`, `tokens.dtcg.json`, `resolved.json`,
120
+ `css/tokens.css`, `reference.md`) but **consumed by no component and documented
121
+ nowhere** — an untiered, dangling hue. Resolution: **removed** (not adopted as
122
+ a categorical seed — there is no data-viz module or demand yet, and a stray
123
+ top-level hue contradicts the tier model). If categorical color lands it ships
124
+ as a governed Tier-4 module (step 7), not a loose token. The new
125
+ `check:color-policy` gate now makes a future untiered hue a hard CI failure.
126
+
127
+ ## Backward-compatibility freeze
128
+
129
+ Hard acceptance criteria for every step below:
130
+
131
+ - **Default rendered output is unchanged** — the red accent defaults, the
132
+ resolved value of every token a component references, and the light/dark
133
+ **visual baselines** are identical. (Removing a token nothing resolves —
134
+ e.g. `--orange` in 0.4.0 — does change `dist/bronto.css` *bytes* but not what
135
+ any selector renders; that is cleanup, not a visual change. The earlier draft
136
+ said "dist bytes unchanged" — the precise invariant is *rendered output*.)
137
+ - Token **names** are not renamed/repurposed (removals of provably-unreferenced
138
+ tokens follow the CONTRIBUTING.md deprecation-policy exception).
139
+ - The existing `--accent-1..6` **token names and roles are stable; exact
140
+ values are visual tuning before 1.0.** The ramp migrated from sRGB to OKLCH in
141
+ `0.4.0` only after `scripts/gen-resolved.mjs` learned
142
+ `color-mix(in oklch, …)` and visual review confirmed no component-rendering
143
+ baseline moved. Future changes to the ramp algorithm require the same
144
+ treatment: resolver support first, computed-style checks, changelog callout,
145
+ and release-note visibility.
146
+
147
+ ## Roadmap (ordered, backward-compatible)
148
+
149
+ 1. **This ADR.** Make the constitution explicit before adding surface. *(done)*
150
+ 2. **`check:color-policy` gate.** Fail on: raw chromatic color in component CSS;
151
+ any untiered palette token; use of the reserved `--chart/--cat/--data-*`
152
+ namespace. Resolve `--orange`. *(done — `scripts/check-color-policy.mjs`,
153
+ wired into `npm run check`; `--orange` removed)*
154
+ 3. **Tier 3 expression tokens.** *(done — `css/dots.css`)* `--dotmatrix-glow`
155
+ (phosphor bloom on lit cells), `--dotmatrix-pulse-min` (the `--pulse` floor),
156
+ `--dotmatrix-reveal-step` (scan cadence); density stays on
157
+ `--dotmatrix-cols/-gap/-dot`. All default to a no-op, so the default render
158
+ is byte-for-byte unchanged; reduced-motion still kills the animations.
159
+ 4. **Optional display colorways** *(done — `tokens/skins.js` →
160
+ `css/skins.css`, exported as `./css/skins.css`, NOT in the default bundle)*:
161
+ `data-bronto-skin="amber-crt | phosphor-green | e-ink"`. **A root-level
162
+ choice, like `data-theme`** (apply on `:root`/`<html>`) — it must live there
163
+ because the accent's derived family (`--accent-strong/-text/-soft`,
164
+ `--bg-accent`, `--field-dot-accent`, `--accent-1..6`) is `color-mix(…
165
+ var(--accent) …)` declared on `:root` and only re-evaluates against the new
166
+ accent on the element that carries it; a skin on a subtree would leave that
167
+ family stale, so the selectors are `:root`-anchored and a subtree skin
168
+ no-ops. Each skin re-points only `--accent` (+ a dark `--dotmatrix-glow`);
169
+ the family + dot-matrix + glyphs follow automatically. Single-hue per skin →
170
+ keeps the one-accent discipline while unlocking range across skins. No
171
+ multi-hue palette. (A future per-subtree skin would need to re-declare the
172
+ derived family — deferred; whole-page is the natural colorway scope.) Skins
173
+ inherit the existing `forced-colors` + `print` behavior in `css/base.css` /
174
+ `css/forms.css` (they touch only the accent, never the canvas).
175
+ 5. **OKLCH for new work first.** *(done for colorways — accents authored in
176
+ OKLCH in `tokens/skins.js`; the contrast tooling now parses `oklch()` →
177
+ sRGB, so skin accents are gated, not eyeballed.)* The core ramp followed in
178
+ step 8 only after resolver and visual checks were in place. For future CSS
179
+ Color 5 work, still upgrade generators/checks first and consider
180
+ `light-dark()` only when it is inside the declared browser floor.
181
+ 6. **APCA advisory reporting.** *(done — `scripts/gen-contrast.mjs`)* APCA-W3
182
+ 0.1.9 `Lc` is computed beside the WCAG ratio for every pairing (core + skins)
183
+ in `docs/contrast.md`. **Advisory only** — WCAG 2.1 AA stays the hard gate.
184
+ 7. **Categorical chart module.** *(done in 0.4.0 — real demand arrived
185
+ (dashboards); `tokens/charts.js` → `css/dataviz.css` + `tokens/charts.json`
186
+ + `tokens/charts.d.ts`, gated by `check:charts`.)* Hybrid accent-led
187
+ (series 1 = `var(--accent)`) + Okabe-Ito colourblind-safe series 2–8 + a
188
+ sequential and a diverging ramp; **dot-matrix `--chart-pattern-*` fills** as
189
+ the mandated second channel. Gated for distinguishability under normal +
190
+ simulated protan/deutan/tritan vision (OKLab ΔE), `var(--chart-*)` forbidden
191
+ in core component CSS (charts-only), never in the default bundle. The
192
+ categorical fills are authored as the Okabe-Ito sRGB hex **verbatim** (a
193
+ CVD-proven *set* — re-deriving per-theme in OKLCH broke its colourblind
194
+ safety, which the gate caught); the ramps are OKLCH. A chart colour's WCAG
195
+ ratio vs the background is reported **advisory** (a fill is not body text;
196
+ the pattern + ΔE gate carry distinguishability).
197
+ 8. **OKLCH `--accent-1..6` ramp migration.** *(done in 0.4.0)* Steps 1–4 mix
198
+ the accent toward `--accent-ramp-end` (white in light, black in dark)
199
+ `in oklch` (perceptually even). The explicit endpoint avoids low-chroma
200
+ background-hue differences between browser engines. `gen-resolved` was
201
+ upgraded first to resolve `color-mix(in oklch,…)` → hex, so the
202
+ contractual `resolved.json`/DTCG never drop the tokens. Verified no rendered
203
+ change: `--accent-1..6` is consumed by **no** component (only JS/chart
204
+ consumers + the theme playground), so no visual baseline moved — the feared
205
+ "breaking + needs baseline review" turned out to touch only token *values*
206
+ (non-contractual under 0.x).
207
+
208
+ ## Risks & mitigations
209
+
210
+ | Risk | Mitigation |
211
+ | --- | --- |
212
+ | **Brand dilution** (secondary accents, decorative hues creep in) | Rule 4/5; `check:color-policy`; off-by-default |
213
+ | **Compat break** from future ramp tuning | Freeze above; resolver + computed-style checks first; release notes always call out value changes |
214
+ | **Tooling drift** (resolver/DTCG/contrast/baseline assume one color space) | Upgrade tooling *before* new colour syntax enters token files; keep computed-style tests |
215
+ | **Chart color leakage** into UI chrome | `check:color-policy` forbids chart tokens in core CSS; docs forbid it |
216
+ | **Motion/status misuse** carrying sole meaning | Rule 6; reduced-motion equivalence; status stays locked, never skin-derived |
217
+ | **APCA overreach** | Advisory only; WCAG 2.2 AA stays the hard gate while WCAG 3 is draft |
218
+
219
+ ## Verification plan
220
+
221
+ - Existing `npm run check`, `npm test`, token/DTCG/resolved/dist/visual/a11y
222
+ gates stay green and **unchanged** for the default build.
223
+ - New `check:color-policy` (raw color, status misuse, chart leakage, untiered
224
+ hue).
225
+ - Contrast matrices extended to: default light/dark, `prefers-contrast`,
226
+ `forced-colors`, **every shipped skin**, and chart tokens if shipped.
227
+ - APCA output as non-blocking generated docs.
228
+ - Playwright computed-style tests for `oklch()` / `color-mix(in oklch,…)`
229
+ before any core reliance.
230
+ - Verify `prefers-reduced-motion`, `prefers-contrast`, `forced-colors`, and
231
+ native `accent-color` for every skin.
232
+ - Confirm optional skin/chart CSS is exported + packed but **excluded** from
233
+ default `dist/bronto.css`.
234
+
235
+ ## Decisions taken (0.4.0)
236
+
237
+ Resolved while implementing steps 3–6 (the open questions the earlier draft
238
+ flagged):
239
+
240
+ - **`--accent-1..6` migrated to OKLCH in 0.4.0.** Token names and roles are
241
+ stable; exact values remain visual tuning before 1.0. The migration shipped
242
+ only after resolver support and visual review confirmed no component baseline
243
+ consumed the changed values.
244
+ - **Colorways ship as a supported package export** (`./css/skins.css`),
245
+ opt-in, excluded from the default bundle. Not docs-only recipes.
246
+ - **Colorways are root-level** (`data-bronto-skin` on `:root`, like
247
+ `data-theme`) — see step 4 for why per-subtree is deferred.
248
+ - **OKLCH is within the existing browser floor** (Chrome 111+/Safari 16.4+/
249
+ Firefox 121+ all support `oklch()`); used for skin accents now. The contrast
250
+ tooling parses it so skins are gated.
251
+ - **Build tooling stays dependency-free.** APCA + the OKLCH→sRGB conversion are
252
+ hand-rolled in `scripts/gen-contrast.mjs` (zero new deps), matching the
253
+ zero-dependency stance.
254
+ - **Data-viz categorical color (step 7) shipped in 0.4.0** — real demand
255
+ arrived (dashboards). Built as a governed Tier-4 module (opt-in, charts-only,
256
+ leakage-gated, CVD-distinguishability-gated). `--cat/--data-*` remain reserved
257
+ alternative-name guards; `--chart-*` is now the live data-viz namespace.
258
+ - **No second UI accent / "duotone" stays Tier-3 ornament only** (rule 5).
259
+
260
+ ## Method
261
+
262
+ Grounded in web research (OKLCH/relative-color/`color-mix` baseline; APCA vs
263
+ WCAG; Radix 12-step; Material 3 HCT; Adobe Leonardo; colorblind-safe
264
+ categorical limits) and a 14-leg AgentMix `deep` multi-POV pass
265
+ (Pi/MiniMax, Aider, OpenCode, Crush, Codex + architecture / implementation-risk
266
+ / testing / security / operations / contrarian legs). Key reconciled dissent:
267
+ several legs warned that an OKLCH ramp swap would be a silent break if tooling
268
+ lagged — accepted as a process constraint, then resolved before 0.4.0 shipped;
269
+ the claim that `--accent-1..6` already serves as a categorical scale was
270
+ rejected (a same-hue ramp encodes sequence/intensity, not independent
271
+ categories); deriving status from skins was rejected (status stays locked).
@@ -0,0 +1,104 @@
1
+ # ADR-0002 — Scope, and the 2026 baseline: what `@ponchia/ui` is and isn't
2
+
3
+ Status: accepted · 2026-06-01 · sets the project's category, browser floor, and
4
+ the line between "ours" and "the consumer's framework"
5
+
6
+ ## Context
7
+
8
+ The 2026 "UI framework" bar is often framed around **application/runtime**
9
+ concerns: signals vs VDOM, resumable/granular hydration, server-driven UI
10
+ streaming, local-first + CRDT state sync, cross-target native compilation
11
+ (SwiftUI/Compose/WebGPU), physics-based interruptible motion, and "neural
12
+ expressive" tactile/haptic, dimensional UI.
13
+
14
+ `@ponchia/ui` is a different species: a **CSS-first design layer** — semantic
15
+ `ui-*` classes in one `@layer`, design tokens as data, and *thin optional*
16
+ behaviors/bindings — that sits **on top of** whatever app framework the
17
+ consumer chooses (ADR-0001 / architecture.md). Conflating the two is a category
18
+ error. This ADR records, against that 2026 bar, what the project deliberately
19
+ provides, pursues, and refuses. Second lens that governs every call: the project
20
+ is currently single-maintainer / single-consumer — **restraint is both the
21
+ brand and the correct strategy**; complexity with no payoff for one user is pure
22
+ cost.
23
+
24
+ ## Decision
25
+
26
+ ### What we ARE (and lean into)
27
+ - **A CSS-first, zero-runtime-dependency design system.** The CSS renders
28
+ server-side with nothing to hydrate; behaviors are optional, SSR-safe,
29
+ delegated progressive enhancement. (This already satisfies the "zero JS until
30
+ interaction" goal for our scope — there is nothing to hydrate.)
31
+ - **AI-/agent-legible by construction.** Plain `ui-*` classes (no component API
32
+ to learn) + `llms.txt` + drift-checked `reference.md` + typed `cls` + DTCG /
33
+ `resolved.json`. An LLM can read and emit our markup today — the "semantic
34
+ rendering" pillar, had, without an SDUI engine.
35
+ - **Accessibility as a gate, and system-signal-adaptive.** CI-enforced WCAG +
36
+ APCA, CVD-gated charts, and live adaptation to `prefers-reduced-motion` /
37
+ `prefers-contrast` / `forced-colors` + `data-density`/`data-contrast`.
38
+ - **Cross-target via tokens, not components.** `resolved.json` / DTCG feed
39
+ canvas/SVG/MapLibre and Figma/Style-Dictionary; the design *language* travels
40
+ even though components don't.
41
+ - **Framework-agnostic, with optional thin React/Solid/Qwik bindings.**
42
+
43
+ ### What we PURSUE (the 2026 leap that fits: CSS-native motion)
44
+ "Motion as a first-class citizen / interruptible / layout morphing" is the one
45
+ manifesto pillar that converges with a CSS-first, zero-dependency identity —
46
+ because the modern platform makes it declarative:
47
+ - **Shipped (0.4.1):** native `<dialog>` (modal/drawer) + backdrop **enter *and*
48
+ exit** transitions via `@starting-style` + `transition-behavior: allow-discrete`
49
+ — zero JS, reduced-motion-aware. (Previously the overlay only animated in.)
50
+ Extended, same approach, to **popover** (both the native `[popover]` top-layer
51
+ and the `.is-open` fallback), **toast** (CSS fade-out on dismiss — the
52
+ behavior adds `.is-leaving` and removes on `transitionend`, falling back to
53
+ instant removal under reduced-motion), and **accordion** auto-height open/close
54
+ via `::details-content` + `interpolate-size: allow-keywords` (gated on
55
+ `@supports selector(::details-content)`; engines without it snap).
56
+ - **Shipped (0.4.1), progressive enhancement:** **scroll-driven** primitives
57
+ (`.ui-scroll-progress`, `.ui-scroll-reveal`) on `scroll()` / `view()`
58
+ timelines, gated on `@supports (animation-timeline: …)` so they degrade to a
59
+ static end-state, and **View Transitions** — a `.ui-vt` `view-transition-name`
60
+ helper, an on-brand `::view-transition-*(root)` cross-fade, and a
61
+ reduced-motion kill-switch for the VT pseudo-tree (which does *not* honour
62
+ reduced-motion on its own). The document-global cross-document opt-in
63
+ (`@view-transition { navigation: auto }`) is documented rather than shipped —
64
+ it can't be layered or scoped, so it stays the consumer's one-liner. No JS
65
+ animation runtime — the platform is the engine.
66
+ - **Next:** track the not-yet-cross-engine features to general availability
67
+ (scroll-driven + `interpolate-size` are Chromium-led today) and continue the
68
+ OKLCH / `light-dark()` / P3 work flagged in ADR-0001.
69
+
70
+ ### Browser floor — deliberately modern
71
+ Floor raised to **Chrome/Edge 125+, Safari 18+, Firefox 129+** (early–mid 2025)
72
+ to build on `@starting-style`, `allow-discrete`, `oklch()`/relative color, and
73
+ `light-dark()` natively. Greenfield stance: **no fallbacks below the floor**;
74
+ not-yet-cross-engine features are enhancement-only. (Acceptable precisely because
75
+ this is new and not yet in production for real products.)
76
+
77
+ ### What is OUT OF SCOPE (permanently — the consumer's framework, or counter-brand)
78
+ - **Reactivity runtime** (signals vs VDOM), **granular/resumable hydration**,
79
+ **server-driven-UI streaming engine** — the consumer's framework (React, Solid,
80
+ Qwik, …). We *compose* with them; we don't compete.
81
+ - **Local-first / CRDTs / IndexedDB / sync** — a data layer, not a design layer.
82
+ Zero payoff for a solo consumer.
83
+ - **Cross-target native compilation** (SwiftUI / Jetpack Compose / WebGPU) — a
84
+ Flutter/RN-class ambition; the token layer already covers the modest
85
+ cross-target need.
86
+ - **Haptics + "vibrant dimensional layering / adaptive lighting / liquid
87
+ glass"** — not merely out of scope but **counter to the thesis** (flat,
88
+ monochrome, anti-skeuomorphic). Our on-brand "dimensional" expression is the
89
+ dot-matrix glow / luminance work (ADR-0001 Tier 3), not Apple-glass.
90
+ - **"Cognitive-load-adaptive" dynamic a11y** — speculative, app-level.
91
+
92
+ ## Consequences
93
+ - New work is judged against this map: if a request belongs to the runtime/data/
94
+ native-compile layer, it's the consumer's, and we document the composition
95
+ instead of building it.
96
+ - The headline forward investment is **CSS-native motion**, progressively
97
+ enhanced — making fluid, interruptible, exit-animated UI the *default* with no
98
+ JS, which is the project's ethos rather than a departure from it.
99
+ - The modern floor is a feature, not a liability, for a greenfield design system.
100
+
101
+ ## Method
102
+ Prompted by a review of the 2026 UI-framework baseline against the actual repo
103
+ (no reactivity/hydration/state code exists — by design) and the single-consumer
104
+ reality. Grounded in ADR-0001 (color) and architecture.md (CSS-is-the-framework).
@@ -0,0 +1,94 @@
1
+ # ADR-0003 — The theme model: a binary base axis, not a named-theme catalog
2
+
3
+ Status: accepted · 2026-06-01 · records why theming is light/dark + derivation +
4
+ orthogonal axes, and the readability re-tune of the dark base. Builds on
5
+ [ADR-0001](0001-color-system.md) (the color tier model) and
6
+ [ADR-0002](0002-scope-and-2026-baseline.md) (scope + restraint).
7
+
8
+ ## Context
9
+
10
+ Recurring question: "isn't a simple dark/light theme outdated — should we ship
11
+ *named themes* instead?" And a real complaint: the dark theme was **hard to
12
+ read**.
13
+
14
+ Two things had to be separated: the **theme architecture** (is binary
15
+ dark/light the right model?) and the **dark palette tuning** (is our dark
16
+ actually good?). They have different answers.
17
+
18
+ ## Decision
19
+
20
+ ### The theme model is a binary base axis + derivation + orthogonal axes — and that is the modern shape, not the dated one
21
+
22
+ `@ponchia/ui` themes along **one binary base axis** (light/dark, honouring
23
+ `prefers-color-scheme`, overridable with `[data-theme]`), with everything else
24
+ *derived* or carried on *orthogonal axes*:
25
+
26
+ - **Base:** light / dark (`css/tokens.css`).
27
+ - **Brand (derivation):** one `--accent` knob → the whole accent family via
28
+ OKLCH `color-mix()` (ADR-0001). Material-3-style single-seed derivation, in
29
+ CSS, with zero runtime.
30
+ - **Colorway:** `data-bronto-skin` (amber-crt / phosphor-green / e-ink) —
31
+ re-points the one accent; opt-in `skins.css`.
32
+ - **Surface:** `data-surface="oled"` — true-black variant of the dark base
33
+ (see below).
34
+ - **Contrast:** `data-contrast="high"` + `prefers-contrast`.
35
+ - **Density:** `data-density="compact|comfortable"`.
36
+
37
+ This is the pattern the leaders converged on (Material 3 tonal + contrast axis;
38
+ GitHub Primer's base × {dimmed, high-contrast, colorblind}; Radix light/dark
39
+ pairs; Tailwind v4 CSS-var tokens; Apple appearance × Increase-Contrast × accent).
40
+ A **flat named-theme catalog** (IBM Carbon's white/g10/g90/g100) is the *older*
41
+ model — and for a single-consumer project (ADR-0002) every named theme is another
42
+ contrast matrix to gate, more DTCG surface, more baselines, for one user who
43
+ picks one look. **We reject the named-theme catalog**: composable axes are more
44
+ powerful and cheaper. A new base palette is added only when a concrete need
45
+ appears (the bar ADR-0001 used for the data-viz module).
46
+
47
+ ### The dark base is re-tuned for readability (and pure black becomes opt-in)
48
+
49
+ The old dark base was **pure `#000` + `--text #f2f2f2`**. By WCAG 2.1 every
50
+ pairing "passed" (18:1), but WCAG 2.x **over-rates dark-mode contrast** (its
51
+ symmetric ratio saturates near black). The APCA column we already compute
52
+ (advisory) told the truth:
53
+
54
+ - `--text` on `--bg`: WCAG 18.76:1 but **APCA Lc 99** — over-driven → *halation*
55
+ on pure black.
56
+ - `--text-dim` on dark surfaces: WCAG 4.7–5.7:1 ("pass") but **APCA Lc 36–37** —
57
+ below the perceptual floor for readable meta text. **This was the "hard to
58
+ read."**
59
+ - Surfaces `#000→#0c0c0c`: an imperceptible elevation step → cards melted into
60
+ the page.
61
+
62
+ Re-tuned dark neutral ramp: base lifted to an elevated near-black
63
+ (`--bg #121212`, panels `#1c1c1c`/`#222`/`#242424`, lines `#383838`/`#555`),
64
+ body text eased to `#e6e6e6` (Lc 99 → ~91, halation gone), and **`--text-dim`
65
+ raised to `#a0a0a0` (Lc 36 → ~48–50)** — the change users feel. Everything still
66
+ clears WCAG AA 4.5:1; accent/status are unchanged. The true-black "Nothing" look
67
+ is preserved as the opt-in **`data-surface="oled"`** (OLED power-saving + brand),
68
+ a CSS-only preset like `data-density`/`data-contrast`.
69
+
70
+ APCA is promoted from purely advisory to a **tracked, non-failing warning** on
71
+ dark text in `check-contrast.mjs` (WCAG stays the hard gate; a hard APCA gate
72
+ remains an open ADR-0001 decision while WCAG 3 is a Working Draft). This is the
73
+ early-warning that the Lc 36 regression would have tripped.
74
+
75
+ ## Consequences
76
+
77
+ - Re-tuning dark token *values* changes the default rendered output in dark mode
78
+ → the dark Playwright visual baselines must be regenerated in the pinned Linux
79
+ container (`visual-baselines.yml`). Kept in 0.4.1 per the maintainer's call.
80
+ - The dark palette is written in three places (the `@media` block, the
81
+ `[data-theme]` block, and the `cssVars.dark` JS mirror), guarded by
82
+ `check:tokens`. Any dark edit is a three-way edit; deduping by generating
83
+ `tokens.css` from the JS model is a tracked follow-up.
84
+ - `data-surface` joins `data-theme`/`data-bronto-skin`/`data-density`/
85
+ `data-contrast` as a documented root-level attribute (theming.md). The kitchen
86
+ sink ships a unified picker (theme × colorway × surface) so the full axis set
87
+ is selectable and visible.
88
+
89
+ ## Method
90
+
91
+ Prompted by the maintainer's question and the "dark is hard to read" report,
92
+ analysed with the project's own APCA data plus a design-systems landscape review
93
+ (Material 3 / Primer / Radix / Tailwind v4 / Apple / Carbon). The fix is palette
94
+ tuning + recording the axis decision, not a re-architecture.