@ponchia/ui 0.6.12 → 0.7.0

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 (51) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/MIGRATIONS.json +14 -0
  3. package/README.md +14 -6
  4. package/behaviors/dialog.d.ts.map +1 -1
  5. package/behaviors/dialog.js +14 -0
  6. package/behaviors/modal.d.ts +4 -0
  7. package/behaviors/modal.d.ts.map +1 -1
  8. package/behaviors/modal.js +4 -0
  9. package/behaviors/splitter.d.ts +2 -0
  10. package/behaviors/splitter.d.ts.map +1 -1
  11. package/behaviors/splitter.js +15 -1
  12. package/behaviors/theme.d.ts +3 -2
  13. package/behaviors/theme.d.ts.map +1 -1
  14. package/behaviors/theme.js +10 -6
  15. package/bin/bronto-ui-check.mjs +286 -0
  16. package/classes/classes.json +7 -0
  17. package/css/disclosure.css +10 -0
  18. package/css/dots.css +43 -18
  19. package/css/feedback.css +35 -0
  20. package/css/report.css +0 -40
  21. package/css/site.css +10 -0
  22. package/dist/bronto.css +1 -1
  23. package/dist/css/disclosure.css +1 -1
  24. package/dist/css/dots.css +1 -1
  25. package/dist/css/feedback.css +1 -1
  26. package/dist/css/report-kit.css +1 -1
  27. package/dist/css/report.css +1 -1
  28. package/dist/css/site.css +1 -1
  29. package/docs/adr/0004-prune-unused-adapters.md +34 -0
  30. package/docs/architecture.md +14 -10
  31. package/docs/command.md +18 -4
  32. package/docs/migrations/0.6-to-0.7.md +85 -0
  33. package/docs/package-contract.md +10 -5
  34. package/docs/reporting.md +8 -8
  35. package/docs/stability.md +10 -9
  36. package/docs/theming.md +49 -5
  37. package/docs/usage.md +57 -17
  38. package/docs/workbench.md +16 -2
  39. package/llms.txt +7 -3
  40. package/package.json +13 -3
  41. package/qwik/index.d.ts.map +1 -1
  42. package/qwik/index.js +4 -0
  43. package/react/index.d.ts.map +1 -1
  44. package/react/index.js +4 -0
  45. package/solid/index.d.ts.map +1 -1
  46. package/solid/index.js +4 -0
  47. package/svelte/index.d.ts.map +1 -1
  48. package/svelte/index.js +4 -0
  49. package/tokens/tokens.dtcg.json +2508 -399
  50. package/vue/index.d.ts.map +1 -1
  51. package/vue/index.js +4 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,83 @@
5
5
  |> `^0` / `*` wildcard does **not** protect you. See README → Versioning, and
6
6
  |> the deprecation policy in CONTRIBUTING.md.
7
7
 
