@ponchia/ui 0.6.8 → 0.6.9

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 (54) hide show
  1. package/CHANGELOG.md +59 -4
  2. package/README.md +2 -2
  3. package/annotations/index.d.ts.map +1 -1
  4. package/annotations/index.js +5 -6
  5. package/behaviors/carousel.d.ts.map +1 -1
  6. package/behaviors/carousel.js +100 -60
  7. package/behaviors/combobox.d.ts.map +1 -1
  8. package/behaviors/combobox.js +167 -113
  9. package/behaviors/connectors.d.ts.map +1 -1
  10. package/behaviors/connectors.js +39 -23
  11. package/behaviors/forms.d.ts.map +1 -1
  12. package/behaviors/forms.js +211 -207
  13. package/behaviors/glyph.d.ts.map +1 -1
  14. package/behaviors/glyph.js +157 -132
  15. package/behaviors/inert.d.ts +1 -1
  16. package/behaviors/inert.d.ts.map +1 -1
  17. package/behaviors/inert.js +1 -1
  18. package/behaviors/internal.js +2 -2
  19. package/behaviors/modal.js +1 -1
  20. package/behaviors/popover.js +5 -5
  21. package/behaviors/table.d.ts +1 -1
  22. package/behaviors/table.d.ts.map +1 -1
  23. package/behaviors/table.js +7 -8
  24. package/behaviors/tabs.js +2 -2
  25. package/behaviors/toast.js +5 -5
  26. package/classes/index.js +48 -34
  27. package/connectors/index.d.ts +2 -2
  28. package/connectors/index.d.ts.map +1 -1
  29. package/connectors/index.js +7 -10
  30. package/css/app.css +3 -4
  31. package/css/base.css +1 -1
  32. package/css/content.css +3 -3
  33. package/css/disclosure.css +3 -3
  34. package/css/dots.css +4 -4
  35. package/css/feedback.css +6 -7
  36. package/css/forms.css +9 -12
  37. package/css/legend.css +1 -1
  38. package/css/marks.css +1 -1
  39. package/css/motion.css +6 -6
  40. package/css/overlay.css +5 -7
  41. package/css/primitives.css +14 -16
  42. package/css/sidenote.css +2 -2
  43. package/css/table.css +2 -2
  44. package/docs/annotations.md +9 -0
  45. package/docs/architecture.md +28 -0
  46. package/docs/interop/react-flow.md +89 -0
  47. package/docs/package-contract.md +2 -0
  48. package/docs/reporting.md +8 -8
  49. package/docs/stability.md +67 -7
  50. package/glyphs/glyphs.js +43 -33
  51. package/llms.txt +10 -4
  52. package/package.json +5 -2
  53. package/schemas/report-claims.v1.schema.json +1 -1
  54. package/tokens/index.js +2 -2
@@ -74,7 +74,7 @@
74
74
  /* Intrinsic aspect-ratio box; the media child fills it. The contract is ONE
75
75
  child (a single <img>/<video>/<iframe>). Scope the fill to :first-child rather
76
76
  than every child: a second child would otherwise be forced to 100%/100% +
77
- object-fit and stack on top, silently breaking the ratio. (audit C34.) */
77
+ object-fit and stack on top, silently breaking the ratio. */
78
78
  .ui-ratio {
79
79
  aspect-ratio: var(--ratio, 16 / 9);
80
80
  }
@@ -100,7 +100,7 @@
100
100
  /* Logical `max-inline-size`, not physical `max-width`: the container is typed
101
101
  `inline-size`, so the inline axis is the one actually tracked — the logical
102
102
  query matches it in any writing mode (a physical `width` query silently
103
- misses in a vertical WM). (component audit C34.) */
103
+ misses in a vertical writing mode). */
104
104
  @container bronto (max-inline-size: 34rem) {
105
105
  .ui-grid {
106
106
  --grid-min: 100%;
@@ -193,7 +193,7 @@
193
193
  /* These tiles hold IDs / hashes / big numbers — the unbreakable-token case is
194
194
  the common one. A grid item defaults to min-inline-size:auto, so a long
195
195
  value pushes the whole statgrid track wider; allow the tile to shrink so the
196
- value can wrap instead (paired with overflow-wrap on __value). (audit C5.) */
196
+ value can wrap instead (paired with overflow-wrap on __value). */
197
197
  min-inline-size: 0;
198
198
  padding: var(--space-md);
199
199
  }
@@ -216,7 +216,7 @@
216
216
  font-weight: var(--display-weight-strong);
217
217
  letter-spacing: 0.02em;
218
218
  line-height: 1.05;
219
- overflow-wrap: anywhere; /* break an unspaced ID/hash rather than overflow (audit C5) */
219
+ overflow-wrap: anywhere; /* break an unspaced ID/hash rather than overflow */
220
220
  }
221
221
 
222
222
  .ui-stat__delta,
@@ -237,7 +237,7 @@
237
237
 
238
238
  /* A direction arrow is the non-colour channel — colour alone (success/danger)
239
239
  fails WCAG 1.4.1, so the tile carries the same ▲/▼ glyph as `.ui-delta`
240
- (C13). Marked aria-hidden-equivalent by being generated content; the sign is
240
+ as a second signal. Marked aria-hidden-equivalent by being generated content; the sign is
241
241
  still in the author's text ("+12%"). */
242
242
  .ui-stat__delta.is-pos::before,
