@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,1063 @@
1
+ # Changelog
2
+
3
+ > **Versioning:** pre-1.0, breaking changes ship in the _minor_. Pin
4
+ > `~0.x`; `^0.x` does **not** protect you. See README → Versioning, and
5
+ > the deprecation policy in CONTRIBUTING.md.
6
+
7
+ ## 0.4.1 — 2026-06-01
8
+
9
+ Patch hardening for the public framework surface, plus the first step of the
10
+ modern-platform motion direction (see [ADR-0002](docs/adr/0002-scope-and-2026-baseline.md)).
11
+
12
+ ### Added
13
+
14
+ - **Static report kit — `@ponchia/ui/css/report.css` + `docs/reporting.md`.**
15
+ An opt-in, PDF-first report layer for LLM-authored and hand-authored HTML:
16
+ report covers, headers, section numbering, summaries, findings, evidence
17
+ blocks, source/appendix/footnote blocks, chart wrappers/legends/fallback
18
+ tables, and print utilities (`ui-print-only`, `ui-screen-only`,
19
+ `ui-break-before`, `ui-break-after`, `ui-keep`, `ui-print-exact`). It stays
20
+ out of the default bundle, ships with the offline LLM docs, and is covered by
21
+ a report fixture, package/export checks, and class-contract validation.
22
+ The layer also includes compact covers, unnumbered report sections, simple
23
+ static chart-bar primitives, and evidence-table framing rules so generated
24
+ reports need less private CSS.
25
+ - **Zero-JS enter _and_ exit motion for native-`<dialog>` overlays.** Modal and
26
+ drawer (and their backdrop) now fade/scale **both ways** via `@starting-style`
27
+ + `transition-behavior: allow-discrete` — previously they only animated in and
28
+ vanished on close. Pure CSS, reduced-motion-aware (snaps with no flash), scoped
29
+ to `dialog.ui-modal` so the controlled `.is-open` path is unchanged.
30
+ - **Enter/exit motion extended to popover, toast, and accordion** (ADR-0002
31
+ "next, same approach"):
32
+ - **Popover** (`.ui-popover`) fades + slides both ways via the same
33
+ `@starting-style` + `allow-discrete` recipe, covering both the native
34
+ `[popover]` top-layer path and the `.is-open` fallback. Zero JS,
35
+ reduced-motion-aware.
36
+ - **Toast** (`.ui-toast`) now plays a CSS fade-out on dismiss instead of being
37
+ yanked from the DOM. The `toast()` behavior adds `.is-leaving` and removes
38
+ the node on `transitionend` (with a timeout fallback); it falls back to
39
+ instant removal under reduced-motion or where no transition is computed, so
40
+ the persistent `aria-live` region is undisturbed.
41
+ - **Accordion** (`.ui-accordion`, native `<details>`) animates auto-height
42
+ open/close via `::details-content` + `interpolate-size: allow-keywords` +
43
+ `content-visibility … allow-discrete`. Strict progressive enhancement —
44
+ gated on `@supports selector(::details-content)`; engines without it (today,
45
+ Firefox/Safari) simply snap, exactly as before.
46
+ - **Scroll-driven motion (progressive enhancement).** `.ui-scroll-progress` (a
47
+ reading-progress bar on a `scroll(root block)` timeline, RTL-aware) and
48
+ `.ui-scroll-reveal` (a JS-free, IntersectionObserver-free reveal on a `view()`
49
+ timeline). Both are gated on `@supports (animation-timeline: …)` and
50
+ `prefers-reduced-motion: no-preference`, so engines without scroll timelines
51
+ (today, Firefox/Safari) keep a static end-state and reduced-motion users get
52
+ no movement.
53
+ - **View Transitions (progressive enhancement).** A `.ui-vt` helper
54
+ (`view-transition-name: var(--ui-vt-name)`) to morph an element across a
55
+ same-document `startViewTransition()` or a cross-document navigation, an
56
+ on-brand default for the `::view-transition-*(root)` cross-fade, and a
57
+ **reduced-motion kill-switch** for the `::view-transition-*` pseudo-tree
58
+ (which the platform does *not* quiet automatically). Cross-document nav stays
59
+ a documented one-liner you add yourself (`@view-transition { navigation: auto }`
60
+ is document-global, so it can't be layered or scoped by the framework).
61
+ - **Optional Qwik bindings — `@ponchia/ui/qwik`.** Same thin-adapter shape as
62
+ the React/Solid bindings (`useDialog`, `useToast`, … `useBrontoBehavior`, plus
63
+ the `cls`/`ui`/`cx` + `applyStoredTheme` re-exports), wrapping the SSR-safe
64
+ behaviors in Qwik's `useVisibleTask$` (run on visible, cleanup on dispose) so a
65
+ resumable page stays zero-JS until interaction. Scope a behavior with a Qwik
66
+ signal: `useDialog({ root: useSignal() })`. `@builder.io/qwik` is an **optional**
67
+ peer dependency, so the core stays zero-dependency. New `examples/qwik-vite`
68
+ builds it through the real Qwik optimizer.
69
+ - **OLED true-black surface variant — `data-surface="oled"`.** The dark base is
70
+ now a readable elevated near-black (see Changed); this opt-in root attribute
71
+ restores pure black for OLED power-saving and the original "Nothing" look.
72
+ CSS-only preset (like `data-density`/`data-contrast`), scoped to the dark
73
+ theme. Documented in `docs/theming.md`.
74
+ - **APCA advisory for dark text.** `check:contrast` now emits a non-failing
75
+ warning when a dark text pairing falls below its perceptual APCA target (WCAG
76
+ stays the hard gate) — the early-warning that would have caught the illegible
77
+ dim text. The kitchen-sink demo gains a unified theme picker (theme × colorway
78
+ × surface, all persisted).
79
+ - **[ADR-0003](docs/adr/0003-theme-model.md)** records the theme model: a binary
80
+ light/dark base × one-knob derivation × orthogonal axes (colorway, surface,
81
+ contrast, density), and why a flat named-theme catalog is rejected.
82
+
83
+ ### Changed
84
+
85
+ - **Dark theme re-tuned for readability.** The dark base moved off pure `#000`
86
+ to an elevated near-black (`--bg #121212`, panels `#1c1c1c`/`#222`/`#242424`,
87
+ lines `#383838`/`#555`); body text eased `#f2f2f2 → #e6e6e6` (APCA Lc 99 → ~91,
88
+ removing halation) and **dim/meta text raised `#858585 → #a0a0a0`** (APCA
89
+ Lc ~36 → ~49 — the actual "hard to read" fix). WCAG 2.x over-rates contrast on
90
+ pure black, so pairings "passed" while reading poorly; the re-tune clears WCAG
91
+ AA on every pairing and lifts perceptual (APCA) contrast. Accent and status
92
+ colours are unchanged; true black stays available via `data-surface="oled"`.
93
+ - **Browser floor raised to Chrome/Edge 125+, Safari 18+, Firefox 129+**
94
+ (early–mid 2025). A deliberate greenfield stance (ADR-0002) so the framework
95
+ can build natively on `@starting-style`, `transition-behavior: allow-discrete`,
96
+ `oklch()`/relative color, and `light-dark()`. No fallbacks ship below the
97
+ floor; not-yet-cross-engine features (View Transitions, scroll-driven
98
+ animations) are enhancement-only and degrade to a static end-state.
99
+ - Bundle budget nudged for the new motion: gzip 13.0 → 13.5 kB (for the dialog
100
+ enter/exit work) and raw 76 → 77 kB (for the popover/toast/accordion motion
101
+ plus the scroll-driven + view-transition CSS). Gzip held at ~13.1 kB — it
102
+ compresses well — so the compressed payload still has headroom.
103
+
104
+ ### Fixed
105
+
106
+ - React and Solid bindings now resolve scoped roots on mount, so `{ root: ref }`
107
+ and resolver callbacks work after framework refs are assigned. Nullish resolver
108
+ results normalize to default behavior instead of crashing destructuring
109
+ behavior initializers.
110
+ - Scoped behavior roots now resolve controlled ids root-first, then
111
+ document-wide. This keeps existing body/portal-mounted dialogs, popovers, and
112
+ disclosure panels working while preventing earlier duplicate ids outside an
113
+ island from shadowing the in-root target.
114
+ - `data-bronto-dismiss="<selector>"` ignores malformed selectors instead of
115
+ throwing during event handling.
116
+ - The one-node glyph mask path now includes a WebKit-prefixed mask declaration,
117
+ and the OKLCH accent ramp uses an explicit white/black neutral endpoint for
118
+ cross-engine browser parity.
119
+
120
+ ### Added
121
+
122
+ - React and Solid Vite examples, CI/release matrix coverage for those examples,
123
+ runtime binding tests, public API stability docs, a release runbook, and
124
+ `npm run size:report`.
125
+
126
+ ## 0.4.0 — 2026-05-31
127
+
128
+ The color-system release — [ADR-0001](docs/adr/0001-color-system.md) steps 1–8.
129
+ A governed evolution beyond pure monochrome: the tier model is written down and
130
+ **enforced** (`check:color-policy`), and the "Nothing" look is proven to be a
131
+ _skin, not the architecture_ — opt-in **colorways** (amber CRT · phosphor green ·
132
+ e-ink), a **data-viz palette** for dashboards (colourblind-safe, gated under
133
+ simulated protan/deutan/tritan vision), Tier-3 dot-matrix display tokens, OKLCH
134
+ authoring, and an APCA advisory contrast track all ship. Plus a reframed README
135
+ + a rendered docs site, and the curated CHANGELOG is now the GitHub Release body.
136
+
137
+ **The default build is unchanged** — the red accent, every retained token name,
138
+ and both theme palettes render identically; colorways and data-viz are opt-in
139
+ entrypoints, never in the default bundle. The only breaking change is the
140
+ removal of the orphan `--orange` token (undocumented, unused) — see below.
141
+
142
+ **BREAKING (orphan token removed)** — **`--orange` / `--orange-soft`** are
143
+ removed. They were defined in every token mirror (`css/tokens.css`,
144
+ `tokens/index.js`, `tokens.dtcg.json`, `resolved.json`, `index.d.ts`,
145
+ `reference.md`) but **referenced by no component and documented nowhere**, and
146
+ untiered under the new color model. As a provably-unreferenced token this is
147
+ removed in a single minor under the CONTRIBUTING.md deprecation-policy
148
+ exception (no working call-site to wind down). Removed rather than adopted; if
149
+ categorical color lands later it ships as a governed, opt-in data-viz module
150
+ (ADR-0001 tier 4), not a stray top-level token. _Migration:_ if you referenced
151
+ `--orange`/`--orange-soft`, define it yourself in a consumer override.
152
+
153
+ ### Added
154
+
155
+ - **The `--accent-1..6` ramp is now perceptually even (OKLCH).** Steps 1–4 mix
156
+ the accent `in oklch` instead of using the old sRGB ramp (ADR-0001 step 8), so
157
+ the ramp reads as evenly-spaced. `scripts/gen-resolved.mjs` learned to resolve
158
+ `color-mix(in oklch,…)` → hex with the same one-channel tolerance browsers
159
+ show, so `tokens/resolved.json`, the DTCG export, and `docs/reference.md` all
160
+ carry the new values. These are
161
+ token **values** (non-contractual under the 0.x policy) and the ramp is not
162
+ consumed by any shipped component, so there is **no change to any component's
163
+ rendering** — only consumers using `var(--accent-1..4)` directly see the
164
+ (intended) shift.
165
+ - **Framework bindings — `@ponchia/ui/react` + `@ponchia/ui/solid`.** Optional,
166
+ thin hooks over the SSR-safe `init*` behaviors (run on mount, clean up on
167
+ unmount/dispose): `useDialog`, `useTabs`, `useMenu`, `useCombobox`,
168
+ `usePopover`, `useDisclosure`, `useFormValidation`, `useTableSort`,
169
+ `useCarousel`, `useDismissible`, `useThemeToggle`, `useDotGlyph`,
170
+ `useToast()`, and the generic `useBrontoBehavior(init, opts)`; `cls`/`ui`/`cx`
171
+ re-exported for one-import DX. `react` / `solid-js` are **optional peer
172
+ dependencies** — the core stays zero-runtime-dependency. Thin adapters over
173
+ the canonical CSS/behaviors layer, not a component library (architecture ADR).
174
+ - **Glyphs: a one-node icon-at-scale render path + a `.ui-icon` wrapper + 5
175
+ circle-family glyphs.** `renderGlyph(name, { render: 'mask', size })` now
176
+ returns a **single** `.ui-icon` element masked by the glyph bitmap (one DOM
177
+ node instead of 256 cells) — for an icon in every table row. It scales with
178
+ the text (`--icon-size`, default `1em`) and inherits `currentColor`. New
179
+ `.ui-icon` CSS primitive (`cls.icon`) drives it. Vocabulary grows to 48 with
180
+ `circle`, `check-circle`, `x-circle`, `plus-circle`, `minus-circle`.
181
+ - **Data-viz colour module — `@ponchia/ui/css/dataviz.css` + `@ponchia/ui/charts.json`.**
182
+ An opt-in Tier-4 chart palette for dashboards (ADR-0001 step 7). **Hybrid
183
+ accent-led**: series 1 is the live `var(--accent)` (the brand stays series 1);
184
+ series 2–8 are the Okabe-Ito colourblind-safe set; plus a sequential ramp
185
+ (heatmaps) and a diverging ramp (gains/losses). **Colour is never the sole
186
+ signal** — each series ships a matching `--chart-pattern-*` dot-matrix fill
187
+ (WCAG 1.4.1). The categorical palette is **gated for distinguishability under
188
+ normal + simulated protan/deutan/tritan vision** (`check:charts`, OKLab ΔE),
189
+ not just eyeballed. Resolved hex per theme in `tokens/charts.json` for
190
+ canvas/SVG/charting libs; typed `ChartTokenName` (`./charts`). **Charts-only,
191
+ never UI chrome** (`check:color-policy` fails on `var(--chart-*)` in core CSS)
192
+ and **never in the default bundle** (separate entrypoint). Sourced from
193
+ `tokens/charts.js` (generated → drift-checked by `check:charts`).
194
+ - **Display colorways — `@ponchia/ui/css/skins.css`.** Opt-in
195
+ `data-bronto-skin="amber-crt | phosphor-green | e-ink"`, a **root-level**
196
+ choice like `data-theme` (apply on `<html>`). Each re-points the one accent
197
+ to a different single hue — the derived accent family, focus ring, dot-matrix
198
+ and glyphs follow automatically; status colours and the neutral canvas are
199
+ untouched. Authored in **OKLCH**, per-theme (a dark + light accent, like the
200
+ core red), and **every skin accent is contrast-gated** to the same WCAG AA /
201
+ 3:1 floors as the core (see `docs/contrast.md`). Shipped as a **separate
202
+ entrypoint, never in the default `dist/bronto.css`** — zero cost unless you
203
+ import it. Sourced from `tokens/skins.js` (generated → drift-checked by
204
+ `check:skins`).
205
+ - **Tier-3 dot-matrix "display expression" tokens** (`css/dots.css`):
206
+ `--dotmatrix-glow` (phosphor bloom on lit cells), `--dotmatrix-pulse-min`
207
+ (the `--pulse` floor), documented `--dotmatrix-reveal-step` (scan cadence).
208
+ All default to a **no-op**, so the default render is unchanged; the phosphor
209
+ colorways use the glow in dark.
210
+ - **APCA advisory contrast** in `docs/contrast.md`. Every pairing (core **and**
211
+ every colorway) now shows an APCA-W3 `Lc` column beside the WCAG ratio — a
212
+ perceptual cross-check. **Advisory only**: WCAG 2.1 AA stays the hard gate
213
+ (`check:contrast`). Implemented dependency-free, alongside an OKLCH→sRGB
214
+ converter so OKLCH-authored values can be measured.
215
+ - **`check:color-policy` gate** (`scripts/check-color-policy.mjs`, wired into
216
+ `npm run check`). Enforces the color constitution: every color-defining token
217
+ (across `global`/`light`/`dark`) must be classified into a tier (this is what
218
+ would have caught `--orange`); the `--chart-*` / `--cat-*` / `--data-*`
219
+ data-viz namespace is reserved; and component CSS may not use raw chromatic
220
+ color (only tiered tokens, with neutral grays / `color-mix()` endpoints).
221
+ - **`check:skins` gate** — `css/skins.css` can't drift from `tokens/skins.js`,
222
+ every skin defines `--accent`, and colorways stay out of the default bundle.
223
+ - **[ADR-0001 — Color system](docs/adr/0001-color-system.md)** — the five-tier
224
+ color constitution and the backward-compatible roadmap. Steps 1–8 are
225
+ implemented in this release: gate + colorways + Tier-3 tokens + OKLCH for new
226
+ work + APCA advisory + data-viz + OKLCH core accent ramp.
227
+
228
+ ## 0.3.6 — 2026-05-31
229
+
230
+ Display glyphs: a small `@ponchia/ui/glyphs` subpath of dot-matrix bitmaps
231
+ rendered on the existing `.ui-dotmatrix` primitive — no SVG, no icon font,
232
+ re-skinned by the same `--field-dot*` tokens. All additive — a new optional
233
+ JS subpath plus one backward-compatible CSS var → a patch under the 0.x
234
+ policy.
235
+
236
+ ### Added
237
+
238
+ - **`@ponchia/ui/glyphs` — a 43-glyph dot-matrix display-icon set.** A frozen
239
+ 16×16 bitmap registry (`GLYPHS`, `GLYPH_NAMES`, `GLYPH_SIZE`) with `glyph()`,
240
+ `glyphCells()`, and `renderGlyph(name, { label, grid, solid, anim, dot, gap })`
241
+ — an SSR-safe HTML string, decorative (`aria-hidden`) by default and
242
+ `role="img"` when labelled. Covers navigation (`arrow-*`, `chevron-*`),
243
+ actions (`check`, `close`, `plus`, `minus`, `search`, `menu`, `gear`,
244
+ `edit`, `trash`, `download`, `upload`, `link`, `refresh`), media (`play`,
245
+ `pause`), state (`eye`, `eye-off`), theming (`sun`, `moon`), objects
246
+ (`mail`, `file`, `folder`, `clock`, `home`, `user`, `heart`, `star`,
247
+ `bell`, `lock`, `info`, `warning`), layout (`grid`, `more-vertical`,
248
+ `more-horizontal`) and `spark` (the two-tone accent demo). Three knobs from
249
+ one source: the default dot look for **display** sizes; **`solid: true`**
250
+ (or `data-bronto-glyph-solid`) which fuses the cells into a square, gapless
251
+ pixel glyph legible as an **inline icon down to ~16px**; and opt-in
252
+ **`anim`** (`reveal` powers the cells on in a scan, `pulse` makes the glyph
253
+ breathe) — decorative only, disabled under `prefers-reduced-motion`, with
254
+ the meaning kept in the static frame + label. `renderGlyph` returns a
255
+ `<span>` (valid inline / inside a `<button>`) and accepts any string for
256
+ dynamic dispatch (`GlyphNameInput = GlyphName | (string & {})`) — unknown
257
+ names hit the documented `''`/`[]`/`undefined` fallback — while the
258
+ `GlyphName` union itself stays strict (typos in annotations are errors). The
259
+ union is generated and CI-drift-checked from the runtime, like the
260
+ `cls`/token maps.
261
+ - **`initDotGlyph()` behavior.** Expands `[data-bronto-glyph]` placeholders
262
+ into a `.ui-dotmatrix` grid in place (optional `data-bronto-glyph-label`),
263
+ idempotent, with a cleanup that fully reverts — the DOM counterpart to
264
+ `renderGlyph`.
265
+
266
+ ### Changed
267
+
268
+ - **`.ui-dotmatrix` gains `--dotmatrix-dot` and `--dotmatrix-dot-radius`
269
+ knobs** — the former for intrinsic dot sizing (`grid-template-columns`
270
+ falls back to the previous `minmax(0, 1fr)` when unset), the latter to
271
+ square off the cells (`--dotmatrix-dot-radius: 0`) for the solid pixel-glyph
272
+ look. Both default to the prior behaviour, so existing matrices are
273
+ unchanged. Adds opt-in `ui-dotmatrix--reveal` / `ui-dotmatrix--pulse`
274
+ animation modifiers (`cls.dotmatrixReveal` / `cls.dotmatrixPulse`), both
275
+ reduced-motion-aware; `--dotmatrix-reveal-step` tunes the reveal scan speed
276
+ (per-cell delay, default `3ms`).
277
+
278
+ ## 0.3.5 — 2026-05-29
279
+
280
+ A consumer-evidence pass: six small primitives that adopters were
281
+ repeatedly hand-rolling, added as pure CSS in
282
+ `@layer bronto` reusing existing tokens. All additive — no new token, no
283
+ new gated contrast pairing, no breaking change → a patch under the 0.x
284
+ policy.
285
+
286
+ ### Added
287
+
288
+ - **`ui-pagehead` — the in-page title bar.** `ui-pagehead` +
289
+ `__title` + `__actions`: eyebrow/breadcrumb + title on the start,
290
+ an actions cluster on the end, one hairline beneath. Composes the
291
+ existing `ui-eyebrow` / `ui-breadcrumb` rather than duplicating them.
292
+ Parts-only (no recipe), like `ui-panel`.
293
+ - **`ui-steps` — a stepper for multi-step flows.** `ui-steps` (an `<ol>`)
294
+ + `__item` + `__item--done`, auto-numbered by CSS counter with hairline
295
+ connectors. State is ARIA-driven: the active step is `aria-current="step"`
296
+ (no class); completed steps take `--done`.
297
+ - **`ui-timeline` — a vertical event list.** `ui-timeline` (an `<ol>`) +
298
+ `__item` + `__time`, on a hairline spine with a per-item marker dot;
299
+ `aria-current` marks the live event (accent marker).
300
+ - **`ui-meter` — a labelled proportion / threshold bar.** `ui-meter` +
301
+ `__fill` + tone modifiers `--accent` / `--success` / `--warning` /
302
+ `--danger`, with a `ui.meter({ tone })` recipe + `MeterOpts`. Distinct
303
+ from `ui-progress` (task progress, can be indeterminate): a meter shows
304
+ a measured static value (coverage, capacity, a KPI). Width is the same
305
+ shared `--value` knob progress uses; author `role="meter"` +
306
+ `aria-valuenow/min/max`.
307
+ - **`ui-kbd` — an inline keyboard-key glyph.** Wrap a `<kbd>`; one per key
308
+ for a shortcut.
309
+ - **`ui-input-icon` — a leading/trailing icon *inside* a control.**
310
+ `ui-input-icon` + `__icon` + `--end`, with a `ui.inputIcon({ end })`
311
+ recipe + `InputIconOpts`. Distinct from `ui-input-group` (an *adjacent*
312
+ affix): the icon overlays inside one `.ui-input`, which keeps full width
313
+ and gains padding on the icon side. Closes the "no framework primitive"
314
+ gap consumers were filling with a bespoke absolute overlay.
315
+ - **`ui-carousel` + `initCarousel` — an image gallery / carousel, and a
316
+ lightbox built on it.** `ui-carousel` (`__stage` / `__viewport` /
317
+ `__slide` / `__prev` / `__next` / `__thumbs` / `__thumb` / `__status`)
318
+ is a **scroll-snap** track, so touch + trackpad swipe (with momentum)
319
+ are the browser's, not hand-rolled. `initCarousel` adds prev/next,
320
+ keyboard (Arrow/Home/End), a thumbnail strip with `aria-current` sync,
321
+ the position counter, and carousel ARIA, keeping a JS index in sync with
322
+ the scroll position both ways (`IntersectionObserver` where available);
323
+ `data-bronto-carousel-loop` wraps; it emits `bronto:change`
324
+ (`{ index }`). A **full-screen lightbox** is the same markup inside a
325
+ native `<dialog class="ui-lightbox">` (+ `ui-lightbox__close`) opened by
326
+ the existing `initDialog` — so the top layer, focus-trap, Escape and
327
+ focus-return come from `<dialog>` with zero new focus code. Closes the
328
+ gallery/lightbox/carousel gap consumers were filling by hand (one
329
+ adopter had shipped three overlapping hand-rolled versions). SSR-safe,
330
+ idempotent, returns a cleanup; declared in `behaviors/index.d.ts` and
331
+ demoed.
332
+ - Demo gains a **"0.3.5 additions"** kitchen-sink section; `docs/usage.md`
333
+ gains meter-vs-progress and steps/timeline/kbd/input-icon guidance; the
334
+ generated `docs/reference.md`, `classes/index.d.ts`,
335
+ `classes/vscode.css-custom-data.json` and `dist/*` pick the new surface
336
+ up automatically.
337
+
338
+ ### Changed
339
+
340
+ - **Bundle gzip budget 12 kB → 13 kB** (raw cap unchanged at 76 kB). The
341
+ six new primitives plus the carousel/lightbox grew `dist/bronto.css` to
342
+ ~73 kB raw / ~12.7 kB gzip (against the 76 kB / 13 kB caps); the gzip cap
343
+ is recalibrated per the bump-deliberately rule in
344
+ `scripts/build-dist.mjs`.
345
+
346
+ ### Packaging & docs
347
+
348
+ - **`fonts/OFL.txt`** — the bundled Doto font is now shipped with its SIL
349
+ Open Font License 1.1 text and attribution (© 2024 The Doto Project
350
+ Authors), as required to redistribute it; README gained a font-license
351
+ note. No code change.
352
+ - **README rewritten** for the npm package page (de-duplicated, accurate
353
+ install/quick-start/theming, absolute links); `package.json` gained a
354
+ compelling `description` + `keywords`; `ROADMAP.md` reconciled to 0.3.5.
355
+ - **Visual regression reworked** to component-scoped per-section snapshots
356
+ (`data-shot` slugs, auto-discovered) so adding a primitive no longer
357
+ drifts every baseline; the `visual-baselines` workflow's change-gate now
358
+ detects brand-new (untracked) baselines.
359
+
360
+ ## 0.3.4 — 2026-05-17
361
+
362
+ A review-driven accessibility + adoption pass (three external reviews →
363
+ independent Opus review → AgentMix deep multi-POV review). All additive:
364
+ new gated artifact, two new shipped docs, one new token. No breaking
365
+ change → a patch under the 0.x policy.
366
+
367
+ ### Added
368
+
369
+ - **`docs/contrast.md` — a published, CI-gated WCAG 2.1 contrast
370
+ matrix.** Every contractual token pairing, the conformance level it is
371
+ *held to*, and its measured sRGB ratio per theme. Generated from the
372
+ resolved token model (`scripts/gen-contrast.mjs`) so it cannot drift
373
+ from the palette, and **gated**: the new `check:contrast` fails
374
+ `npm run check` (and so release) if any gated pairing drops below its
375
+ floor. Text pairings are held to AA 4.5:1; non-text UI to 3:1;
376
+ decorative hairlines are reported but WCAG-1.4.11-exempt by design
377
+ (the low ratio is published, not hidden). New `exports`/`files`
378
+ entry; indexed in `llms.txt` + README.
379
+ - **`docs/usage.md` — the decision guide.** When to use which primitive
380
+ (badge vs chip vs status dot), the unset density default and its two
381
+ presets, prose-in-card, when to reach for a behavior. Hand-written and
382
+ stable (contract, like `theming.md`); ships in the tarball for offline
383
+ agents/consumers.
384
+ - **`info` status tone — token wired through the full status family.**
385
+ New `--info` / `--info-soft` tokens (+ `--bronto-color-info` semantic
386
+ alias), dual-theme, **and** the consumers that make it real:
387
+ `ui-badge--info`, `ui-alert--info`, `ui-toast--info`, `ui-dot--info`,
388
+ with `ui.badge`/`ui.alert`/`ui.dot` recipes, `BadgeOpts`/`DotOpts`/
389
+ `Tone` types, the toast `ToastOpts` tone, the generated reference and
390
+ the demo. This deliberately reverses the 0.3.2 `ui-badge--info`
391
+ deferral: shipping a token no component consumes would be the exact
392
+ dead-token defect this release exists to remove. Its gated contrast
393
+ row is now a real indicator (measured ≥3:1 on surface — light 5.77:1,
394
+ dark 8.41:1). Status hues (`success`/`warning`/`danger`/`info`) are
395
+ outside the rationed-accent rule by design.
396
+ - **`docs/theming.md` — a full re-skin recipe.** Demonstrates that the
397
+ "Nothing" identity is a token skin, not the architecture: a per-theme
398
+ override block (measured AA-passing accents) restyles the whole system
399
+ with no fork.
400
+
401
+ ### Changed
402
+
403
+ - `.ui-dotbar i` radius `1px` → `var(--radius-sm)` (byte-identical
404
+ render — `--radius-sm` *is* `1px` — but now responds to a radius
405
+ re-skin; no visual-baseline drift).
406
+ - **`--dot-font` is now a live knob.** It and `--display` were defined
407
+ identically and nothing consumed `--dot-font`, yet README / Astro
408
+ guide / `fonts.css` all advertised it as a self-host/re-skin override
409
+ (a documented dead token — the exact defect this release exists to
410
+ remove). `--display` now derives from `--dot-font`
411
+ (`--display: var(--dot-font)`): byte-identical render today, but
412
+ overriding `--dot-font` as documented finally works. Removing the
413
+ token instead would have been breaking (names are contractual), so
414
+ the patch-safe fix is to make the advertised knob real.
415
+ - Contrast gate now also covers text on `--surface-muted`
416
+ (`--text-soft`, `--text-dim`) — that surface is rendered as a fill by
417
+ forms/disclosure and `--text-dim` there is the tightest real margin
418
+ (~4.7:1). Passing today; now gated so a future palette nudge can't
419
+ silently regress it. (Found by the full-codebase audit.)
420
+ - ROADMAP reconciled against shipped reality: the entire original 0.3.1
421
+ checklist has been delivered for several releases. CHANGELOG is now
422
+ the stated source of truth.
423
+
424
+ ### Fixed
425
+
426
+ - `scripts/gen-contrast.mjs` colour parser now rejects non-finite
427
+ channels and handles percent alpha, so a `NaN` ratio can no longer
428
+ silently pass the gate (`check:contrast` also fails explicitly on a
429
+ non-finite ratio). Found by the AgentMix review.
430
+ - `demo/theme-playground.html` WCAG linearisation threshold aligned to
431
+ the canonical `0.04045` (was `0.03928`); points at the gated
432
+ implementation as the source of truth.
433
+ - `check-classes` now strips CSS comments before scraping selectors. It
434
+ is the one contract check that scrapes CSS rather than diffing a
435
+ generator, so a `.ui-*` named only inside a comment could previously
436
+ satisfy the `cls`⇄selector contract — a real (if currently
437
+ unexercised) drift hole. Closed by the full-codebase audit.
438
+ - **`ToastOpts` was runtime-public but type-private.** `toast()` has
439
+ always accepted (and tested) `assertive` / `closable`, but the typed
440
+ `ToastOpts` stopped at `tone`/`title`/`duration`, so a TypeScript
441
+ consumer couldn't pass documented options. Added both (+ a type
442
+ test). `check:dts` covers only the *generated* classes/tokens `.d.ts`,
443
+ not the hand-written behaviors one — this drift was invisible to the
444
+ gate.
445
+ - **`initFormValidation` suppressed native bubbles too late.**
446
+ `form.noValidate` was set inside the submit/blur handlers, so the
447
+ *first* invalid real-browser submit of a pre-existing form showed the
448
+ UA bubble instead of the Bronto error summary (the demo masked it
449
+ with authored `novalidate`). It is now set at init for matched forms
450
+ and restored on cleanup; dynamically-added forms stay covered by the
451
+ in-handler set.
452
+ - **Combobox could select a filtered-out option.** Typing a query that
453
+ hid the active option left `aria-activedescendant` stale; Enter then
454
+ selected the hidden option. `filter()` now clears stale active state
455
+ and Enter is visibility-guarded (WAI-ARIA APG correctness).
456
+ - `check-dist` now also asserts the on-disk `dist/**/*.css` set exactly
457
+ matches the generated bundles — since the whole `dist/` ships, a
458
+ stale/renamed leaf would otherwise reach npm undetected.
459
+ - README bundle-size figures refreshed (~64 kB raw / ~11 kB gzip; the
460
+ enforced ceiling remains `check-dist`'s budget). Prior `~54/~10`
461
+ prose had drifted since 0.3.2.
462
+
463
+ ## 0.3.3 — 2026-05-16
464
+
465
+ A second consumer-evidence pass ("felt it twice") plus
466
+ agent-discoverability. All additive — new classes/recipes, a new
467
+ generated artifact, a widened input type, a freed primitive with a
468
+ permanent alias. No breaking change → a patch under the 0.x policy.
469
+
470
+ ### Added
471
+
472
+ - **`@ponchia/ui/tokens/resolved.json`** — every colour token resolved
473
+ to a static `#rrggbb` / `rgba(...)` per theme, with `var()` and sRGB
474
+ `color-mix()` evaluated at build time. For render targets that cannot
475
+ read CSS custom properties: MapLibre GPU paint, `<canvas>`, WebGL,
476
+ SVG, server-side image gen. Generated + drift-checked (new
477
+ `check:resolved` gate); no new colours introduced (`check:shiki`
478
+ unaffected). Resolves the deferred charts-palette gap now that a 2nd,
479
+ non-charting consumer has hit it.
480
+ - **`ui-button--sm` / `ui-button--lg`** — token-driven size scale for
481
+ dense tooling (toolbars, pagination, table actions) and hero CTAs.
482
+ `ui.button({ size: 'sm' | 'lg' })`. Default size unchanged.
483
+ - **`ui-empty-state`** — the empty-state primitive is now
484
+ shell-agnostic; `ui-app-empty-state` remains a permanent grouped alias
485
+ (byte-identical render, no baseline drift). Same playbook as 0.3.2
486
+ `ui-stat`.
487
+ - **`ui-app-rail__account`** — a framework-blessed identity / sign-out
488
+ slot, so admin shells stop hand-rolling it; stays visible when the
489
+ rail collapses on mobile.
490
+ - **`ui-modal` controlled (`is-open`) path** — a portal/React modal that
491
+ can't be a native `<dialog>` wears the same skin + open layout via
492
+ `is-open` (`ui.modal({ open: true })`). Backdrop and focus-trap remain
493
+ the consumer's responsibility (the `<dialog>` path still gets them
494
+ free).
495
+ - **`llms.txt`** at the package root — a self-contained agent entrypoint
496
+ (typed contract, the `@layer bronto` override rule, import surface,
497
+ shipped offline references).
498
+ - **`docs/reference.md` and `docs/theming.md` now ship in the npm
499
+ tarball** — an offline coding agent gets the full class catalog and
500
+ token contract from `node_modules/@ponchia/ui/`. The reference also
501
+ now documents the table-local `is-num`/`is-pos`/`is-neg` state classes
502
+ and the ARIA-driven composition/state model. New `exports` subpaths:
503
+ `./llms.txt`, `./docs/reference.md`, `./docs/theming.md`,
504
+ `./tokens/resolved.json`.
505
+
506
+ ### Changed
507
+
508
+ - **`ClassValue` widened to clsx parity** (`string | number | boolean |
509
+ null | undefined | …`). The idiomatic React `reactNode && 'cls'` guard
510
+ (where the node may be `0` / `''`) now type-checks; the runtime `cx`
511
+ already skipped every falsy value, so this is a non-breaking type
512
+ relaxation.
513
+ - `check-pack.mjs` relaxed from a blanket `docs/` ship-block to a
514
+ **curated allowlist** (`docs/reference.md`, `docs/theming.md` only); a
515
+ consistency assertion fails if that set drifts from `package.json`
516
+ `files`. The rest of `docs/` stays dev-only by design — a deliberate,
517
+ documented narrowing of the earlier runtime-only stance.
518
+
519
+ ### Deferred (deliberately not shipped)
520
+
521
+ - **React/Solid binding layer.** The duplicated form/badge "glue" two
522
+ consumers feel is ARIA-driven by design (`aria-invalid`, `aria-busy`,
523
+ `aria-describedby`), so the agnostic surface is already complete; the
524
+ remaining duplication is the framework-binding layer, still deferred.
525
+ The reference now documents the composition model explicitly.
526
+ - **`ui-app-rail__foot` mobile `display:none`.** Whether the foot should
527
+ survive the mobile breakpoint is a visual/possibly-breaking call; the
528
+ new `__account` slot (which does survive) covers the actual need
529
+ without changing existing behaviour.
530
+
531
+ ## 0.3.2 — 2026-05-16
532
+
533
+ Re-skin-proven adoption pass: a real content-site consumer rebuilt
534
+ several idioms bespoke because a primitive was missing or shell-locked.
535
+ This promotes the genuinely generic, token-only ones upstream. **All
536
+ additions are non-breaking** (additive classes/recipes; the metric tile
537
+ keeps its admin-shell name as a permanent alias) — a patch per the 0.x
538
+ policy.
539
+
540
+ ### Added
541
+
542
+ - **`ui-stat` / `ui-statgrid`** — the metric tile is now shell-agnostic
543
+ (label + display value + signed delta). The admin-shell
544
+ `ui-app-metric*` / `ui-app-metrics` names remain as permanent aliases
545
+ grouped on the same rules (byte-identical output, no baseline drift).
546
+ - **`ui-link--cta`** — the eyebrow-faced action link (accent · display ·
547
+ uppercase + arrow glyph), composed from the same tokens as
548
+ `ui-eyebrow`. `ui.link({ cta: true })`.
549
+ - **`ui-badge--dot`** — leading state dot; composes with any badge tone
550
+ (`is`-tone tints the dot). `ui.badge({ tone, dot: true })`.
551
+ - **`ui-eyebrow--sm`** — restores the dense size step (was
552
+ `eyebrow--tight` in 0.2.2). `ui.eyebrow({ sm: true })`.
553
+ - **`ui-container--wide`** — documented wide preset (`--container-wide`,
554
+ default 82rem) for app/marketing shells. `ui.container({ wide: true })`.
555
+ - **`ui-siteheader--sticky`** — structural sticky only (the floating-card
556
+ skin stays consumer identity, deliberately not shipped).
557
+ - **`ui-dotmatrix`** (+`__cell`, `--hot`, `--accent`) — data-bound dot
558
+ grid, the on-brand counterpart to the decorative `ui-dotgrid`; the
559
+ data→cell mapping stays the consumer's.
560
+ - **`ui-num`** (+`--pos`, `--neg`, `--muted`) — the tabular / end-aligned
561
+ / P&L-tone numeric vocabulary the table has shipped since 0.1.0, freed
562
+ from `.ui-table` so cards, stats and inline figures share one
563
+ contract. `ui.num({ tone })`. (Two independent admin consumers were
564
+ reinventing `.text-green`/right-align outside a table.)
565
+ - **`ui-badge--muted`** — the idle / unknown / "no signal yet" status
566
+ tone, distinct from the default tinted badge. `ui.badge({ tone:
567
+ 'muted' })`. Token-safe, no new hue.
568
+
569
+ ### Fixed
570
+
571
+ - **`ui-card--interactive`** had no keyboard cue (cards typically wrap a
572
+ single link): added `:focus-within` border + a `--shadow-raised` ring
573
+ on hover. a11y completeness fix.
574
+
575
+ ### Changed
576
+
577
+ - dist raw size budget recalibrated 64 kB → 76 kB (post-0.3.2 bundle is
578
+ ~64.5 kB raw / ~11.1 kB gzip; ~18% raw headroom restored). The gzip
579
+ cap (12 kB) is unchanged — the wire-size contract still holds.
580
+
581
+ ## 0.3.1 — 2026-05-16
582
+
583
+ Adoption + gap-closing pass driven by a 12-perspective review (two Opus
584
+ analyses + two five-model AgentMix deep runs). **All additions are
585
+ non-breaking** (additive classes/tokens/behaviors; short token names
586
+ kept as permanent aliases) — so this is a patch, per the 0.x policy
587
+ (only breaking changes bump the minor); the 0.3.0 legacy removal
588
+ stands. Tracked scope: ROADMAP.md.
589
+
590
+ ### Added
591
+
592
+ - **Components/behaviors** (all SSR-safe, idempotent, cleanup-returning,
593
+ dependency-free): `ui-combobox` + `initCombobox` (WAI-ARIA APG
594
+ combobox); `ui-popover` + `initPopover` (collision-aware, native
595
+ top-layer when available); `initFormValidation` (Constraint
596
+ Validation → `aria-invalid`/`aria-describedby` + error summary);
597
+ `initTableSort` (sortable `aria-sort` headers + row selection,
598
+ `bronto:selectionchange`); `.ui-button[aria-busy]` loading state;
599
+ toast dismiss button + separate assertive region for `danger`.
600
+ - **Forms:** `ui-input-group`(+`__addon`), `ui-file`, `ui-range`,
601
+ `ui-error-summary`.
602
+ - **Layout:** `ui-sidebar`, `ui-switcher`, `ui-center`, `ui-ratio`;
603
+ opt-in `ui-cq` container queries for `ui-grid` / `ui-app-metrics`.
604
+ - **Tokens:** semantic `--bronto-color-*` tier, `--accent-1..6` /
605
+ `--surface-1..6` ramps, `--z-*` stacking scale (every framework
606
+ `z-index` now resolves through it; values unchanged).
607
+ - **DX:** generated drift-checked `docs/reference.md`; VS Code
608
+ `classes/vscode.css-custom-data.json` (token IntelliSense, exported
609
+ as `./vscode.css-custom-data.json`); per-framework integration guides
610
+ + Tailwind interop recipe; `examples/{vanilla-vite,astro,sveltekit}`
611
+ + consumer-smoke CI matrix; README badges; `ROADMAP.md`;
612
+ `MIGRATIONS.json` + `docs/migrations/0.2-to-0.3.md`;
613
+ `demo/theme-playground.html` (live contrast checker).
614
+ - **a11y:** `role="switch"` contract + forced-colors switch cues;
615
+ `@supports(anchor-name)` tooltip un-clipping.
616
+
617
+ ### Fixed
618
+
619
+ - **Release hygiene:** the `0.3.0` section was still labelled
620
+ `unreleased` after `v0.3.0` shipped to npm `latest`. Dated it
621
+ correctly and added a `check:release` gate (in `npm run check`) that
622
+ fails when `package.json`'s version maps to an `unreleased` changelog
623
+ heading, so a published version can never again be marked unreleased.
624
+
625
+ ### Changed
626
+
627
+ - `npm run check` is now 14 gates (+`check:release`, `check:reference`,
628
+ `check:vscode`); `prepack` regenerates the reference + VS Code data.
629
+ - CONTRIBUTING.md documents a deprecate-one-minor policy so
630
+ "minor may break" is predictable.
631
+
632
+ ## 0.3.0 — 2026-05-16
633
+
634
+ ### Multi-POV review hardening
635
+
636
+ A six-perspective review (DX/contract, CSS architecture, a11y,
637
+ release/supply-chain, plus AgentMix) drove this pass.
638
+
639
+ **BREAKING (token / contract level)**
640
+
641
+ - **New `--accent-text` token** (= `var(--accent-strong)`). Everywhere
642
+ the accent was used as _foreground text_ (links on hover, active
643
+ nav/tab/accordion, prose markers, eyebrows, chips, breadcrumb,
644
+ pagination) now resolves through `--accent-text`, not raw `--accent`,
645
+ so a pale re-brand no longer silently fails text contrast.
646
+ _Migration:_ none for default themes (visually identical). If you
647
+ re-brand `--accent` to a light hue, also set `--accent-text` to a
648
+ dark-enough value (see docs/theming.md).
649
+ - **`--focus-ring` is now solid `var(--accent)`** (was an unused
650
+ 50%/55% transparent mix) and every focus outline is wired to it.
651
+ Default focus appearance is unchanged; the `[data-contrast=high]` /
652
+ `prefers-contrast` promotion and per-theme `--focus-ring` overrides
653
+ now actually take effect. _Migration:_ none unless you relied on the
654
+ (previously dead) token value.
655
+ - **`classes/index.d.ts` / `tokens/index.d.ts` are now generated
656
+ literal types.** `cls` exposes literal keys+values; token views use
657
+ `ColorKey`/`ScaleKey`/`*TokenName` unions; `themeColor` takes
658
+ `ThemeName`. Mistyped keys are now compile errors. _Migration:_ fix
659
+ any code that relied on the old `Record<string,string>` (e.g. reading
660
+ a non-existent key and getting `string` instead of an error). JS
661
+ token keys are kebab-case — `themeColor('dark')['accent-soft']`.
662
+
663
+ **Fixed**
664
+
665
+ - **a11y (WCAG AA):** `.ui-chip--accent`, legacy `.eyebrow` group and
666
+ `.tag-list--compact` first child no longer use raw `--accent` as
667
+ small text (was ~3.9:1 in light).
668
+ - **a11y:** native `<dialog>` returns focus to its trigger on _every_
669
+ close path (Esc, close button, backdrop light-dismiss, programmatic).
670
+ - **a11y:** the toast `aria-live` stack is a persistent region — no
671
+ longer created-then-destroyed per drain, fixing dropped first /
672
+ post-drain screen-reader announcements.
673
+ - **a11y:** `.ui-tab:focus-visible` is now visually distinct from the
674
+ active-tab underline (inset ring).
675
+ - **DX:** the `.` export is conditional (`style`/`default`) instead of a
676
+ bare `.css` string, so type-aware tooling no longer mis-resolves the
677
+ package root. The root is CSS-only (documented).
678
+
679
+ **Changed (non-breaking)**
680
+
681
+ - Behavior initializers (`initThemeToggle`, `dismissible`, `initDialog`,
682
+ `initDisclosure`, `initTabs`) are now idempotent — re-init (HMR,
683
+ framework remount, repeat calls) replaces rather than stacking
684
+ duplicate listeners. Tab ids use a module-global counter so separate
685
+ islands never collide on `bronto-tab-1`.
686
+ - New drift gate `check:dts` (generated `.d.ts` ⇄ JS runtime), wired
687
+ into `npm run check` and `prepack`. `docs/architecture.md` drift table
688
+ and release-gating section corrected to the real four-job DAG
689
+ (`validate` + `e2e` → `publish-npm` → `release-notes`).
690
+ - README: explicit "do not mix a bundle with a raw leaf import" hazard
691
+ warning; a Versioning section; size/`@import`-depth prose de-drifted.
692
+ - Tests: +3 unit tests (dialog focus-return, initializer idempotency,
693
+ global-unique tab ids); +5 e2e a11y tests (RTL axe pass, dialog
694
+ focus-return on Escape, persistent toast live region, disclosure
695
+ toggle, modal computed-contrast instead of a blanket rule disable);
696
+ demo gained a `[data-bronto-disclosure]` instance (was untested).
697
+ _Release note:_ the visual-snapshot baselines (`test/e2e/__screenshots__`)
698
+ are intentionally stale after the contrast / focus-ring / legacy-removal
699
+ changes (cross-OS rasterisation means they can only be authored in the
700
+ pinned container, not on a dev machine). Regenerate them with one click:
701
+ run the **“Update visual baselines”** workflow (`workflow_dispatch`,
702
+ `.github/workflows/visual-baselines.yml`) from this branch — it rebuilds
703
+ them in `mcr.microsoft.com/playwright:v1.60.0-jammy` and commits them
704
+ back, after which the `e2e` gate goes green on its own. Red by design
705
+ until that runs.
706
+
707
+ **BREAKING (legacy vocabulary removed / migrated)**
708
+
709
+ The whole non-`ui-*` surface is gone; everything shipped is now under the
710
+ `.ui-*` contract and the `check-classes` drift gate.
711
+
712
+ - **Deleted (had `ui-*` equivalents):** `css/layout.css`, `css/cards.css`,
713
+ `css/typography.css` and their entire vocabulary — `.hero`,
714
+ `.project-*`, `.post-card`, `.essay-*`, `.metric-tile`, `.callout`,
715
+ `.eyebrow`, bare `.button`, `.section-head`, `.tag-list`,
716
+ `.profile-link-list`, `.page-*`, `.home-*`, `.signal-panel`,
717
+ `.worklog-summary`, … _Migration:_ use the `ui-*` content layer —
718
+ `.ui-prose`/`.ui-quote` (long-form), `.ui-card`, `.ui-eyebrow`,
719
+ `.ui-button`, `.ui-tag`/`.ui-tags`, `.ui-grid`/`.ui-stack`, the
720
+ `ui-site*` shell. `.skip-link` → `.ui-skiplink`; `.site-nav` →
721
+ `.ui-sitenav`; `.site-menu` → `.ui-sitemenu` (responsive nav) or the
722
+ new `.ui-menu-host` (a `<details>` + `.ui-menu` dropdown wrapper).
723
+ - **Renamed → first-class:** the admin shell `.app-*` → **`.ui-app-*`**
724
+ (`ui-app-shell`/`-rail`/`-topbar`/`-toolbar`/`-nav`/`-panel`/
725
+ `-content`/`-main`/`-metrics`/`-metric`/`-empty-state`, with the same
726
+ `__part` / `--mod` suffixes). The theme toggle `.theme-toggle__*` →
727
+ **`.ui-themetoggle__*`**. _Migration:_ rename these class strings in
728
+ consumer markup (or use the new `cls.app*` / `cls.themetoggle*` /
729
+ `cls.menuHost` entries). They are now typed and drift-checked.
730
+ - **Bundle collapsed:** `css/responsive.css` and `css/index.css` removed.
731
+ `ui-*` components own their breakpoints, so there is no core/full
732
+ split: `@ponchia/ui/css` now resolves to `css/core.css` (one bundle),
733
+ `@ponchia/ui` → the single `dist/bronto.css` (~54 kB / ~10 kB gzip,
734
+ was ~70/12). _Removed exports:_ `./css/index.css`,
735
+ `./css/responsive.css`, `./dist/bronto-core.css`, and the deleted
736
+ leaves' `./css/{layout,typography,cards}.css`. _Migration:_ import
737
+ `@ponchia/ui` or `@ponchia/ui/css`.
738
+
739
+ **BREAKING (per-leaf imports are now layer-safe)**
740
+
741
+ - Every `@ponchia/ui/css/<leaf>.css` export now resolves to a
742
+ self-`@layer bronto`-wrapped build (`dist/css/<leaf>.css`), so a
743
+ direct leaf import is layered by default and safe to mix with the
744
+ bundle — the silent cascade-inversion footgun is gone. The raw,
745
+ full-specificity source is still available as a deliberate escape
746
+ hatch at the explicit **`@ponchia/ui/css/unlayered/<leaf>.css`**
747
+ path. _Migration:_ none if you import the bundle. If you imported a
748
+ raw leaf *expecting* unlayered/full-specificity behaviour, switch
749
+ that import to the `css/unlayered/*` path; otherwise the now-layered
750
+ leaf is the correct (safe) default. Drift-checked by `check-dist`.
751
+
752
+ ### Tooling (external-review triage)
753
+
754
+ Adopted what fits the CSS-first / zero-runtime-dep / curated-artifact
755
+ ADR; declined what doesn't (recorded so the decision isn't re-litigated).
756
+
757
+ **Added**
758
+
759
+ - **TypeScript type gate** — `tsconfig.json` + `test/types.test-d.ts`
760
+ + `check:types` (in `npm run check`). Compiles the published `.d.ts`
761
+ and asserts, via `@ts-expect-error`, that the generated literal
762
+ `cls`/token types reject typos and `themeColor` rejects non-`ThemeName`.
763
+ Completes the review's "auto-generate .d.ts, kill drift" item
764
+ (generation + `check-dts` landed earlier this minor; this proves the
765
+ result). `typescript` is devDep-only — no runtime/types-export change.
766
+ - **Prettier** — `.prettierrc` + `check:format`/`format`, in
767
+ `npm run check`. Scoped to hand-authored non-CSS source; CSS stays
768
+ Stylelint-owned, generated artifacts and the curated Markdown/`demo`
769
+ are `.prettierignore`d so formatters never fight generators.
770
+ - **GitHub issue/PR templates** — collect `@ponchia/ui` version,
771
+ consuming framework, and surface; PR template carries the
772
+ contract/SemVer/a11y checklist.
773
+ - **Bundle-size budget tightened** — `check-dist` `BUDGET` recalibrated
774
+ 90 kB→64 kB raw / 16 kB→12 kB gzip (bundle is ~54/~10 post-cleanup),
775
+ so regrowth is gated, not just catastrophic blowouts. (Dependabot was
776
+ already added earlier this minor.)
777
+
778
+ **Declined (rationale)** — Storybook (heavy React/Vite toolchain vs the
779
+ framework-agnostic zero-dep ADR; `demo/index.html` is the self-driving
780
+ surface); Style Dictionary as a dependency (the shipped
781
+ `tokens.dtcg.json` *is* the deliberate bring-your-own platform interop;
782
+ no native consumers exist — consumers run SD themselves);
783
+ standard-version/auto-changelog (the curated narrative CHANGELOG is an
784
+ asset; commits are already conventional); Renovate (Dependabot chosen);
785
+ Lighthouse CI (the axe a11y gate + size budget already cover the
786
+ regression vectors).
787
+
788
+ ### Post-review fixes (independent Opus + AgentMix pass on this branch)
789
+
790
+ - **Fixed (HIGH, regression introduced here):** the persistent-toast
791
+ rAF deferral could resurrect an already-dismissed first toast into the
792
+ `aria-live` region (dismiss within the first frame). Now guarded by a
793
+ `dismissed` flag; `dismiss()` is idempotent. +2 unit tests polyfilling
794
+ rAF (the jsdom env had no `requestAnimationFrame`, so the path was
795
+ previously untested).
796
+ - **Fixed (HIGH, regression introduced here):** the layered per-leaf
797
+ `@ponchia/ui/css/fonts.css` (now `dist/css/fonts.css`) referenced
798
+ `url(../fonts/*)`, which from `dist/css/` resolves to the unshipped
799
+ `dist/fonts/`. `build-dist` now rewrites `../fonts/` → `../../fonts/`
800
+ for the deeper per-leaf files (the flattened bundle at `dist/` is
801
+ unaffected and unchanged). `check-dist` now also resolves every
802
+ `url(...)` in each generated file against its own location, so this
803
+ class of depth bug can't recur.
804
+ - **Fixed (docs):** README SemVer guidance was wrong — at `0.x` npm
805
+ resolves `^0.3.0` and `~0.3.0` identically (`>=0.3.0 <0.4.0`); both
806
+ hold back the breaking `0.4.0`. Corrected. Removed the stale
807
+ "legacy `site-*`/`.tag-list` kept as back-compat" line (they were
808
+ deleted this release). De-duplicated the `check-dist` paragraph in
809
+ architecture.md.
810
+ - **Hardened:** the release `publish-npm` step uses
811
+ `npm publish --ignore-scripts`, so `NODE_AUTH_TOKEN` is never exposed
812
+ to the prepack/prepublishOnly lifecycle; it ships the artifacts already
813
+ byte-verified by `validate` on the same commit.
814
+
815
+ ### Further discovered-issue cleanup
816
+
817
+ Bounded, sensible items surfaced by the reviews, now closed:
818
+
819
+ - **a11y:** new `initMenu` behavior — Escape / outside-click /
820
+ close-on-activate (with focus return to `<summary>`) for a native
821
+ `<details data-bronto-menu>` `.ui-menu` dropdown. Deliberately a
822
+ disclosure of buttons, not an over-claimed ARIA menu (review M3).
823
+ Wired in the demo; unit-tested.
824
+ - **a11y:** the active tab's selected state is re-asserted under
825
+ `forced-colors: active` (`border/colour: Highlight`) — it was
826
+ invisible in Windows High Contrast (review L3).
827
+ - **a11y (demo):** the pagination "previous" control is now a real
828
+ `disabled` button (was a focusable/clickable `aria-disabled`,
829
+ misleading on the axe-gated integration surface); arrow controls
830
+ gained accessible names; active page uses `aria-current="page"`
831
+ (review M1).
832
+ - **CI:** GitHub Pages now deploys only after the `CI` workflow
833
+ concludes **successfully** on `main` (`workflow_run` trigger, not a
834
+ bare push) — a red-e2e/broken demo can no longer be published
835
+ independently of the gates.
836
+ - **docs:** theming.md documents the one accent surface the framework
837
+ can't tune — native control `accent-color` under a pale re-brand
838
+ (review css M2).
839
+
840
+ ### Content-site layer
841
+
842
+ Promotes the proven, hand-rolled site shell into the first-class typed
843
+ contract so consumers stop reimplementing it. (The legacy `site-*` /
844
+ `.tag-list` back-compat classes referenced here were **removed** in the
845
+ same release — see the "legacy vocabulary removed / migrated" BREAKING
846
+ section above; they are not shipped.)
847
+
848
+ - **`site.css`**: `ui-container` (+`--narrow`), `ui-siteheader`
849
+ (`__brand`/`__actions`), `ui-sitenav` (active via `aria-current`, dot
850
+ cue, responsive collapse into `ui-sitemenu` — native `<details>`, no
851
+ JS), `ui-sitefooter` (`__links`), `ui-skiplink`, `ui-tags`/`ui-tag`
852
+ (`--accent`; neutral content labels, distinct from interactive
853
+ `ui-chip`), `ui-meta` (dot-separated meta row).
854
+ - **`ui-quote`** (+ `__cite`) in `content.css`: a pull-quote companion
855
+ to `.ui-prose` — emphasis by scale + a short accent rule, not a box.
856
+ - **Shiki**: `@ponchia/ui/shiki/nothing.json` — a documented optional
857
+ VS Code/TextMate theme (rationed: brand accent + greyscale),
858
+ drift-checked (`check:shiki` keeps it on-palette and in sync with the
859
+ dark `--accent`). Bring-your-own-highlighter, like `tokens.dtcg.json`.
860
+ - Full contract treatment: 18 classes + `ui.container`/`ui.tag` recipes
861
+ + `.d.ts` (guarded), cascade/exports/dist wired, demo + docs. The
862
+ `not-a-gap` items from the source review (theme-toggle CSS already
863
+ exists in `navigation.css`; `ui-timeline` too consumer-specific) were
864
+ deliberately excluded.
865
+
866
+ ## 0.2.2 — 2026-05-15
867
+
868
+ Component + mobile expansion, then a framework-grade hardening pass
869
+ (RTL, a11y, theming contract, Markdown content layer). Additive — no
870
+ existing token/selector values changed except documented WCAG fixes.
871
+
872
+ ### Framework hardening
873
+
874
+ - **RTL / logical properties**: every `css/*` physical property is now
875
+ logical (`*-inline/-block-*`), enforced by `stylelint-use-logical`
876
+ (`csstools/use-logical: always`). Render-neutral in LTR; RTL mirrors
877
+ cleanly (verified, incl. the drawer).
878
+ - **A11y**: `initTabs` behavior (WAI-ARIA Tabs keyboard pattern);
879
+ `forced-colors` (Windows High Contrast) support in `base.css`; WCAG
880
+ contrast fixes — light `--text-dim`/`--warning`, dark `--text-dim`
881
+ now ≥ 4.5:1. Badge variants drop tone-on-tinted-tone text (failed AA
882
+ at small bold) — tone now rides the border + tint, text inherits the
883
+ high-contrast neutral. `.ui-eyebrow` uses `--accent-strong` so it
884
+ clears 4.5:1 on soft surfaces too. (All surfaced by the new axe gate.)
885
+ - **Theming contract**: `--accent` is one knob — the whole accent family
886
+ is `color-mix`-derived (ratios tuned to the prior hex, ≈ zero default
887
+ drift). `data-density` (compact/comfortable) and `data-contrast=high`
888
+ + `@media (prefers-contrast: more)` presets. See `docs/theming.md`.
889
+ - **content.css**: `.ui-prose` (+ `--compact`) styles raw
890
+ Markdown-renderer HTML — headings, lists, quote, code, tables, media,
891
+ figures — with **zero per-element classes**, keeping documents
892
+ semantic and machine-readable.
893
+ - **CI regression safety**: Playwright visual snapshots of the demo
894
+ (dark / light / RTL / modal) + `@axe-core/playwright` WCAG 2.1 A/AA
895
+ gates (both themes, modal, tab keyboard pattern), as a new `visual`
896
+ CI job pinned to the Playwright container the baselines were authored
897
+ in (byte-stable, no cross-OS font flake). Catches CSS/markup
898
+ regressions structure-checks can't.
899
+ - **Prebuilt bundles**: `dist/bronto.css` + `dist/bronto-core.css` —
900
+ the `@import` graph flattened + conservatively minified into one
901
+ `@layer bronto` file (~62 kB / ~11 kB gzip), no load waterfall.
902
+ Exposed as `@ponchia/ui` (`.`) and `./dist/*`; `check:dist` keeps it
903
+ byte-fresh and in a size budget; built in `prepack`. README documents
904
+ the evergreen support floor (Chrome 111+/Safari 16.4+/Firefox 121+).
905
+
906
+ ### Multi-agent review response
907
+
908
+ Acted on a deep read-only review (AgentMix `deep`), verifying each
909
+ finding against the code first:
910
+
911
+ - **Public TS contract**: `classes/index.d.ts` was stale — added the 9
912
+ missing `ui.*` recipes (`alert`, `toast`, `progress`, `dotspinner`,
913
+ `dotbar`, `modal`, `tab`, `avatar`, `prose`) + option types, and a new
914
+ drift guard in `check-classes` so a recipe without a declaration now
915
+ fails `npm run check`.
916
+ - **RTL completeness**: `[dir='rtl']` mirrors for the cases the lint
917
+ plugin can't convert — switch thumb, theme-toggle thumb, `<select>`
918
+ marker (`background-position`), arrow-link hover nudge.
919
+ - **Bug**: `.ui-progress__bar` transitioned the (now non-existent)
920
+ physical `width`; animate `inline-size`.
921
+ - **Release gating**: `release.yml` now runs the containerised
922
+ visual/a11y suite and `publish-npm`/`release-notes` depend on it — a
923
+ tagged release can't skip what every branch push runs.
924
+ - **Hardening**: `initTabs` scopes to its own group (nested-safe);
925
+ `initDialog` root semantics documented (dialogs are document-global
926
+ by design); `scripts/serve.mjs` binds loopback + strict path
927
+ containment; fixed an invalid selector in `docs/theming.md`.
928
+
929
+ ### Second review pass (PR #3)
930
+
931
+ Independent Opus review said SHIP; the AgentMix `deep` mix flagged
932
+ more — verified each, fixed the real ones:
933
+
934
+ - **Release hygiene**: `package-lock.json` synced to `0.2.2`;
935
+ `release.yml` `release-notes` now `needs: publish-npm` (no GitHub
936
+ Release for a version that failed to publish — no split-brain).
937
+ - **RTL**: `.ui-toast-stack` used physical `inset` → logical
938
+ `inset-block`/`inset-inline` so the stack mirrors in RTL.
939
+ - **`initTabs`**: a `root` that *is* the `[data-bronto-tabs]` element is
940
+ now initialised (querySelectorAll only sees descendants); tabs are
941
+ cross-linked to panels via `aria-controls`/`aria-labelledby`
942
+ (APG-complete), ids minted only where absent.
943
+ - **Toast**: dropped the per-item `role="status"` nested inside the
944
+ `aria-live` stack (double-announcement risk).
945
+ - **DTCG**: `--shadow: none` was typed `color` (name matched the colour
946
+ regex, value failed the shadow test) — shadows are now classified by
947
+ name first → `$type: "shadow"`.
948
+ - **Tests**: new `ui.*` recipe-output coverage + a `.d.ts`-declaration
949
+ assertion; `initTabs` root-self/APG test; `tokens.dtcg.json` shape
950
+ test (shadow typing + the null-+-extension invariant); `serve.mjs`
951
+ `safePath` traversal unit test. The RTL e2e now waits for the
952
+ mirrored end-state instead of racing a fixed sleep (the CSS was
953
+ correct; the old test read mid-transition).
954
+
955
+ ### Discovered follow-ups
956
+
957
+ Closed the remaining items surfaced across the review/verification:
958
+
959
+ - **Regression tests for the review fixes**: `serve.mjs` refactored to a
960
+ pure exported `safePath()` with a traversal/sibling-prefix unit test;
961
+ `initTabs` nested-isolation test — which **caught a real bug in the
962
+ first nested fix** (the outer group's delegated click still fired for
963
+ nested tabs); now gated on owned membership, not DOM containment. New
964
+ e2e assertion that RTL truly mirrors the switch transform + select
965
+ marker (not just box model).
966
+ - **Print** (`@media print` in `base.css`): ink-on-white, chrome hidden,
967
+ `break-inside` guards, prose link URLs surfaced, `@page` margin.
968
+ - **prefers-reduced-data**: points `--display`/`--dot-font` at the mono
969
+ stack so the Doto webfont is never fetched for data-saver users.
970
+ - **DTCG export**: `@ponchia/ui/tokens.dtcg.json` (W3C Design Tokens
971
+ format) generated from the model, drift-checked (`check:dtcg`), built
972
+ in `prepack`. Runtime-derived tokens are spec-shaped with
973
+ `$value:null` + `$extensions` rather than fabricated numbers.
974
+
975
+ ### Earlier in this cycle
976
+
977
+ Component + mobile expansion. No token/selector changes to existing
978
+ classes — purely additive; existing consumers are unaffected.
979
+
980
+ - **Dot loaders**: new orbital `ui-dotspinner` (the Nothing-signature
981
+ ring loader, `--sm`/`--lg`), `ui-dotbar--indeterminate` sweep, and a
982
+ linear `ui-progress` (determinate via `--value`, plus
983
+ `ui-progress--indeterminate`).
984
+ - **feedback.css**: `ui-alert` / callout (tones + dismissible), `ui-toast`
985
+ + `ui-toast-stack`, CSS-only `ui-tooltip`.
986
+ - **overlay.css**: `ui-modal` + `ui-modal--drawer` on native `<dialog>`
987
+ (bottom-sheet on mobile), `ui-menu` dropdown.
988
+ - **disclosure.css**: `ui-tabs` (ARIA + `.is-active` contract, scrollable
989
+ on mobile), `ui-accordion` (styled `<details>`), `:has()`-driven
990
+ `ui-segmented`, `ui-breadcrumb`, `ui-pagination`, `ui-avatar` /
991
+ `ui-avatar-group`.
992
+ - **Behaviors**: `initDialog` (native `<dialog>` open/close + backdrop
993
+ light-dismiss) and `toast()`. SSR-safe; covered by the test suite.
994
+ - **Mobile**: 44px touch targets for buttons/inputs/checkboxes on coarse
995
+ pointers; component-level breakpoints for modal/drawer/menu/tabs.
996
+ - **Contract**: 53 new classes added to the typed `cls` registry + recipes
997
+ (`ui.alert`, `ui.toast`, `ui.progress`, `ui.dotspinner`, `ui.modal`,
998
+ `ui.tab`, `ui.avatar`); `npm run check` and the 20-test suite stay green.
999
+ - **Docs**: removed the stale "not published yet" install note (the
1000
+ package is live on npm); documented the new layers and behaviors.
1001
+
1002
+ ## 0.2.1 — 2026-05-15
1003
+
1004
+ - Remove private project names and personal paths from docs and CSS
1005
+ comments (no code/selector/token changes). Supersedes 0.2.0, which
1006
+ carried those references in shipped CSS comments.
1007
+
1008
+ ## 0.2.0 — 2026-05-15
1009
+
1010
+ Architecture: keep plain CSS as the universal substrate, add thin optional
1011
+ layers on top (see `docs/architecture.md`). No `css/*` selector/token values
1012
+ changed — existing consumers are visually unaffected.
1013
+
1014
+ - **Cascade**: the whole framework now ships inside `@layer bronto`, so
1015
+ un-layered consumer CSS overrides it without specificity fights. Applied
1016
+ only at the bundle entrypoints (`core.css`, `index.css`); source files
1017
+ unchanged. _Behavioural change for consumers that override via specificity._
1018
+ - **Fonts**: `@font-face` moved from `tokens.css` to `css/fonts.css` with
1019
+ package-relative URLs (`../fonts/*`) — no more absolute `/fonts`
1020
+ assumption. Bundled into `core.css`/`index.css`; exported standalone.
1021
+ - **`@ponchia/ui/tokens`**: design tokens as data (`index.js` canonical,
1022
+ `index.json` generated, `themeColor()` helper, typed).
1023
+ - **`@ponchia/ui/classes`**: typed class-name contract — `cls` registry,
1024
+ `ui.*` recipe builders, `cx()`. Framework-agnostic, returns strings.
1025
+ - **`@ponchia/ui/behaviors`**: vanilla, SSR-safe, dependency-free helpers —
1026
+ `applyStoredTheme`, `initThemeToggle`, `dismissible`, `initDisclosure`.
1027
+ - **Drift control**: `npm run check` adds `check-tokens` and
1028
+ `check-classes`; the demo now drives itself via the shipped modules.
1029
+ - **Packaging**: `exports` for the new entrypoints (with `types`),
1030
+ `sideEffects` for tree-shaking, `files` widened.
1031
+ - **Distribution decided**: published to npm as **`@ponchia/ui`** (the
1032
+ `@bronto` scope isn't ownable; the `@layer bronto` / `data-bronto-*`
1033
+ namespace is unchanged). `private` removed, `publishConfig`
1034
+ (`access: public`, `provenance: true`), `repository`/`homepage`/`bugs`
1035
+ added. `release.yml` now gates on a real `npm publish` job
1036
+ (`validate` → `publish-npm`). Rationale + pre-publish blockers
1037
+ (LICENSE, `NPM_TOKEN`, version bump) in `docs/architecture.md`.
1038
+
1039
+ ## 0.1.0 — 2026-05-15
1040
+
1041
+ First standalone release. Extracted into its own standalone package.
1042
+
1043
+ - Re-skinned to a Nothing-inspired design language: monochrome dual
1044
+ light/dark palette, single red accent, Doto dot-matrix display type, flat
1045
+ hairline surfaces, sharp radii, no soft shadows.
1046
+ - New `motion.css` — keyframes (migrated from the old responsive layer so
1047
+ core-only consumers keep their animations) plus reveal / stagger /
1048
+ skeleton / spinner / caret utilities and full reduced-motion handling.
1049
+ - New `dots.css` — dot-grid surfaces, dotted rule, status dot (with live
1050
+ pulse), dot loader, dot progress bar, matrix-reveal.
1051
+ - New `forms.css` — input, select, textarea, search, switch, checkbox.
1052
+ - New `table.css` — `ui-table` with dense / comfortable / lined variants and
1053
+ numeric helpers for admin dashboards.
1054
+ - Expanded `app.css` into a full admin shell: sidebar rail with dot nav,
1055
+ sticky blurred topbar, toolbar, panel, metric tiles, empty state, mobile
1056
+ rail collapse.
1057
+ - Re-skinned primitives, navigation (dot active indicator), and the
1058
+ semantic typography/eyebrows.
1059
+ - Renamed `theme.css` → `tokens.css`; dropped the `components.css`
1060
+ indirection; `core.css` now bundles the full set; `index.css` =
1061
+ core + responsive. `package.json` exports updated accordingly.
1062
+ - Doto fonts vendored into `fonts/` as the canonical home.
1063
+ - Added `demo/index.html` kitchen sink.