@ponchia/ui 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +230 -8
  2. package/MIGRATIONS.json +92 -0
  3. package/README.md +9 -6
  4. package/annotations/index.d.ts +280 -0
  5. package/annotations/index.js +522 -0
  6. package/behaviors/carousel.js +197 -0
  7. package/behaviors/combobox.js +195 -0
  8. package/behaviors/command.js +187 -0
  9. package/behaviors/connectors.js +96 -0
  10. package/behaviors/crosshair.js +58 -0
  11. package/behaviors/dialog.js +73 -0
  12. package/behaviors/disclosure.js +25 -0
  13. package/behaviors/dismissible.js +24 -0
  14. package/behaviors/forms.js +158 -0
  15. package/behaviors/glyph.js +109 -0
  16. package/behaviors/index.d.ts +79 -0
  17. package/behaviors/index.js +18 -1409
  18. package/behaviors/internal.js +50 -0
  19. package/behaviors/legend.js +46 -0
  20. package/behaviors/menu.js +46 -0
  21. package/behaviors/popover.js +108 -0
  22. package/behaviors/spotlight.js +53 -0
  23. package/behaviors/table.js +109 -0
  24. package/behaviors/tabs.js +103 -0
  25. package/behaviors/theme.js +82 -0
  26. package/behaviors/toast.js +152 -0
  27. package/classes/index.d.ts +280 -2
  28. package/classes/index.js +313 -2
  29. package/connectors/index.d.ts +71 -0
  30. package/connectors/index.js +179 -0
  31. package/css/analytical.css +21 -0
  32. package/css/annotations.css +292 -0
  33. package/css/command.css +97 -0
  34. package/css/connectors.css +93 -0
  35. package/css/crosshair.css +100 -0
  36. package/css/feedback.css +51 -0
  37. package/css/fonts.css +11 -7
  38. package/css/generated.css +117 -0
  39. package/css/legend.css +268 -0
  40. package/css/marks.css +144 -0
  41. package/css/primitives.css +18 -0
  42. package/css/report.css +12 -31
  43. package/css/selection.css +46 -0
  44. package/css/sources.css +179 -0
  45. package/css/spotlight.css +104 -0
  46. package/css/state.css +121 -0
  47. package/css/tokens.css +25 -37
  48. package/css/workbench.css +83 -0
  49. package/dist/bronto.css +1 -1
  50. package/dist/css/analytical.css +1 -0
  51. package/dist/css/annotations.css +1 -0
  52. package/dist/css/command.css +1 -0
  53. package/dist/css/connectors.css +1 -0
  54. package/dist/css/crosshair.css +1 -0
  55. package/dist/css/feedback.css +1 -1
  56. package/dist/css/fonts.css +1 -1
  57. package/dist/css/generated.css +1 -0
  58. package/dist/css/legend.css +1 -0
  59. package/dist/css/marks.css +1 -0
  60. package/dist/css/primitives.css +1 -1
  61. package/dist/css/report.css +1 -1
  62. package/dist/css/selection.css +1 -0
  63. package/dist/css/sources.css +1 -0
  64. package/dist/css/spotlight.css +1 -0
  65. package/dist/css/state.css +1 -0
  66. package/dist/css/workbench.css +1 -0
  67. package/docs/adr/0003-theme-model.md +7 -4
  68. package/docs/annotations.md +345 -0
  69. package/docs/architecture.md +202 -0
  70. package/docs/command.md +95 -0
  71. package/docs/connectors.md +91 -0
  72. package/docs/crosshair.md +63 -0
  73. package/docs/generated.md +91 -0
  74. package/docs/legends.md +168 -0
  75. package/docs/marks.md +86 -0
  76. package/docs/reference.md +309 -3
  77. package/docs/reporting.md +49 -14
  78. package/docs/selection.md +40 -0
  79. package/docs/sources.md +110 -0
  80. package/docs/spotlight.md +78 -0
  81. package/docs/stability.md +16 -1
  82. package/docs/state.md +85 -0
  83. package/docs/usage.md +22 -0
  84. package/docs/workbench.md +72 -0
  85. package/fonts/doto-400.woff2 +0 -0
  86. package/fonts/doto-500.woff2 +0 -0
  87. package/fonts/doto-600.woff2 +0 -0
  88. package/fonts/doto-700.woff2 +0 -0
  89. package/fonts/doto-800.woff2 +0 -0
  90. package/fonts/doto-900.woff2 +0 -0
  91. package/llms.txt +229 -6
  92. package/package.json +69 -4
  93. package/qwik/index.d.ts +5 -0
  94. package/qwik/index.js +20 -0
  95. package/react/index.d.ts +5 -0
  96. package/react/index.js +10 -0
  97. package/solid/index.d.ts +5 -0
  98. package/solid/index.js +10 -0
  99. package/tokens/index.js +9 -5
  100. package/fonts/doto-400.ttf +0 -0
  101. package/fonts/doto-500.ttf +0 -0
  102. package/fonts/doto-600.ttf +0 -0
  103. package/fonts/doto-700.ttf +0 -0
  104. package/fonts/doto-800.ttf +0 -0
  105. package/fonts/doto-900.ttf +0 -0
