@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.
- package/CHANGELOG.md +77 -0
- package/MIGRATIONS.json +14 -0
- package/README.md +14 -6
- package/behaviors/dialog.d.ts.map +1 -1
- package/behaviors/dialog.js +14 -0
- package/behaviors/modal.d.ts +4 -0
- package/behaviors/modal.d.ts.map +1 -1
- package/behaviors/modal.js +4 -0
- package/behaviors/splitter.d.ts +2 -0
- package/behaviors/splitter.d.ts.map +1 -1
- package/behaviors/splitter.js +15 -1
- package/behaviors/theme.d.ts +3 -2
- package/behaviors/theme.d.ts.map +1 -1
- package/behaviors/theme.js +10 -6
- package/bin/bronto-ui-check.mjs +286 -0
- package/classes/classes.json +7 -0
- package/css/disclosure.css +10 -0
- package/css/dots.css +43 -18
- package/css/feedback.css +35 -0
- package/css/report.css +0 -40
- package/css/site.css +10 -0
- package/dist/bronto.css +1 -1
- package/dist/css/disclosure.css +1 -1
- package/dist/css/dots.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/report-kit.css +1 -1
- package/dist/css/report.css +1 -1
- package/dist/css/site.css +1 -1
- package/docs/adr/0004-prune-unused-adapters.md +34 -0
- package/docs/architecture.md +14 -10
- package/docs/command.md +18 -4
- package/docs/migrations/0.6-to-0.7.md +85 -0
- package/docs/package-contract.md +10 -5
- package/docs/reporting.md +8 -8
- package/docs/stability.md +10 -9
- package/docs/theming.md +49 -5
- package/docs/usage.md +57 -17
- package/docs/workbench.md +16 -2
- package/llms.txt +7 -3
- package/package.json +13 -3
- package/qwik/index.d.ts.map +1 -1
- package/qwik/index.js +4 -0
- package/react/index.d.ts.map +1 -1
- package/react/index.js +4 -0
- package/solid/index.d.ts.map +1 -1
- package/solid/index.js +4 -0
- package/svelte/index.d.ts.map +1 -1
- package/svelte/index.js +4 -0
- package/tokens/tokens.dtcg.json +2508 -399
- package/vue/index.d.ts.map +1 -1
- 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
|
[](https://www.npmjs.com/package/@ponchia/ui)
|
|
4
4
|
[](https://www.npmjs.com/package/@ponchia/ui#provenance)
|
|
5
5
|
[](https://github.com/Ponchia/bronto-ui/blob/main/package.json)
|
|
6
|
-
[](https://github.com/Ponchia/bronto-ui/blob/main/scripts/check-dist.mjs)
|
|
7
7
|
[](https://github.com/Ponchia/bronto-ui/actions/workflows/ci.yml)
|
|
8
8
|
[](https://scorecard.dev/viewer/?uri=github.com/Ponchia/bronto-ui)
|
|
9
9
|
[](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.
|
|
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
|
+
**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,
|
|
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`,
|
|
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,
|
|
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"}
|
package/behaviors/dialog.js
CHANGED
|
@@ -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 });
|
package/behaviors/modal.d.ts
CHANGED
|
@@ -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
|
*/
|
package/behaviors/modal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"AAmQA;;;GAGG;AAEH
|
|
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"}
|
package/behaviors/modal.js
CHANGED
|
@@ -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
|
*/
|
package/behaviors/splitter.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/behaviors/splitter.js
CHANGED
|
@@ -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.
|
package/behaviors/theme.d.ts
CHANGED
|
@@ -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>
|
|
26
|
-
* racing the click handler.
|
|
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
|
package/behaviors/theme.d.ts.map
CHANGED
|
@@ -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
|
|
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"}
|
package/behaviors/theme.js
CHANGED
|
@@ -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>
|
|
62
|
-
* racing the click handler.
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/classes/classes.json
CHANGED
|
@@ -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",
|
package/css/disclosure.css
CHANGED
|
@@ -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 {
|