@ponchia/ui 0.6.6 → 0.6.7

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 (108) hide show
  1. package/CHANGELOG.md +105 -6
  2. package/README.md +36 -23
  3. package/behaviors/carousel.d.ts.map +1 -1
  4. package/behaviors/carousel.js +3 -0
  5. package/behaviors/dialog.d.ts.map +1 -1
  6. package/behaviors/dialog.js +14 -8
  7. package/behaviors/forms.d.ts.map +1 -1
  8. package/behaviors/forms.js +11 -5
  9. package/behaviors/index.d.ts +2 -0
  10. package/behaviors/index.d.ts.map +1 -1
  11. package/behaviors/index.js +2 -0
  12. package/behaviors/internal.d.ts +2 -1
  13. package/behaviors/internal.d.ts.map +1 -1
  14. package/behaviors/internal.js +23 -3
  15. package/behaviors/legend.d.ts.map +1 -1
  16. package/behaviors/legend.js +41 -9
  17. package/behaviors/splitter.d.ts +26 -0
  18. package/behaviors/splitter.d.ts.map +1 -0
  19. package/behaviors/splitter.js +200 -0
  20. package/behaviors/table.js +3 -3
  21. package/behaviors/theme.js +2 -2
  22. package/classes/classes.json +230 -4
  23. package/classes/index.d.ts +49 -1
  24. package/classes/index.js +56 -1
  25. package/classes/vscode.css-custom-data.json +1 -1
  26. package/css/analytical.css +3 -1
  27. package/css/app.css +4 -4
  28. package/css/clamp.css +92 -0
  29. package/css/figure.css +102 -0
  30. package/css/highlights.css +50 -0
  31. package/css/interval.css +90 -0
  32. package/css/primitives.css +2 -3
  33. package/css/report-kit.css +38 -0
  34. package/css/report.css +23 -4
  35. package/css/sidenote.css +12 -2
  36. package/css/site.css +2 -1
  37. package/css/sources.css +5 -0
  38. package/css/state.css +120 -1
  39. package/css/table.css +4 -0
  40. package/css/tokens.css +9 -9
  41. package/css/workbench.css +101 -8
  42. package/dist/bronto.css +1 -1
  43. package/dist/css/analytical.css +1 -1
  44. package/dist/css/app.css +1 -1
  45. package/dist/css/clamp.css +1 -0
  46. package/dist/css/figure.css +1 -0
  47. package/dist/css/highlights.css +1 -0
  48. package/dist/css/interval.css +1 -0
  49. package/dist/css/primitives.css +1 -1
  50. package/dist/css/report-kit.css +1 -0
  51. package/dist/css/report.css +1 -1
  52. package/dist/css/sidenote.css +1 -1
  53. package/dist/css/site.css +1 -1
  54. package/dist/css/sources.css +1 -1
  55. package/dist/css/state.css +1 -1
  56. package/dist/css/table.css +1 -1
  57. package/dist/css/tokens.css +1 -1
  58. package/dist/css/workbench.css +1 -1
  59. package/docs/adr/0002-scope-and-2026-baseline.md +1 -1
  60. package/docs/architecture.md +67 -43
  61. package/docs/clamp.md +49 -0
  62. package/docs/contrast.md +34 -24
  63. package/docs/d2.md +37 -0
  64. package/docs/figure.md +71 -0
  65. package/docs/frontier-primitives.md +25 -24
  66. package/docs/highlights.md +52 -0
  67. package/docs/interop/tailwind.md +148 -0
  68. package/docs/interval.md +55 -0
  69. package/docs/legends.md +3 -2
  70. package/docs/mermaid.md +6 -0
  71. package/docs/migrations/0.2-to-0.3.md +80 -0
  72. package/docs/migrations/0.3-to-0.4.md +48 -0
  73. package/docs/migrations/0.4-to-0.5.md +96 -0
  74. package/docs/migrations/0.5-to-0.6.md +82 -0
  75. package/docs/package-contract.md +40 -2
  76. package/docs/reference.md +78 -5
  77. package/docs/reporting.md +113 -58
  78. package/docs/sidenote.md +7 -1
  79. package/docs/sources.md +1 -1
  80. package/docs/stability.md +5 -3
  81. package/docs/state.md +67 -10
  82. package/docs/theming.md +10 -2
  83. package/docs/usage.md +31 -11
  84. package/docs/workbench.md +59 -18
  85. package/llms.txt +80 -12
  86. package/package.json +66 -6
  87. package/qwik/index.d.ts +1 -0
  88. package/qwik/index.d.ts.map +1 -1
  89. package/qwik/index.js +26 -21
  90. package/react/index.d.ts +1 -0
  91. package/react/index.d.ts.map +1 -1
  92. package/react/index.js +4 -1
  93. package/schemas/report-claims.v1.schema.json +137 -0
  94. package/solid/index.d.ts +2 -0
  95. package/solid/index.d.ts.map +1 -1
  96. package/solid/index.js +3 -0
  97. package/svelte/index.d.ts +88 -0
  98. package/svelte/index.d.ts.map +1 -0
  99. package/svelte/index.js +166 -0
  100. package/tailwind.css +87 -0
  101. package/tokens/figma.variables.json +2241 -0
  102. package/tokens/index.js +1 -1
  103. package/tokens/index.json +2 -2
  104. package/tokens/resolved.json +3 -3
  105. package/tokens/tokens.dtcg.json +1 -1
  106. package/vue/index.d.ts +79 -0
  107. package/vue/index.d.ts.map +1 -0
  108. package/vue/index.js +197 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,105 @@
5
5
  |> `^0` / `*` wildcard does **not** protect you. See README → Versioning, and
6
6
  |> the deprecation policy in CONTRIBUTING.md.
7
7
 
