@ponchia/ui 0.6.6 → 0.6.8

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 (160) hide show
  1. package/CHANGELOG.md +175 -6
  2. package/README.md +38 -25
  3. package/annotations/index.d.ts.map +1 -1
  4. package/annotations/index.js +21 -3
  5. package/behaviors/carousel.d.ts.map +1 -1
  6. package/behaviors/carousel.js +91 -32
  7. package/behaviors/combobox.d.ts.map +1 -1
  8. package/behaviors/combobox.js +117 -43
  9. package/behaviors/command.d.ts.map +1 -1
  10. package/behaviors/command.js +74 -14
  11. package/behaviors/connectors.d.ts.map +1 -1
  12. package/behaviors/connectors.js +92 -9
  13. package/behaviors/crosshair.d.ts.map +1 -1
  14. package/behaviors/crosshair.js +47 -1
  15. package/behaviors/dialog.d.ts.map +1 -1
  16. package/behaviors/dialog.js +37 -16
  17. package/behaviors/disclosure.d.ts.map +1 -1
  18. package/behaviors/disclosure.js +33 -3
  19. package/behaviors/dismissible.d.ts.map +1 -1
  20. package/behaviors/dismissible.js +3 -2
  21. package/behaviors/forms.d.ts.map +1 -1
  22. package/behaviors/forms.js +78 -5
  23. package/behaviors/glyph.d.ts.map +1 -1
  24. package/behaviors/glyph.js +17 -2
  25. package/behaviors/index.d.ts +2 -0
  26. package/behaviors/index.d.ts.map +1 -1
  27. package/behaviors/index.js +2 -0
  28. package/behaviors/inert.js +3 -2
  29. package/behaviors/internal.d.ts +2 -1
  30. package/behaviors/internal.d.ts.map +1 -1
  31. package/behaviors/internal.js +25 -4
  32. package/behaviors/legend.d.ts +0 -5
  33. package/behaviors/legend.d.ts.map +1 -1
  34. package/behaviors/legend.js +78 -14
  35. package/behaviors/menu.d.ts.map +1 -1
  36. package/behaviors/menu.js +13 -8
  37. package/behaviors/modal.d.ts.map +1 -1
  38. package/behaviors/modal.js +77 -19
  39. package/behaviors/popover.d.ts +4 -3
  40. package/behaviors/popover.d.ts.map +1 -1
  41. package/behaviors/popover.js +89 -9
  42. package/behaviors/sources.d.ts.map +1 -1
  43. package/behaviors/sources.js +14 -2
  44. package/behaviors/splitter.d.ts +26 -0
  45. package/behaviors/splitter.d.ts.map +1 -0
  46. package/behaviors/splitter.js +239 -0
  47. package/behaviors/spotlight.d.ts.map +1 -1
  48. package/behaviors/spotlight.js +28 -2
  49. package/behaviors/table.d.ts.map +1 -1
  50. package/behaviors/table.js +105 -13
  51. package/behaviors/tabs.d.ts.map +1 -1
  52. package/behaviors/tabs.js +82 -18
  53. package/behaviors/theme.d.ts.map +1 -1
  54. package/behaviors/theme.js +26 -6
  55. package/classes/classes.json +230 -4
  56. package/classes/index.d.ts +64 -3
  57. package/classes/index.js +56 -2
  58. package/classes/vscode.css-custom-data.json +1 -1
  59. package/connectors/index.d.ts +39 -6
  60. package/connectors/index.d.ts.map +1 -1
  61. package/connectors/index.js +67 -9
  62. package/css/analytical.css +3 -1
  63. package/css/annotations.css +12 -0
  64. package/css/app.css +4 -4
  65. package/css/clamp.css +92 -0
  66. package/css/crosshair.css +27 -2
  67. package/css/feedback.css +2 -30
  68. package/css/figure.css +102 -0
  69. package/css/highlights.css +50 -0
  70. package/css/interval.css +90 -0
  71. package/css/navigation.css +12 -0
  72. package/css/primitives.css +2 -3
  73. package/css/report-kit.css +38 -0
  74. package/css/report.css +23 -4
  75. package/css/sidenote.css +12 -2
  76. package/css/site.css +2 -1
  77. package/css/sources.css +5 -0
  78. package/css/state.css +120 -1
  79. package/css/table.css +4 -0
  80. package/css/tokens.css +25 -9
  81. package/css/workbench.css +101 -8
  82. package/dist/bronto.css +1 -1
  83. package/dist/css/analytical.css +1 -1
  84. package/dist/css/annotations.css +1 -1
  85. package/dist/css/app.css +1 -1
  86. package/dist/css/clamp.css +1 -0
  87. package/dist/css/crosshair.css +1 -1
  88. package/dist/css/feedback.css +1 -1
  89. package/dist/css/figure.css +1 -0
  90. package/dist/css/highlights.css +1 -0
  91. package/dist/css/interval.css +1 -0
  92. package/dist/css/navigation.css +1 -1
  93. package/dist/css/primitives.css +1 -1
  94. package/dist/css/report-kit.css +1 -0
  95. package/dist/css/report.css +1 -1
  96. package/dist/css/sidenote.css +1 -1
  97. package/dist/css/site.css +1 -1
  98. package/dist/css/sources.css +1 -1
  99. package/dist/css/state.css +1 -1
  100. package/dist/css/table.css +1 -1
  101. package/dist/css/tokens.css +1 -1
  102. package/dist/css/workbench.css +1 -1
  103. package/docs/adr/0001-color-system.md +3 -2
  104. package/docs/adr/0002-scope-and-2026-baseline.md +1 -1
  105. package/docs/annotations.md +12 -1
  106. package/docs/architecture.md +105 -48
  107. package/docs/clamp.md +49 -0
  108. package/docs/command.md +4 -1
  109. package/docs/connectors.md +16 -0
  110. package/docs/contrast.md +34 -24
  111. package/docs/crosshair.md +1 -1
  112. package/docs/d2.md +37 -0
  113. package/docs/dots.md +4 -1
  114. package/docs/figure.md +71 -0
  115. package/docs/frontier-primitives.md +25 -24
  116. package/docs/glyphs.md +11 -0
  117. package/docs/highlights.md +52 -0
  118. package/docs/interop/tailwind.md +148 -0
  119. package/docs/interval.md +55 -0
  120. package/docs/legends.md +3 -2
  121. package/docs/mermaid.md +6 -0
  122. package/docs/migrations/0.2-to-0.3.md +80 -0
  123. package/docs/migrations/0.3-to-0.4.md +48 -0
  124. package/docs/migrations/0.4-to-0.5.md +96 -0
  125. package/docs/migrations/0.5-to-0.6.md +82 -0
  126. package/docs/package-contract.md +44 -6
  127. package/docs/reference.md +78 -5
  128. package/docs/reporting.md +126 -60
  129. package/docs/sidenote.md +7 -1
  130. package/docs/sources.md +1 -1
  131. package/docs/stability.md +23 -5
  132. package/docs/state.md +67 -10
  133. package/docs/theming.md +12 -4
  134. package/docs/usage.md +47 -13
  135. package/docs/vega.md +4 -4
  136. package/docs/workbench.md +59 -18
  137. package/llms.txt +89 -16
  138. package/package.json +82 -6
  139. package/qwik/index.d.ts +1 -0
  140. package/qwik/index.d.ts.map +1 -1
  141. package/qwik/index.js +26 -21
  142. package/react/index.d.ts +1 -0
  143. package/react/index.d.ts.map +1 -1
  144. package/react/index.js +4 -1
  145. package/schemas/report-claims.v1.schema.json +137 -0
  146. package/solid/index.d.ts +2 -0
  147. package/solid/index.d.ts.map +1 -1
  148. package/solid/index.js +3 -0
  149. package/svelte/index.d.ts +114 -0
  150. package/svelte/index.d.ts.map +1 -0
  151. package/svelte/index.js +193 -0
  152. package/tailwind.css +87 -0
  153. package/tokens/figma.variables.json +2241 -0
  154. package/tokens/index.js +1 -1
  155. package/tokens/index.json +2 -2
  156. package/tokens/resolved.json +3 -3
  157. package/tokens/tokens.dtcg.json +1 -1
  158. package/vue/index.d.ts +116 -0
  159. package/vue/index.d.ts.map +1 -0
  160. package/vue/index.js +228 -0