8
+ ## 0.7.0 — 2026-07-20
9
+
10
+ A consumer-first contract-hardening release. Ten real applications and tools,
11
+ plus a web-platform and design-system landscape review, found more value in
12
+ repairing and pruning the existing surface than in adding components.
13
+
14
+ ### BREAKING
15
+
16
+ - **DTCG 2025.10 values.** `tokens.dtcg.json` now emits portable structured
17
+ color, dimension, duration, numeric, and cubic-bezier values. It no longer
18
+ emits CSS strings for typed values or `$value: null` placeholders. Derived
19
+ colors are resolved per light/dark theme; the original CSS expression remains
20
+ under `$extensions["com.ponchia.css"].authoredValue`. CSS-only shadow
21
+ expressions and em-based letter-spacing remain in `tokens.json` rather than
22
+ pretending to be portable DTCG values; the DTCG root extension lists those
23
+ deliberate omissions. Update JSON readers using
24
+ [`docs/migrations/0.6-to-0.7.md`](docs/migrations/0.6-to-0.7.md).
25
+
26
+ ### Added
27
+
28
+ - **Consumer contract checker.** The zero-dependency `bronto-ui-check` binary
29
+ scans consumer source for literal `ui-*` classes absent from `classes.json`
30
+ and unresolved Bronto-like `var(--*)` references. It understands local token
31
+ definitions, strips source comments, ignores Markdown prose and build/vendor
32
+ directories, supports explicit allowlists, and can emit JSON.
33
+ - **Non-drag splitter controls.** Buttons inside a splitter can use
34
+ `data-bronto-splitter-adjust="-10"` / `"10"` to change the first pane by a
35
+ signed percentage-point delta. This gives pointer users the same resize
36
+ function without requiring a dragging gesture.
37
+ - **DTCG semantic gate.** `check:dtcg` validates every emitted typed value and
38
+ rejects null placeholders or malformed structured values before publication.
39
+
40
+ ### Changed
41
+
42
+ - **Meter ownership.** The already-public `ui-meter__row`, `__label`, and
43
+ `__value` styling moves from optional `report.css` into core `feedback.css`.
44
+ Core consumers now receive the layout the public class contract promised;
45
+ report-kit output remains visually unchanged. Together with the explicit
46
+ 24×24px coarse-pointer utility-link floors below, this intentionally adds
47
+ 1,121 B raw / 164 B gzip to the default bundle versus 0.6.12 (now 89.9 kB raw /
48
+ 15.4 kB gzip) and raises the hard budget only enough to admit those contracts.
49
+ - **Release evidence.** CI and release documentation now treat real-consumer
50
+ literal validation, packed-tarball upgrades, and payload reporting as the
51
+ evidence for 1.0 readiness.
52
+
53
+ ### Fixed
54
+
55
+ - **Live dot composition.** `ui-dot--live` now describes motion only. A
56
+ standalone live dot defaults to success, while an explicit accent, success,
57
+ warning, danger, or info tone controls both the dot and its pulse ring,
58
+ including forced-colors mode.
59
+ - **OS theme synchronization.** An OS `prefers-color-scheme` change now emits
60
+ the existing `bronto:themechange` event when no explicit theme is set, keeping
61
+ consumer-rendered charts, icons, and labels synchronized with the CSS theme.
62
+ - **Named dialogs.** `initDialog()` now warns once when an opened native dialog
63
+ has no `aria-label`, `aria-labelledby`, or `title`. All shipped framework
64
+ examples provide a name.
65
+ - **Touch target floors.** Breadcrumb and footer utility links reach the WCAG
66
+ 2.5.8 24 CSS-pixel floor under a coarse pointer.
67
+
68
+ ### Deprecated
69
+
70
+ - **Framework adapter subpaths.** `@ponchia/ui/react`, `/solid`, `/qwik`,
71
+ `/svelte`, and `/vue` remain compatible in 0.7 but are scheduled for removal
72
+ no earlier than 0.8. None of the ten inspected consumers imports them; use the
73
+ vanilla behavior initializers in each framework's mount/cleanup lifecycle.
74
+ - **Controlled non-native modal.** `initModal()`, its adapter bindings, and
75
+ `data-bronto-modal` remain compatible in 0.7 but are scheduled for removal no
76
+ earlier than 0.8. Prefer native `<dialog>` with `initDialog()`.
77
+
78
+ ### Verified
79
+
80
+ - Unit, type, generated-artifact, package, schema, DTCG, class/token contract,
81
+ browser, accessibility, packed-example, and real-consumer checks are release
82
+ gates. The release evidence records consumer classes and imported surfaces
83
+ without exposing private project details.
84
+
8
85
  ## 0.6.12 — 2026-07-10
9
86
 
10
87
  A stabilization patch. It changes no public class, token name, behavior name,
package/MIGRATIONS.json CHANGED
@@ -101,6 +101,20 @@
101
101
  }
102
102
  ],
103
103
  "codemod": "No codemod — the .ui-chart* renderer has no replacement class. rg -l 'ui-chart\\\\b|ui-chart__' to find call sites, then rebuild each as a Vega-Lite chart (@ponchia/ui/vega) or a token-themed inline <svg>. See docs/vega.md."
104
+ },
105
+ {
106
+ "from": "0.6",
107
+ "to": "0.7",
108
+ "summary": "Contract-hardening release. tokens.dtcg.json now uses conforming DTCG 2025.10 structured values instead of CSS strings/null placeholders. Splitters gain an additive non-drag pointer path. Framework adapters and initModal are deprecated, but remain compatible throughout 0.7.",
109
+ "safe": [],
110
+ "manual": [
111
+ {
112
+ "old": "tokens.dtcg.json scalar color/dimension values and $value:null CSS-expression placeholders",
113
+ "new": "DTCG 2025.10 structured typed values resolved per light/dark theme",
114
+ "note": "Update tooling to read color objects ({ colorSpace, components, alpha, optional hex }) and dimension objects ({ value, unit }). Read tokens.json when the authored CSS var()/color-mix() expression is required. CSS-only shadow expressions are not part of the portable DTCG projection."
115
+ }
116
+ ],
117
+ "codemod": "No source codemod. Update the DTCG reader at its JSON access boundary, then run npx --no-install bronto-ui-check against consumer source. See docs/migrations/0.6-to-0.7.md."
104
118
  }
105
119
  ]