243
243
  .ui-app-metric__delta.is-pos::before {
@@ -259,7 +259,7 @@
259
259
  .ui-num {
260
260
  /* inline-block so `text-align: end` actually applies: on a bare inline element
261
261
  it computes but never paints (the box is shrink-wrapped), so an author who
262
- followed the docs to right-align an inline figure saw no effect. (audit C17.) */
262
+ followed the docs to right-align an inline figure saw no effect. */
263
263
  display: inline-block;
264
264
  font-variant-numeric: tabular-nums;
265
265
  text-align: end;
@@ -436,8 +436,8 @@
436
436
  this looks-dead state is NOT keyboard-inert: for that, prefer native
437
437
  `<button disabled>`, run `initDisabledGuard()` (it intercepts Enter/Space on
438
438
  aria-disabled controls), or add `tabindex="-1"` (and drop `href` on a link).
439
- See docs/usage.md "Disabled vs aria-disabled". (a11y review C3 / audit C4;
440
- native :disabled already inert.) */
439
+ See docs/usage.md "Disabled vs aria-disabled"; native :disabled is already
440
+ inert. */
441
441
  .ui-button[aria-disabled='true'],
442
442
  .ui-link[aria-disabled='true'] {
443
443
  pointer-events: none;
@@ -445,7 +445,7 @@
445
445
 
446
446
  /* The button family dims + shows not-allowed via its `:disabled` rule above, but
447
447
  a disabled LINK got only pointer-events:none — it looked fully live. Give it
448
- the same visual disabled cue. (component audit C30.) */
448
+ the same visual disabled cue. */
449
449
  .ui-link[aria-disabled='true'] {
450
450
  cursor: not-allowed;
451
451
  opacity: 0.45;
@@ -480,7 +480,7 @@
480
480
  !important `animation-duration`, so the old non-important `1.4s` slow-spin
481
481
  here was dead code AND left a broken, transparent-topped ring looking like a
482
482
  rendering bug. Drop the dead rule; show a STATIC complete ring instead — a
483
- still busy cue with no implied motion. (component audit C15.) */
483
+ still busy cue with no implied motion. */
484
484
  .ui-button[aria-busy='true']::before {
485
485
  border-block-start-color: currentcolor;
486
486
  }
@@ -530,8 +530,7 @@
530
530
 
531
531
  /* RTL: the logical borders flip sides, but a fixed `rotate(45deg)` then points
532
532
  the chevron UP rather than toward the inline-end (the reading-forward way).
533
- Mirror the rotation so the resting affordance points forward in RTL too.
534
- (component audit C14.) */
533
+ Mirror the rotation so the resting affordance points forward in RTL too. */
535
534
  [dir='rtl'] .ui-link--arrow::after,
536
535
  [dir='rtl'] .ui-link--cta::after {
537
536
  transform: rotate(-45deg);
@@ -540,8 +539,7 @@
540
539
  /* Standalone CTA links are tap targets, not inline prose links: on a coarse
541
540
  pointer float them to the WCAG 2.5.8 AA 24px floor (the 2.5.8 inline-link
542
541
  exception doesn't cover a block-level call-to-action, which is what these
543
- are). Buttons already auto-grow to ~44px on coarse pointers. (component
544
- audit C14.) */
542
+ are). Buttons already auto-grow to ~44px on coarse pointers. */
545
543
  @media (pointer: coarse) {
546
544
  .ui-link--arrow,
547
545
  .ui-link--cta {
@@ -600,7 +598,7 @@
600
598
 
601
599
  /* Accent mixes 45% (vs 40% for the status tones below) on purpose: the brand
602
600
  hue is lower-chroma here than the status hues, so it needs a touch more to
603
- read at the same border weight. Intentional, not a copy-paste slip. (Q15.) */
601
+ read at the same border weight. Intentional, not a copy-paste slip. */
604
602
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
605
603
  }
606
604
 
@@ -694,7 +692,7 @@
694
692
  font-family: var(--mono);
695
693
  margin: 0;
696
694
  min-inline-size: 0;
697
- overflow-wrap: anywhere; /* IDs/hashes/paths are the common value — break, don't overflow (audit C5) */
695
+ overflow-wrap: anywhere; /* IDs/hashes/paths are the common value — break, don't overflow */
698
696
  }
699
697
 
700
698
  /* --- Hover (pointer only) --- */
package/css/sidenote.css CHANGED
@@ -41,7 +41,7 @@
41
41
  /* The inline superscript that anchors a numbered sidenote. Use --accent-text,
42
42
  not raw --accent: this is readable text and must clear WCAG AA 4.5:1 even
43
43
  after a one-knob re-brand to a paler --accent (raw --accent drops to ~1.5:1).
44
- Same accent-as-text contract as .ui-eyebrow / .ui-link--cta. (audit C6.) */
44
+ Same accent-as-text contract as .ui-eyebrow / .ui-link--cta. */
45
45
  .ui-sidenote__ref {
46
46
  color: var(--accent-text);
47
47
  counter-increment: ui-sidenote;
@@ -54,7 +54,7 @@
54
54
  }
55
55
 
56
56
  /* The note repeats its number (display only — the ref already incremented).
57
- --accent-text for the same WCAG-AA reason as the ref above. (audit C6.) */
57
+ --accent-text for the same WCAG-AA reason as the ref above. */
58
58
  .ui-sidenote::before {
59
59
  color: var(--accent-text);
60
60
  content: counter(ui-sidenote) '. ';
package/css/table.css CHANGED
@@ -39,7 +39,7 @@
39
39
 
40
40
  /* Keep the sticky header above body cells — cheap insurance for the
41
41
  sticky-header + pinned/positioned-column combo, where an un-z-indexed th
42
- scrolls under a positioned cell. (audit C30.) */
42
+ scrolls under a positioned cell. */
43
43
  z-index: 1;
44
44
  }
45
45
 
@@ -171,7 +171,7 @@
171
171
 
172
172
  /* --- Loading state: set aria-busy + .ui-table-wrap--loading on the wrap. The
173
173
  modifier is named for the element it goes ON (the wrap), not `.ui-table`, so
174
- the BEM host matches the documented placement. (component audit C19.) --- */
174
+ the BEM host matches the documented placement. --- */
175
175
  .ui-table-wrap--loading {
176
176
  opacity: 0.6;
177
177
  pointer-events: none;
@@ -13,6 +13,15 @@ points away from it, and a **note** carries the visible explanation.
13
13
  Use it with any SVG renderer. Bronto supplies classes and tiny geometry helpers;
14
14
  it does not own chart scales, mutate the DOM, or provide draggable edit mode.
15
15
 
16
+ For new annotation-engine work, use the sibling `@ponchia/annotations` package:
17
+ it owns placement, collision handling, SVG/React renderers, editable layers, and
18
+ Mermaid/D2/Vega/React Flow adapters. This `@ponchia/ui/annotations` subpath
19
+ stays a dependency-free compatibility surface for static Bronto SVG helpers, so
20
+ installing the UI package never pulls in the annotation engine and public
21
+ declarations never type-reference it. Combine the two by using Bronto's
22
+ `css/annotations.css` for the visual grammar, or the annotation package's Bronto
23
+ CSS bridge when the engine emits its own layer.
24
+
16
25
  ```js
17
26
  import {
18
27
  annotationParts,
@@ -76,6 +76,13 @@ on top of the CSS, none of which require a framework commitment**:
76
76
  `index.js` is a barrel; each behavior lives in its own module
77
77
  (`dialog.js`, `combobox.js`, …) over a shared `internal.js` of DOM helpers,
78
78
  so the public import surface is unchanged.
79
+ - **connectors/** and **annotations/** — dependency-free SVG geometry helpers
80
+ for report and analytical figures. `@ponchia/ui/annotations` intentionally
81
+ stays a small static-helper compatibility layer for the Bronto subject /
82
+ connector / note grammar. The richer annotation engine - placement,
83
+ collision handling, SVG/React renderers, editing, and diagram/chart adapters
84
+ - lives in the sibling `@ponchia/annotations` package and must not be pulled
85
+ in as a runtime or public type dependency of `@ponchia/ui`.
79
86
  - **glyphs/** — static bitmap data and SSR-safe render helpers. The
80
87
  256-cell DOM renderers are for display and solid inline icons; the `.ui-icon`
81
88
  mask renderer is for dense icon-at-scale use.
@@ -102,6 +109,26 @@ on top of the CSS, none of which require a framework commitment**:
102
109
  `/svelte`, `/vue`, `/skins`, `/charts`, `/mermaid`, `/d2`, `/vega`). This is
103
110
  a permanent, intentional contract.
104
111
 
112
+ ### Surface admission rule
113
+
114
+ The default bundle is the shared app/service identity, not the complete Bronto
115
+ catalog. A new public surface must choose one lane before it ships:
116
+
117
+ - **core identity** — universal application chrome or accessibility/platform
118
+ glue that belongs in `dist/bronto.css`;
119
+ - **opt-in toolbox** — report, analytical, provenance, generated-content,
120
+ renderer, workbench, or command vocabulary that ships as an explicit CSS/JS
121
+ subpath;
122
+ - **recipe/docs only** — a pattern that can be taught without creating a new
123
+ class, token, behavior, export, or package path.
124
+
125
+ When the answer is unclear, choose recipe/docs first. Promote to an opt-in leaf
126
+ only after a real consumer repeats the pattern; promote to core only when it is
127
+ clearly shared application identity. Bronto owns visual grammar, token handoff,
128
+ pure geometry, and delegated accessibility behavior. It does not own chart
129
+ scales, data fetching, persistence, routing, workflow execution, action
130
+ registries, virtualized grids, or framework component APIs.
131
+
105
132
  ## Repository layout
106
133
 
107
134
  The repo root mixes five kinds of directory that look alike but follow very
@@ -162,6 +189,7 @@ are copied into consumer reports.
162
189
  | published tarball == intended `files` only | `check-pack.mjs` |
163
190
  | packed core JS/JSON public subpaths import without optional framework peers, packed JS named exports exactly match source modules, peer-backed adapters import after peers are linked, concrete CSS/doc/font subpaths resolve, and packed behavior initializers/toast no-op in a clean consumer with no DOM globals | `check-consumer-surface.mjs` |
164
191
  | packed typed public subpaths compile through package exports in a clean TypeScript consumer | `check-consumer-types.mjs` |
192
+ | function-level cyclomatic complexity stays ≤12 and function NLOC stays within budget, with no per-function exception list | `check-complexity.mjs` |
165
193
  | GitHub Actions workflow syntax and embedded shell snippets lint | `check:workflows` (`github-actionlint`) |
166
194
  | every shipped CSS leaf is classified as foundation or has explicit docs/demo/e2e ownership | `check-component-matrix.mjs` |
167
195
  | every public behavior export has explicit docs, unit-test, and browser-test ownership | `check-behavior-matrix.mjs` |
@@ -0,0 +1,89 @@
1
+ # React Flow interop
2
+
3
+ `@ponchia/ui` does not wrap or depend on React Flow / Xyflow. Use React Flow as
4
+ the renderer and keep graph state, layout, hit-testing, and node components in
5
+ the host application.
6
+
7
+ The one Bronto-specific gotcha is the base media reset:
8
+
9
+ ```css
10
+ img,
11
+ svg {
12
+ display: block;
13
+ max-inline-size: 100%;
14
+ }
15
+ ```
16
+
17
+ That reset is correct for normal inline SVGs, report figures, icons, and rendered
18
+ diagrams. React Flow edge SVGs are different: the edge layer uses absolutely
19
+ positioned SVG wrappers that paint by overflow. If `max-inline-size: 100%` is
20
+ allowed to constrain those wrappers, edges can disappear or clip.
21
+
22
+ ## Edge SVG escape hatch
23
+
24
+ Scope the escape hatch to the React Flow surface in your app stylesheet. Keep it
25
+ un-layered, or place it in an app layer declared after `bronto`.
26
+
27
+ ```css
28
+ /* app.css */
29
+ @import '@ponchia/ui';
30
+
31
+ .flow-surface .react-flow__edges svg,
32
+ .flow-surface .react-flow__edge svg {
33
+ display: initial;
34
+ max-inline-size: none;
35
+ overflow: visible;
36
+ }
37
+ ```
38
+
39
+ ```tsx
40
+ import { ReactFlow } from '@xyflow/react';
41
+ import '@xyflow/react/dist/style.css';
42
+
43
+ export function GraphView() {
44
+ return (
45
+ <div className="flow-surface">
46
+ <ReactFlow nodes={nodes} edges={edges} />
47
+ </div>
48
+ );
49
+ }
50
+ ```
51
+
52
+ Do not remove Bronto's global SVG reset to fix this. The incompatibility is
53
+ specific to React Flow's edge-layer geometry, so the override should stay scoped
54
+ to the canvas.
55
+
56
+ ## HTML edge labels
57
+
58
+ If you use React Flow's `EdgeLabelRenderer`, remember that labels are HTML
59
+ overlays, not SVG text. Container/group nodes can paint over them. Lift the label
60
+ overlay only inside the graph surface, and keep labels pointer-transparent unless
61
+ they are deliberately interactive:
62
+
63
+ ```css
64
+ .flow-surface .react-flow__edgelabel-renderer {
65
+ z-index: var(--z-raised, 10);
66
+ }
67
+
68
+ .flow-surface .flow-edge-label {
69
+ pointer-events: none;
70
+ }
71
+ ```
72
+
73
+ For dense operational diagrams, prefer sparse edge labels plus a side inspector
74
+ for detail. Use Bronto primitives inside nodes or inspectors (`ui-badge`,
75
+ `ui-dot`, `ui-property`, `ui-legend`) rather than adding a Bronto graph-node
76
+ component before a second consumer proves the pattern repeats.
77
+
78
+ ## What belongs in the app
79
+
80
+ Keep these outside `@ponchia/ui`:
81
+
82
+ - authored or automatic node placement;
83
+ - graph data models and domain status names;
84
+ - brand/product icons;
85
+ - animation semantics, such as travelling reconciliation dots;
86
+ - click/hover selection behavior.
87
+
88
+ Bronto should provide the visual vocabulary and interop recipe. The graph
89
+ renderer and product meaning remain the host application's job.
@@ -169,6 +169,7 @@ semantic versioning contract for the surfaces listed here.
169
169
  | `./docs/workbench.md` | `./docs/workbench.md` | Shipped documentation | Stable path | Markdown documentation shipped in the tarball. Paths are public reading assets within a compatible minor. |
170
170
  | `./docs/command.md` | `./docs/command.md` | Shipped documentation | Stable path | Markdown documentation shipped in the tarball. Paths are public reading assets within a compatible minor. |
171
171
  | `./docs/interop/tailwind.md` | `./docs/interop/tailwind.md` | Shipped documentation | Stable path | Markdown documentation shipped in the tarball. Paths are public reading assets within a compatible minor. |
172
+ | `./docs/interop/react-flow.md` | `./docs/interop/react-flow.md` | Shipped documentation | Stable path | Markdown documentation shipped in the tarball. Paths are public reading assets within a compatible minor. |
172
173
  | `./docs/migrations/0.2-to-0.3.md` | `./docs/migrations/0.2-to-0.3.md` | Shipped documentation | Stable path | Markdown documentation shipped in the tarball. Paths are public reading assets within a compatible minor. |
173
174
  | `./docs/migrations/0.3-to-0.4.md` | `./docs/migrations/0.3-to-0.4.md` | Shipped documentation | Stable path | Markdown documentation shipped in the tarball. Paths are public reading assets within a compatible minor. |
174
175
  | `./docs/migrations/0.4-to-0.5.md` | `./docs/migrations/0.4-to-0.5.md` | Shipped documentation | Stable path | Markdown documentation shipped in the tarball. Paths are public reading assets within a compatible minor. |
@@ -266,6 +267,7 @@ always includes `package.json`, `README.md`, `LICENSE`, and
266
267
  | `docs/workbench.md` | Shipped documentation | Curated Markdown reading asset shipped in the npm tarball. |
267
268
  | `docs/command.md` | Shipped documentation | Curated Markdown reading asset shipped in the npm tarball. |
268
269
  | `docs/interop/tailwind.md` | Shipped documentation | Curated Markdown reading asset shipped in the npm tarball. |
270
+ | `docs/interop/react-flow.md` | Shipped documentation | Curated Markdown reading asset shipped in the npm tarball. |
269
271
  | `docs/migrations/0.2-to-0.3.md` | Shipped documentation | Curated Markdown reading asset shipped in the npm tarball. |
270
272
  | `docs/migrations/0.3-to-0.4.md` | Shipped documentation | Curated Markdown reading asset shipped in the npm tarball. |
271
273
  | `docs/migrations/0.4-to-0.5.md` | Shipped documentation | Curated Markdown reading asset shipped in the npm tarball. |
package/docs/reporting.md CHANGED
@@ -54,18 +54,18 @@ No install? Link the same files from a CDN. Pin the version — pre-1.0, breakin
54
54
  changes ship in the minor (see [stability.md](./stability.md)):
55
55
 
56
56
  ```html
57
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/dist/bronto.css" />
58
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/dist/css/report-kit.css" />
57
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.9/dist/bronto.css" />
58
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.9/dist/css/report-kit.css" />
59
59
  ```
60
60
 
61
61
  Leaf-by-leaf CDN imports use the same `dist/css/` paths:
62
62
 
63
63
  ```html
64
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/dist/bronto.css" />
65
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/dist/css/report.css" />
66
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/dist/css/dataviz.css" />
67
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/dist/css/annotations.css" />
68
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/dist/css/legend.css" />
64
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.9/dist/bronto.css" />
65
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.9/dist/css/report.css" />
66
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.9/dist/css/dataviz.css" />
67
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.9/dist/css/annotations.css" />
68
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.9/dist/css/legend.css" />
69
69
  ```
70
70
 
71
71
  The CDN serves the package's own `fonts/` next to the CSS, so font URLs resolve
@@ -879,7 +879,7 @@ or validation runtime.
879
879
 
880
880
  ```json
881
881
  {
882
- "$schema": "https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.8/schemas/report-claims.v1.schema.json",
882
+ "$schema": "https://cdn.jsdelivr.net/npm/@ponchia/ui@0.6.9/schemas/report-claims.v1.schema.json",
883
883
  "schemaVersion": "bronto-report-claims.v1",
884
884
  "report": { "title": "Decision readiness", "type": "decision" },
885
885
  "claims": [
package/docs/stability.md CHANGED
@@ -11,28 +11,75 @@ For the exhaustive package-manifest inventory — every `exports` key, every
11
11
  shipped `files` entry, and the generated artifact provenance map — see
12
12
  [package-contract.md](./package-contract.md).
13
13
 
14
+ ## Path To 1.0
15
+
16
+ `1.0.0` is a stability declaration, not a catalog milestone. The package is
17
+ ready for 1.0 when the existing public contract is boring to upgrade:
18
+
19
+ - **Core boundary settled.** The default bundle contains shared app/service
20
+ identity only. Report, analytical, provenance, generated-content, renderer,
21
+ workbench, and command surfaces remain opt-in unless they solve universal
22
+ application chrome.
23
+ - **Refusal list enforced by review.** The package still refuses chart scales,
24
+ data fetching, persistence, routing, workflow execution, global action
25
+ registries, virtualized grids, framework component APIs, theme marketplaces,
26
+ and second-accent visual systems.
27
+ - **Consumer proof is routine.** Packed tarball checks, packed example builds,
28
+ packed TypeScript resolution, and at least one real downstream upgrade prove
29
+ each release candidate; source-tree checks are not treated as enough.
30
+ - **Generated contracts stay registry-backed.** Exports, shipped docs, CSS
31
+ leaves, examples, generated artifacts, visual baselines, and ownership
32
+ matrices derive from shared local registries wherever possible.
33
+ - **Bundle budget has headroom.** The default CSS bundle and tarball size have
34
+ deliberate margin, or any budget increase is explicit in the changelog.
35
+ - **Deprecation history is clean.** Every breaking change has a changelog note,
36
+ migration entry when machine-actionable, and the deprecate-one-minor policy
37
+ has been followed or explicitly exempted for provably-unreferenced surface.
38
+
39
+ ### 1.0 Readiness Ledger
40
+
41
+ This ledger is the release-candidate checklist. A row is ready only when the
42
+ evidence column is green for the candidate commit; prose approval alone is not
43
+ enough.
44
+
45
+ | Criterion | Current evidence | 1.0 bar |
46
+ | --- | --- | --- |
47
+ | Core boundary settled | `check:exports`, `check:dist`, `check:component-matrix`, `check:report`, and the `CORE_BUNDLE` registry keep default CSS, opt-in leaves, and report/analytical surfaces separate. | No unexplained growth in the default bundle; any move from opt-in to core is called out in `CHANGELOG.md`. |
48
+ | Refusal list enforced by review | `test/analytical-boundary.test.mjs`, `check:contract`, `check:behavior-matrix`, `check:helper-matrix`, and review against `docs/architecture.md` keep scales, routing, persistence, workflow execution, global action registries, and framework component APIs out of package-owned behavior. | No public helper, behavior, or doc implies Bronto owns application state, data, routing, workflow execution, or framework component rendering. |
49
+ | Consumer proof is routine | `check:pack`, `check:consumer-surface`, `check:consumer-types`, `check:examples`, `check:publint`, and `check:attw` prove the packed npm artifact before release; the release process requires a public-safe Release evidence note. | Every 1.0 candidate has packed import/type/example proof plus at least one real downstream upgrade note that names the consumer class, imported surface, and result without leaking private repo or product names. |
50
+ | Generated contracts stay registry-backed | `check:fresh`, `check:classes`, `check:dts-emit`, `check:doc-links`, `check:public-metadata`, `check:public-hygiene`, and the matrix gates prove generated artifacts, shipped docs, public hygiene, and ownership maps do not drift. | New public surface has one registry/source of truth before it gets a hand-authored gate row. |
51
+ | Bundle budget has headroom | `check:dist`, `check:public-metadata`, `check:pack`, and the README size badge keep default bundle and tarball claims visible. | Budget increases are intentional, reviewed, and named in `CHANGELOG.md`; accidental growth fails before release. |
52
+ | Deprecation history is clean | `check:migrations`, `check:release`, `check:versions`, `MIGRATIONS.json`, and this deprecation policy tie breaking changes to changelog and migration evidence. | No removal ships without either a deprecate-one-minor trail or an explicit BREAKING note for provably-unreferenced surface. |
53
+
54
+ After 1.0, breaking changes move to majors. Until then, the table below is the
55
+ current public-surface matrix and the release policy above still applies.
56
+
14
57
  | Surface | Stability | Contract |
15
58
  | --- | --- | --- |
16
59
  | CSS package root (`@ponchia/ui`) | Stable | CSS-only entrypoint. CSS side-effect imports are supported in CSS-aware bundlers; Node/runtime JS root imports are not. |
17
60
  | JS module format | Stable | JS subpaths are ESM-only. CommonJS consumers use dynamic `import()`. |
18
61
  | CSS class names (`.ui-*`) | Stable | Names and documented modifier semantics are public. Internal selector structure and leaf-file boundaries may change. |
19
62
  | Class recipes (`@ponchia/ui/classes`) | Stable | Exported `cls`, `ui`, `cx`, `attrs`, recipe names, ARIA attribute helper names, and option unions are public. |
20
- | Class vocabulary as data (`@ponchia/ui/classes.json`) | Stable additive | The JSON shape (`groups`/`classes`/`states`/`customProperties`) and its entries are public — for validating markup from a non-JS/non-TS host. Generated from `cls` (the `classes` list cannot drift from the CSS); `states`/`customProperties` are gated against the stylesheet. New classes/hooks are additive. |
63
+ | Class vocabulary as data (`@ponchia/ui/classes.json`, `@ponchia/ui/vscode.css-custom-data.json`) | Stable additive | The JSON shape (`groups`/`classes`/`states`/`customProperties`) and class/custom-property entries are public — for validating markup from a non-JS/non-TS host or editor integration. Generated from `cls` and CSS selectors; `classes.json`, `.d.ts`, reference docs, and VS Code custom data are drift-checked together. New classes/hooks are additive. |
21
64
  | Design tokens | Stable names/roles | Token names and documented roles are public. Exact values and generated colour math outputs may change for visual tuning before 1.0. |
22
65
  | `--accent-1..6` | Stable names/roles | A subtle-to-bold accent ramp derived from `--accent`. Exact resolved values are visual tuning; algorithm changes require release-note visibility and resolver/browser checks. |
23
- | Tokens as data (`tokens.json`, `tokens.dtcg.json`, `tokens/resolved.json`) | Stable additive | The JSON shapes are public for non-CSS/non-JS consumers. `resolved.json` exposes `light`/`dark` (resolved colours) and `scale` (resolved non-colour scales). Token names/roles are stable; exact resolved values are visual tuning (pin `~0.x`). |
24
- | Schemas (`schemas/*.schema.json`) | Stable additive | Declarative JSON Schema contracts for package-adjacent tooling data. Existing schema files and enum values are public within a compatible minor; new optional properties and new schema files are additive. No validator runtime ships. |
66
+ | Tokens as data (`tokens.json`, `tokens.dtcg.json`, `tokens/resolved.json`, `tokens/figma.variables.json`) | Stable additive | The JSON shapes are public for non-CSS/non-JS consumers and handoff tooling. `resolved.json` exposes `light`/`dark` (resolved colours) and `scale` (resolved non-colour scales). `tokens/figma.variables.json` mirrors the token contract for local Figma Variables import/sync. Token names/roles are stable; exact resolved values are visual tuning (pin `~0.x`). |
67
+ | Schemas (`schemas/*.schema.json`, `schemas/report-claims.v1.schema.json`) | Stable additive | Declarative JSON Schema contracts for package-adjacent tooling data. Existing schema files and enum values are public within a compatible minor; new optional properties and new schema files are additive. No validator runtime ships. |
25
68
  | Theme axes | Mixed | `data-theme` (light/dark) is the **contractual** base. `data-surface="oled"`, `data-density`, and `data-contrast` are **convenience presets** — best-effort visual variants, **not** part of the stability contract; their presence and exact values may change for tuning. Computed-style smoke tests guard that the presets apply to their intended token families. |
69
+ | Tailwind v4 bridge (`@ponchia/ui/tailwind`, `@ponchia/ui/tailwind.css`) | Stable additive | CSS-only token/variant bridge for Tailwind v4. It maps Bronto tokens and variants into Tailwind namespaces; it must not import Bronto component CSS or change the default bundle. |
26
70
  | Behavior attributes (`data-bronto-*`) | Stable | Attribute names and documented markup relationships are public. Behavior internals are not. |
27
71
  | Behavior functions (`@ponchia/ui/behaviors`) | Stable | Exported function names, option names, custom events, SSR no-op behavior, idempotency, and cleanup-returning contract are public. |
28
72
  | Glyph registry/renderers (`@ponchia/ui/glyphs`) | Stable additive | Existing glyph names stay valid. New glyphs are additive. Renderer option names and accessibility defaults are public. |
29
73
  | `.ui-icon` mask renderer | Stable | Class name, `--icon-size`, currentColor inheritance, and `--icon-mask` contract are public. The internal data URL encoding is not. |
30
74
  | Framework lifecycle adapters (`react`/`solid`/`qwik`/`svelte`/`vue`) | Stable thin adapters | Hook/action/directive names, optional peer behavior where applicable, root ref/signal/resolver support, and cleanup lifecycle are public. They remain wrappers over vanilla behaviors, not component APIs. |
31
- | Skins (`@ponchia/ui/skins`, `css/skins.css`) | Stable additive | Existing skin names stay valid. New skins are additive. Skins are root-level choices. |
32
- | Charts (`@ponchia/ui/charts`, `charts.json`, `css/dataviz.css`) | Stable additive | Token names, JSON shape, and 8 categorical slots are public. Exact palette values may tune if gates and release notes justify it. |
75
+ | Skins (`@ponchia/ui/skins`, `css/skins.css`) | Stable additive | Existing skin names stay valid. New skins are additive. Skins are root-level choices. Skin CSS is opt-in, not in the default bundle. |
76
+ | Charts (`@ponchia/ui/charts`, `charts.json`, `css/dataviz.css`) | Stable additive | Token names, JSON shape, and 8 categorical slots are public. `css/dataviz.css` is opt-in, not in the default bundle. Exact palette values may tune if gates and release notes justify it. |
77
+ | External renderer themes (`@ponchia/ui/mermaid`, `@ponchia/ui/mermaid.json`, `@ponchia/ui/d2`, `@ponchia/ui/d2.json`, `@ponchia/ui/vega`, `@ponchia/ui/vega.json`) | Stable additive | Theme helper names, JSON shapes, and supported renderer theme slots are public. Values are resolved colours because Mermaid, D2, and Vega cannot consume Bronto CSS variables directly. Exact colours may tune with token changes, but `check:mermaid`, `check:d2`, and `check:vega` must prove every exported theme resolves with no `var()` leaks. No renderer runtime ships. |
78
+ | Shiki theme data (`@ponchia/ui/shiki/nothing.json`) | Stable additive | The bundled Shiki theme JSON shape and token-derived scope roles are public for syntax-highlighting consumers. Exact colours may tune with the token model and must stay generated from the governed palette. |
33
79
  | Reports (`css/report.css`, `.ui-report*`, print utilities) | Stable additive | Report class names, BEM part names, and print utility names are public. Report CSS is opt-in and not imported by the default bundle. The data key now lives in the standalone Legends layer (below), not `css/report.css`; charting is via the Vega theme target (`@ponchia/ui/vega`, see [vega](./vega.md)) or a token-themed inline SVG, not a shipped renderer. |
34
80
  | Report kit roll-up (`css/report-kit.css`) | Stable additive | A convenience `@import` of the complete static-report vocabulary. The set of leaves it bundles may grow additively; each leaf also stays individually exported. Opt-in, not in the default bundle. |
35
- | Annotations (`@ponchia/ui/annotations`, `css/annotations.css`, `.ui-annotation*`) | Stable additive | SVG annotation class names, recipe option names, and helper function names are public. Helper internals and exact path-control heuristics may tune before 1.0. |
81
+ | Figure stage (`css/figure.css`, `.ui-figure*`) | Stable additive | Figure class names, overlay/key/fallback-data slots, and report composition hooks are public. Opt-in, not in the default bundle. Bronto owns the figure frame, not chart rendering, scales, or data mapping. |
82
+ | Annotations (`@ponchia/ui/annotations`, `css/annotations.css`, `.ui-annotation*`) | Stable additive | SVG annotation class names, recipe option names, and helper function names are public. Helper internals and exact path-control heuristics may tune before 1.0. Opt-in, not in the default bundle. Rich placement, renderer, editing, and chart/diagram adapter APIs belong to the sibling `@ponchia/annotations` package; `@ponchia/ui` does not depend on it at runtime or through public declarations. |
36
83
  | Legends (`css/legend.css`, `.ui-legend*`, `@ponchia/ui/behaviors` `initLegend`) | Stable additive | Legend class names, recipe option names, and the `bronto:legend:toggle` event contract (`aria-pressed="true"` ⇒ shown) are public. Opt-in, not in the default bundle; swatch colours are gated to the `--chart-*` palette. |
37
84
  | Marks (`css/marks.css`, `.ui-mark*`, `.ui-bracket-note*`) | Stable additive | Text-mark and bracket-note class names and recipe option names are public. Opt-in, not in the default bundle. Uses semantic tones only. |
38
85
  | Connectors (`@ponchia/ui/connectors`, `css/connectors.css`, `.ui-connector*`, `initConnectors`) | Stable additive | Connector class names, the `data-bronto-connector` attribute contract, geometry helper function names, and recipe options are public. Helper internals/heuristics may tune before 1.0. Opt-in, not in the default bundle. |
@@ -41,13 +88,26 @@ shipped `files` entry, and the generated artifact provenance map — see
41
88
  | Selection states (`css/selection.css`, `.ui-sel*`) | Stable additive | The `.ui-sel`/`--on`/`--off`/`--maybe` emphasis classes and recipe options are public. Opt-in, cross-cutting. The host owns selection logic; Bronto only styles the states. |
42
89
  | Analytical roll-up (`css/analytical.css`) | Stable additive | A convenience `@import` of the nine analytical leaves (figure, annotations, legend, marks, connectors, spotlight, crosshair, selection, highlights). The set of leaves it bundles may grow additively; each leaf also stays individually exported. Opt-in, not in the default bundle. |
43
90
  | Sources / provenance (`css/sources.css`, `.ui-citation*`, `.ui-source-card*`, `.ui-source-list*`, `.ui-provenance*`, `.ui-src--*`, `initSources`) | Stable additive | Citation/source/provenance class names, the cross-cutting `.ui-src--*` trust-state modifiers (always paired with an author label), the optional `data-bronto-sources` / `data-bronto-source-ref` behavior contract, `bronto:source:focus`, and the `ui.citation`/`ui.source`/`ui.provenance` recipes + `cls.sourceList` are public. Opt-in, not in the default bundle. |
91
+ | Interval ranges (`css/interval.css`, `.ui-interval*`) | Stable additive | Interval class names and the normalised `--lo`/`--hi`/`--v` custom-property contract are public. Opt-in, not in the default bundle. The host owns domains, units, and estimate math. |
92
+ | Clamp blocks (`css/clamp.css`, `.ui-clamp*`) | Stable additive | Clamp class/part names, expanded/collapsed affordance slots, and print-expansion behavior are public. Opt-in, not in the default bundle. The host owns disclosure copy and state persistence. |
93
+ | Text highlights (`css/highlights.css`, `.ui-highlights`) | Stable additive | Highlight container classes and token-backed Custom Highlight API paint are public. Opt-in, not in the default bundle. The host owns range registration and search/current-match logic. |
44
94
  | Lifecycle state (`css/state.css`, `.ui-state*`, `.ui-syncbar`) | Stable additive | The `.ui-state`/`__label`/`__detail`/`--busy` classes, the canonical lifecycle state modifiers, `.ui-syncbar`, and the `ui.state` recipe are public. Opt-in, not in the default bundle. |
45
95
  | Generated / AI-trust (`css/generated.css`, `.ui-generated*`, `.ui-origin-label*`, `.ui-reasoning*`, `.ui-tool-log`, `.ui-tool-call*`) | Stable additive | The generated-content, origin-label (incl. `--ai`), reasoning-trace and tool-log/tool-call class names and the `ui.originLabel` recipe are public. Opt-in, not in the default bundle. Not a chat kit; no confidence widget. |
46
96
  | Workbench (`css/workbench.css`, `.ui-splitter*`, `.ui-inspector*`, `.ui-property*`, `.ui-selectionbar*`, `initSplitter`) | Stable additive | Splitter, inspector, property-row and selection-bar class + BEM part names are public (no recipe). `data-bronto-splitter`, `--splitter-pos`, `bronto:splitter:resize`, and the `initSplitter` cleanup contract are public. Opt-in, not in the default bundle. The host owns pane content, persistence, collapse policy, and selection state. |
47
97
  | Command palette (`css/command.css`, `.ui-command*`, `initCommand`, `useCommand` / `command` / `vCommand`) | Stable additive | Command class/part names, the `data-bronto-command` attribute, and the event contract — `bronto:command:select` (`detail: { value, label }`) and `bronto:command:close` — are public, plus the framework binding adapters. Bronto filters + navigates (APG combobox/listbox); the host owns the action registry/execution. Opt-in, not in the default bundle, no global hotkey. |
98
+ | Spark microcharts (`css/spark.css`, `.ui-spark*`) | Stable additive | Spark class names and inline sizing/label slots are public. Opt-in, not in the default bundle. The host owns data reduction and accessible surrounding text. |
99
+ | Bullet graphs (`css/bullet.css`, `.ui-bullet*`) | Stable additive | Bullet class names and measure/target/range custom-property slots are public. Opt-in, not in the default bundle. The host owns thresholds, units, and data mapping. |
100
+ | Diffs (`css/diff.css`, `.ui-diff*`) | Stable additive | Diff container/line/gutter class names and add/remove/highlight state modifiers are public. Opt-in, not in the default bundle. Bronto styles evidence; it does not compute diffs. |
101
+ | Code evidence (`css/code.css`, `.ui-code*`) | Stable additive | Code block/gutter/line-state class names and token-backed syntax-theme roles are public. Opt-in, not in the default bundle. Bronto styles code-as-evidence; it does not parse or highlight code at runtime. |
102
+ | Sidenotes (`css/sidenote.css`, `.ui-sidenote`, `.ui-marginnote`) | Stable additive | Sidenote and marginnote class names, numbering behavior, and responsive in-flow fallback are public. Opt-in, not in the default bundle. |
103
+ | Text references (`css/textref.css`, `.ui-textref`) | Stable additive | Text-reference class names and `::target-text` styling are public. Opt-in, not in the default bundle. The host owns URL text fragments and quote provenance. |
104
+ | Terms / glossary (`css/term.css`, `.ui-term`, `.ui-glossary`) | Stable additive | Term and glossary class names plus native-popover definition hooks are public. Opt-in, not in the default bundle. The host owns glossary content and terminology policy. |
105
+ | Contents rail (`css/toc.css`, `.ui-toc*`) | Stable additive | TOC rail class/part names and current-section state classes are public. Opt-in, not in the default bundle. The host owns section observation and active-state updates. |
106
+ | Tree outlines (`css/tree.css`, `.ui-tree*`) | Stable additive | Tree outline class names, depth styling, and native `<details>` composition are public. Opt-in, not in the default bundle. The host owns tree data, lazy loading, and selection state. |
48
107
  | Controlled-modal focus trap (`initModal`, `useModal`, `data-bronto-modal`) | Stable additive | For the `.ui-modal.is-open` (non-`<dialog>`) path: the `data-bronto-modal` opt-in marker, the `inert`-based focus trap + focus-return, and the cancelable `bronto:modal:close` (`detail: { reason }`) event are public. The consumer still owns the `is-open` class; the behavior never changes visibility. The native `<dialog>` path (`initDialog`) is the default and gets the trap for free. |
49
108
  | Keyboard-shortcut hint (`.ui-shortcut`, `.ui-shortcut__sep`) | Stable additive | Class names for the chord/sequence hint over `.ui-kbd` are public. Ships in the core layer (class-only, no recipe). |
50
- | Generated docs shipped in npm | Stable paths | `llms.txt` and exported docs paths stay shipped and resolvable within a compatible minor. Markdown/text assets are for reading unless your runtime has a loader. Generated content may change with the source contract. |
109
+ | Agent and migration data (`llms.txt`, `MIGRATIONS.json`) | Stable additive | `llms.txt` stays shipped as the offline agent entrypoint. `MIGRATIONS.json` stays a machine-readable migration map for breaking renames/removals. New migration entries are additive; removal of a migration record requires the same breaking-change discipline as the surface it describes. |
110
+ | Generated docs shipped in npm | Stable paths | Exported docs paths stay shipped and resolvable within a compatible minor. Markdown/text assets are for reading unless your runtime has a loader. Generated content may change with the source contract. |
51
111
  | Demo, examples, tests, scripts | Internal | Useful for learning and verification, but not shipped runtime API unless a path is explicitly exported in `package.json`. |
52
112
 
53
113
  ## Deprecation Policy
package/glyphs/glyphs.js CHANGED
@@ -1518,17 +1518,34 @@ export function renderGlyph(name, options = {}) {
1518
1518
  if (!rows) return '';
1519
1519
  const { grid = true, solid = false, anim, label, dot, gap, render, size } = options;
1520
1520
 
1521
+ if (render === 'mask') return renderMaskGlyph(rows, { label, size });
1522
+
1523
+ const cells = glyphCells(name);
1524
+ const style = dotmatrixStyle({ solid, dot, gap });
1525
+ const cls = dotmatrixClass(anim);
1526
+ const stagger = anim === 'reveal';
1527
+ const showPanel = grid && !solid;
1528
+ const inner = cells
1529
+ .map((cell, index) => renderGlyphCell(cell, index, { showPanel, stagger }))
1530
+ .join('');
1531
+
1532
+ // A `<span>` (not `<div>`): it's phrasing content, so the glyph is valid
1533
+ // inline and inside a `<button>` (its content model is phrasing-only) — the
1534
+ // inline-icon use. `.ui-dotmatrix`'s `display: grid` works on a span.
1535
+ return `<span class="${cls}" style="${style.join(';')}" ${a11yAttrs(label)}>${inner}</span>`;
1536
+ }
1537
+
1538
+ function renderMaskGlyph(rows, { label, size }) {
1521
1539
  // One-node icon: a single `.ui-icon` span masked by the glyph's bitmap, so
1522
1540
  // it scales to any font-size and inherits `currentColor` — for icon-at-scale
1523
1541
  // (e.g. one in every table row) where the GLYPH_SIZE²-cell path is too heavy.
1524
- if (render === 'mask') {
1525
- const a11yM = label ? `role="img" aria-label="${esc(label)}"` : 'aria-hidden="true"';
1526
- const sz = size && cssLen(size) ? `--icon-size:${cssLen(size)};` : '';
1527
- return `<span class="ui-icon" style="${sz}--icon-mask:${maskUrl(rows)}" ${a11yM}></span>`;
1528
- }
1529
-
1530
- const cells = glyphCells(name);
1542
+ const iconSize = size && cssLen(size) ? `--icon-size:${cssLen(size)};` : '';
1543
+ return `<span class="ui-icon" style="${iconSize}--icon-mask:${maskUrl(rows)}" ${a11yAttrs(
1544
+ label,
1545
+ )}></span>`;
1546
+ }
1531
1547
 
1548
+ function dotmatrixStyle({ solid, dot, gap }) {
1532
1549
  const style = [`--dotmatrix-cols:${GLYPH_SIZE}`];
1533
1550
  const dotLen = dot && cssLen(dot);
1534
1551
  const gapLen = gap && cssLen(gap);
@@ -1537,42 +1554,35 @@ export function renderGlyph(name, options = {}) {
1537
1554
  // `minmax(0, 1fr)` and the 16×16 matrix balloons to fill its container
1538
1555
  // (full-bleed, ~1250px) — the string API would then render an icon-intent call
1539
1556
  // very differently from the DOM `initDotGlyph` path, which already defaults to
1540
- // 0.08em. Mirror that default here so both paths render the same icon. (C7.)
1557
+ // 0.08em. Mirror that default here so both paths render the same icon.
1541
1558
  style.push(`--dotmatrix-dot:${dotLen || '0.08em'}`);
1542
1559
  // Solid mode fuses the dots into a crisp pixel glyph: square cells, no gap.
1543
1560
  if (solid) style.push('--dotmatrix-dot-radius:0', '--dotmatrix-gap:0');
1544
1561
  else if (gapLen) style.push(`--dotmatrix-gap:${gapLen}`);
1562
+ return style;
1563
+ }
1545
1564
 
1546
- const cls =
1547
- anim === 'reveal'
1548
- ? 'ui-dotmatrix ui-dotmatrix--reveal'
1549
- : anim === 'pulse'
1550
- ? 'ui-dotmatrix ui-dotmatrix--pulse'
1551
- : 'ui-dotmatrix';
1552
- // `reveal` staggers each cell by its row-major index via `--i`.
1553
- const stagger = anim === 'reveal';
1554
-
1555
- const a11y = label ? `role="img" aria-label="${esc(label)}"` : 'aria-hidden="true"';
1565
+ function dotmatrixClass(anim) {
1566
+ if (anim === 'reveal') return 'ui-dotmatrix ui-dotmatrix--reveal';
1567
+ if (anim === 'pulse') return 'ui-dotmatrix ui-dotmatrix--pulse';
1568
+ return 'ui-dotmatrix';
1569
+ }
1556
1570
 
1571
+ function renderGlyphCell(cell, index, { showPanel, stagger }) {
1572
+ const cl = cell.on ? cellClass(cell) : 'ui-dotmatrix__cell';
1573
+ const cellStyle = [];
1557
1574
  // Off cells keep the cell class (so they hold their grid track and 1:1
1558
1575
  // aspect-ratio); `grid: false` (implied by `solid`) only drops their lit
1559
1576
  // background, for the glyph-only look, without collapsing all-off rows.
1560
- const showPanel = grid && !solid;
1561
- const inner = cells
1562
- .map((c, i) => {
1563
- const cl = c.on ? cellClass(c) : 'ui-dotmatrix__cell';
1564
- const cellStyle = [];
1565
- if (!c.on && !showPanel) cellStyle.push('background:transparent');
1566
- if (stagger) cellStyle.push(`--i:${i}`);
1567
- const s = cellStyle.length ? ` style="${cellStyle.join(';')}"` : '';
1568
- return `<span class="${cl}"${s}></span>`;
1569
- })
1570
- .join('');
1577
+ if (!cell.on && !showPanel) cellStyle.push('background:transparent');
1578
+ // `reveal` staggers each cell by its row-major index via `--i`.
1579
+ if (stagger) cellStyle.push(`--i:${index}`);
1580
+ const style = cellStyle.length ? ` style="${cellStyle.join(';')}"` : '';
1581
+ return `<span class="${cl}"${style}></span>`;
1582
+ }
1571
1583
 
1572
- // A `<span>` (not `<div>`): it's phrasing content, so the glyph is valid
1573
- // inline and inside a `<button>` (its content model is phrasing-only) — the
1574
- // inline-icon use. `.ui-dotmatrix`'s `display: grid` works on a span.
1575
- return `<span class="${cls}" style="${style.join(';')}" ${a11y}>${inner}</span>`;
1584
+ function a11yAttrs(label) {
1585
+ return label ? `role="img" aria-label="${esc(label)}"` : 'aria-hidden="true"';
1576
1586
  }
1577
1587
 
1578
1588
  // Character → glyph name for renderReadout. Punctuation reuses the readout face