@@ -1,4 +1,56 @@
1
- import { hasDom, resolveHost, noop, bindOnce, collectHosts, focusInto } from './internal.js';
1
+ import {
2
+ hasDom,
3
+ resolveHost,
4
+ noop,
5
+ bindOnce,
6
+ collectHosts,
7
+ focusInto,
8
+ closestSafe,
9
+ } from './internal.js';
10
+
11
+ function insideOpenPopover(target, modal) {
12
+ const classPanel = closestSafe(target, '.ui-popover.is-open');
13
+ if (classPanel && modal.contains(classPanel)) return true;
14
+
15
+ const nativePanel = closestSafe(target, '[popover]');
16
+ if (!nativePanel || !modal.contains(nativePanel)) return false;
17
+ try {
18
+ return nativePanel.matches(':popover-open');
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ const activeModals = [];
25
+
26
+ const snapshotAttrs = (el, names) => {
27
+ const attrs = {};
28
+ for (const name of names) {
29
+ attrs[name] = {
30
+ had: el.hasAttribute(name),
31
+ value: el.getAttribute(name),
32
+ };
33
+ }
34
+ return attrs;
35
+ };
36
+
37
+ const restoreAttrs = (el, attrs) => {
38
+ for (const [name, state] of Object.entries(attrs)) {
39
+ if (state.had) el.setAttribute(name, state.value);
40
+ else el.removeAttribute(name);
41
+ }
42
+ };
43
+
44
+ const pushActiveModal = (modal) => {
45
+ const index = activeModals.indexOf(modal);
46
+ if (index !== -1) activeModals.splice(index, 1);
47
+ activeModals.push(modal);
48
+ };
49
+
50
+ const removeActiveModal = (modal) => {
51
+ const index = activeModals.indexOf(modal);
52
+ if (index !== -1) activeModals.splice(index, 1);
53
+ };
2
54
 
3
55
  /**
4
56
  * @typedef {object} ModalCloseDetail
@@ -45,22 +97,6 @@ export function initModal({ root } = {}) {
45
97
  let opener = null;
46
98
  let inerted = [];
47
99
 
48
- // A controlled modal must announce AS a modal dialog, not a generic group —
49
- // parity with initPopover. Apply a dialog role + aria-modal (unless the
50
- // author set a role), and dev-warn on a missing accessible name since we
51
- // can't invent a good one. (component audit C13.)
52
- if (!modal.hasAttribute('role')) modal.setAttribute('role', 'dialog');
53
- if (!modal.hasAttribute('aria-modal')) modal.setAttribute('aria-modal', 'true');
54
- const named =
55
- modal.hasAttribute('aria-label') ||
56
- modal.hasAttribute('aria-labelledby') ||
57
- modal.hasAttribute('title');
58
- if (!named && typeof console !== 'undefined') {
59
- console.warn(
60
- `[bronto] initModal(): a [data-bronto-modal] has no accessible name — add aria-label or aria-labelledby so it is announced as a named dialog.`,
61
- );
62
- }
63
-
64
100
  // Inert every sibling at each ancestor level up to <body>: the rest of the
65
101
  // page becomes non-focusable/non-interactive while the modal subtree stays
66
102
  // live. Skip already-inert nodes so release() can't un-inert something the
@@ -68,6 +104,7 @@ export function initModal({ root } = {}) {
68
104
  const trap = () => {
69
105
  if (opener) return; // already trapped
70
106
  opener = document.activeElement;
107
+ pushActiveModal(modal);
71
108
  let el = modal;
72
109
  while (el && el.parentElement && el !== document.body) {
73
110
  for (const sib of el.parentElement.children) {
@@ -83,6 +120,7 @@ export function initModal({ root } = {}) {
83
120
 
84
121
  const release = () => {
85
122
  if (!opener) return;
123
+ removeActiveModal(modal);
86
124
  for (const el of inerted) el.inert = false;
87
125
  inerted = [];
88
126
  const back = opener;
@@ -94,6 +132,8 @@ export function initModal({ root } = {}) {
94
132
 
95
133
  const onKey = (e) => {
96
134
  if (e.key === 'Escape' && opener) {
135
+ if (activeModals.at(-1) !== modal) return;
136
+ if (insideOpenPopover(e.target, modal)) return;
97
137
  modal.dispatchEvent(
98
138
  new CustomEvent('bronto:modal:close', {
99
139
  detail: { reason: 'escape' },
@@ -104,10 +144,27 @@ export function initModal({ root } = {}) {
104
144
  }
105
145
  };
106
146
 
107
- const observer = typeof MutationObserver === 'function' ? new MutationObserver(sync) : null;
108
-
109
147
  cleanups.push(
110
148
  bindOnce(modal, 'modal', () => {
149
+ const attrs = snapshotAttrs(modal, ['role', 'aria-modal', 'tabindex']);
150
+
151
+ // A controlled modal must announce AS a modal dialog, not a generic group —
152
+ // parity with initPopover. Apply a dialog role + aria-modal (unless the
153
+ // author set a role), and dev-warn on a missing accessible name since we
154
+ // can't invent a good one. (component audit C13.)
155
+ if (!modal.hasAttribute('role')) modal.setAttribute('role', 'dialog');
156
+ if (!modal.hasAttribute('aria-modal')) modal.setAttribute('aria-modal', 'true');
157
+ const named =
158
+ modal.hasAttribute('aria-label') ||
159
+ modal.hasAttribute('aria-labelledby') ||
160
+ modal.hasAttribute('title');
161
+ if (!named && typeof console !== 'undefined') {
162
+ console.warn(
163
+ `[bronto] initModal(): a [data-bronto-modal] has no accessible name — add aria-label or aria-labelledby so it is announced as a named dialog.`,
164
+ );
165
+ }
166
+
167
+ const observer = typeof MutationObserver === 'function' ? new MutationObserver(sync) : null;
111
168
  observer?.observe(modal, { attributes: true, attributeFilter: ['class'] });
112
169
  document.addEventListener('keydown', onKey, true);
113
170
  if (modal.classList.contains('is-open')) trap(); // already open at init
@@ -115,6 +172,7 @@ export function initModal({ root } = {}) {
115
172
  observer?.disconnect();
116
173
  document.removeEventListener('keydown', onKey, true);
117
174
  release();
175
+ restoreAttrs(modal, attrs);
118
176
  };
119
177
  }),
120
178
  );
@@ -2,9 +2,10 @@
2
2
  * Collision-aware popover, dependency-free. A `[data-bronto-popover]`
3
3
  * trigger toggles the `.ui-popover` panel whose id it names. The panel
4
4
  * is placed under the trigger and **flips above** when it would
5
- * overflow the viewport, with its inline edge clamped on-screen the
6
- * thing the CSS-only tooltip can't do near edges / inside scroll
7
- * containers. If the panel has the native `popover` attribute and the
5
+ * overflow the viewport, with its inline edge clamped on-screen and tall
6
+ * panels constrained to scroll inside the viewport the thing the CSS-only
7
+ * tooltip can't do near edges / inside scroll containers. If the panel has
8
+ * the native `popover` attribute and the
8
9
  * Popover API is available it is shown in the top layer (never
9
10
  * clipped); otherwise an `.is-open` class is toggled. Manages
10
11
  * `aria-expanded` / `aria-controls`, closes on Escape and outside
@@ -1 +1 @@
1
- {"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["popover.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAwJ3C"}
1
+ {"version":3,"file":"popover.d.ts","sourceRoot":"","sources":["popover.js"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAkM3C"}
@@ -1,12 +1,50 @@
1
- import { hasDom, resolveHost, noop, bindOnce, byIdInHost, focusInto } from './internal.js';
1
+ import {
2
+ hasDom,
3
+ resolveHost,
4
+ noop,
5
+ bindOnce,
6
+ byIdInHost,
7
+ focusInto,
8
+ collectHosts,
9
+ closestSafe,
10
+ } from './internal.js';
11
+
12
+ const snapshotAttrs = (el, names) => {
13
+ const attrs = {};
14
+ for (const name of names) {
15
+ attrs[name] = {
16
+ had: el.hasAttribute(name),
17
+ value: el.getAttribute(name),
18
+ };
19
+ }
20
+ return attrs;
21
+ };
22
+
23
+ const restoreAttrs = (el, attrs) => {
24
+ for (const [name, state] of Object.entries(attrs)) {
25
+ if (state.had) el.setAttribute(name, state.value);
26
+ else el.removeAttribute(name);
27
+ }
28
+ };
29
+
30
+ const snapshotStyle = (el, names) => {
31
+ const styles = {};
32
+ for (const name of names) styles[name] = el.style[name];
33
+ return styles;
34
+ };
35
+
36
+ const restoreStyle = (el, styles) => {
37
+ for (const [name, value] of Object.entries(styles)) el.style[name] = value;
38
+ };
2
39
 
3
40
  /**
4
41
  * Collision-aware popover, dependency-free. A `[data-bronto-popover]`
5
42
  * trigger toggles the `.ui-popover` panel whose id it names. The panel
6
43
  * is placed under the trigger and **flips above** when it would
7
- * overflow the viewport, with its inline edge clamped on-screen the
8
- * thing the CSS-only tooltip can't do near edges / inside scroll
9
- * containers. If the panel has the native `popover` attribute and the
44
+ * overflow the viewport, with its inline edge clamped on-screen and tall
45
+ * panels constrained to scroll inside the viewport the thing the CSS-only
46
+ * tooltip can't do near edges / inside scroll containers. If the panel has
47
+ * the native `popover` attribute and the
10
48
  * Popover API is available it is shown in the top layer (never
11
49
  * clipped); otherwise an `.is-open` class is toggled. Manages
12
50
  * `aria-expanded` / `aria-controls`, closes on Escape and outside
@@ -34,6 +72,27 @@ export function initPopover({ root } = {}) {
34
72
  const GAP = 8;
35
73
  let openPanel = null;
36
74
  let openTrigger = null;
75
+ const triggerStates = new Map();
76
+ const panelStates = new Map();
77
+
78
+ const rememberTrigger = (trigger) => {
79
+ if (!triggerStates.has(trigger)) {
80
+ triggerStates.set(
81
+ trigger,
82
+ snapshotAttrs(trigger, ['aria-haspopup', 'aria-controls', 'aria-expanded']),
83
+ );
84
+ }
85
+ };
86
+
87
+ const rememberPanel = (panel) => {
88
+ if (!panelStates.has(panel)) {
89
+ panelStates.set(panel, {
90
+ attrs: snapshotAttrs(panel, ['role', 'tabindex']),
91
+ open: panel.classList.contains('is-open'),
92
+ style: snapshotStyle(panel, ['maxBlockSize', 'top', 'left']),
93
+ });
94
+ }
95
+ };
37
96
 
38
97
  // The trigger advertises `aria-haspopup="dialog"`, so the open panel must BE a
39
98
  // dialog: a role, an accessible name, and focus moved into it (C6) — see the
@@ -41,12 +100,20 @@ export function initPopover({ root } = {}) {
41
100
 
42
101
  const place = (trigger, panel) => {
43
102
  const r = trigger.getBoundingClientRect();
103
+ panel.style.maxBlockSize = 'none';
44
104
  const pw = panel.offsetWidth;
45
- const ph = panel.offsetHeight;
105
+ const ph = Math.max(panel.offsetHeight, panel.scrollHeight);
46
106
  const vw = view?.innerWidth ?? 0;
47
107
  const vh = view?.innerHeight ?? 0;
48
- let top = r.bottom + GAP;
49
- if (top + ph > vh && r.top - GAP - ph >= 0) top = r.top - GAP - ph;
108
+ const maxHeight = Math.max(0, vh - GAP * 2);
109
+ const below = Math.max(0, vh - r.bottom - GAP * 2);
110
+ const above = Math.max(0, r.top - GAP * 2);
111
+ const placeAbove = ph > below && above > below;
112
+ const available = placeAbove ? above : below;
113
+ const height = Math.min(ph, available || maxHeight);
114
+ panel.style.maxBlockSize = `${height}px`;
115
+ let top = placeAbove ? r.top - GAP - height : r.bottom + GAP;
116
+ if (vh) top = Math.max(GAP, Math.min(top, vh - height - GAP));
50
117
  let left = r.left;
51
118
  if (vw) left = Math.max(GAP, Math.min(left, vw - pw - GAP));
52
119
  panel.style.top = `${Math.max(GAP, top)}px`;
@@ -77,6 +144,8 @@ export function initPopover({ root } = {}) {
77
144
 
78
145
  const open = (trigger, panel) => {
79
146
  close();
147
+ rememberTrigger(trigger);
148
+ rememberPanel(panel);
80
149
  // Live up to the advertised `aria-haspopup="dialog"`: give the panel a
81
150
  // dialog role (unless the author set one) so AT announces it as the promised
82
151
  // dialog rather than a generic group (C6).
@@ -99,7 +168,7 @@ export function initPopover({ root } = {}) {
99
168
  };
100
169
 
101
170
  const onClick = (e) => {
102
- const trigger = e.target.closest?.('[data-bronto-popover]');
171
+ const trigger = closestSafe(e.target, '[data-bronto-popover]');
103
172
  if (trigger && host.contains(trigger)) {
104
173
  const panel = byIdInHost(host, trigger.getAttribute('data-bronto-popover'));
105
174
  if (!panel) return;
@@ -133,9 +202,11 @@ export function initPopover({ root } = {}) {
133
202
  // never routes through close(), so aria-expanded would otherwise go stale).
134
203
  const seedTeardowns = [];
135
204
  const seed = () => {
136
- for (const trigger of host.querySelectorAll('[data-bronto-popover]')) {
205
+ for (const trigger of collectHosts(host, '[data-bronto-popover]')) {
137
206
  const panel = byIdInHost(host, trigger.getAttribute('data-bronto-popover'));
138
207
  if (!panel) continue;
208
+ rememberTrigger(trigger);
209
+ rememberPanel(panel);
139
210
  if (!trigger.hasAttribute('aria-haspopup')) trigger.setAttribute('aria-haspopup', 'dialog');
140
211
  trigger.setAttribute('aria-controls', panel.id);
141
212
  if (!trigger.hasAttribute('aria-expanded')) trigger.setAttribute('aria-expanded', 'false');
@@ -169,7 +240,16 @@ export function initPopover({ root } = {}) {
169
240
  view?.addEventListener('scroll', onReflow, true);
170
241
  view?.addEventListener('resize', onReflow);
171
242
  return () => {
243
+ close();
172
244
  for (const t of seedTeardowns.splice(0)) t();
245
+ for (const [trigger, attrs] of triggerStates) restoreAttrs(trigger, attrs);
246
+ triggerStates.clear();
247
+ for (const [panel, state] of panelStates) {
248
+ restoreAttrs(panel, state.attrs);
249
+ panel.classList.toggle('is-open', state.open);
250
+ restoreStyle(panel, state.style);
251
+ }
252
+ panelStates.clear();
173
253
  document.removeEventListener('click', onClick);
174
254
  document.removeEventListener('keydown', onKey);
175
255
  view?.removeEventListener('scroll', onReflow, true);
@@ -1 +1 @@
1
- {"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["sources.js"],"names":[],"mappings":"AA2CA;;;;;;;;;;;GAWG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAwG3C;;;;;QAjJa,MAAM;;;;cACN,OAAO;;;;YACP,OAAO"}
1
+ {"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["sources.js"],"names":[],"mappings":"AA4CA;;;;;;;;;;;GAWG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAmH3C;;;;;QA5Ja,MAAM;;;;cACN,OAAO;;;;YACP,OAAO"}
@@ -6,6 +6,7 @@ import {
6
6
  byIdInHost,
7
7
  collectHosts,
8
8
  scrollIntoViewSafe,
9
+ closestSafe,
9
10
  } from './internal.js';
10
11
 
11
12
  /**
@@ -64,6 +65,7 @@ export function initSources({ root } = {}) {
64
65
  for (const island of islands) {
65
66
  const timers = new Set();
66
67
  const seeded = [];
68
+ const activeSources = new Set();
67
69
 
68
70
  const targetFor = (ref) => {
69
71
  const id = sourceId(ref);
@@ -101,17 +103,25 @@ export function initSources({ root } = {}) {
101
103
  }
102
104
  };
103
105
 
106
+ const clearGeneratedActive = () => {
107
+ for (const source of activeSources) source.classList.remove(ACTIVE);
108
+ activeSources.clear();
109
+ };
110
+
104
111
  const focusSource = (ref, source) => {
105
112
  for (const card of island.querySelectorAll(`.${ACTIVE}`)) card.classList.remove(ACTIVE);
113
+ clearGeneratedActive();
106
114
  for (const timer of timers) clearTimeout(timer);
107
115
  timers.clear();
108
116
 
109
117
  source.classList.add(ACTIVE);
118
+ activeSources.add(source);
110
119
  source.focus?.({ preventScroll: true });
111
120
  scrollIntoViewSafe(source);
112
121
 
113
122
  const timer = setTimeout(() => {
114
123
  source.classList.remove(ACTIVE);
124
+ activeSources.delete(source);
115
125
  timers.delete(timer);
116
126
  }, 1600);
117
127
  timers.add(timer);
@@ -125,7 +135,7 @@ export function initSources({ root } = {}) {
125
135
  };
126
136
 
127
137
  const onClick = (e) => {
128
- const ref = e.target.closest?.(REF_SELECTOR);
138
+ const ref = closestSafe(e.target, REF_SELECTOR);
129
139
  if (!ref || !island.contains(ref)) return;
130
140
  const source = targetFor(ref);
131
141
  if (!source) return;
@@ -134,12 +144,14 @@ export function initSources({ root } = {}) {
134
144
  };
135
145
 
136
146
  const cleanup = bindOnce(island, 'sources', () => {
147
+ const activeState = Array.from(island.querySelectorAll(`.${ACTIVE}`));
137
148
  seed();
138
149
  island.addEventListener('click', onClick);
139
150
  return () => {
140
151
  island.removeEventListener('click', onClick);
141
152
  for (const timer of timers) clearTimeout(timer);
142
153
  timers.clear();
154
+ clearGeneratedActive();
143
155
  for (const item of seeded.splice(0)) {
144
156
  if (item.hadDescribedBy) item.ref.setAttribute('aria-describedby', item.describedBy);
145
157
  else item.ref.removeAttribute('aria-describedby');
@@ -147,7 +159,7 @@ export function initSources({ root } = {}) {
147
159
  else item.ref.removeAttribute('title');
148
160
  if (item.source && item.hadTabindex === false) item.source.removeAttribute('tabindex');
149
161
  }
150
- for (const card of island.querySelectorAll(`.${ACTIVE}`)) card.classList.remove(ACTIVE);
162
+ for (const source of activeState) source.classList.add(ACTIVE);
151
163
  };
152
164
  });
153
165
 
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Wire focusable ARIA splitters. Each `[data-bronto-splitter]` host contains
3
+ * two `.ui-splitter__pane` elements separated by one `.ui-splitter__handle`
4
+ * (`role="separator"`). The behavior keeps `--splitter-pos` and
5
+ * `aria-valuenow` in sync for keyboard and pointer resizing, then dispatches
6
+ * `bronto:splitter:resize` with `{ value, orientation }`.
7
+ *
8
+ * Bronto owns the control affordance only. The host owns pane content,
9
+ * persistence, min/max policy, collapse behavior, and any saved layout state.
10
+ * SSR-safe and idempotent per splitter; returns a cleanup function.
11
+ *
12
+ * @param {import('./internal.js').DelegateOpts} [opts]
13
+ * @returns {import('./internal.js').Cleanup}
14
+ */
15
+ export function initSplitter({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
16
+ export type SplitterResizeDetail = {
17
+ /**
18
+ * The first pane size as a 0..100 percentage.
19
+ */
20
+ value: number;
21
+ /**
22
+ * Splitter orientation.
23
+ */
24
+ orientation: "vertical" | "horizontal";
25
+ };
26
+ //# sourceMappingURL=splitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["splitter.js"],"names":[],"mappings":"AAwNA;;;;;;;;;;;;;GAaG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAU3C;;;;;WAlOa,MAAM;;;;iBACN,UAAU,GAAG,YAAY"}
@@ -0,0 +1,239 @@
1
+ import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js';
2
+
3
+ const SELECTOR = '[data-bronto-splitter]';
4
+ const HANDLE_SELECTOR = '.ui-splitter__handle';
5
+ const DEFAULT_MIN = 20;
6
+ const DEFAULT_MAX = 80;
7
+ const DEFAULT_VALUE = 50;
8
+ const STEP = 2;
9
+ const LARGE_STEP = 10;
10
+
11
+ /**
12
+ * @typedef {object} SplitterResizeDetail
13
+ * @property {number} value The first pane size as a 0..100 percentage.
14
+ * @property {'vertical' | 'horizontal'} orientation Splitter orientation.
15
+ */
16
+
17
+ const num = (v, fallback) => {
18
+ const n = Number.parseFloat(String(v ?? '').trim());
19
+ return Number.isFinite(n) ? n : fallback;
20
+ };
21
+
22
+ const fmt = (v) => String(Math.round(v * 10) / 10);
23
+
24
+ const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
25
+
26
+ const readCssValue = (splitter) => splitter.style.getPropertyValue('--splitter-pos');
27
+
28
+ const readOrientation = (splitter, handle) => {
29
+ const data = splitter.getAttribute('data-bronto-splitter');
30
+ if (data === 'horizontal' || data === 'vertical') return data;
31
+ if (splitter.classList.contains('ui-splitter--horizontal')) return 'horizontal';
32
+ if (splitter.classList.contains('ui-splitter--vertical')) return 'vertical';
33
+ return handle.getAttribute('aria-orientation') === 'horizontal' ? 'horizontal' : 'vertical';
34
+ };
35
+
36
+ const getView = (el) => el.ownerDocument?.defaultView || null;
37
+
38
+ const dispatchResize = (splitter, detail) => {
39
+ splitter.dispatchEvent(
40
+ new CustomEvent('bronto:splitter:resize', {
41
+ bubbles: true,
42
+ detail,
43
+ }),
44
+ );
45
+ };
46
+
47
+ const snapshotAttrs = (el, names) => {
48
+ const out = {};
49
+ for (const name of names) {
50
+ out[name] = {
51
+ had: el.hasAttribute(name),
52
+ value: el.getAttribute(name),
53
+ };
54
+ }
55
+ return out;
56
+ };
57
+
58
+ const restoreAttrs = (el, attrs) => {
59
+ for (const [name, attr] of Object.entries(attrs)) {
60
+ if (attr.had) el.setAttribute(name, attr.value);
61
+ else el.removeAttribute(name);
62
+ }
63
+ };
64
+
65
+ const snapshotStyleProp = (el, name) => ({
66
+ value: el.style.getPropertyValue(name),
67
+ priority: el.style.getPropertyPriority(name),
68
+ });
69
+
70
+ const restoreStyleProp = (el, name, prop) => {
71
+ if (prop.value) el.style.setProperty(name, prop.value, prop.priority);
72
+ else el.style.removeProperty(name);
73
+ };
74
+
75
+ function wireSplitter(splitter) {
76
+ const handle = splitter.querySelector(HANDLE_SELECTOR);
77
+ if (!handle) return noop;
78
+
79
+ return bindOnce(splitter, 'splitter', () => {
80
+ const handleAttrs = snapshotAttrs(handle, [
81
+ 'role',
82
+ 'tabindex',
83
+ 'aria-orientation',
84
+ 'aria-valuemin',
85
+ 'aria-valuemax',
86
+ 'aria-valuenow',
87
+ ]);
88
+ const splitterPos = snapshotStyleProp(splitter, '--splitter-pos');
89
+ const orientation = readOrientation(splitter, handle);
90
+ const min = num(handle.getAttribute('aria-valuemin'), DEFAULT_MIN);
91
+ const max = Math.max(min, num(handle.getAttribute('aria-valuemax'), DEFAULT_MAX));
92
+ let value = clamp(
93
+ num(handle.getAttribute('aria-valuenow'), num(readCssValue(splitter), DEFAULT_VALUE)),
94
+ min,
95
+ max,
96
+ );
97
+ let activePointer = null;
98
+
99
+ const apply = (next, { emit = true } = {}) => {
100
+ value = clamp(next, min, max);
101
+ const label = fmt(value);
102
+ splitter.style.setProperty('--splitter-pos', `${label}%`);
103
+ handle.setAttribute('aria-valuenow', label);
104
+ if (emit) dispatchResize(splitter, { value, orientation });
105
+ };
106
+
107
+ if (!handle.hasAttribute('role')) handle.setAttribute('role', 'separator');
108
+ if (!handle.hasAttribute('tabindex')) handle.tabIndex = 0;
109
+ if (!handle.hasAttribute('aria-orientation'))
110
+ handle.setAttribute('aria-orientation', orientation);
111
+ if (!handle.hasAttribute('aria-valuemin')) handle.setAttribute('aria-valuemin', fmt(min));
112
+ if (!handle.hasAttribute('aria-valuemax')) handle.setAttribute('aria-valuemax', fmt(max));
113
+ apply(value, { emit: false });
114
+
115
+ const fromPointer = (event) => {
116
+ const rect = splitter.getBoundingClientRect();
117
+ const size = orientation === 'horizontal' ? rect.height : rect.width;
118
+ if (!size) return value;
119
+ if (orientation === 'horizontal') {
120
+ return ((event.clientY - rect.top) / size) * 100;
121
+ }
122
+ const view = getView(splitter);
123
+ const dir = view?.getComputedStyle?.(splitter).direction;
124
+ const x = dir === 'rtl' ? rect.right - event.clientX : event.clientX - rect.left;
125
+ return (x / size) * 100;
126
+ };
127
+
128
+ const capturePointer = (pointerId) => {
129
+ if (pointerId === undefined || pointerId === null) return;
130
+ try {
131
+ handle.setPointerCapture?.(pointerId);
132
+ } catch {
133
+ /* Pointer capture is an affordance; drag still works through document listeners. */
134
+ }
135
+ };
136
+
137
+ const releasePointer = (pointerId = activePointer) => {
138
+ if (pointerId === undefined || pointerId === null) return;
139
+ try {
140
+ if (!handle.hasPointerCapture || handle.hasPointerCapture(pointerId)) {
141
+ handle.releasePointerCapture?.(pointerId);
142
+ }
143
+ } catch {
144
+ /* The element may have been removed or capture may already be gone. */
145
+ }
146
+ };
147
+
148
+ const onKeydown = (event) => {
149
+ let next = value;
150
+ if (event.key === 'Home') next = min;
151
+ else if (event.key === 'End') next = max;
152
+ else if (event.key === 'PageUp') next += LARGE_STEP;
153
+ else if (event.key === 'PageDown') next -= LARGE_STEP;
154
+ else if (event.key === 'ArrowRight' || event.key === 'ArrowDown')
155
+ next += event.shiftKey ? LARGE_STEP : STEP;
156
+ else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp')
157
+ next -= event.shiftKey ? LARGE_STEP : STEP;
158
+ else return;
159
+ event.preventDefault();
160
+ apply(next);
161
+ };
162
+
163
+ const onPointerMove = (event) => {
164
+ if (
165
+ activePointer !== null &&
166
+ event.pointerId !== undefined &&
167
+ event.pointerId !== activePointer
168
+ )
169
+ return;
170
+ apply(fromPointer(event));
171
+ };
172
+
173
+ const onPointerUp = (event) => {
174
+ if (
175
+ activePointer !== null &&
176
+ event.pointerId !== undefined &&
177
+ event.pointerId !== activePointer
178
+ )
179
+ return;
180
+ releasePointer(event.pointerId);
181
+ activePointer = null;
182
+ handle.classList.remove('is-active');
183
+ splitter.ownerDocument.removeEventListener('pointermove', onPointerMove);
184
+ splitter.ownerDocument.removeEventListener('pointerup', onPointerUp);
185
+ splitter.ownerDocument.removeEventListener('pointercancel', onPointerUp);
186
+ };
187
+
188
+ const onPointerDown = (event) => {
189
+ if (event.button !== undefined && event.button !== 0) return;
190
+ event.preventDefault();
191
+ activePointer = event.pointerId ?? null;
192
+ capturePointer(activePointer);
193
+ handle.classList.add('is-active');
194
+ apply(fromPointer(event));
195
+ splitter.ownerDocument.addEventListener('pointermove', onPointerMove);
196
+ splitter.ownerDocument.addEventListener('pointerup', onPointerUp);
197
+ splitter.ownerDocument.addEventListener('pointercancel', onPointerUp);
198
+ };
199
+
200
+ handle.addEventListener('keydown', onKeydown);
201
+ handle.addEventListener('pointerdown', onPointerDown);
202
+ return () => {
203
+ handle.removeEventListener('keydown', onKeydown);
204
+ handle.removeEventListener('pointerdown', onPointerDown);
205
+ splitter.ownerDocument.removeEventListener('pointermove', onPointerMove);
206
+ splitter.ownerDocument.removeEventListener('pointerup', onPointerUp);
207
+ splitter.ownerDocument.removeEventListener('pointercancel', onPointerUp);
208
+ releasePointer();
209
+ handle.classList.remove('is-active');
210
+ activePointer = null;
211
+ restoreAttrs(handle, handleAttrs);
212
+ restoreStyleProp(splitter, '--splitter-pos', splitterPos);
213
+ };
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Wire focusable ARIA splitters. Each `[data-bronto-splitter]` host contains
219
+ * two `.ui-splitter__pane` elements separated by one `.ui-splitter__handle`
220
+ * (`role="separator"`). The behavior keeps `--splitter-pos` and
221
+ * `aria-valuenow` in sync for keyboard and pointer resizing, then dispatches
222
+ * `bronto:splitter:resize` with `{ value, orientation }`.
223
+ *
224
+ * Bronto owns the control affordance only. The host owns pane content,
225
+ * persistence, min/max policy, collapse behavior, and any saved layout state.
226
+ * SSR-safe and idempotent per splitter; returns a cleanup function.
227
+ *
228
+ * @param {import('./internal.js').DelegateOpts} [opts]
229
+ * @returns {import('./internal.js').Cleanup}
230
+ */
231
+ export function initSplitter({ root } = {}) {
232
+ if (!hasDom()) return noop;
233
+ const host = resolveHost(root);
234
+ if (!host) return noop;
235
+ const splitters = collectHosts(host, SELECTOR);
236
+ if (!splitters.length) return noop;
237
+ const cleanups = splitters.map(wireSplitter).filter(Boolean);
238
+ return () => cleanups.forEach((fn) => fn());
239
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"spotlight.d.ts","sourceRoot":"","sources":["spotlight.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAuC3C"}
1
+ {"version":3,"file":"spotlight.d.ts","sourceRoot":"","sources":["spotlight.js"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;GAcG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6C3C"}