106
120
  }
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@ponchia/ui?logo=npm)](https://www.npmjs.com/package/@ponchia/ui)
4
4
  [![npm provenance](https://img.shields.io/badge/npm-provenance-blue?logo=npm)](https://www.npmjs.com/package/@ponchia/ui#provenance)
5
5
  [![runtime deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen)](https://github.com/Ponchia/bronto-ui/blob/main/package.json)
6
- [![default CSS](https://img.shields.io/badge/default%20CSS-~89kB%20%2F%20~15kB%20gzip-informational)](https://github.com/Ponchia/bronto-ui/blob/main/scripts/check-dist.mjs)
6
+ [![default CSS](https://img.shields.io/badge/default%20CSS-~90kB%20%2F%20~15kB%20gzip-informational)](https://github.com/Ponchia/bronto-ui/blob/main/scripts/check-dist.mjs)
7
7
  [![CI](https://github.com/Ponchia/bronto-ui/actions/workflows/ci.yml/badge.svg)](https://github.com/Ponchia/bronto-ui/actions/workflows/ci.yml)
8
8
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Ponchia/bronto-ui/badge)](https://scorecard.dev/viewer/?uri=github.com/Ponchia/bronto-ui)
9
9
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Ponchia/bronto-ui/blob/main/LICENSE)
@@ -77,15 +77,16 @@ for the thesis.
77
77
  npm i @ponchia/ui
78
78
  ```
79
79
 
80
- Or drop it in with no build step, straight from a CDN:
80
+ Or drop it in with no build step, straight from a CDN (replace the version only
81
+ when deliberately upgrading across a breaking pre-1.0 minor):
81
82
 
82
83
  ```html
83
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.12/dist/bronto.css">
84
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.7.0/dist/bronto.css">
84
85
  ```
85
86
 
86
87
  ## Quick start
87
88
 
88
- **1. Load the CSS.** One flattened, minified default CSS bundle — the standard component set, one request (~89 kB raw / ~15 kB gzip) — that is `dist/bronto.css`, not the whole package tarball:
89
+ **1. Load the CSS.** One flattened, minified default CSS bundle — the standard component set, one request (~90 kB raw / ~15 kB gzip) — that is `dist/bronto.css`, not the whole package tarball:
89
90
 
90
91
  ```css
91
92
  @import '@ponchia/ui'; /* via a bundler */
@@ -187,7 +188,13 @@ Not an afterthought — a gate. Every contractual token pairing has a declared W
187
188
 
188
189
  ## Works with anything
189
190
 
190
- The CSS is the framework, so it works with React, Svelte/SvelteKit, Astro, Vue, Solid, Qwik or plain HTML — there's no component runtime to adopt. The optional `classes` and `behaviors` entrypoints pull in **no** UI framework and are SSR-safe. For React, Solid and Qwik there are also **optional thin bindings** — `@ponchia/ui/react`, `@ponchia/ui/solid` and `@ponchia/ui/qwik` wrap the behaviors as hooks (`useDialog`, `useToast`, …); `react`/`solid-js`/`@builder.io/qwik` are optional peer deps. Svelte and Vue get dependency-free lifecycle adapters too: `@ponchia/ui/svelte` exports actions, and `@ponchia/ui/vue` exports directives/plugin helpers over the same behavior layer.
191
+ The CSS is the framework, so it works with React, Svelte/SvelteKit, Astro, Vue,
192
+ Solid, Qwik or plain HTML — there's no component runtime to adopt. The optional
193
+ `classes` and framework-neutral `behaviors` entrypoints pull in **no** UI
194
+ framework and are SSR-safe. The React, Solid, Qwik, Svelte, and Vue lifecycle
195
+ adapter subpaths remain compatible in 0.7 but are deprecated for removal no
196
+ earlier than 0.8; initialize vanilla behaviors in the framework's ordinary
197
+ mount/cleanup lifecycle instead.
191
198
 
192
199
  Per-framework getting-started guides + runnable example apps live in the repo:
193
200
 
@@ -204,7 +211,8 @@ Per-framework getting-started guides + runnable example apps live in the repo:
204
211
 
205
212
  ## Extras
206
213
 
207
- - **Tokens as data** — `import tokens, { themeColor, cssVars } from '@ponchia/ui/tokens'` (plus `tokens.json`, W3C DTCG `tokens.dtcg.json`, `tokens/resolved.json` for concrete values in canvas/SVG/MapLibre, and `tokens/figma.variables.json` for local Figma Variables import/sync scripts).
214
+ - **Tokens as data** — `import tokens, { themeColor, cssVars } from '@ponchia/ui/tokens'` (plus authored `tokens.json`, a structured DTCG 2025.10 `tokens.dtcg.json`, `tokens/resolved.json` for concrete values in canvas/SVG/MapLibre, and `tokens/figma.variables.json` for local Figma Variables import/sync scripts).
215
+ - **Consumer contract checker** — after an upgrade, run `npx --no-install bronto-ui-check src` to catch unknown literal `ui-*` classes and unresolved Bronto token references in code, style, and template sources before build or deployment. Comments, generated/vendor directories, and Markdown prose are excluded.
208
216
  - **Chart colours for dashboards** — `import charts from '@ponchia/ui/charts.json' with { type: 'json' }` in Node ESM, or the same path through a bundler JSON import (resolved hex per theme; series 1 = your accent) plus the opt-in `@ponchia/ui/css/dataviz.css`.
209
217
  - **Static reports for LLMs** — add `@ponchia/ui/css/report-kit.css` for the complete report vocabulary, or `@ponchia/ui/css/report.css` plus the specific leaves a smaller report needs. Sidecar claim/source contracts can validate against `@ponchia/ui/schemas/report-claims.v1.schema.json`. Full cookbook: `docs/reporting.md`.
210
218
  - **Modern-platform motion** — overlays (modal/drawer/popover), toasts and the `<details>` accordion animate **in and out** with zero JS (`@starting-style` + `allow-discrete`, `::details-content` + `interpolate-size`). Progressive-enhancement extras: `.ui-scroll-progress` / `.ui-scroll-reveal` (scroll-driven, no JS) and `.ui-vt` for View Transitions. All degrade to a static end-state and respect `prefers-reduced-motion`. For smooth **cross-document** navigations, add the document-global one-liner to your own top-level (unlayered) CSS: `@view-transition { navigation: auto; }`.
@@ -1 +1 @@
1
- {"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["dialog.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,sCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAgG3C"}
1
+ {"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["dialog.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,sCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA8G3C"}
@@ -26,8 +26,21 @@ export function initDialog({ root } = {}) {
26
26
  if (!doc) return noop;
27
27
  const managedDialogs = new Set();
28
28
  const focusRestorers = new Map();
29
+ const warnedNameless = new WeakSet();
29
30
  const canManageDialog = (dlg, origin) => host.contains(origin) || managedDialogs.has(dlg);
30
31
 
32
+ const warnIfNameless = (dlg) => {
33
+ const named =
34
+ dlg.hasAttribute('aria-label') ||
35
+ dlg.hasAttribute('aria-labelledby') ||
36
+ dlg.hasAttribute('title');
37
+ if (named || warnedNameless.has(dlg) || typeof console === 'undefined') return;
38
+ warnedNameless.add(dlg);
39
+ console.warn(
40
+ `[bronto] initDialog(): dialog #${dlg.id || '(without id)'} has no accessible name — add aria-label, aria-labelledby, or title so it is announced as a named dialog.`,
41
+ );
42
+ };
43
+
31
44
  const openFrom = (opener) => {
32
45
  const dlg = byIdInHost(host, opener.getAttribute('data-bronto-open'));
33
46
  if (!dlg || typeof dlg.showModal !== 'function' || dlg.open) return false;
@@ -47,6 +60,7 @@ export function initDialog({ root } = {}) {
47
60
  } catch {
48
61
  return false;
49
62
  }
63
+ warnIfNameless(dlg);
50
64
  managedDialogs.add(dlg);
51
65
  focusRestorers.set(dlg, restoreFocus);
52
66
  dlg.addEventListener('close', restoreFocus, { once: true });
@@ -28,6 +28,10 @@
28
28
  *
29
29
  * SSR-safe, idempotent per modal; returns a cleanup function.
30
30
  *
31
+ * @deprecated Use a native `<dialog>` with `initDialog()`. This controlled
32
+ * non-dialog path remains compatible in 0.7 and is scheduled for removal no
33
+ * earlier than 0.8 because no real consumer adopted it.
34
+ *
31
35
  * @param {import('./internal.js').DelegateOpts} [opts]
32
36
  * @returns {import('./internal.js').Cleanup}
33
37
  */
@@ -1 +1 @@
1
- {"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"AAmQA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6E3C;;;;;YA3Ga,QAAQ"}
1
+ {"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"AAmQA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6E3C;;;;;YA/Ga,QAAQ"}
@@ -288,6 +288,10 @@ function targetIsOwnedPopover(target, modal, state) {
288
288
  *
289
289
  * SSR-safe, idempotent per modal; returns a cleanup function.
290
290
  *
291
+ * @deprecated Use a native `<dialog>` with `initDialog()`. This controlled
292
+ * non-dialog path remains compatible in 0.7 and is scheduled for removal no
293
+ * earlier than 0.8 because no real consumer adopted it.
294
+ *
291
295
  * @param {import('./internal.js').DelegateOpts} [opts]
292
296
  * @returns {import('./internal.js').Cleanup}
293
297
  */
@@ -4,6 +4,8 @@
4
4
  * (`role="separator"`). The behavior keeps `--splitter-pos` and
5
5
  * `aria-valuenow` in sync for keyboard and pointer resizing, then dispatches
6
6
  * `bronto:splitter:resize` with `{ value, orientation }`.
7
+ * Buttons inside the splitter may set `data-bronto-splitter-adjust="-10"` or
8
+ * `"10"` to provide the required single-pointer, non-drag resize path.
7
9
  *
8
10
  * Bronto owns the control affordance only. The host owns pane content,
9
11
  * persistence, min/max policy, collapse behavior, and any saved layout state.
@@ -1 +1 @@
1
- {"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["splitter.js"],"names":[],"mappings":"AA6OA;;;;;;;;;;;;;GAaG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAU3C;;;;;WAvPa,MAAM;;;;iBACN,UAAU,GAAG,YAAY"}
1
+ {"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["splitter.js"],"names":[],"mappings":"AAyPA;;;;;;;;;;;;;;;GAeG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAU3C;;;;;WApQa,MAAM;;;;iBACN,UAAU,GAAG,YAAY"}
@@ -1,7 +1,8 @@
1
- import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, collectHosts, closestSafe } from './internal.js';
2
2
 
3
3
  const SELECTOR = '[data-bronto-splitter]';
4
4
  const HANDLE_SELECTOR = '.ui-splitter__handle';
5
+ const ADJUST_SELECTOR = '[data-bronto-splitter-adjust]';
5
6
  const DEFAULT_MIN = 20;
6
7
  const DEFAULT_MAX = 80;
7
8
  const DEFAULT_VALUE = 50;
@@ -218,11 +219,22 @@ function wireSplitter(splitter) {
218
219
  splitter.ownerDocument.addEventListener('pointercancel', onPointerUp);
219
220
  };
220
221
 
222
+ const onClick = (event) => {
223
+ const control = closestSafe(event.target, ADJUST_SELECTOR);
224
+ if (!control || control.closest(SELECTOR) !== splitter) return;
225
+ const delta = num(control.getAttribute('data-bronto-splitter-adjust'), Number.NaN);
226
+ if (!Number.isFinite(delta) || delta === 0) return;
227
+ event.preventDefault();
228
+ apply(value + delta);
229
+ };
230
+
221
231
  handle.addEventListener('keydown', onKeydown);
222
232
  handle.addEventListener('pointerdown', onPointerDown);
233
+ splitter.addEventListener('click', onClick);
223
234
  return () => {
224
235
  handle.removeEventListener('keydown', onKeydown);
225
236
  handle.removeEventListener('pointerdown', onPointerDown);
237
+ splitter.removeEventListener('click', onClick);
226
238
  splitter.ownerDocument.removeEventListener('pointermove', onPointerMove);
227
239
  splitter.ownerDocument.removeEventListener('pointerup', onPointerUp);
228
240
  splitter.ownerDocument.removeEventListener('pointercancel', onPointerUp);
@@ -241,6 +253,8 @@ function wireSplitter(splitter) {
241
253
  * (`role="separator"`). The behavior keeps `--splitter-pos` and
242
254
  * `aria-valuenow` in sync for keyboard and pointer resizing, then dispatches
243
255
  * `bronto:splitter:resize` with `{ value, orientation }`.
256
+ * Buttons inside the splitter may set `data-bronto-splitter-adjust="-10"` or
257
+ * `"10"` to provide the required single-pointer, non-drag resize path.
244
258
  *
245
259
  * Bronto owns the control affordance only. The host owns pane content,
246
260
  * persistence, min/max policy, collapse behavior, and any saved layout state.
@@ -22,8 +22,9 @@ export function applyStoredTheme({ storageKey, root }?: ApplyThemeOpts): void;
22
22
  * persists to localStorage, and **always** sets `data-theme` on <html>
23
23
  * (a theme is document-global). State is reflected via `aria-pressed`
24
24
  * and a `bronto:themechange` CustomEvent ({ detail: { theme } }) is
25
- * dispatched on <html> so consumers can sync their own UI without
26
- * racing the click handler. A control may set
25
+ * dispatched on <html> for both explicit changes and OS preference changes,
26
+ * so consumers can sync their own UI without racing the click/media handler.
27
+ * A control may set
27
28
  * `data-bronto-theme-toggle="dark"` to force a specific theme.
28
29
  *
29
30
  * `root` scopes event delegation and which controls are queried/reflected
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["theme.js"],"names":[],"mappings":"AAuBA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wDAHW,cAAc,GACZ,IAAI,CAahB;AAED;;;;;;;;;;;;;;GAcG;AACH,uDAHW,gBAAgB,GAAG,OAAO,eAAe,EAAE,YAAY,GACrD,OAAO,eAAe,EAAE,OAAO,CAgH3C;;;;;;;;;;6BAzJY,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;WAIpC,OAAO,GAAG,MAAM"}
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["theme.js"],"names":[],"mappings":"AAuBA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wDAHW,cAAc,GACZ,IAAI,CAahB;AAED;;;;;;;;;;;;;;;GAeG;AACH,uDAHW,gBAAgB,GAAG,OAAO,eAAe,EAAE,YAAY,GACrD,OAAO,eAAe,EAAE,OAAO,CAmH3C;;;;;;;;;;6BA7JY,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;WAIpC,OAAO,GAAG,MAAM"}
@@ -58,8 +58,9 @@ export function applyStoredTheme({ storageKey = 'bronto-theme', root } = {}) {
58
58
  * persists to localStorage, and **always** sets `data-theme` on <html>
59
59
  * (a theme is document-global). State is reflected via `aria-pressed`
60
60
  * and a `bronto:themechange` CustomEvent ({ detail: { theme } }) is
61
- * dispatched on <html> so consumers can sync their own UI without
62
- * racing the click handler. A control may set
61
+ * dispatched on <html> for both explicit changes and OS preference changes,
62
+ * so consumers can sync their own UI without racing the click/media handler.
63
+ * A control may set
63
64
  * `data-bronto-theme-toggle="dark"` to force a specific theme.
64
65
  *
65
66
  * `root` scopes event delegation and which controls are queried/reflected
@@ -98,6 +99,11 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
98
99
  return prefersDark() ? 'dark' : 'light';
99
100
  };
100
101
 
102
+ const emitThemeChange = (theme) => {
103
+ const ThemeEvent = doc.defaultView?.CustomEvent ?? CustomEvent;
104
+ docEl.dispatchEvent(new ThemeEvent('bronto:themechange', { detail: { theme }, bubbles: true }));
105
+ };
106
+
101
107
  const reflect = () => {
102
108
  const c = current();
103
109
  collectHosts(host, '[data-bronto-theme-toggle]').forEach((el) => {
@@ -134,7 +140,7 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
134
140
  clearSchemeListener();
135
141
  return;
136
142
  }
137
- reflect();
143
+ emitThemeChange(current());
138
144
  }
139
145
 
140
146
  const onThemeChange = () => {
@@ -156,9 +162,7 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
156
162
  /* storage blocked — theme still applies for this session */
157
163
  }
158
164
  reflect();
159
- docEl.dispatchEvent(
160
- new CustomEvent('bronto:themechange', { detail: { theme: next }, bubbles: true }),
161
- );
165
+ emitThemeChange(next);
162
166
  };
163
167
 
164
168
  return bindOnce(host, 'themeToggle', () => {
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+ /** Validate literal Bronto classes and CSS custom-property references in a consumer. */
3
+ import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
4
+ import { extname, resolve, relative, dirname } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+ const classesManifest = JSON.parse(
9
+ readFileSync(resolve(packageRoot, 'classes/classes.json'), 'utf8'),
10
+ );
11
+ const tokensManifest = JSON.parse(readFileSync(resolve(packageRoot, 'tokens/index.json'), 'utf8'));
12
+
13
+ const SOURCE_EXTENSIONS = new Set([
14
+ '.astro',
15
+ '.css',
16
+ '.html',
17
+ '.js',
18
+ '.jsx',
19
+ '.mjs',
20
+ '.svelte',
21
+ '.ts',
22
+ '.tsx',
23
+ '.vue',
24
+ ]);
25
+ const SKIP_DIRS = new Set([
26
+ '.git',
27
+ '.astro',
28
+ '.next',
29
+ '.nuxt',
30
+ '.output',
31
+ '.pytest_cache',
32
+ '.svelte-kit',
33
+ '.tox',
34
+ '.vercel',
35
+ '.venv',
36
+ '.vite',
37
+ '__pycache__',
38
+ 'build',
39
+ 'coverage',
40
+ 'dist',
41
+ 'node_modules',
42
+ 'out',
43
+ 'playwright-report',
44
+ 'public',
45
+ 'storybook-static',
46
+ 'test-results',
47
+ 'vendor',
48
+ 'venv',
49
+ ]);
50
+
51
+ const knownClasses = new Set(classesManifest.classes);
52
+ // CSS generic font-family keywords share the ui-* prefix but are never class
53
+ // literals. Keep the scanner broad enough to catch JS-built class strings
54
+ // while excluding these standardized non-class identifiers.
55
+ const nonClassIdentifiers = new Set(['ui-monospace', 'ui-rounded', 'ui-sans-serif', 'ui-serif']);
56
+ const knownTokens = new Set(
57
+ Object.values(tokensManifest.cssVars).flatMap((group) => Object.keys(group)),
58
+ );
59
+ for (const property of classesManifest.customProperties) knownTokens.add(property.name);
60
+ for (const entry of readdirSync(resolve(packageRoot, 'dist/css'), { withFileTypes: true })) {
61
+ if (!entry.isFile() || extname(entry.name) !== '.css') continue;
62
+ const css = readFileSync(resolve(packageRoot, 'dist/css', entry.name), 'utf8');
63
+ for (const match of css.matchAll(/(--[a-z][\w-]*)\s*:/gi)) knownTokens.add(match[1]);
64
+ }
65
+ const reservedTokenPrefixes = new Set(
66
+ [...knownTokens].map((name) => `--${name.slice(2).split('-')[0]}`),
67
+ );
68
+ const LINE_COMMENT_EXTENSIONS = new Set([
69
+ '.astro',
70
+ '.html',
71
+ '.js',
72
+ '.jsx',
73
+ '.mjs',
74
+ '.svelte',
75
+ '.ts',
76
+ '.tsx',
77
+ '.vue',
78
+ ]);
79
+ const HTML_COMMENT_EXTENSIONS = new Set(['.astro', '.html', '.svelte', '.vue']);
80
+
81
+ const blankComment = (value) => value.replace(/[^\r\n]/g, ' ');
82
+
83
+ function quoteEnd(text, start, quote) {
84
+ for (let index = start + 1; index < text.length; index += 1) {
85
+ if (text[index] === '\\') index += 1;
86
+ else if (text[index] === quote) return index + 1;
87
+ }
88
+ return text.length;
89
+ }
90
+
91
+ function stripSlashComments(text, lineComments) {
92
+ let output = '';
93
+ let index = 0;
94
+ while (index < text.length) {
95
+ const char = text[index];
96
+ const next = text[index + 1];
97
+ if (char === '"' || char === "'" || char === '`') {
98
+ const end = quoteEnd(text, index, char);
99
+ output += text.slice(index, end);
100
+ index = end;
101
+ continue;
102
+ }
103
+ if (char === '/' && next === '*') {
104
+ const close = text.indexOf('*/', index + 2);
105
+ const end = close === -1 ? text.length : close + 2;
106
+ output += blankComment(text.slice(index, end));
107
+ index = end;
108
+ continue;
109
+ }
110
+ if (lineComments && char === '/' && next === '/') {
111
+ const newline = text.indexOf('\n', index + 2);
112
+ const end = newline === -1 ? text.length : newline;
113
+ output += blankComment(text.slice(index, end));
114
+ index = end;
115
+ continue;
116
+ }
117
+ output += char;
118
+ index += 1;
119
+ }
120
+ return output;
121
+ }
122
+
123
+ function sourceText(text, extension) {
124
+ const withoutHtml = HTML_COMMENT_EXTENSIONS.has(extension)
125
+ ? text.replace(/<!--[\s\S]*?-->/g, blankComment)
126
+ : text;
127
+ return stripSlashComments(withoutHtml, LINE_COMMENT_EXTENSIONS.has(extension));
128
+ }
129
+
130
+ function lineAt(text, index) {
131
+ return text.slice(0, index).split('\n').length;
132
+ }
133
+
134
+ function filesUnder(input) {
135
+ const absolute = resolve(input);
136
+ const stat = statSync(absolute);
137
+ if (stat.isFile()) return SOURCE_EXTENSIONS.has(extname(absolute)) ? [absolute] : [];
138
+ if (!stat.isDirectory()) return [];
139
+ const files = [];
140
+ for (const entry of readdirSync(absolute, { withFileTypes: true })) {
141
+ if (entry.isSymbolicLink()) continue;
142
+ const child = resolve(absolute, entry.name);
143
+ if (entry.isDirectory()) {
144
+ if (!SKIP_DIRS.has(entry.name)) files.push(...filesUnder(child));
145
+ } else if (entry.isFile() && SOURCE_EXTENSIONS.has(extname(entry.name))) {
146
+ files.push(child);
147
+ }
148
+ }
149
+ return files;
150
+ }
151
+
152
+ function tokenPrefix(name) {
153
+ return `--${name.slice(2).split('-')[0]}`;
154
+ }
155
+
156
+ export function checkPaths(inputs, { allowClasses = [], allowTokens = [] } = {}) {
157
+ const allowedClasses = new Set([...knownClasses, ...allowClasses]);
158
+ const allowedTokens = new Set([...knownTokens, ...allowTokens]);
159
+ const files = [...new Set(inputs.flatMap(filesUnder))].sort();
160
+ const documents = files.map((file) => {
161
+ const text = readFileSync(file, 'utf8');
162
+ return { file, text, scanned: sourceText(text, extname(file)) };
163
+ });
164
+ const locallyDefinedTokens = new Set();
165
+ for (const { scanned } of documents) {
166
+ for (const match of scanned.matchAll(/(--[a-z][\w-]*)\s*:/gi)) {
167
+ locallyDefinedTokens.add(match[1]);
168
+ }
169
+ }
170
+
171
+ const findings = [];
172
+ const seen = new Set();
173
+ const add = (finding) => {
174
+ const key = `${finding.kind}:${finding.file}:${finding.line}:${finding.name}`;
175
+ if (seen.has(key)) return;
176
+ seen.add(key);
177
+ findings.push(finding);
178
+ };
179
+
180
+ for (const { file, text, scanned } of documents) {
181
+ for (const match of scanned.matchAll(/(?<![/\w-])ui-[a-z0-9](?:[\w-]*[a-z0-9])?(?![\w/-])/gi)) {
182
+ if (!allowedClasses.has(match[0]) && !nonClassIdentifiers.has(match[0])) {
183
+ add({ kind: 'class', file, line: lineAt(text, match.index), name: match[0] });
184
+ }
185
+ }
186
+ for (const match of scanned.matchAll(/var\(\s*(--[a-z][\w-]*)/gi)) {
187
+ const name = match[1];
188
+ if (
189
+ reservedTokenPrefixes.has(tokenPrefix(name)) &&
190
+ !allowedTokens.has(name) &&
191
+ !locallyDefinedTokens.has(name)
192
+ ) {
193
+ add({ kind: 'token', file, line: lineAt(text, match.index), name });
194
+ }
195
+ }
196
+ }
197
+
198
+ return { files: files.length, findings };
199
+ }
200
+
201
+ function usage() {
202
+ return (
203
+ `Usage: bronto-ui-check [options] [path ...]\n\n` +
204
+ `Validate literal ui-* classes and unresolved Bronto-like var(--*) references.\n\n` +
205
+ `Options:\n` +
206
+ ` --allow-class NAME Allow one consumer-owned ui-* class (repeatable)\n` +
207
+ ` --allow-token NAME Allow one consumer-owned --* token (repeatable)\n` +
208
+ ` --json Print machine-readable JSON\n` +
209
+ ` --help Show this help\n`
210
+ );
211
+ }
212
+
213
+ function parseArgs(args) {
214
+ const options = { allowClasses: [], allowTokens: [], json: false, paths: [] };
215
+ for (let index = 0; index < args.length; index += 1) {
216
+ const arg = args[index];
217
+ if (arg === '--help') return { ...options, help: true };
218
+ if (arg === '--json') options.json = true;
219
+ else if (arg === '--allow-class') {
220
+ const value = args[++index];
221
+ if (!/^ui-[a-z0-9][\w-]*$/i.test(value || '')) {
222
+ throw new Error('--allow-class requires one ui-* class name');
223
+ }
224
+ options.allowClasses.push(value);
225
+ } else if (arg === '--allow-token') {
226
+ const value = args[++index];
227
+ if (!/^--[a-z][\w-]*$/i.test(value || '')) {
228
+ throw new Error('--allow-token requires one --* custom-property name');
229
+ }
230
+ options.allowTokens.push(value);
231
+ } else if (arg.startsWith('--')) throw new Error(`Unknown option: ${arg}`);
232
+ else options.paths.push(arg);
233
+ }
234
+ return options;
235
+ }
236
+
237
+ function main() {
238
+ let options;
239
+ try {
240
+ options = parseArgs(process.argv.slice(2));
241
+ } catch (error) {
242
+ console.error(error.message);
243
+ console.error(usage());
244
+ process.exitCode = 2;
245
+ return;
246
+ }
247
+ if (options.help) {
248
+ console.log(usage());
249
+ return;
250
+ }
251
+ const paths = options.paths.length ? options.paths : ['.'];
252
+ let result;
253
+ try {
254
+ result = checkPaths(paths, options);
255
+ } catch (error) {
256
+ console.error(`[bronto-ui-check] ${error.message}`);
257
+ process.exitCode = 2;
258
+ return;
259
+ }
260
+ const cwd = process.cwd();
261
+ const output = {
262
+ files: result.files,
263
+ findings: result.findings.map((finding) => ({
264
+ ...finding,
265
+ file: relative(cwd, finding.file) || '.',
266
+ })),
267
+ };
268
+ if (options.json) console.log(JSON.stringify(output, null, 2));
269
+ else if (output.findings.length) {
270
+ for (const finding of output.findings) {
271
+ console.error(
272
+ `${finding.file}:${finding.line} unknown Bronto ${finding.kind} ${finding.name}`,
273
+ );
274
+ }
275
+ console.error(
276
+ `[bronto-ui-check] ${output.findings.length} finding(s) in ${output.files} source file(s)`,
277
+ );
278
+ } else {
279
+ console.log(`[bronto-ui-check] ${output.files} source file(s) match the shipped contract`);
280
+ }
281
+ if (output.findings.length) process.exitCode = 1;
282
+ }
283
+
284
+ // npm installs bins as symlinks in node_modules/.bin. Compare their real path
285
+ // so the CLI runs both through that public entrypoint and by its package path.
286
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) main();
@@ -2791,6 +2791,13 @@
2791
2791
  "behavior": "initSplitter",
2792
2792
  "note": "keyboard + pointer ARIA window-splitter behavior; updates --splitter-pos and aria-valuenow, then emits bronto:splitter:resize. The host owns persistence and pane state."
2793
2793
  },
2794
+ {
2795
+ "name": "data-bronto-splitter-adjust",
2796
+ "on": "a button inside a .ui-splitter host",
2797
+ "value": "signed percentage-point delta, for example -10 or 10",
2798
+ "behavior": "initSplitter",
2799
+ "note": "single-pointer non-drag alternative to moving the separator; activation clamps to aria-valuemin/aria-valuemax and emits bronto:splitter:resize."
2800
+ },
2794
2801
  {
2795
2802
  "name": "data-bronto-source-ref",
2796
2803
  "on": "a button/control that references a source card",
@@ -272,6 +272,16 @@
272
272
  }
273
273
  }
274
274
 
275
+ @media (pointer: coarse) {
276
+ .ui-breadcrumb__item a {
277
+ align-items: center;
278
+ display: inline-flex;
279
+ justify-content: center;
280
+ min-block-size: max(24px, 1.6rem);
281
+ min-inline-size: max(24px, 1.6rem);
282
+ }
283
+ }
284
+
275
285
  /* --- Pagination --- */
276
286
 
277
287
  .ui-pagination {