@@ -0,0 +1,58 @@
1
+ import { hasDom, noop, bindOnce } from './internal.js';
2
+
3
+ /**
4
+ * Track the pointer over a plot and drive a crosshair. Each
5
+ * `[data-bronto-crosshair]` is the plot; it contains a `.ui-crosshair` overlay.
6
+ * On pointer move the behavior sets `--crosshair-x/y` (pixels within the plot)
7
+ * on the overlay, marks it `.is-active`, and dispatches
8
+ * `bronto:crosshair:move` with `{ x, y, fx, fy }` (px + 0..1 fractions);
9
+ * `bronto:crosshair:leave` on exit.
10
+ *
11
+ * Bronto reports WHERE the pointer is — it does not find the nearest datum or
12
+ * map pixels to data values (that needs the host's scales). SSR-safe,
13
+ * idempotent per plot; returns a cleanup function.
14
+ */
15
+ export function initCrosshair({ root } = {}) {
16
+ if (!hasDom()) return noop;
17
+ const host = root || document;
18
+ const plots = [];
19
+ if (host !== document && host.matches?.('[data-bronto-crosshair]')) plots.push(host);
20
+ plots.push(...host.querySelectorAll('[data-bronto-crosshair]'));
21
+ if (!plots.length) return noop;
22
+
23
+ const cleanups = [];
24
+ for (const plot of plots) {
25
+ const overlay = plot.querySelector('.ui-crosshair');
26
+ if (!overlay) continue;
27
+ const onMove = (e) => {
28
+ const r = plot.getBoundingClientRect();
29
+ if (!r.width || !r.height) return;
30
+ const x = e.clientX - r.left;
31
+ const y = e.clientY - r.top;
32
+ overlay.style.setProperty('--crosshair-x', `${x}px`);
33
+ overlay.style.setProperty('--crosshair-y', `${y}px`);
34
+ overlay.classList.add('is-active');
35
+ plot.dispatchEvent(
36
+ new CustomEvent('bronto:crosshair:move', {
37
+ bubbles: true,
38
+ detail: { x, y, fx: x / r.width, fy: y / r.height },
39
+ }),
40
+ );
41
+ };
42
+ const onLeave = () => {
43
+ overlay.classList.remove('is-active');
44
+ plot.dispatchEvent(new CustomEvent('bronto:crosshair:leave', { bubbles: true }));
45
+ };
46
+ cleanups.push(
47
+ bindOnce(plot, 'crosshair', () => {
48
+ plot.addEventListener('pointermove', onMove);
49
+ plot.addEventListener('pointerleave', onLeave);
50
+ return () => {
51
+ plot.removeEventListener('pointermove', onMove);
52
+ plot.removeEventListener('pointerleave', onLeave);
53
+ };
54
+ }),
55
+ );
56
+ }
57
+ return () => cleanups.forEach((fn) => fn());
58
+ }
@@ -0,0 +1,73 @@
1
+ import { hasDom, noop, bindOnce, byIdInHost } from './internal.js';
2
+
3
+ /**
4
+ * Wire native <dialog> open/close glue (the one bit <dialog> can't do
5
+ * declaratively). Click `[data-bronto-open="dialogId"]` calls
6
+ * `showModal()` on `#dialogId`; click `[data-bronto-close]` closes the
7
+ * nearest enclosing <dialog>. Clicking the backdrop of a dialog that has
8
+ * `[data-bronto-dialog-light]` closes it too. On open the trigger is
9
+ * remembered and focus is returned to it on *every* close path (Esc,
10
+ * close button, backdrop light-dismiss, programmatic) via the native
11
+ * `close` event, so keyboard/SR users are never dropped at `<body>`.
12
+ * SSR-safe and idempotent; returns cleanup.
13
+ *
14
+ * `root` scopes delegated triggers (default `document`). Controlled targets are
15
+ * resolved root-first, then document-wide, so scoped islands win duplicate-id
16
+ * conflicts without breaking body/portal-mounted overlays.
17
+ */
18
+ export function initDialog({ root } = {}) {
19
+ if (!hasDom()) return noop;
20
+ const host = root || document;
21
+ const managedDialogs = new WeakSet();
22
+ const canManageDialog = (dlg, origin) => host.contains(origin) || managedDialogs.has(dlg);
23
+
24
+ const openFrom = (opener) => {
25
+ const dlg = byIdInHost(host, opener.getAttribute('data-bronto-open'));
26
+ if (!dlg || typeof dlg.showModal !== 'function' || dlg.open) return;
27
+ managedDialogs.add(dlg);
28
+ dlg.addEventListener(
29
+ 'close',
30
+ () => {
31
+ if (opener.isConnected && typeof opener.focus === 'function') opener.focus();
32
+ },
33
+ { once: true },
34
+ );
35
+ dlg.showModal();
36
+ };
37
+
38
+ const closeFrom = (closer) => {
39
+ const dlg = closer.closest('dialog');
40
+ if (dlg && dlg.open && canManageDialog(dlg, closer)) dlg.close();
41
+ };
42
+
43
+ const lightDismiss = (dlg) => {
44
+ if (
45
+ dlg.tagName === 'DIALOG' &&
46
+ dlg.open &&
47
+ dlg.hasAttribute('data-bronto-dialog-light') &&
48
+ canManageDialog(dlg, dlg)
49
+ ) {
50
+ dlg.close();
51
+ }
52
+ };
53
+
54
+ const onClick = (e) => {
55
+ const opener = e.target.closest('[data-bronto-open]');
56
+ if (opener && host.contains(opener)) {
57
+ openFrom(opener);
58
+ return;
59
+ }
60
+ const closer = e.target.closest('[data-bronto-close]');
61
+ if (closer) {
62
+ closeFrom(closer);
63
+ return;
64
+ }
65
+ // Light-dismiss: a click whose target is the <dialog> itself is the
66
+ // backdrop (content sits in child elements).
67
+ lightDismiss(e.target);
68
+ };
69
+ return bindOnce(host, 'dialog', () => {
70
+ document.addEventListener('click', onClick);
71
+ return () => document.removeEventListener('click', onClick);
72
+ });
73
+ }
@@ -0,0 +1,25 @@
1
+ import { hasDom, noop, bindOnce, byIdInHost } from './internal.js';
2
+
3
+ /**
4
+ * Disclosure: a `[data-bronto-disclosure]` trigger toggles the element
5
+ * referenced by its `aria-controls` id, keeping `aria-expanded` and the
6
+ * panel's `hidden` attribute in sync.
7
+ */
8
+ export function initDisclosure({ root } = {}) {
9
+ if (!hasDom()) return noop;
10
+ const host = root || document;
11
+ const onClick = (e) => {
12
+ const trigger = e.target.closest('[data-bronto-disclosure]');
13
+ if (!trigger || !host.contains(trigger)) return;
14
+ const id = trigger.getAttribute('aria-controls');
15
+ const panel = byIdInHost(host, id);
16
+ if (!panel) return;
17
+ const open = trigger.getAttribute('aria-expanded') === 'true';
18
+ trigger.setAttribute('aria-expanded', String(!open));
19
+ panel.hidden = open;
20
+ };
21
+ return bindOnce(host, 'disclosure', () => {
22
+ host.addEventListener('click', onClick);
23
+ return () => host.removeEventListener('click', onClick);
24
+ });
25
+ }
@@ -0,0 +1,24 @@
1
+ import { hasDom, noop, bindOnce, closestSafe } from './internal.js';
2
+
3
+ /**
4
+ * Click on `[data-bronto-dismiss]` removes the nearest ancestor matching
5
+ * `[data-bronto-dismissible]` (or the selector given as the attribute
6
+ * value). Emits a cancelable `bronto:dismiss` event first.
7
+ */
8
+ export function dismissible({ root } = {}) {
9
+ if (!hasDom()) return noop;
10
+ const host = root || document;
11
+ const onClick = (e) => {
12
+ const btn = e.target.closest('[data-bronto-dismiss]');
13
+ if (!btn || !host.contains(btn)) return;
14
+ const sel = btn.getAttribute('data-bronto-dismiss');
15
+ const target = sel ? closestSafe(btn, sel) : btn.closest('[data-bronto-dismissible]');
16
+ if (!target) return;
17
+ const ev = new CustomEvent('bronto:dismiss', { bubbles: true, cancelable: true });
18
+ if (target.dispatchEvent(ev)) target.remove();
19
+ };
20
+ return bindOnce(host, 'dismissible', () => {
21
+ host.addEventListener('click', onClick);
22
+ return () => host.removeEventListener('click', onClick);
23
+ });
24
+ }
@@ -0,0 +1,158 @@
1
+ import { hasDom, noop, bindOnce, nextFieldUid } from './internal.js';
2
+
3
+ /**
4
+ * Accessible form validation glue for `<form data-bronto-validate>`.
5
+ * Progressive enhancement over the native Constraint Validation API —
6
+ * the framework already ships the `[aria-invalid]` / `.ui-hint--error`
7
+ * styling; this wires the a11y plumbing every consumer would otherwise
8
+ * re-implement (and usually get wrong):
9
+ *
10
+ * - suppresses the native error bubbles (`form.noValidate`),
11
+ * - on blur and on submit sets `aria-invalid` and writes the browser's
12
+ * `validationMessage` into the field's error slot
13
+ * (`[data-bronto-error]` inside the `.ui-field`, falling back to a
14
+ * `.ui-hint`), linked via `aria-describedby`,
15
+ * - on an invalid submit, fills the form's
16
+ * `[data-bronto-error-summary]` (a `.ui-error-summary`) with
17
+ * in-page links to each bad field, focuses it, and blocks submit.
18
+ *
19
+ * Pure enhancement: with JS off the form still submits and the browser
20
+ * validates natively. SSR-safe, idempotent; returns a cleanup function.
21
+ */
22
+ export function initFormValidation({ root } = {}) {
23
+ if (!hasDom()) return noop;
24
+ const host = root || document;
25
+
26
+ const ensureId = (el, prefix) => {
27
+ if (!el.id) el.id = `${prefix}-${nextFieldUid()}`;
28
+ return el.id;
29
+ };
30
+
31
+ const slotFor = (control) => {
32
+ const field = control.closest('.ui-field');
33
+ if (!field) return null;
34
+ return field.querySelector('[data-bronto-error]') || field.querySelector('.ui-hint');
35
+ };
36
+
37
+ const link = (control, slot) => {
38
+ const slotId = ensureId(slot, 'bronto-err');
39
+ const ids = (control.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
40
+ if (!ids.includes(slotId)) {
41
+ ids.push(slotId);
42
+ control.setAttribute('aria-describedby', ids.join(' '));
43
+ }
44
+ };
45
+
46
+ const validateField = (control) => {
47
+ if (!control.willValidate) return true;
48
+ const ok = control.validity.valid;
49
+ const slot = slotFor(control);
50
+ if (ok) {
51
+ control.removeAttribute('aria-invalid');
52
+ if (slot) {
53
+ slot.textContent = '';
54
+ if (slot.classList.contains('ui-hint')) slot.classList.remove('ui-hint--error');
55
+ }
56
+ } else {
57
+ control.setAttribute('aria-invalid', 'true');
58
+ if (slot) {
59
+ slot.textContent = control.validationMessage;
60
+ if (slot.classList.contains('ui-hint')) slot.classList.add('ui-hint--error');
61
+ link(control, slot);
62
+ }
63
+ }
64
+ return ok;
65
+ };
66
+
67
+ const controlsOf = (form) =>
68
+ [...form.elements].filter(
69
+ (el) => el.willValidate && el.type !== 'submit' && el.type !== 'button',
70
+ );
71
+
72
+ const refreshSummary = (form, invalid) => {
73
+ const summary = form.querySelector('[data-bronto-error-summary]');
74
+ if (!summary) return;
75
+ if (!invalid.length) {
76
+ summary.hidden = true;
77
+ summary.replaceChildren();
78
+ return;
79
+ }
80
+ const title = document.createElement('p');
81
+ title.className = 'ui-error-summary__title';
82
+ title.textContent = 'There is a problem';
83
+ const list = document.createElement('ul');
84
+ list.className = 'ui-error-summary__list';
85
+ for (const c of invalid) {
86
+ const id = ensureId(c, 'bronto-field');
87
+ const li = document.createElement('li');
88
+ const a = document.createElement('a');
89
+ a.href = `#${id}`;
90
+ a.textContent = c.validationMessage;
91
+ a.addEventListener('click', (e) => {
92
+ e.preventDefault();
93
+ c.focus();
94
+ });
95
+ li.appendChild(a);
96
+ list.appendChild(li);
97
+ }
98
+ summary.replaceChildren(title, list);
99
+ summary.setAttribute('role', 'alert');
100
+ summary.tabIndex = -1;
101
+ summary.hidden = false;
102
+ };
103
+
104
+ const onSubmit = (e) => {
105
+ const form = e.target.closest?.('[data-bronto-validate]');
106
+ if (!form) return;
107
+ form.noValidate = true;
108
+ const invalid = controlsOf(form).filter((c) => !validateField(c));
109
+ refreshSummary(form, invalid);
110
+ if (invalid.length) {
111
+ e.preventDefault();
112
+ const summary = form.querySelector('[data-bronto-error-summary]');
113
+ (summary && !summary.hidden ? summary : invalid[0]).focus();
114
+ }
115
+ };
116
+
117
+ const onBlur = (e) => {
118
+ const control = e.target;
119
+ if (!control.willValidate) return;
120
+ const form = control.closest?.('[data-bronto-validate]');
121
+ if (!form) return;
122
+ form.noValidate = true;
123
+ validateField(control);
124
+ const summary = form.querySelector('[data-bronto-error-summary]');
125
+ if (summary && !summary.hidden)
126
+ refreshSummary(
127
+ form,
128
+ controlsOf(form).filter((c) => !c.validity.valid),
129
+ );
130
+ };
131
+
132
+ return bindOnce(host, 'formValidation', () => {
133
+ // Suppress native bubbles UP FRONT for forms present at init. The
134
+ // in-handler `noValidate = true` only fires after the first
135
+ // submit/blur, so the very first invalid real-browser submit would
136
+ // otherwise show the native UA bubble instead of the Bronto
137
+ // summary — contradicting the documented contract. (Forms added
138
+ // after init are still covered by the in-handler set.)
139
+ // Feature-detect rather than `instanceof Element` — `Element` is not
140
+ // a guaranteed global (SSR / the no-DOM test env), and `host` is
141
+ // either `document` (no `.matches`) or a root Element.
142
+ const selfForm =
143
+ typeof host.matches === 'function' && host.matches('[data-bronto-validate]') ? [host] : [];
144
+ const forms = [...selfForm, ...(host.querySelectorAll?.('[data-bronto-validate]') ?? [])];
145
+ const priorNoValidate = new Map();
146
+ for (const f of forms) {
147
+ priorNoValidate.set(f, f.noValidate);
148
+ f.noValidate = true;
149
+ }
150
+ host.addEventListener('submit', onSubmit, true);
151
+ host.addEventListener('focusout', onBlur);
152
+ return () => {
153
+ host.removeEventListener('submit', onSubmit, true);
154
+ host.removeEventListener('focusout', onBlur);
155
+ for (const [f, v] of priorNoValidate) f.noValidate = v;
156
+ };
157
+ });
158
+ }
@@ -0,0 +1,109 @@
1
+ import { hasDom, noop } from './internal.js';
2
+ import { GLYPH_SIZE, glyphCells } from '../glyphs/glyphs.js';
3
+
4
+ function restoreAttr(el, name, prev) {
5
+ if (prev === null) el.removeAttribute(name);
6
+ else el.setAttribute(name, prev);
7
+ }
8
+
9
+ /**
10
+ * Expand `[data-bronto-glyph="name"]` placeholders into a `.ui-dotmatrix`
11
+ * grid of GLYPH_SIZE² cells — the DOM counterpart to renderGlyph() from
12
+ * `@ponchia/ui/glyphs`, for when you'd rather drop a placeholder than inline
13
+ * the markup. Decorative by default (`aria-hidden`); add
14
+ * `data-bronto-glyph-label` to expose it as `role="img"`. An unknown glyph
15
+ * name is left untouched. Idempotent (skips an already-expanded host); the
16
+ * returned cleanup removes the cells and restores the original attributes.
17
+ */
18
+ export function initDotGlyph({ root } = {}) {
19
+ if (!hasDom()) return noop;
20
+ const host = root || document;
21
+ const els = [];
22
+ if (host !== document && host.matches?.('[data-bronto-glyph]')) els.push(host);
23
+ els.push(...(host.querySelectorAll?.('[data-bronto-glyph]') ?? []));
24
+ const cleanups = [];
25
+
26
+ for (const el of els) {
27
+ // Scope to DIRECT-child cells (the ones we append) — so a placeholder that
28
+ // legitimately nests its own .ui-dotmatrix is neither mis-read as already
29
+ // expanded here nor have its inner cells removed by cleanup below.
30
+ if (el.querySelector(':scope > .ui-dotmatrix__cell')) continue; // already expanded
31
+ const cells = glyphCells(el.getAttribute('data-bronto-glyph'));
32
+ if (!cells.length) continue; // unknown glyph — leave the placeholder as-is
33
+
34
+ const label = el.getAttribute('data-bronto-glyph-label');
35
+ // `data-bronto-glyph-solid` → square, gapless pixel glyph (legible small),
36
+ // the DOM counterpart to renderGlyph's `solid` option. Implies glyph-only.
37
+ const solid = el.hasAttribute('data-bronto-glyph-solid');
38
+ // `data-bronto-glyph-anim="reveal|pulse"` → decorative animation (the DOM
39
+ // counterpart to renderGlyph's `anim`; reduced-motion-safe via CSS).
40
+ const animAttr = el.getAttribute('data-bronto-glyph-anim');
41
+ const animClass =
42
+ animAttr === 'reveal'
43
+ ? 'ui-dotmatrix--reveal'
44
+ : animAttr === 'pulse'
45
+ ? 'ui-dotmatrix--pulse'
46
+ : null;
47
+ const hadAnimClass = animClass ? el.classList.contains(animClass) : false;
48
+ const hadMatrix = el.classList.contains('ui-dotmatrix');
49
+ const hadCols = el.style.getPropertyValue('--dotmatrix-cols');
50
+ const hadRadius = el.style.getPropertyValue('--dotmatrix-dot-radius');
51
+ const hadGap = el.style.getPropertyValue('--dotmatrix-gap');
52
+ const hadAriaHidden = el.getAttribute('aria-hidden');
53
+ const hadRole = el.getAttribute('role');
54
+ const hadAriaLabel = el.getAttribute('aria-label');
55
+
56
+ el.classList.add('ui-dotmatrix');
57
+ if (animClass) el.classList.add(animClass);
58
+ el.style.setProperty('--dotmatrix-cols', String(GLYPH_SIZE));
59
+ if (solid) {
60
+ el.style.setProperty('--dotmatrix-dot-radius', '0');
61
+ el.style.setProperty('--dotmatrix-gap', '0');
62
+ }
63
+ if (label) {
64
+ el.setAttribute('role', 'img');
65
+ el.setAttribute('aria-label', label);
66
+ el.removeAttribute('aria-hidden'); // a labelled img must not also be hidden
67
+ } else {
68
+ el.setAttribute('aria-hidden', 'true');
69
+ }
70
+
71
+ const frag = document.createDocumentFragment();
72
+ cells.forEach((c, i) => {
73
+ const span = document.createElement('span');
74
+ span.className = !c.on
75
+ ? 'ui-dotmatrix__cell'
76
+ : c.tone === 'hot'
77
+ ? 'ui-dotmatrix__cell ui-dotmatrix__cell--hot'
78
+ : c.tone === 'accent'
79
+ ? 'ui-dotmatrix__cell ui-dotmatrix__cell--accent'
80
+ : 'ui-dotmatrix__cell';
81
+ if (!c.on && solid) span.style.background = 'transparent'; // glyph-only
82
+ if (animAttr === 'reveal') span.style.setProperty('--i', String(i)); // scan stagger
83
+ frag.appendChild(span);
84
+ });
85
+ el.appendChild(frag);
86
+
87
+ cleanups.push(() => {
88
+ el.querySelectorAll(':scope > .ui-dotmatrix__cell').forEach((n) => n.remove());
89
+ if (!hadMatrix) el.classList.remove('ui-dotmatrix');
90
+ if (animClass && !hadAnimClass) el.classList.remove(animClass);
91
+ if (solid) {
92
+ if (hadRadius) el.style.setProperty('--dotmatrix-dot-radius', hadRadius);
93
+ else el.style.removeProperty('--dotmatrix-dot-radius');
94
+ if (hadGap) el.style.setProperty('--dotmatrix-gap', hadGap);
95
+ else el.style.removeProperty('--dotmatrix-gap');
96
+ }
97
+ if (hadCols) el.style.setProperty('--dotmatrix-cols', hadCols);
98
+ else el.style.removeProperty('--dotmatrix-cols');
99
+ restoreAttr(el, 'aria-hidden', hadAriaHidden);
100
+ restoreAttr(el, 'role', hadRole);
101
+ restoreAttr(el, 'aria-label', hadAriaLabel);
102
+ // Don't leave behind empty class=""/style="" we ourselves created.
103
+ if (el.getAttribute('class') === '') el.removeAttribute('class');
104
+ if (el.getAttribute('style') === '') el.removeAttribute('style');
105
+ });
106
+ }
107
+
108
+ return () => cleanups.forEach((fn) => fn());
109
+ }
@@ -156,3 +156,82 @@ export declare function toast(message: string, opts?: ToastOpts): Cleanup;
156
156
  * attributes.
157
157
  */
158
158
  export declare function initDotGlyph(opts?: DelegateOpts): Cleanup;
159
+
160
+ /** `bronto:legend:toggle` CustomEvent detail. `series` is the entry's
161
+ * `data-series`, or its 0-based index when unset. `active` is the new state
162
+ * (`true` ⇒ series shown). */
163
+ export interface LegendToggleDetail {
164
+ series: string | number;
165
+ active: boolean;
166
+ }
167
+
168
+ /**
169
+ * Wire `[data-bronto-legend]` interactive legends. Each `.ui-legend__item` is a
170
+ * `<button aria-pressed>`; activating it flips `aria-pressed`, toggles
171
+ * `.is-inactive`, and dispatches `bronto:legend:toggle`
172
+ * ({@link LegendToggleDetail}) on the legend. Bronto owns the control + its
173
+ * state only — the host hides its own series and owns any `aria-live`
174
+ * announcement (`aria-pressed="true"` ⇒ series shown). SSR-safe, idempotent per
175
+ * host. Returns a cleanup function.
176
+ */
177
+ export declare function initLegend(opts?: DelegateOpts): Cleanup;
178
+
179
+ /**
180
+ * Draw + keep leader lines in sync. Each `[data-bronto-connector]` is a
181
+ * `.ui-connector` SVG overlaying a positioned container; `data-from`/`data-to`
182
+ * are the ids of the elements to connect (with optional `data-shape`,
183
+ * `data-from-side`/`data-to-side`, `data-end`). Computes geometry via the
184
+ * `@ponchia/ui/connectors` helpers and redraws on resize/scroll. Bronto owns no
185
+ * layout. SSR-safe, idempotent per host; returns a cleanup that disconnects
186
+ * observers/listeners. Re-run after adding/removing connectors.
187
+ */
188
+ export declare function initConnectors(opts?: DelegateOpts): Cleanup;
189
+
190
+ /**
191
+ * Position a spotlight cutout over a target. Each `[data-bronto-spotlight]` is a
192
+ * `.ui-spotlight` overlay; `data-target` is the id of the element to highlight.
193
+ * Sets `--spot-x/y/w/h` and re-places on resize/scroll and when `data-target`
194
+ * changes. NOT a tour engine — the host owns step order/advancing/visibility.
195
+ * SSR-safe, idempotent per host; returns a cleanup function.
196
+ */
197
+ export declare function initSpotlight(opts?: DelegateOpts): Cleanup;
198
+
199
+ /** `bronto:crosshair:move` CustomEvent detail — pointer position over the plot
200
+ * in pixels and as 0..1 fractions. Bronto reports where; mapping to data is
201
+ * the host's. */
202
+ export interface CrosshairMoveDetail {
203
+ x: number;
204
+ y: number;
205
+ fx: number;
206
+ fy: number;
207
+ }
208
+
209
+ /**
210
+ * Track the pointer over `[data-bronto-crosshair]` plots and drive a contained
211
+ * `.ui-crosshair` overlay: sets `--crosshair-x/y` (px), marks `.is-active`, and
212
+ * dispatches `bronto:crosshair:move` ({@link CrosshairMoveDetail}) /
213
+ * `bronto:crosshair:leave`. Reports the pointer position only — it does not find
214
+ * the nearest datum or map pixels to data. SSR-safe, idempotent per plot;
215
+ * returns a cleanup function.
216
+ */
217
+ export declare function initCrosshair(opts?: DelegateOpts): Cleanup;
218
+
219
+ /** `bronto:command:select` CustomEvent detail — the chosen command's value and
220
+ * visible label. The host executes it; Bronto only filters and navigates. */
221
+ export interface CommandSelectDetail {
222
+ value: string;
223
+ label: string;
224
+ }
225
+
226
+ /**
227
+ * Filter + keyboard-navigate a DOM-authored command list inside
228
+ * `[data-bronto-command]` (the `.ui-command` shell). Owns ids,
229
+ * `role=combobox/listbox/option`, `aria-activedescendant`, a roving active item,
230
+ * substring filtering (hiding empty groups), full keyboard
231
+ * (Down/Up/Home/End/Enter/Escape), and pointer select. Emits
232
+ * `bronto:command:select` ({@link CommandSelectDetail}) on choose and
233
+ * `bronto:command:close` on Escape; the host owns the action registry, routing,
234
+ * and execution. No global Cmd/Ctrl+K. SSR-safe, idempotent per instance;
235
+ * returns a cleanup function.
236
+ */
237
+ export declare function initCommand(opts?: DelegateOpts): Cleanup;