@ponchia/ui 0.5.0 → 0.6.3

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 (196) hide show
  1. package/CHANGELOG.md +386 -4
  2. package/MIGRATIONS.json +14 -0
  3. package/README.md +29 -6
  4. package/annotations/index.d.ts +398 -276
  5. package/annotations/index.d.ts.map +1 -0
  6. package/annotations/index.js +350 -77
  7. package/behaviors/carousel.d.ts +28 -0
  8. package/behaviors/carousel.d.ts.map +1 -0
  9. package/behaviors/carousel.js +20 -16
  10. package/behaviors/combobox.d.ts +40 -0
  11. package/behaviors/combobox.d.ts.map +1 -0
  12. package/behaviors/combobox.js +111 -29
  13. package/behaviors/command.d.ts +41 -0
  14. package/behaviors/command.d.ts.map +1 -0
  15. package/behaviors/command.js +27 -15
  16. package/behaviors/connectors.d.ts +17 -0
  17. package/behaviors/connectors.d.ts.map +1 -0
  18. package/behaviors/connectors.js +7 -5
  19. package/behaviors/crosshair.d.ts +42 -0
  20. package/behaviors/crosshair.d.ts.map +1 -0
  21. package/behaviors/crosshair.js +23 -6
  22. package/behaviors/dialog.d.ts +20 -0
  23. package/behaviors/dialog.d.ts.map +1 -0
  24. package/behaviors/dialog.js +6 -2
  25. package/behaviors/disclosure.d.ts +10 -0
  26. package/behaviors/disclosure.d.ts.map +1 -0
  27. package/behaviors/disclosure.js +6 -2
  28. package/behaviors/dismissible.d.ts +10 -0
  29. package/behaviors/dismissible.d.ts.map +1 -0
  30. package/behaviors/dismissible.js +6 -2
  31. package/behaviors/forms.d.ts +27 -0
  32. package/behaviors/forms.d.ts.map +1 -0
  33. package/behaviors/forms.js +54 -13
  34. package/behaviors/glyph.d.ts +14 -0
  35. package/behaviors/glyph.d.ts.map +1 -0
  36. package/behaviors/glyph.js +28 -5
  37. package/behaviors/index.d.ts +31 -237
  38. package/behaviors/index.d.ts.map +1 -0
  39. package/behaviors/index.js +17 -0
  40. package/behaviors/inert.d.ts +20 -0
  41. package/behaviors/inert.d.ts.map +1 -0
  42. package/behaviors/inert.js +46 -0
  43. package/behaviors/internal.d.ts +25 -0
  44. package/behaviors/internal.d.ts.map +1 -0
  45. package/behaviors/internal.js +77 -1
  46. package/behaviors/legend.d.ts +35 -0
  47. package/behaviors/legend.d.ts.map +1 -0
  48. package/behaviors/legend.js +32 -2
  49. package/behaviors/menu.d.ts +16 -0
  50. package/behaviors/menu.d.ts.map +1 -0
  51. package/behaviors/menu.js +6 -2
  52. package/behaviors/modal.d.ts +41 -0
  53. package/behaviors/modal.d.ts.map +1 -0
  54. package/behaviors/modal.js +124 -0
  55. package/behaviors/popover.d.ts +28 -0
  56. package/behaviors/popover.d.ts.map +1 -0
  57. package/behaviors/popover.js +78 -7
  58. package/behaviors/spotlight.d.ts +17 -0
  59. package/behaviors/spotlight.d.ts.map +1 -0
  60. package/behaviors/spotlight.js +7 -5
  61. package/behaviors/table.d.ts +36 -0
  62. package/behaviors/table.d.ts.map +1 -0
  63. package/behaviors/table.js +84 -17
  64. package/behaviors/tabs.d.ts +20 -0
  65. package/behaviors/tabs.d.ts.map +1 -0
  66. package/behaviors/tabs.js +17 -14
  67. package/behaviors/theme.d.ts +54 -0
  68. package/behaviors/theme.d.ts.map +1 -0
  69. package/behaviors/theme.js +22 -3
  70. package/behaviors/toast.d.ts +49 -0
  71. package/behaviors/toast.d.ts.map +1 -0
  72. package/behaviors/toast.js +47 -3
  73. package/classes/classes.json +2527 -0
  74. package/classes/index.d.ts +134 -15
  75. package/classes/index.js +280 -80
  76. package/classes/vscode.css-custom-data.json +12 -0
  77. package/connectors/index.d.ts +201 -69
  78. package/connectors/index.d.ts.map +1 -0
  79. package/connectors/index.js +142 -25
  80. package/css/app.css +69 -13
  81. package/css/base.css +15 -10
  82. package/css/bullet.css +108 -0
  83. package/css/code.css +98 -0
  84. package/css/connectors.css +17 -0
  85. package/css/content.css +22 -3
  86. package/css/crosshair.css +7 -7
  87. package/css/dataviz.css +5 -1
  88. package/css/diff.css +153 -0
  89. package/css/disclosure.css +53 -7
  90. package/css/dots.css +94 -7
  91. package/css/feedback.css +97 -7
  92. package/css/forms.css +113 -4
  93. package/css/legend.css +16 -9
  94. package/css/marks.css +38 -8
  95. package/css/motion.css +98 -53
  96. package/css/navigation.css +7 -0
  97. package/css/overlay.css +90 -3
  98. package/css/primitives.css +158 -13
  99. package/css/report.css +73 -56
  100. package/css/sidenote.css +67 -0
  101. package/css/site.css +16 -2
  102. package/css/sources.css +43 -1
  103. package/css/spark.css +62 -0
  104. package/css/spotlight.css +1 -1
  105. package/css/table.css +9 -2
  106. package/css/term.css +110 -0
  107. package/css/textref.css +63 -0
  108. package/css/toc.css +91 -0
  109. package/css/tokens.css +49 -1
  110. package/css/tree.css +134 -0
  111. package/css/workbench.css +1 -1
  112. package/dist/bronto.css +1 -1
  113. package/dist/css/analytical.css +1 -1
  114. package/dist/css/app.css +1 -1
  115. package/dist/css/base.css +1 -1
  116. package/dist/css/bullet.css +1 -0
  117. package/dist/css/code.css +1 -0
  118. package/dist/css/connectors.css +1 -1
  119. package/dist/css/content.css +1 -1
  120. package/dist/css/crosshair.css +1 -1
  121. package/dist/css/diff.css +1 -0
  122. package/dist/css/disclosure.css +1 -1
  123. package/dist/css/dots.css +1 -1
  124. package/dist/css/feedback.css +1 -1
  125. package/dist/css/forms.css +1 -1
  126. package/dist/css/legend.css +1 -1
  127. package/dist/css/marks.css +1 -1
  128. package/dist/css/motion.css +1 -1
  129. package/dist/css/navigation.css +1 -1
  130. package/dist/css/overlay.css +1 -1
  131. package/dist/css/primitives.css +1 -1
  132. package/dist/css/report.css +1 -1
  133. package/dist/css/sidenote.css +1 -0
  134. package/dist/css/site.css +1 -1
  135. package/dist/css/sources.css +1 -1
  136. package/dist/css/spark.css +1 -0
  137. package/dist/css/spotlight.css +1 -1
  138. package/dist/css/table.css +1 -1
  139. package/dist/css/term.css +1 -0
  140. package/dist/css/textref.css +1 -0
  141. package/dist/css/toc.css +1 -0
  142. package/dist/css/tokens.css +1 -1
  143. package/dist/css/tree.css +1 -0
  144. package/dist/css/workbench.css +1 -1
  145. package/docs/adr/0003-theme-model.md +1 -1
  146. package/docs/annotations.md +133 -14
  147. package/docs/architecture.md +49 -6
  148. package/docs/bullet.md +78 -0
  149. package/docs/code.md +76 -0
  150. package/docs/contrast.md +116 -92
  151. package/docs/d2.md +196 -0
  152. package/docs/diff.md +146 -0
  153. package/docs/legends.md +23 -3
  154. package/docs/marks.md +9 -2
  155. package/docs/mermaid.md +169 -0
  156. package/docs/reference.md +201 -26
  157. package/docs/reporting.md +416 -57
  158. package/docs/sidenote.md +64 -0
  159. package/docs/sources.md +27 -0
  160. package/docs/spark.md +78 -0
  161. package/docs/stability.md +10 -2
  162. package/docs/term.md +81 -0
  163. package/docs/textref.md +78 -0
  164. package/docs/theming.md +44 -5
  165. package/docs/toc.md +83 -0
  166. package/docs/tree.md +74 -0
  167. package/docs/usage.md +354 -16
  168. package/docs/vega.md +244 -0
  169. package/docs/workbench.md +7 -1
  170. package/glyphs/glyphs.js +13 -5
  171. package/llms.txt +285 -14
  172. package/package.json +95 -17
  173. package/qwik/index.d.ts +44 -59
  174. package/qwik/index.d.ts.map +1 -0
  175. package/qwik/index.js +65 -3
  176. package/react/index.d.ts +41 -61
  177. package/react/index.d.ts.map +1 -0
  178. package/react/index.js +63 -3
  179. package/solid/index.d.ts +68 -61
  180. package/solid/index.d.ts.map +1 -0
  181. package/solid/index.js +66 -3
  182. package/tokens/d2.d.ts +38 -0
  183. package/tokens/d2.js +71 -0
  184. package/tokens/d2.json +43 -0
  185. package/tokens/index.d.ts +5 -5
  186. package/tokens/index.js +15 -1
  187. package/tokens/index.json +9 -0
  188. package/tokens/mermaid.d.ts +23 -0
  189. package/tokens/mermaid.js +181 -0
  190. package/tokens/mermaid.json +163 -0
  191. package/tokens/resolved.json +45 -1
  192. package/tokens/skins.js +3 -2
  193. package/tokens/tokens.dtcg.json +26 -0
  194. package/tokens/vega.d.ts +34 -0
  195. package/tokens/vega.js +155 -0
  196. package/tokens/vega.json +179 -0