8
+ ## 0.6.7 — 2026-06-15
9
+
10
+ ### Added
11
+
12
+ - **Tailwind v4 bridge.** `@ponchia/ui/tailwind` / `@ponchia/ui/tailwind.css`
13
+ ships a CSS-first `@theme inline` bridge, `bronto-dark` and `bronto-oled`
14
+ custom variants, and source-registration guidance for Tailwind v4 projects.
15
+ `docs/interop/tailwind.md` documents `@reference` / `@source`, and
16
+ `examples/tailwind-vite` builds from the packed tarball and browser-smokes
17
+ the emitted Bronto utilities.
18
+ - **Svelte and Vue lifecycle adapters.** `@ponchia/ui/svelte` exports
19
+ dependency-free actions over the delegated behavior layer, and
20
+ `@ponchia/ui/vue` exports directives, plugin helpers, and the shared
21
+ imperative `useToast()` helper.
22
+ The SvelteKit example now uses the Svelte adapter, and `examples/vue-vite`
23
+ covers Vue consumer install/build/runtime smoke from the packed package.
24
+ - **Figma Variables handoff artifact.** `tokens/figma.variables.json` is
25
+ generated from the token source, exported as
26
+ `@ponchia/ui/tokens/figma.variables.json`, drift-checked in `check:fresh`,
27
+ and documented as a local import/sync handoff alongside the DTCG token export.
28
+ - **Report-primitive batch** — four additive opt-in leaves for static reports
29
+ and explanation surfaces: `ui-figure` (`css/figure.css`) for reusable
30
+ chart/diagram/media stages with overlay, key and fallback-data slots;
31
+ `ui-interval` (`css/interval.css`) for host-normalised low/high evidence
32
+ windows; `ui-clamp` (`css/clamp.css`) for bounded excerpts with CSS-only
33
+ show-more / show-less; and `ui-highlights` (`css/highlights.css`) for named
34
+ CSS Custom Highlight API paints (`bronto-evidence`, `bronto-search`,
35
+ `bronto-current`). All stay out of `dist/bronto.css`; `figure.css` and
36
+ `highlights.css` join the analytical roll-up.
37
+ - **Background-job state primitive.** `state.css` now includes `ui-job` for
38
+ persistent asynchronous work: written status, determinate progress via
39
+ `--job-progress`, optional actions, compact mode, and queued/running/blocked/
40
+ failed/complete tones. The host still owns polling, retries, cancellation,
41
+ and announcements.
42
+ - **Workbench splitter primitive.** `workbench.css` now includes
43
+ `ui-splitter` / `__pane` / `__handle` plus the optional `initSplitter`
44
+ behavior (`data-bronto-splitter`) for focusable ARIA separator handles,
45
+ keyboard and pointer resizing, `--splitter-pos` / `aria-valuenow` sync, and a
46
+ `bronto:splitter:resize` event. React, Solid, Qwik, Svelte, and Vue adapters
47
+ expose the matching hook/action/directive.
48
+ - **Service identity demo and report kit.** The public demo set now includes
49
+ `demo/service.html`, a cross-service `ui-app-shell` specimen that keeps the
50
+ default bundle centered on shared app identity. Static reports get the new
51
+ opt-in `@ponchia/ui/css/report-kit.css` roll-up for the complete report
52
+ vocabulary, plus `@ponchia/ui/schemas/report-claims.v1.schema.json` for
53
+ claim/source sidecar validation.
54
+ - **Public-package hardening gates.** `check:public-hygiene`,
55
+ `check:variables`, and `check:migrations` are now part of the aggregate
56
+ `npm run check` chain, covering packed public text leaks, undefined CSS
57
+ custom-property references, and migration-map/doc alignment.
58
+
59
+ ### Fixed
60
+
61
+ - **Silent CSS token typos.** `css/workbench.css` now uses the real
62
+ `--focus-ring` token for splitter focus outlines, and `css/report.css` uses
63
+ `--text-base` for finding/evidence value text. The new variable-reference
64
+ gate prevents the same class of no-op declaration from shipping again.
65
+ - **Print: stat tiles and table rows no longer slice across PDF page
66
+ boundaries** — the report-layer `@media print` break-inside guard covered
67
+ the report shell (`.ui-report__*`, `.ui-claim`, `.ui-evidence-item`) but
68
+ not `.ui-stat` tiles, generic `.ui-statgrid` children, or `tr`, so a stat
69
+ card landing on a page boundary printed half its value on one page and the
70
+ delta on the next (observed in a consumer report's PDF export). Guarded at
71
+ the item level — the tile / the row, not the container — so a long
72
+ statgrid or table can still span pages.
73
+
74
+ - **Sidenote gutter contract actually works now** — `--sidenote-width` /
75
+ `--sidenote-gap` are root-scoped instead of declared only on the notes.
76
+ The documented host wiring (container
77
+ `padding-inline-end: calc(var(--sidenote-width) + var(--sidenote-gap))`)
78
+ referenced vars an ancestor could never see, so the calc was invalid and
79
+ silently reserved NO gutter — the floated notes spilled past the page edge
80
+ (caught by a real report's visual QA). The demo now uses the documented
81
+ calc verbatim (it previously masked the bug with a hardcoded literal), and
82
+ a wide-viewport e2e asserts the contract: gutter resolves, float stays
83
+ on-page, no horizontal scroll. Overriding either knob on the container now
84
+ re-sizes the notes and the gutter together.
85
+
86
+ ### Changed
87
+
88
+ - `docs/d2.md` tokenize recipe covers the full embedded-style surface
89
+ (`.color-*` / `.background-color-*` rules, not just `fill`/`stroke`) and
90
+ warns that `tooltip:`/`|md` shapes embed GitHub-Primer styling that
91
+ survives tokenization. `docs/sidenote.md` documents the root-scoped knobs.
92
+ - Local verification is now reproducible through named scripts:
93
+ `npm run test:e2e:nonpixel` runs every non-screenshot Playwright spec across
94
+ Chromium, Firefox, and WebKit, while `npm run test:examples` packs the real
95
+ tarball, builds all examples in temp directories, and browser-smokes the
96
+ runtime examples. `CONTRIBUTING.md`, `docs/release.md`, and
97
+ `docs/architecture.md` describe when to use those local gates versus the
98
+ pinned-container screenshot gate.
99
+ - `check:examples` keeps the example inventory, CI matrix, browser-smoke list,
100
+ README rows, and preview ports aligned from one registry; `check:dead` now
101
+ runs in the aggregate `npm run check` chain.
102
+ - README, package metadata, usage docs, workbench examples, and the docs index
103
+ now frame `@ponchia/ui` as the shared UI identity layer for services, tools,
104
+ sites, and reports, with reports as an opt-in consumer rather than the center
105
+ of the package.
106
+
8
107
  ## 0.6.6 — 2026-06-10
9
108
 
10
109
  Consolidation pass from the 2026-06-10 multi-agent audit: two real PDF-export
@@ -68,13 +167,13 @@ changes, no `MIGRATIONS.json` entry.
68
167
  post-0.6.0 leaves. The broadened scan immediately caught the demo/reference
69
168
  class defects above. `core.css` imports are now closed over an explicit
70
169
  CORE_BUNDLE allowlist in both directions.
71
- - `ROADMAP.md` / `docs/frontier-primitives.md` reconciled to 0.6.5: the
170
+ - `ROADMAP.md` / `docs/frontier-primitives.md` reconciled to 0.6.7: the
72
171
  report/provenance/explanation lane is named the proven core; ui-job,
73
- ui-conflict, ui-splitter, the tree roving-focus kernel, and
74
- command/workbench follow-ons are explicitly dormant until a real app
75
- consumer exists; the 2026-06-09 scout keeps (ui-interval, ui-clamp,
76
- ui-highlights) are recorded as report-lane candidates gated behind the
77
- routing hub. The adoption stance is stated in ROADMAP.
172
+ ui-conflict, drag/drop workbench follow-ons, the tree roving-focus kernel,
173
+ and command follow-ons are explicitly dormant until a real app consumer
174
+ exists; the 2026-06-09 scout keeps (ui-interval, ui-clamp, ui-highlights) are
175
+ recorded as report-lane candidates gated behind the routing hub. The adoption
176
+ stance is stated in ROADMAP.
78
177
 
79
178
  ## 0.6.5 — 2026-06-09
80
179
 
package/README.md CHANGED
@@ -8,25 +8,27 @@
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)
10
10
 
11
- **A CSS-first design system for interfaces that explain themselves.** It works in
12
- plain HTML, every modern framework, and print/PDF, with no component runtime to
13
- adopt and zero runtime dependencies.
11
+ **A CSS-first identity and UI layer for services, tools, sites, and reports.** It
12
+ works in plain HTML, every modern framework, and print/PDF, with no component
13
+ runtime to adopt and zero runtime dependencies.
14
14
 
15
15
  The look is a deliberate stance: a neutral monochrome canvas with one rationed
16
16
  accent (colour signals, it never decorates), dot-matrix display type, and
17
17
  hairline borders — all re-skinnable from a single `--accent` knob (opt-in
18
18
  amber-CRT, phosphor-green, and e-ink colorways).
19
19
 
20
- Beyond the standard component set it ships an opt-in analytical & report layer —
21
- SVG annotations, legends, leader-line connectors, a guided-focus spotlight, text
22
- marks, a colourblind-safe data-viz palette, and a static/PDF report grammar for
23
- dashboards and LLM-authored reports.
20
+ The default bundle is the shared service identity: app shells, navigation,
21
+ forms, tables, feedback, overlays, workflow surfaces, prose, motion and the
22
+ token system that lets each app feel related without sharing a component
23
+ runtime. On top of that it ships opt-in tooling/report layers — workbench panes,
24
+ commands, system state, provenance, figure stages, annotations, legends,
25
+ evidence highlights, intervals, data-viz tokens and a static/PDF report grammar.
24
26
 
25
- ### [Live demo →](https://ponchia.github.io/bronto-ui/)  ·  [Static report →](https://ponchia.github.io/bronto-ui/demo/report-standalone.html)  ·  [Theme playground →](https://ponchia.github.io/bronto-ui/demo/theme-playground.html)
27
+ ### [Live demo →](https://ponchia.github.io/bronto-ui/)  ·  [Service shell →](https://ponchia.github.io/bronto-ui/demo/service.html)  ·  [Static report →](https://ponchia.github.io/bronto-ui/demo/report-standalone.html)  ·  [Theme playground →](https://ponchia.github.io/bronto-ui/demo/theme-playground.html)
26
28
 
27
29
  The demo is the kitchen sink — every component, light/dark, RTL, live theming.
28
- The static report is the differentiator in one file: a complete, no-build,
29
- no-JS, Chromium-PDF-ready report built from the report grammar.
30
+ The service shell shows the shared app identity; the static report shows the
31
+ same system under a no-build, no-JS, Chromium-PDF-ready constraint.
30
32
 
31
33
  <p>
32
34
  <img alt="A research report built with the @ponchia/ui report layer" src="https://raw.githubusercontent.com/Ponchia/bronto-ui/main/demo/_preview-report-research.png" width="49%" />
@@ -48,7 +50,13 @@ and the accent is a spotlight, not a paint bucket. Because everything lives in a
48
50
  single `@layer bronto`, your own un-layered CSS overrides the framework with no
49
51
  specificity fight and no `!important`.
50
52
 
51
- It ships a complete, accessible **standard component set**, but that's not where it competes — its differentiator is the opt-in **analytical & communication layer** sketched above. The line that holds it together: each primitive owns only its visual grammar and pure geometry — no chart engine, no state, no hit-testing. See **[docs/frontier-primitives.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/frontier-primitives.md)** for the thesis.
53
+ It ships a complete, accessible **standard component set** because that is the
54
+ identity layer every service consumes. Its sharper edge is the opt-in **tooling,
55
+ analytical, and communication layer** sketched above. The line that holds it
56
+ together: each primitive owns only its visual grammar and pure geometry — no
57
+ chart engine, no domain state, no hit-testing. See
58
+ **[docs/frontier-primitives.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/frontier-primitives.md)**
59
+ for the thesis.
52
60
 
53
61
  ## Install
54
62
 
@@ -59,7 +67,7 @@ npm i @ponchia/ui
59
67
  Or drop it in with no build step, straight from a CDN:
60
68
 
61
69
  ```html
62
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui/dist/bronto.css">
70
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.7/dist/bronto.css">
63
71
  ```
64
72
 
65
73
  ## Quick start
@@ -77,7 +85,7 @@ Or drop it in with no build step, straight from a CDN:
77
85
 
78
86
  > Prefer source leaves through a bundler? Use `@import '@ponchia/ui/css'` (a thin `@import` fan-out) instead. Both resolve the Doto `@font-face` with relative URLs, so there's no `/fonts` path assumption.
79
87
 
80
- > The bundle is the standard component set. The opt-in analytical & report layers — `report.css`, `dataviz.css`, `annotations.css`, `legend.css`, and the rest — are **not** in `bronto.css`; link each one you need from `dist/css/`. For LLM-authored static reports see [docs/reporting.md](docs/reporting.md).
88
+ > The bundle is the standard component set. The opt-in analytical & report layers — `report.css`, `dataviz.css`, `figure.css`, `annotations.css`, `legend.css`, `interval.css`, `clamp.css`, `highlights.css`, and the rest — are **not** in `bronto.css`; link each one you need from `dist/css/`. For LLM-authored static reports see [docs/reporting.md](docs/reporting.md).
81
89
 
82
90
  **2. Write markup with `ui-*` classes** (primary is the default button; modifiers are opt-in):
83
91
 
@@ -130,10 +138,10 @@ Arrows, chevrons, check/close/plus/minus, search/menu/gear, info/warning/bell/lo
130
138
  - **Feedback** — alert/callout, toast, tooltip, `ui-progress` (task) and `ui-meter` (measured value).
131
139
  - **Overlay** — modal + drawer on native `<dialog>`, dropdown menu, `ui-carousel` + `ui-lightbox` (gallery, user-driven — not an auto-slider).
132
140
  - **Disclosure & nav** — tabs, accordion, segmented, breadcrumb, pagination, `ui-steps`, `ui-timeline`, `ui-pagehead`, `ui-kbd`.
133
- - **Shells** — an admin dashboard shell (`ui-app-*`) and a content/marketing site shell (`ui-site*`, `ui-container`).
141
+ - **Shells** — a service/app shell (`ui-app-*`) and a content/marketing site shell (`ui-site*`, `ui-container`).
134
142
  - **Prose** — `.ui-prose` styles raw, unclassed semantic HTML (Markdown / CMS / LLM output) with zero classes.
135
- - **Analytical & communication primitives** _(opt-in)_ — `@ponchia/ui/css/analytical.css`: SVG **annotations** (subject/connector/note), standalone **legends**/data-keys, text/evidence **marks**, leader-line **connectors** (+ a pure `@ponchia/ui/connectors` geometry kernel), a guided-focus **spotlight**, a **crosshair**/readout, and a cross-cutting **selection** vocabulary. Each owns its visual grammar + pure geometry and refuses scales/state/hit-testing — figures that explain themselves, not a chart engine. Plus standalone **`source`/provenance** (trust) and **lifecycle `state`** leaves.
136
- - **Reports** _(opt-in)_ — `@ponchia/ui/css/report.css`, a static/PDF-first report grammar for LLM-authored HTML: covers, decisions, claims, sections, severity-labelled findings, evidence packets, evidence ledgers, action registers, source-card bindings, figures, chart wrappers and print utilities.
143
+ - **Analytical & communication primitives** _(opt-in)_ — `@ponchia/ui/css/analytical.css`: **figure** stages, SVG **annotations** (subject/connector/note), standalone **legends**/data-keys, text/evidence **marks**, leader-line **connectors** (+ a pure `@ponchia/ui/connectors` geometry kernel), a guided-focus **spotlight**, a **crosshair**/readout, a cross-cutting **selection** vocabulary, and CSS Custom Highlight API **highlights**. Each owns its visual grammar + pure geometry and refuses scales/state/hit-testing — figures that explain themselves, not a chart engine. Plus standalone **`source`/provenance** (trust), **interval**, **clamp**, and **lifecycle `state`** leaves.
144
+ - **Reports** _(opt-in)_ — `@ponchia/ui/css/report-kit.css` for a complete static/PDF report vocabulary, or `@ponchia/ui/css/report.css` plus only the leaves a narrow report uses. Covers, decisions, claims, sections, severity-labelled findings, evidence packets, evidence ledgers, action registers, source-card bindings, `ui-figure` composition, intervals, bounded excerpts, chart wrappers and print utilities.
137
145
  - **Motion & dots** — the dot-matrix motif kit: dot grid, status dots, dot loaders, the orbital spinner, matrix reveal — all reduced-motion aware.
138
146
  - **Glyphs** — `@ponchia/ui/glyphs`, a 71-glyph dot-matrix icon set on the `.ui-dotmatrix` primitive (display marks + crisp `solid` inline icons + one-node `.ui-icon` mask rendering).
139
147
  - **Colorways** _(opt-in)_ — `data-bronto-skin="amber-crt | phosphor-green | e-ink"`: a root-level recolour of the one accent (OKLCH, per-theme, contrast-gated), never in the default bundle.
@@ -143,13 +151,17 @@ Full generated catalog of every class: **[docs/reference.md](https://github.com/
143
151
 
144
152
  ## Theming: one knob
145
153
 
146
- Everything accent-colored derives from a single `--accent` variable via `color-mix()`. Re-brand the entire app — both light and dark — with one declaration, globally or scoped to any subtree:
154
+ Everything accent-colored derives from a single `--accent` variable via `color-mix()`. Re-brand the entire app — both light and dark — with root-level, per-theme overrides:
147
155
 
148
156
  ```css
149
157
  :root { --accent: #2f6df6; } /* whole app blue */
150
- .promo { --accent: #16a34a; } /* …or just this section green */
151
158
  ```
152
159
 
160
+ Scoped `--accent` overrides recolor direct accent uses in that subtree, but the
161
+ derived family (`--accent-text`, `--accent-soft`, focus/dot/ramp tokens) is a
162
+ root-level skin contract. Use the full per-theme recipe in
163
+ [`docs/theming.md`](docs/theming.md) for production rebrands.
164
+
153
165
  Buttons, focus rings, dot motifs, accent borders and soft fills all follow automatically. Light/dark is `data-theme="light"` / `"dark"` on `<html>` (defaults to `prefers-color-scheme`); `data-density` and `data-contrast` give density and contrast presets. A full re-skin (radius, display face, dot density, surfaces) is a handful more token overrides — the default monochrome look is **one skin, not the architecture**.
154
166
 
155
167
  **One system, many skins.** That the knob is real isn't a claim — it ships: drop in `@ponchia/ui/css/skins.css` and set `data-bronto-skin="amber-crt | phosphor-green | e-ink"` on `<html>` for a complete, contrast-gated recolour (the derived accent family, focus ring, dot-matrix and glyphs all follow). Colour is governed in **tiers** — neutral canvas · one accent · locked status · display expression · opt-in data-viz — so it always earns its place; the full constitution is **[ADR-0001](https://github.com/Ponchia/bronto-ui/blob/main/docs/adr/0001-color-system.md)**.
@@ -162,7 +174,7 @@ Not an afterthought — a gate. Every contractual token pairing has a declared W
162
174
 
163
175
  ## Works with anything
164
176
 
165
- 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, so the core stays zero-dependency.
177
+ 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.
166
178
 
167
179
  Per-framework getting-started guides + runnable example apps live in the repo:
168
180
 
@@ -171,16 +183,17 @@ Per-framework getting-started guides + runnable example apps live in the repo:
171
183
  | Vanilla / Vite / plain HTML | [vanilla.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/vanilla.md) | [`examples/vanilla-vite`](https://github.com/Ponchia/bronto-ui/tree/main/examples/vanilla-vite) |
172
184
  | Astro | [astro.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/astro.md) | [`examples/astro`](https://github.com/Ponchia/bronto-ui/tree/main/examples/astro) |
173
185
  | SvelteKit | [sveltekit.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/sveltekit.md) | [`examples/sveltekit`](https://github.com/Ponchia/bronto-ui/tree/main/examples/sveltekit) |
186
+ | Vue | [vue.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/vue.md) | [`examples/vue-vite`](https://github.com/Ponchia/bronto-ui/tree/main/examples/vue-vite) |
174
187
  | React | [react-solid.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/react-solid.md) | [`examples/react-vite`](https://github.com/Ponchia/bronto-ui/tree/main/examples/react-vite) |
175
188
  | Solid | [react-solid.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/react-solid.md) | [`examples/solid-vite`](https://github.com/Ponchia/bronto-ui/tree/main/examples/solid-vite) |
176
189
  | Qwik | [react-solid.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/getting-started/react-solid.md) | [`examples/qwik-vite`](https://github.com/Ponchia/bronto-ui/tree/main/examples/qwik-vite) |
177
- | Tailwind / cascade-layer interop | [tailwind.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/interop/tailwind.md) | |
190
+ | Tailwind v4 bridge / cascade-layer interop | [tailwind.md](https://github.com/Ponchia/bronto-ui/blob/main/docs/interop/tailwind.md) | [`examples/tailwind-vite`](https://github.com/Ponchia/bronto-ui/tree/main/examples/tailwind-vite) |
178
191
 
179
192
  ## Extras
180
193
 
181
- - **Tokens as data** — `import tokens, { themeColor, cssVars } from '@ponchia/ui/tokens'` (plus `tokens.json`, W3C DTCG `tokens.dtcg.json`, and `tokens/resolved.json` concrete hex per theme for canvas/SVG/MapLibre).
194
+ - **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).
182
195
  - **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`.
183
- - **Static reports for LLMs** — add `@ponchia/ui/css/report.css` for report structure, claim/evidence/action grammar, source-card bindings, and print utilities; pair with `@ponchia/ui/css/dataviz.css` only when the report contains charts. Full cookbook: `docs/reporting.md`.
196
+ - **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`.
184
197
  - **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; }`.
185
198
  - **Editor IntelliSense** — point VS Code at the shipped custom-data file so every token autocompletes in `var(--…)`:
186
199
  ```json
@@ -188,7 +201,7 @@ Per-framework getting-started guides + runnable example apps live in the repo:
188
201
  ```
189
202
  - **For AI coding agents** — the package ships `llms.txt` at its root plus `docs/reference.md`, `docs/usage.md`, `docs/reporting.md`, `docs/theming.md`, `docs/contrast.md`, `docs/stability.md`, `docs/package-contract.md`, the color constitution `docs/adr/0001-color-system.md` and the `CHANGELOG` inside the tarball, so an offline agent has the full API and rationale without guessing.
190
203
 
191
- > The package root is **CSS-only**. Use `@import '@ponchia/ui'` in CSS, or `import '@ponchia/ui'` only as a CSS side-effect import in a CSS-aware bundler (Vite, Astro, SvelteKit, webpack). Do not import the package root from Node/runtime JS. JS entrypoints are explicit subpaths: `/tokens`, `/classes`, `/behaviors`, `/glyphs`, `/react`, `/solid`, `/qwik`, `/skins`, and `/charts`.
204
+ > The package root is **CSS-only**. Use `@import '@ponchia/ui'` in CSS, or `import '@ponchia/ui'` only as a CSS side-effect import in a CSS-aware bundler (Vite, Astro, SvelteKit, webpack). Do not import the package root from Node/runtime JS. CSS helper subpaths are explicit too, including `/tailwind` for the Tailwind v4 theme/variant bridge. JS entrypoints are explicit subpaths: `/tokens`, `/classes`, `/behaviors`, `/glyphs`, `/annotations`, `/connectors`, `/react`, `/solid`, `/qwik`, `/svelte`, `/vue`, `/skins`, `/charts`, `/mermaid`, `/d2`, and `/vega`.
192
205
  > JS subpaths are **ESM-only**. CommonJS consumers should use dynamic
193
206
  > `import('@ponchia/ui/behaviors')`.
194
207
 
@@ -1 +1 @@
1
- {"version":3,"file":"carousel.d.ts","sourceRoot":"","sources":["carousel.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAuK3C"}
1
+ {"version":3,"file":"carousel.d.ts","sourceRoot":"","sources":["carousel.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA0K3C"}
@@ -74,6 +74,9 @@ export function initCarousel({ root } = {}) {
74
74
  if (!b) continue;
75
75
  if (b.tagName === 'BUTTON' && !b.hasAttribute('type')) b.type = 'button';
76
76
  }
77
+ for (const b of thumbs) {
78
+ if (b.tagName === 'BUTTON' && !b.hasAttribute('type')) b.type = 'button';
79
+ }
77
80
  if (prevBtn && !prevBtn.hasAttribute('aria-label'))
78
81
  prevBtn.setAttribute('aria-label', 'Previous');
79
82
  if (nextBtn && !nextBtn.hasAttribute('aria-label')) nextBtn.setAttribute('aria-label', 'Next');
@@ -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,CA0D3C"}
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,CAgE3C"}
@@ -23,19 +23,19 @@ export function initDialog({ root } = {}) {
23
23
  const host = resolveHost(root);
24
24
  if (!host) return noop;
25
25
  const managedDialogs = new WeakSet();
26
+ const focusRestorers = new Map();
26
27
  const canManageDialog = (dlg, origin) => host.contains(origin) || managedDialogs.has(dlg);
27
28
 
28
29
  const openFrom = (opener) => {
29
30
  const dlg = byIdInHost(host, opener.getAttribute('data-bronto-open'));
30
31
  if (!dlg || typeof dlg.showModal !== 'function' || dlg.open) return;
31
32
  managedDialogs.add(dlg);
32
- dlg.addEventListener(
33
- 'close',
34
- () => {
35
- if (opener.isConnected && typeof opener.focus === 'function') opener.focus();
36
- },
37
- { once: true },
38
- );
33
+ const restoreFocus = () => {
34
+ focusRestorers.delete(dlg);
35
+ if (opener.isConnected && typeof opener.focus === 'function') opener.focus();
36
+ };
37
+ focusRestorers.set(dlg, restoreFocus);
38
+ dlg.addEventListener('close', restoreFocus, { once: true });
39
39
  dlg.showModal();
40
40
  };
41
41
 
@@ -72,6 +72,12 @@ export function initDialog({ root } = {}) {
72
72
  };
73
73
  return bindOnce(host, 'dialog', () => {
74
74
  document.addEventListener('click', onClick);
75
- return () => document.removeEventListener('click', onClick);
75
+ return () => {
76
+ document.removeEventListener('click', onClick);
77
+ for (const [dlg, restoreFocus] of focusRestorers) {
78
+ dlg.removeEventListener('close', restoreFocus);
79
+ }
80
+ focusRestorers.clear();
81
+ };
76
82
  });
77
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"forms.d.ts","sourceRoot":"","sources":["forms.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,8CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6K3C"}
1
+ {"version":3,"file":"forms.d.ts","sourceRoot":"","sources":["forms.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,8CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAmL3C"}
@@ -29,6 +29,12 @@ export function initFormValidation({ root } = {}) {
29
29
  if (!hasDom()) return noop;
30
30
  const host = resolveHost(root);
31
31
  if (!host) return noop;
32
+ let priorNoValidate = new Map();
33
+
34
+ const suppressNativeValidation = (form) => {
35
+ if (!priorNoValidate.has(form)) priorNoValidate.set(form, form.noValidate);
36
+ form.noValidate = true;
37
+ };
32
38
 
33
39
  const ensureId = (el, prefix) => {
34
40
  if (!el.id) el.id = `${prefix}-${nextFieldUid()}`;
@@ -150,7 +156,7 @@ export function initFormValidation({ root } = {}) {
150
156
  const onSubmit = (e) => {
151
157
  const form = e.target.closest?.('[data-bronto-validate]');
152
158
  if (!form) return;
153
- form.noValidate = true;
159
+ suppressNativeValidation(form);
154
160
  const invalid = controlsOf(form).filter((c) => !validateField(c));
155
161
  refreshSummary(form, invalid);
156
162
  if (invalid.length) {
@@ -165,7 +171,7 @@ export function initFormValidation({ root } = {}) {
165
171
  if (!control.willValidate) return;
166
172
  const form = control.closest?.('[data-bronto-validate]');
167
173
  if (!form) return;
168
- form.noValidate = true;
174
+ suppressNativeValidation(form);
169
175
  validateField(control);
170
176
  const summary = form.querySelector('[data-bronto-error-summary]');
171
177
  if (summary && !summary.hidden)
@@ -183,10 +189,9 @@ export function initFormValidation({ root } = {}) {
183
189
  // summary — contradicting the documented contract. (Forms added
184
190
  // after init are still covered by the in-handler set.)
185
191
  const forms = collectHosts(host, '[data-bronto-validate]');
186
- const priorNoValidate = new Map();
192
+ priorNoValidate = new Map();
187
193
  for (const f of forms) {
188
- priorNoValidate.set(f, f.noValidate);
189
- f.noValidate = true;
194
+ suppressNativeValidation(f);
190
195
  }
191
196
  host.addEventListener('submit', onSubmit, true);
192
197
  host.addEventListener('focusout', onBlur);
@@ -194,6 +199,7 @@ export function initFormValidation({ root } = {}) {
194
199
  host.removeEventListener('submit', onSubmit, true);
195
200
  host.removeEventListener('focusout', onBlur);
196
201
  for (const [f, v] of priorNoValidate) f.noValidate = v;
202
+ priorNoValidate.clear();
197
203
  };
198
204
  });
199
205
  }
@@ -18,6 +18,7 @@ export { initSpotlight } from "./spotlight.js";
18
18
  export { initCrosshair } from "./crosshair.js";
19
19
  export { initCommand } from "./command.js";
20
20
  export { initSources } from "./sources.js";
21
+ export { initSplitter } from "./splitter.js";
21
22
  export type Cleanup = import("./internal.js").Cleanup;
22
23
  export type DelegateOpts = import("./internal.js").DelegateOpts;
23
24
  export type ThemeStorageOpts = import("./theme.js").ThemeStorageOpts;
@@ -29,5 +30,6 @@ export type LegendToggleDetail = import("./legend.js").LegendToggleDetail;
29
30
  export type CrosshairMoveDetail = import("./crosshair.js").CrosshairMoveDetail;
30
31
  export type CommandSelectDetail = import("./command.js").CommandSelectDetail;
31
32
  export type SourceFocusDetail = import("./sources.js").SourceFocusDetail;
33
+ export type SplitterResizeDetail = import("./splitter.js").SplitterResizeDetail;
32
34
  export { applyStoredTheme, initThemeToggle } from "./theme.js";
33
35
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;sBAmBa,OAAO,eAAe,EAAE,OAAO;2BAC/B,OAAO,eAAe,EAAE,YAAY;+BACpC,OAAO,YAAY,EAAE,gBAAgB;6BACrC,OAAO,YAAY,EAAE,cAAc;gCACnC,OAAO,YAAY,EAAE,iBAAiB;wBACtC,OAAO,YAAY,EAAE,SAAS;+BAC9B,OAAO,YAAY,EAAE,gBAAgB;iCACrC,OAAO,aAAa,EAAE,kBAAkB;kCACxC,OAAO,gBAAgB,EAAE,mBAAmB;kCAC5C,OAAO,cAAc,EAAE,mBAAmB;gCAC1C,OAAO,cAAc,EAAE,iBAAiB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;sBAmBa,OAAO,eAAe,EAAE,OAAO;2BAC/B,OAAO,eAAe,EAAE,YAAY;+BACpC,OAAO,YAAY,EAAE,gBAAgB;6BACrC,OAAO,YAAY,EAAE,cAAc;gCACnC,OAAO,YAAY,EAAE,iBAAiB;wBACtC,OAAO,YAAY,EAAE,SAAS;+BAC9B,OAAO,YAAY,EAAE,gBAAgB;iCACrC,OAAO,aAAa,EAAE,kBAAkB;kCACxC,OAAO,gBAAgB,EAAE,mBAAmB;kCAC5C,OAAO,cAAc,EAAE,mBAAmB;gCAC1C,OAAO,cAAc,EAAE,iBAAiB;mCACxC,OAAO,eAAe,EAAE,oBAAoB"}
@@ -28,6 +28,7 @@
28
28
  * @typedef {import('./crosshair.js').CrosshairMoveDetail} CrosshairMoveDetail
29
29
  * @typedef {import('./command.js').CommandSelectDetail} CommandSelectDetail
30
30
  * @typedef {import('./sources.js').SourceFocusDetail} SourceFocusDetail
31
+ * @typedef {import('./splitter.js').SplitterResizeDetail} SplitterResizeDetail
31
32
  */
32
33
  export { applyStoredTheme, initThemeToggle } from './theme.js';
33
34
  export { dismissible } from './dismissible.js';
@@ -50,3 +51,4 @@ export { initSpotlight } from './spotlight.js';
50
51
  export { initCrosshair } from './crosshair.js';
51
52
  export { initCommand } from './command.js';
52
53
  export { initSources } from './sources.js';
54
+ export { initSplitter } from './splitter.js';
@@ -19,7 +19,8 @@ export type Cleanup = () => void;
19
19
  export type DelegateOpts = {
20
20
  /**
21
21
  * Event-delegation root; also scopes which controls are queried. Default: `document`.
22
+ * `null` means a scope was requested but is not ready yet, so the behavior no-ops.
22
23
  */
23
- root?: Document | Element | undefined;
24
+ root?: Document | Element | null | undefined;
24
25
  };
25
26
  //# sourceMappingURL=internal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AA8BA,iEAGC;AAeD,sEAUC;AAED,oDAQC;AAED,yDAMC;AAMD,8DAIC;AAID;;SAMC;AAUD,gDAQC;AAMD,+DAKC;AA9GM,6BAAqB;AAErB,kCAAoD;AAsBpD,uCAAqC;;;;;sBAjC/B,MAAM,IAAI"}
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AAiDA,iEAIC;AAeD,sEAUC;AAED,oDAQC;AAED,yDAMC;AAMD,8DAIC;AAID;;SAMC;AAUD,gDAQC;AAMD,+DAKC;AAjIM,6BAAqB;AAErB,kCAAoD;AAyCpD,uCAAqC;;;;;sBArD/B,MAAM,IAAI"}
@@ -9,16 +9,34 @@
9
9
  * behavior's listeners/observers.
10
10
  *
11
11
  * @typedef {object} DelegateOpts
12
- * @property {Document | Element} [root]
12
+ * @property {Document | Element | null} [root]
13
13
  * Event-delegation root; also scopes which controls are queried. Default: `document`.
14
+ * `null` means a scope was requested but is not ready yet, so the behavior no-ops.
14
15
  */
15
16
 
16
17
  export const noop = () => {};
17
18
 
18
19
  export const hasDom = () => typeof document !== 'undefined';
19
20
 
21
+ function isDelegationHost(value) {
22
+ if (!value || typeof value !== 'object') return false;
23
+ if (value.nodeType === 9) {
24
+ return (
25
+ typeof value.addEventListener === 'function' && typeof value.querySelectorAll === 'function'
26
+ );
27
+ }
28
+ if (value.nodeType === 1) {
29
+ return (
30
+ typeof value.addEventListener === 'function' &&
31
+ typeof value.matches === 'function' &&
32
+ typeof value.querySelectorAll === 'function'
33
+ );
34
+ }
35
+ return false;
36
+ }
37
+
20
38
  // Resolve the delegation host from an init call's `root` option, distinguishing
21
- // three cases so an unattached/null root never silently widens to whole-document
39
+ // cases so an unattached/null root never silently widens to whole-document
22
40
  // delegation (the "scoped island hijacks every control" foot-gun):
23
41
  // • root absent/undefined → no scope requested → delegate from `fallback`
24
42
  // (default `document`). This is the intended global-wiring path.
@@ -26,11 +44,13 @@ export const hasDom = () => typeof document !== 'undefined';
26
44
  // framework ref still null at mount). Return null so the caller no-ops
27
45
  // instead of hijacking the whole document.
28
46
  // • root is an element → use it.
47
+ // • root is anything else → no-op; the public contract is Document | Element.
29
48
  // The bindings (@ponchia/ui/{react,solid,qwik}) emit `root: null` for the
30
49
  // not-ready case precisely so this distinction survives across the boundary.
31
50
  export function resolveHost(root, fallback = document) {
32
51
  if (root === null) return null;
33
- return root || fallback;
52
+ if (root === undefined) return isDelegationHost(fallback) ? fallback : null;
53
+ return isDelegationHost(root) ? root : null;
34
54
  }
35
55
 
36
56
  // Monotonic counter for auto-minted field / list ids, shared across
@@ -1 +1 @@
1
- {"version":3,"file":"legend.d.ts","sourceRoot":"","sources":["legend.js"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,sCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAmD3C;;;;;YAvEa,MAAM,GAAG,MAAM;;;;YACf,OAAO"}
1
+ {"version":3,"file":"legend.d.ts","sourceRoot":"","sources":["legend.js"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,sCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAmF3C;;;;;YAvGa,MAAM,GAAG,MAAM;;;;YACf,OAAO"}
@@ -29,14 +29,18 @@ export function initLegend({ root } = {}) {
29
29
  const host = resolveHost(root);
30
30
  if (!host) return noop;
31
31
  const isButton = (el) => el.tagName === 'BUTTON' || el.getAttribute('role') === 'button';
32
- const onClick = (e) => {
33
- const item = e.target.closest('.ui-legend__item');
32
+ const legendFor = (item) => {
34
33
  if (!item || !host.contains(item)) return;
35
34
  const legend = item.closest('[data-bronto-legend]');
36
35
  if (!legend || !host.contains(legend)) return;
36
+ return legend;
37
+ };
38
+ const toggle = (item) => {
39
+ const legend = legendFor(item);
40
+ if (!legend) return;
37
41
  // The contract requires a real `<button>` (keyboard-operable, focusable). A
38
- // non-button item is mouse-only refuse to toggle it rather than ship a
39
- // pointer-only control (WCAG 2.1.1 C11). The author is warned at bind.
42
+ // non-button item is mouse-only unless role=button is keyboard-normalized
43
+ // below refuse anything else rather than ship a pointer-only control.
40
44
  if (!isButton(item)) return;
41
45
  const active = item.getAttribute('aria-pressed') !== 'false';
42
46
  const next = !active;
@@ -54,23 +58,51 @@ export function initLegend({ root } = {}) {
54
58
  }),
55
59
  );
56
60
  };
61
+ const onClick = (e) => {
62
+ toggle(e.target.closest('.ui-legend__item'));
63
+ };
64
+ const onKey = (e) => {
65
+ if (e.key !== 'Enter' && e.key !== ' ') return;
66
+ const item = e.target.closest('.ui-legend__item');
67
+ if (!item || item.tagName === 'BUTTON' || item.getAttribute('role') !== 'button') return;
68
+ e.preventDefault();
69
+ toggle(item);
70
+ };
57
71
  return bindOnce(host, 'legend', () => {
58
- // Warn once per non-button item present at bind: it gets cursor:pointer from
59
- // the CSS but is neither focusable nor keyboard-operable (C11).
72
+ // Normalize role=button entries and warn once per unsupported non-button
73
+ // item present at bind. A real <button> remains the recommended markup.
74
+ const legends = [...(host.querySelectorAll?.('[data-bronto-legend]') ?? [])];
75
+ for (const legend of legends) {
76
+ for (const el of legend.querySelectorAll('.ui-legend__item')) {
77
+ if (el.closest('[data-bronto-legend]') !== legend) continue;
78
+ if (el.tagName === 'BUTTON' && !el.hasAttribute('type')) el.type = 'button';
79
+ if (
80
+ el.tagName !== 'BUTTON' &&
81
+ el.getAttribute('role') === 'button' &&
82
+ !el.hasAttribute('tabindex')
83
+ ) {
84
+ el.tabIndex = 0;
85
+ }
86
+ }
87
+ }
60
88
  if (typeof console !== 'undefined') {
61
- for (const legend of host.querySelectorAll?.('[data-bronto-legend]') ?? []) {
89
+ for (const legend of legends) {
62
90
  const stray = [...legend.querySelectorAll('.ui-legend__item')].some(
63
91
  (el) => el.closest('[data-bronto-legend]') === legend && !isButton(el),
64
92
  );
65
93
  if (stray) {
66
94
  console.warn(
67
- '[bronto] initLegend(): interactive legend entries must be <button> (or role="button") to be keyboard-operable a non-button .ui-legend__item is ignored.',
95
+ '[bronto] initLegend(): interactive legend entries must be <button> or role="button" — unsupported .ui-legend__item controls are ignored.',
68
96
  );
69
97
  break;
70
98
  }
71
99
  }
72
100
  }
73
101
  host.addEventListener('click', onClick);
74
- return () => host.removeEventListener('click', onClick);
102
+ host.addEventListener('keydown', onKey);
103
+ return () => {
104
+ host.removeEventListener('click', onClick);
105
+ host.removeEventListener('keydown', onKey);
106
+ };
75
107
  });
76
108
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Wire focusable ARIA splitters. Each `[data-bronto-splitter]` host contains
3
+ * two `.ui-splitter__pane` elements separated by one `.ui-splitter__handle`
4
+ * (`role="separator"`). The behavior keeps `--splitter-pos` and
5
+ * `aria-valuenow` in sync for keyboard and pointer resizing, then dispatches
6
+ * `bronto:splitter:resize` with `{ value, orientation }`.
7
+ *
8
+ * Bronto owns the control affordance only. The host owns pane content,
9
+ * persistence, min/max policy, collapse behavior, and any saved layout state.
10
+ * SSR-safe and idempotent per splitter; returns a cleanup function.
11
+ *
12
+ * @param {import('./internal.js').DelegateOpts} [opts]
13
+ * @returns {import('./internal.js').Cleanup}
14
+ */
15
+ export function initSplitter({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
16
+ export type SplitterResizeDetail = {
17
+ /**
18
+ * The first pane size as a 0..100 percentage.
19
+ */
20
+ value: number;
21
+ /**
22
+ * Splitter orientation.
23
+ */
24
+ orientation: "vertical" | "horizontal";
25
+ };
26
+ //# sourceMappingURL=splitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["splitter.js"],"names":[],"mappings":"AAiLA;;;;;;;;;;;;;GAaG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAU3C;;;;;WA3La,MAAM;;;;iBACN,UAAU,GAAG,YAAY"}