@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.
- package/CHANGELOG.md +1063 -0
- package/README.md +31 -14
- package/behaviors/index.d.ts +3 -1
- package/behaviors/index.js +179 -106
- package/classes/index.d.ts +41 -0
- package/classes/index.js +42 -0
- package/classes/vscode.css-custom-data.json +18 -22
- package/css/dataviz.css +96 -0
- package/css/disclosure.css +29 -0
- package/css/dots.css +32 -2
- package/css/feedback.css +53 -0
- package/css/motion.css +88 -0
- package/css/overlay.css +52 -1
- package/css/report.css +382 -0
- package/css/skins.css +54 -0
- package/css/tokens.css +56 -32
- package/dist/bronto.css +1 -1
- package/dist/css/dataviz.css +1 -0
- package/dist/css/disclosure.css +1 -1
- package/dist/css/dots.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/motion.css +1 -1
- package/dist/css/overlay.css +1 -1
- package/dist/css/report.css +1 -0
- package/dist/css/skins.css +1 -0
- package/dist/css/tokens.css +1 -1
- package/docs/adr/0001-color-system.md +271 -0
- package/docs/adr/0002-scope-and-2026-baseline.md +104 -0
- package/docs/adr/0003-theme-model.md +94 -0
- package/docs/contrast.md +170 -51
- package/docs/reference.md +118 -19
- package/docs/reporting.md +270 -0
- package/docs/stability.md +37 -0
- package/docs/theming.md +84 -7
- package/docs/usage.md +67 -8
- package/glyphs/glyphs.d.ts +14 -1
- package/glyphs/glyphs.js +143 -2
- package/llms.txt +101 -5
- package/package.json +79 -5
- package/qwik/index.d.ts +55 -0
- package/qwik/index.js +129 -0
- package/react/index.d.ts +57 -0
- package/react/index.js +84 -0
- package/solid/index.d.ts +57 -0
- package/solid/index.js +85 -0
- package/tokens/charts.d.ts +37 -0
- package/tokens/charts.js +96 -0
- package/tokens/charts.json +61 -0
- package/tokens/index.d.ts +3 -3
- package/tokens/index.js +16 -18
- package/tokens/index.json +32 -36
- package/tokens/resolved.json +39 -41
- package/tokens/skins.d.ts +27 -0
- package/tokens/skins.js +62 -0
- 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.
|