@@ -1,4 +1,11 @@
1
- import { hasDom, noop, bindOnce } from './internal.js';
1
+ import {
2
+ hasDom,
3
+ resolveHost,
4
+ noop,
5
+ bindOnce,
6
+ scrollIntoViewSafe,
7
+ collectHosts,
8
+ } from './internal.js';
2
9
 
3
10
  /**
4
11
  * Image carousel / gallery, built on CSS scroll-snap so touch + trackpad
@@ -22,13 +29,15 @@ import { hasDom, noop, bindOnce } from './internal.js';
22
29
  * Emits `bronto:change` ({ detail: { index } }) on every index change
23
30
  * (button, key, thumbnail, or swipe). SSR-safe, idempotent per carousel;
24
31
  * returns a cleanup function.
32
+ *
33
+ * @param {import('./internal.js').DelegateOpts} [opts]
34
+ * @returns {import('./internal.js').Cleanup}
25
35
  */
26
36
  export function initCarousel({ root } = {}) {
27
37
  if (!hasDom()) return noop;
28
- const host = root || document;
29
- const boxes = [];
30
- if (host !== document && host.matches?.('[data-bronto-carousel]')) boxes.push(host);
31
- boxes.push(...(host.querySelectorAll?.('[data-bronto-carousel]') ?? []));
38
+ const host = resolveHost(root);
39
+ if (!host) return noop;
40
+ const boxes = collectHosts(host, '[data-bronto-carousel]');
32
41
  const cleanups = [];
33
42
 
34
43
  for (const box of boxes) {
@@ -103,16 +112,7 @@ export function initCarousel({ root } = {}) {
103
112
  const emit = () =>
104
113
  box.dispatchEvent(new CustomEvent('bronto:change', { detail: { index }, bubbles: true }));
105
114
 
106
- // jsdom (and any layout-less env) has no scrollIntoView; it's a pure
107
- // affordance, so never let it break index/aria sync — same guard as
108
- // initCombobox.
109
- const reveal = (el) => {
110
- try {
111
- el?.scrollIntoView({ block: 'nearest', inline: 'center' });
112
- } catch {
113
- /* no layout — ignore */
114
- }
115
- };
115
+ const reveal = (el) => scrollIntoViewSafe(el, { block: 'nearest', inline: 'center' });
116
116
 
117
117
  const goTo = (i, { emitChange = true } = {}) => {
118
118
  const next = loop ? (i + n) % n : Math.max(0, Math.min(n - 1, i));
@@ -176,13 +176,17 @@ export function initCarousel({ root } = {}) {
176
176
  },
177
177
  { root: viewport, threshold: 0.6 },
178
178
  );
179
- slides.forEach((s) => io.observe(s));
180
179
  }
181
180
 
182
181
  render();
183
182
  const bound = bindOnce(box, 'carousel', () => {
184
183
  viewport.addEventListener('keydown', onKey);
185
184
  box.addEventListener('click', onClick);
185
+ // Observe inside the add callback so observe/disconnect pair with the
186
+ // binding lifecycle: a re-init tears down the prior binding (which
187
+ // disconnects the old observer) before this starts, so two observers
188
+ // never watch the same slides — even for one tick.
189
+ slides.forEach((s) => io?.observe(s));
186
190
  return () => {
187
191
  viewport.removeEventListener('keydown', onKey);
188
192
  box.removeEventListener('click', onClick);
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Editable combobox with a filtered listbox popup, implementing the
3
+ * WAI-ARIA APG combobox pattern (the widget the framework most lacked
4
+ * and consumers most often build badly). Dependency-free, no
5
+ * positioning library — the list is CSS-anchored under the input.
6
+ *
7
+ * The input MUST have an accessible name — a `<label>`, `aria-label`, or
8
+ * `aria-labelledby` (a placeholder does not count). A nameless `role="combobox"`
9
+ * is a silent screen-reader failure, so the behavior warns at dev time when it
10
+ * finds one, and mirrors the input's name onto the listbox.
11
+ *
12
+ * Markup: `[data-bronto-combobox]` wrapping an `<input role="combobox">`
13
+ * (`.ui-combobox__input`) and a `<ul role="listbox">`
14
+ * (`.ui-combobox__list`) of `<li role="option">` (`.ui-combobox__option`,
15
+ * optional `data-value`). An optional `.ui-combobox__empty` (hidden at rest)
16
+ * shows when nothing matches. The behavior owns ids, `aria-expanded`,
17
+ * `aria-controls`, `aria-activedescendant`, roving active option,
18
+ * type-to-filter, full keyboard (Down/Up/Home/End/Enter/Escape/Tab),
19
+ * pointer select, and outside-click close. On select the **visible input shows
20
+ * the option's text label**, while the emitted `bronto:change` CustomEvent
21
+ * carries the option's `data-value` code: `{ detail: { value, label } }` (value
22
+ * falls back to the label when there is no `data-value`). SSR-safe, idempotent
23
+ * per instance; returns a cleanup function.
24
+ *
25
+ * Single-select APG deviations (intentional, for a filtering text combobox):
26
+ * ArrowDown on a CLOSED list opens + filters rather than pre-activating the
27
+ * first option, and Tab closes the list without committing the merely-highlighted
28
+ * option (only Enter/click commits). Both are safe for single-select.
29
+ *
30
+ * Options are read from the DOM at init; if you replace the listbox contents
31
+ * (e.g. async/remote results), either re-run initCombobox, or add
32
+ * `data-bronto-combobox-live` to the `[data-bronto-combobox]` host so a
33
+ * MutationObserver re-reads the options in place (opt-in — off by default so
34
+ * the common static case stays observer-free).
35
+ *
36
+ * @param {import('./internal.js').DelegateOpts} [opts]
37
+ * @returns {import('./internal.js').Cleanup}
38
+ */
39
+ export function initCombobox({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
40
+ //# sourceMappingURL=combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"combobox.d.ts","sourceRoot":"","sources":["combobox.js"],"names":[],"mappings":"AAWA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAqO3C"}
@@ -1,4 +1,13 @@
1
- import { hasDom, noop, bindOnce, nextFieldUid } from './internal.js';
1
+ import {
2
+ hasDom,
3
+ resolveHost,
4
+ noop,
5
+ bindOnce,
6
+ nextFieldUid,
7
+ scrollIntoViewSafe,
8
+ wrapIndex,
9
+ collectHosts,
10
+ } from './internal.js';
2
11
 
3
12
  /**
4
13
  * Editable combobox with a filtered listbox popup, implementing the
@@ -6,23 +15,43 @@ import { hasDom, noop, bindOnce, nextFieldUid } from './internal.js';
6
15
  * and consumers most often build badly). Dependency-free, no
7
16
  * positioning library — the list is CSS-anchored under the input.
8
17
  *
18
+ * The input MUST have an accessible name — a `<label>`, `aria-label`, or
19
+ * `aria-labelledby` (a placeholder does not count). A nameless `role="combobox"`
20
+ * is a silent screen-reader failure, so the behavior warns at dev time when it
21
+ * finds one, and mirrors the input's name onto the listbox.
22
+ *
9
23
  * Markup: `[data-bronto-combobox]` wrapping an `<input role="combobox">`
10
24
  * (`.ui-combobox__input`) and a `<ul role="listbox">`
11
25
  * (`.ui-combobox__list`) of `<li role="option">` (`.ui-combobox__option`,
12
- * optional `data-value`). An optional `.ui-combobox__empty` shows when
13
- * nothing matches. The behavior owns ids, `aria-expanded`,
26
+ * optional `data-value`). An optional `.ui-combobox__empty` (hidden at rest)
27
+ * shows when nothing matches. The behavior owns ids, `aria-expanded`,
14
28
  * `aria-controls`, `aria-activedescendant`, roving active option,
15
29
  * type-to-filter, full keyboard (Down/Up/Home/End/Enter/Escape/Tab),
16
- * pointer select, and outside-click close; it emits a `bronto:change`
17
- * CustomEvent ({ detail: { value } }) on selection. SSR-safe,
18
- * idempotent per instance; returns a cleanup function.
30
+ * pointer select, and outside-click close. On select the **visible input shows
31
+ * the option's text label**, while the emitted `bronto:change` CustomEvent
32
+ * carries the option's `data-value` code: `{ detail: { value, label } }` (value
33
+ * falls back to the label when there is no `data-value`). SSR-safe, idempotent
34
+ * per instance; returns a cleanup function.
35
+ *
36
+ * Single-select APG deviations (intentional, for a filtering text combobox):
37
+ * ArrowDown on a CLOSED list opens + filters rather than pre-activating the
38
+ * first option, and Tab closes the list without committing the merely-highlighted
39
+ * option (only Enter/click commits). Both are safe for single-select.
40
+ *
41
+ * Options are read from the DOM at init; if you replace the listbox contents
42
+ * (e.g. async/remote results), either re-run initCombobox, or add
43
+ * `data-bronto-combobox-live` to the `[data-bronto-combobox]` host so a
44
+ * MutationObserver re-reads the options in place (opt-in — off by default so
45
+ * the common static case stays observer-free).
46
+ *
47
+ * @param {import('./internal.js').DelegateOpts} [opts]
48
+ * @returns {import('./internal.js').Cleanup}
19
49
  */
20
50
  export function initCombobox({ root } = {}) {
21
51
  if (!hasDom()) return noop;
22
- const host = root || document;
23
- const boxes = [];
24
- if (host !== document && host.matches?.('[data-bronto-combobox]')) boxes.push(host);
25
- boxes.push(...(host.querySelectorAll?.('[data-bronto-combobox]') ?? []));
52
+ const host = resolveHost(root);
53
+ if (!host) return noop;
54
+ const boxes = collectHosts(host, '[data-bronto-combobox]');
26
55
  const cleanups = [];
27
56
 
28
57
  for (const box of boxes) {
@@ -30,20 +59,59 @@ export function initCombobox({ root } = {}) {
30
59
  const list = box.querySelector('[role="listbox"], .ui-combobox__list');
31
60
  if (!input || !list) continue;
32
61
  const empty = box.querySelector('.ui-combobox__empty');
33
- const options = [...list.querySelectorAll('[role="option"], .ui-combobox__option')];
34
-
35
62
  const listId = list.id || (list.id = `bronto-cb-list-${nextFieldUid()}`);
36
- options.forEach((o, i) => {
37
- if (!o.id) o.id = `${listId}-opt-${i}`;
38
- o.setAttribute('role', 'option');
39
- });
63
+ // Re-readable so the opt-in MutationObserver (`data-bronto-combobox-live`)
64
+ // can pick up async/replaced option nodes without a full re-init. `visible`,
65
+ // `filter`, `move`, etc. close over this binding, so reassigning it is enough.
66
+ let options = [];
67
+ const syncOptions = () => {
68
+ options = [...list.querySelectorAll('[role="option"], .ui-combobox__option')];
69
+ options.forEach((o, i) => {
70
+ if (!o.id) o.id = `${listId}-opt-${i}`;
71
+ o.setAttribute('role', 'option');
72
+ });
73
+ };
74
+ syncOptions();
40
75
  list.setAttribute('role', 'listbox');
76
+ // Give the listbox its own accessible name (a bare role=listbox is unnamed
77
+ // to a screen reader) by mirroring the input's REAL name. (a11y review C30.)
78
+ // The placeholder is deliberately NOT in this chain: the input warning below
79
+ // already rejects a placeholder as an inadequate name, so papering the
80
+ // listbox over with it would contradict that — if there's no real name the
81
+ // listbox stays unnamed and the warning is the signal. (component audit C28.)
82
+ if (!list.hasAttribute('aria-label') && !list.hasAttribute('aria-labelledby')) {
83
+ const name = input.getAttribute('aria-label') || input.labels?.[0]?.textContent?.trim();
84
+ if (name) list.setAttribute('aria-label', name);
85
+ }
86
+ // A `role="combobox"` with no accessible name is a silent AT failure. A
87
+ // placeholder is not a robust name (it can vanish and is ignored by some
88
+ // AT), so warn unless there is a real label/aria-label/aria-labelledby/title
89
+ // (C7). We can't invent a good name, hence a dev-time warning, not a guess.
90
+ const inputNamed =
91
+ input.hasAttribute('aria-label') ||
92
+ input.hasAttribute('aria-labelledby') ||
93
+ !!input.labels?.length ||
94
+ input.hasAttribute('title');
95
+ if (!inputNamed && typeof console !== 'undefined') {
96
+ console.warn(
97
+ '[bronto] initCombobox(): the combobox input has no accessible name — add a <label>, aria-label, or aria-labelledby (a placeholder is not enough).',
98
+ );
99
+ }
41
100
  input.setAttribute('role', 'combobox');
42
101
  input.setAttribute('aria-controls', listId);
43
102
  input.setAttribute('aria-autocomplete', 'list');
44
103
  input.setAttribute('aria-expanded', 'false');
45
104
  input.setAttribute('autocomplete', 'off');
46
105
  list.hidden = true;
106
+ // Hide the empty-state at rest: it must only appear once a filter yields no
107
+ // matches, never on an idle combobox. Without this an author who omits
108
+ // `hidden` on `.ui-combobox__empty` ships a box that reads "No matches"
109
+ // before the user has typed anything. (component audit C10.) Make it a
110
+ // status live region so its appearance is announced. (component audit C38.)
111
+ if (empty) {
112
+ empty.hidden = true;
113
+ if (!empty.hasAttribute('role')) empty.setAttribute('role', 'status');
114
+ }
47
115
 
48
116
  let active = -1;
49
117
  const visible = () => options.filter((o) => !o.hidden);
@@ -53,13 +121,7 @@ export function initCombobox({ root } = {}) {
53
121
  if (opt) {
54
122
  opt.classList.add('is-active');
55
123
  input.setAttribute('aria-activedescendant', opt.id);
56
- // jsdom's scrollIntoView throws "Not implemented"; it is a
57
- // pure affordance, so never let it break keyboard nav.
58
- try {
59
- opt.scrollIntoView({ block: 'nearest' });
60
- } catch {
61
- /* non-DOM/headless environment — ignore */
62
- }
124
+ scrollIntoViewSafe(opt);
63
125
  } else {
64
126
  input.removeAttribute('aria-activedescendant');
65
127
  }
@@ -96,13 +158,19 @@ export function initCombobox({ root } = {}) {
96
158
  };
97
159
 
98
160
  const select = (opt) => {
99
- input.value = opt.dataset.value ?? opt.textContent.trim();
161
+ // Show the human LABEL in the input; emit the `data-value` CODE in the
162
+ // event. The natural pattern is code in `data-value`, label in the text —
163
+ // putting the code in the visible input silently shows the user a raw code.
164
+ // (component audit C10.)
165
+ const label = opt.textContent.trim();
166
+ const value = opt.dataset.value ?? label;
167
+ input.value = label;
100
168
  options.forEach((o) => o.setAttribute('aria-selected', String(o === opt)));
101
169
  close();
102
170
  input.focus();
103
171
  box.dispatchEvent(
104
172
  new CustomEvent('bronto:change', {
105
- detail: { value: input.value },
173
+ detail: { value, label },
106
174
  bubbles: true,
107
175
  }),
108
176
  );
@@ -112,10 +180,7 @@ export function initCombobox({ root } = {}) {
112
180
  const vis = visible();
113
181
  if (!vis.length) return;
114
182
  open();
115
- const curIdx = vis.indexOf(options[active]);
116
- let next = curIdx + delta;
117
- if (next < 0) next = vis.length - 1;
118
- if (next >= vis.length) next = 0;
183
+ const next = wrapIndex(vis.indexOf(options[active]), delta, vis.length);
119
184
  active = options.indexOf(vis[next]);
120
185
  setActive(options[active]);
121
186
  };
@@ -138,6 +203,15 @@ export function initCombobox({ root } = {}) {
138
203
  return true;
139
204
  };
140
205
 
206
+ // Live re-sync after the option nodes change under us. The active option may
207
+ // be gone, so drop it; re-filter against the current input only while open.
208
+ const relist = () => {
209
+ syncOptions();
210
+ active = -1;
211
+ setActive(null);
212
+ if (!list.hidden) filter();
213
+ };
214
+
141
215
  const onInput = () => filter();
142
216
  const onKey = (e) => {
143
217
  switch (e.key) {
@@ -181,7 +255,15 @@ export function initCombobox({ root } = {}) {
181
255
  input.addEventListener('keydown', onKey);
182
256
  list.addEventListener('click', onOptionClick);
183
257
  document.addEventListener('click', onDocClick);
258
+ // Opt-in: keep options in sync with a list mutated after init (async /
259
+ // remote results). Off by default so the common static case stays free.
260
+ const observer =
261
+ box.hasAttribute('data-bronto-combobox-live') && typeof MutationObserver === 'function'
262
+ ? new MutationObserver(relist)
263
+ : null;
264
+ observer?.observe(list, { childList: true, subtree: true });
184
265
  return () => {
266
+ observer?.disconnect();
185
267
  input.removeEventListener('input', onInput);
186
268
  input.removeEventListener('keydown', onKey);
187
269
  list.removeEventListener('click', onOptionClick);
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @typedef {object} CommandSelectDetail
3
+ * @property {string} value The chosen command's value.
4
+ * @property {string} label The chosen command's visible label.
5
+ */
6
+ /**
7
+ * Command palette — filter + keyboard-navigate a DOM-authored command list.
8
+ * The CSS shell (`.ui-command`) is opt-in; this wires the listbox behavior the
9
+ * shell needs. Bronto filters and navigates; the HOST owns the action registry,
10
+ * permission checks, routing, async effects, and command execution (it listens
11
+ * for `bronto:command:select`). There is no global Cmd/Ctrl+K — open the palette
12
+ * yourself (e.g. a `<dialog>` via `initDialog`).
13
+ *
14
+ * Markup: `[data-bronto-command]` wrapping an `<input>` (`.ui-command__input`)
15
+ * and a list (`.ui-command__list`) of `.ui-command__item` rows (optional
16
+ * `data-value`), interleaved with `.ui-command__group` labels and an optional
17
+ * `.ui-command__empty`. The behavior owns ids, `role=combobox/listbox/option`,
18
+ * `aria-activedescendant`, a roving active item, substring filtering (hiding
19
+ * empty groups), full keyboard (Down/Up/Home/End/Enter/Escape), and pointer
20
+ * select. It emits `bronto:command:select` ({ detail: { value, label } }) on
21
+ * choose and `bronto:command:close` on Escape. SSR-safe, idempotent per
22
+ * instance; returns a cleanup function.
23
+ *
24
+ * Items are read from the DOM at init; re-run initCommand after replacing the
25
+ * command list so filtering/navigation see the current nodes.
26
+ *
27
+ * @param {import('./internal.js').DelegateOpts} [opts]
28
+ * @returns {import('./internal.js').Cleanup}
29
+ */
30
+ export function initCommand({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
31
+ export type CommandSelectDetail = {
32
+ /**
33
+ * The chosen command's value.
34
+ */
35
+ value: string;
36
+ /**
37
+ * The chosen command's visible label.
38
+ */
39
+ label: string;
40
+ };
41
+ //# sourceMappingURL=command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["command.js"],"names":[],"mappings":"AAWA;;;;GAIG;AAEH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA+J3C;;;;;WAzLa,MAAM;;;;WACN,MAAM"}
@@ -1,4 +1,19 @@
1
- import { hasDom, noop, bindOnce, nextFieldUid } from './internal.js';
1
+ import {
2
+ hasDom,
3
+ resolveHost,
4
+ noop,
5
+ bindOnce,
6
+ nextFieldUid,
7
+ collectHosts,
8
+ scrollIntoViewSafe,
9
+ wrapIndex,
10
+ } from './internal.js';
11
+
12
+ /**
13
+ * @typedef {object} CommandSelectDetail
14
+ * @property {string} value The chosen command's value.
15
+ * @property {string} label The chosen command's visible label.
16
+ */
2
17
 
3
18
  /**
4
19
  * Command palette — filter + keyboard-navigate a DOM-authored command list.
@@ -17,13 +32,18 @@ import { hasDom, noop, bindOnce, nextFieldUid } from './internal.js';
17
32
  * select. It emits `bronto:command:select` ({ detail: { value, label } }) on
18
33
  * choose and `bronto:command:close` on Escape. SSR-safe, idempotent per
19
34
  * instance; returns a cleanup function.
35
+ *
36
+ * Items are read from the DOM at init; re-run initCommand after replacing the
37
+ * command list so filtering/navigation see the current nodes.
38
+ *
39
+ * @param {import('./internal.js').DelegateOpts} [opts]
40
+ * @returns {import('./internal.js').Cleanup}
20
41
  */
21
42
  export function initCommand({ root } = {}) {
22
43
  if (!hasDom()) return noop;
23
- const host = root || document;
24
- const palettes = [];
25
- if (host !== document && host.matches?.('[data-bronto-command]')) palettes.push(host);
26
- palettes.push(...(host.querySelectorAll?.('[data-bronto-command]') ?? []));
44
+ const host = resolveHost(root);
45
+ if (!host) return noop;
46
+ const palettes = collectHosts(host, '[data-bronto-command]');
27
47
  const cleanups = [];
28
48
 
29
49
  for (const box of palettes) {
@@ -58,11 +78,7 @@ export function initCommand({ root } = {}) {
58
78
  if (item) {
59
79
  active = items.indexOf(item);
60
80
  input.setAttribute('aria-activedescendant', item.id);
61
- try {
62
- item.scrollIntoView({ block: 'nearest' });
63
- } catch {
64
- /* headless — scrollIntoView is a pure affordance */
65
- }
81
+ scrollIntoViewSafe(item);
66
82
  } else {
67
83
  active = -1;
68
84
  input.removeAttribute('aria-activedescendant');
@@ -101,11 +117,7 @@ export function initCommand({ root } = {}) {
101
117
  const move = (delta) => {
102
118
  const vis = visible();
103
119
  if (!vis.length) return;
104
- const cur = vis.indexOf(items[active]);
105
- let next = cur + delta;
106
- if (next < 0) next = vis.length - 1;
107
- if (next >= vis.length) next = 0;
108
- setActive(vis[next]);
120
+ setActive(vis[wrapIndex(vis.indexOf(items[active]), delta, vis.length)]);
109
121
  };
110
122
 
111
123
  const choose = (item) => {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Draw + keep leader lines in sync. Each `[data-bronto-connector]` is an
3
+ * `.ui-connector` SVG overlaying a positioned container; `data-from`/`data-to`
4
+ * are the ids of the elements to connect. Optional `data-shape`
5
+ * (`straight`|`elbow`|`curve`), `data-from-side`/`data-to-side`
6
+ * (`top`|`right`|`bottom`|`left`|`center`), and `data-end` (`arrow`|`dot`|`none`).
7
+ *
8
+ * Bronto computes the geometry (the pure `@ponchia/ui/connectors` helpers) and
9
+ * sets the path; it owns no layout. Redraws on resize/scroll via a
10
+ * ResizeObserver + listeners. SSR-safe, idempotent per host; returns a cleanup
11
+ * that disconnects everything. Re-run after adding/removing connectors.
12
+ *
13
+ * @param {import('./internal.js').DelegateOpts} [opts]
14
+ * @returns {import('./internal.js').Cleanup}
15
+ */
16
+ export function initConnectors({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
17
+ //# sourceMappingURL=connectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["connectors.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AACH,0CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA+E3C"}
@@ -1,4 +1,4 @@
1
- import { hasDom, noop, bindOnce, byIdInHost } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, byIdInHost, collectHosts } from './internal.js';
2
2
  import { connectRects, arrowHead, dotMark } from '../connectors/index.js';
3
3
 
4
4
  const SVGNS = 'http://www.w3.org/2000/svg';
@@ -14,13 +14,15 @@ const SVGNS = 'http://www.w3.org/2000/svg';
14
14
  * sets the path; it owns no layout. Redraws on resize/scroll via a
15
15
  * ResizeObserver + listeners. SSR-safe, idempotent per host; returns a cleanup
16
16
  * that disconnects everything. Re-run after adding/removing connectors.
17
+ *
18
+ * @param {import('./internal.js').DelegateOpts} [opts]
19
+ * @returns {import('./internal.js').Cleanup}
17
20
  */
18
21
  export function initConnectors({ root } = {}) {
19
22
  if (!hasDom()) return noop;
20
- const host = root || document;
21
- const connectors = [];
22
- if (host !== document && host.matches?.('[data-bronto-connector]')) connectors.push(host);
23
- connectors.push(...host.querySelectorAll('[data-bronto-connector]'));
23
+ const host = resolveHost(root);
24
+ if (!host) return noop;
25
+ const connectors = collectHosts(host, '[data-bronto-connector]');
24
26
  if (!connectors.length) return noop;
25
27
 
26
28
  const draw = () => {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @typedef {object} CrosshairMoveDetail
3
+ * @property {number} x Pointer x within the plot, in pixels.
4
+ * @property {number} y Pointer y within the plot, in pixels.
5
+ * @property {number} fx Pointer x as a 0..1 fraction of the plot width.
6
+ * @property {number} fy Pointer y as a 0..1 fraction of the plot height.
7
+ */
8
+ /**
9
+ * Track the pointer over a plot and drive a crosshair. Each
10
+ * `[data-bronto-crosshair]` is the plot; it contains a `.ui-crosshair` overlay.
11
+ * On pointer move the behavior sets `--crosshair-x/y` (pixels within the plot)
12
+ * on the overlay, marks it `.is-active`, and dispatches
13
+ * `bronto:crosshair:move` with `{ x, y, fx, fy }` (px + 0..1 fractions);
14
+ * `bronto:crosshair:leave` on exit.
15
+ *
16
+ * Bronto reports WHERE the pointer is — it does not find the nearest datum or
17
+ * map pixels to data values (that needs the host's scales). SSR-safe,
18
+ * idempotent per plot; returns a cleanup function.
19
+ *
20
+ * @param {import('./internal.js').DelegateOpts} [opts]
21
+ * @returns {import('./internal.js').Cleanup}
22
+ */
23
+ export function initCrosshair({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
24
+ export type CrosshairMoveDetail = {
25
+ /**
26
+ * Pointer x within the plot, in pixels.
27
+ */
28
+ x: number;
29
+ /**
30
+ * Pointer y within the plot, in pixels.
31
+ */
32
+ y: number;
33
+ /**
34
+ * Pointer x as a 0..1 fraction of the plot width.
35
+ */
36
+ fx: number;
37
+ /**
38
+ * Pointer y as a 0..1 fraction of the plot height.
39
+ */
40
+ fy: number;
41
+ };
42
+ //# sourceMappingURL=crosshair.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crosshair.d.ts","sourceRoot":"","sources":["crosshair.js"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;GAcG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAmD3C;;;;;OAtEa,MAAM;;;;OACN,MAAM;;;;QACN,MAAM;;;;QACN,MAAM"}
@@ -1,4 +1,12 @@
1
- import { hasDom, noop, bindOnce } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js';
2
+
3
+ /**
4
+ * @typedef {object} CrosshairMoveDetail
5
+ * @property {number} x Pointer x within the plot, in pixels.
6
+ * @property {number} y Pointer y within the plot, in pixels.
7
+ * @property {number} fx Pointer x as a 0..1 fraction of the plot width.
8
+ * @property {number} fy Pointer y as a 0..1 fraction of the plot height.
9
+ */
2
10
 
3
11
  /**
4
12
  * Track the pointer over a plot and drive a crosshair. Each
@@ -11,13 +19,15 @@ import { hasDom, noop, bindOnce } from './internal.js';
11
19
  * Bronto reports WHERE the pointer is — it does not find the nearest datum or
12
20
  * map pixels to data values (that needs the host's scales). SSR-safe,
13
21
  * idempotent per plot; returns a cleanup function.
22
+ *
23
+ * @param {import('./internal.js').DelegateOpts} [opts]
24
+ * @returns {import('./internal.js').Cleanup}
14
25
  */
15
26
  export function initCrosshair({ root } = {}) {
16
27
  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]'));
28
+ const host = resolveHost(root);
29
+ if (!host) return noop;
30
+ const plots = collectHosts(host, '[data-bronto-crosshair]');
21
31
  if (!plots.length) return noop;
22
32
 
23
33
  const cleanups = [];
@@ -29,7 +39,14 @@ export function initCrosshair({ root } = {}) {
29
39
  if (!r.width || !r.height) return;
30
40
  const x = e.clientX - r.left;
31
41
  const y = e.clientY - r.top;
32
- overlay.style.setProperty('--crosshair-x', `${x}px`);
42
+ // The CSS positions the vertical rule / readout with a *logical* inset
43
+ // (inset-inline-start), so --crosshair-x must be the distance from the
44
+ // inline-start edge — the physical left in LTR, the physical right in RTL.
45
+ // Emitting the physical x instead made the RTL rule land off-plot. The
46
+ // public `detail.x`/`fx` stay physical-from-left so host scale-mapping
47
+ // keeps one stable coordinate space regardless of direction.
48
+ const rtl = getComputedStyle(plot).direction === 'rtl';
49
+ overlay.style.setProperty('--crosshair-x', `${rtl ? r.right - e.clientX : x}px`);
33
50
  overlay.style.setProperty('--crosshair-y', `${y}px`);
34
51
  overlay.classList.add('is-active');
35
52
  plot.dispatchEvent(
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Wire native <dialog> open/close glue (the one bit <dialog> can't do
3
+ * declaratively). Click `[data-bronto-open="dialogId"]` calls
4
+ * `showModal()` on `#dialogId`; click `[data-bronto-close]` closes the
5
+ * nearest enclosing <dialog>. Clicking the backdrop of a dialog that has
6
+ * `[data-bronto-dialog-light]` closes it too. On open the trigger is
7
+ * remembered and focus is returned to it on *every* close path (Esc,
8
+ * close button, backdrop light-dismiss, programmatic) via the native
9
+ * `close` event, so keyboard/SR users are never dropped at `<body>`.
10
+ * SSR-safe and idempotent; returns cleanup.
11
+ *
12
+ * `root` scopes delegated triggers (default `document`). Controlled targets are
13
+ * resolved root-first, then document-wide, so scoped islands win duplicate-id
14
+ * conflicts without breaking body/portal-mounted overlays.
15
+ *
16
+ * @param {import('./internal.js').DelegateOpts} [opts]
17
+ * @returns {import('./internal.js').Cleanup}
18
+ */
19
+ export function initDialog({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
20
+ //# sourceMappingURL=dialog.d.ts.map
@@ -0,0 +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,4 +1,4 @@
1
- import { hasDom, noop, bindOnce, byIdInHost } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, byIdInHost } from './internal.js';
2
2
 
3
3
  /**
4
4
  * Wire native <dialog> open/close glue (the one bit <dialog> can't do
@@ -14,10 +14,14 @@ import { hasDom, noop, bindOnce, byIdInHost } from './internal.js';
14
14
  * `root` scopes delegated triggers (default `document`). Controlled targets are
15
15
  * resolved root-first, then document-wide, so scoped islands win duplicate-id
16
16
  * conflicts without breaking body/portal-mounted overlays.
17
+ *
18
+ * @param {import('./internal.js').DelegateOpts} [opts]
19
+ * @returns {import('./internal.js').Cleanup}
17
20
  */
18
21
  export function initDialog({ root } = {}) {
19
22
  if (!hasDom()) return noop;
20
- const host = root || document;
23
+ const host = resolveHost(root);
24
+ if (!host) return noop;
21
25
  const managedDialogs = new WeakSet();
22
26
  const canManageDialog = (dlg, origin) => host.contains(origin) || managedDialogs.has(dlg);
23
27
 
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Disclosure: a `[data-bronto-disclosure]` trigger toggles the element
3
+ * referenced by its `aria-controls` id, keeping `aria-expanded` and the
4
+ * panel's `hidden` attribute in sync.
5
+ *
6
+ * @param {import('./internal.js').DelegateOpts} [opts]
7
+ * @returns {import('./internal.js').Cleanup}
8
+ */
9
+ export function initDisclosure({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
10
+ //# sourceMappingURL=disclosure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disclosure.d.ts","sourceRoot":"","sources":["disclosure.js"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,0CAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAoB3C"}