@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,5 +1,25 @@
1
1
  import { hasDom, resolveHost, noop, bindOnce, byIdInHost, collectHosts } from './internal.js';
2
2
 
3
+ const SPOT_PROPS = ['--spot-x', '--spot-y', '--spot-w', '--spot-h'];
4
+
5
+ const snapshotSpotProps = (spot) =>
6
+ Object.fromEntries(
7
+ SPOT_PROPS.map((name) => [
8
+ name,
9
+ {
10
+ value: spot.style.getPropertyValue(name),
11
+ priority: spot.style.getPropertyPriority(name),
12
+ },
13
+ ]),
14
+ );
15
+
16
+ const restoreSpotProps = (spot, props) => {
17
+ for (const [name, prop] of Object.entries(props)) {
18
+ if (prop.value) spot.style.setProperty(name, prop.value, prop.priority);
19
+ else spot.style.removeProperty(name);
20
+ }
21
+ };
22
+
3
23
  /**
4
24
  * Position a spotlight cutout over a target element. Each
5
25
  * `[data-bronto-spotlight]` is a `.ui-spotlight` overlay; `data-target` is the
@@ -19,10 +39,9 @@ export function initSpotlight({ root } = {}) {
19
39
  if (!hasDom()) return noop;
20
40
  const host = resolveHost(root);
21
41
  if (!host) return noop;
22
- const spots = collectHosts(host, '[data-bronto-spotlight]');
23
- if (!spots.length) return noop;
24
42
 
25
43
  const place = () => {
44
+ const spots = collectHosts(host, '[data-bronto-spotlight]');
26
45
  for (const spot of spots) {
27
46
  const target = byIdInHost(host, spot.dataset.target);
28
47
  if (!target) continue;
@@ -35,6 +54,12 @@ export function initSpotlight({ root } = {}) {
35
54
  };
36
55
 
37
56
  return bindOnce(host, 'spotlight', () => {
57
+ const spots = collectHosts(host, '[data-bronto-spotlight]');
58
+ if (!spots.length) return noop;
59
+ const states = spots.map((spot) => ({
60
+ spot,
61
+ props: snapshotSpotProps(spot),
62
+ }));
38
63
  place();
39
64
  const view = host.defaultView || host.ownerDocument?.defaultView || null;
40
65
  const MO = view?.MutationObserver;
@@ -50,6 +75,7 @@ export function initSpotlight({ root } = {}) {
50
75
  mo?.disconnect();
51
76
  view?.removeEventListener('resize', place);
52
77
  view?.removeEventListener('scroll', place, true);
78
+ for (const state of states) restoreSpotProps(state.spot, state.props);
53
79
  };
54
80
  });
55
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["table.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6I3C"}
1
+ {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["table.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAyO3C"}
@@ -1,4 +1,4 @@
1
- import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, collectHosts, closestSafe } from './internal.js';
2
2
 
3
3
  /**
4
4
  * Client-side sortable + selectable data table. Wires
@@ -41,17 +41,97 @@ export function initTableSort({ root } = {}) {
41
41
  const tables = collectHosts(host, '[data-bronto-sortable]');
42
42
  const cleanups = [];
43
43
 
44
+ const snapshotAttrs = (el, names) => {
45
+ const out = {};
46
+ for (const name of names) {
47
+ out[name] = {
48
+ had: el.hasAttribute(name),
49
+ value: el.getAttribute(name),
50
+ };
51
+ }
52
+ return out;
53
+ };
54
+
55
+ const restoreAttrs = (el, attrs) => {
56
+ for (const [name, attr] of Object.entries(attrs)) {
57
+ if (attr.had) el.setAttribute(name, attr.value);
58
+ else el.removeAttribute(name);
59
+ }
60
+ };
61
+
44
62
  for (const table of tables) {
45
63
  const tbody = table.tBodies[0];
46
64
  if (!tbody) continue;
47
65
 
48
- // Seed the resting `aria-sort="none"` on every sortable header so AT
49
- // announces the column as sortable from the start (it was unset until the
50
- // first click C10).
51
- for (const sort of table.querySelectorAll('.ui-table__sort')) {
52
- const th = sort.closest('th');
53
- if (th && !th.hasAttribute('aria-sort')) th.setAttribute('aria-sort', 'none');
54
- }
66
+ const headerStates = new WeakMap();
67
+ const headers = [];
68
+ const sortStates = new WeakMap();
69
+ const sorters = [];
70
+ const rowStates = new WeakMap();
71
+ const rows = [];
72
+ const checkboxStates = new WeakMap();
73
+ const checkboxes = [];
74
+
75
+ const rememberHeaderState = (th) => {
76
+ if (!th || headerStates.has(th)) return;
77
+ headerStates.set(th, snapshotAttrs(th, ['aria-sort']));
78
+ headers.push(th);
79
+ };
80
+ const rememberSorterState = (sorter) => {
81
+ if (!sorter || sortStates.has(sorter)) return;
82
+ sortStates.set(sorter, snapshotAttrs(sorter, ['type']));
83
+ sorters.push(sorter);
84
+ };
85
+ const rememberRowState = (row) => {
86
+ if (!row || rowStates.has(row)) return;
87
+ rowStates.set(row, snapshotAttrs(row, ['aria-selected']));
88
+ rows.push(row);
89
+ };
90
+ const rememberCheckboxState = (box) => {
91
+ if (!box || checkboxStates.has(box)) return;
92
+ checkboxStates.set(box, {
93
+ checked: box.checked,
94
+ indeterminate: box.indeterminate,
95
+ });
96
+ checkboxes.push(box);
97
+ rememberRowState(box.closest?.('tr'));
98
+ };
99
+ const seedSorters = () => {
100
+ // Seed the resting `aria-sort="none"` on every sortable header so AT
101
+ // announces the column as sortable from the start (it was unset until the
102
+ // first click — C10).
103
+ for (const sort of table.querySelectorAll('.ui-table__sort')) {
104
+ rememberSorterState(sort);
105
+ if (sort.tagName === 'BUTTON' && !sort.hasAttribute('type')) sort.type = 'button';
106
+ const th = sort.closest('th');
107
+ rememberHeaderState(th);
108
+ if (th && !th.hasAttribute('aria-sort')) th.setAttribute('aria-sort', 'none');
109
+ }
110
+ };
111
+ const rememberTableState = () => {
112
+ const rowOrder = [...tbody.rows];
113
+ rowOrder.forEach(rememberRowState);
114
+ seedSorters();
115
+ const allBox = table.querySelector('[data-bronto-select-all]');
116
+ rememberCheckboxState(allBox);
117
+ table.querySelectorAll('[data-bronto-select]').forEach(rememberCheckboxState);
118
+ return { rowOrder };
119
+ };
120
+ const restoreTableState = (state) => {
121
+ for (const sorter of sorters) restoreAttrs(sorter, sortStates.get(sorter));
122
+ for (const th of headers) restoreAttrs(th, headerStates.get(th));
123
+ for (const row of rows) restoreAttrs(row, rowStates.get(row));
124
+ for (const box of checkboxes) {
125
+ const boxState = checkboxStates.get(box);
126
+ if (!boxState) continue;
127
+ box.checked = boxState.checked;
128
+ box.indeterminate = boxState.indeterminate;
129
+ }
130
+ const ordered = state.rowOrder.filter((row) => row.parentElement === tbody);
131
+ const orderedSet = new Set(ordered);
132
+ const extras = [...tbody.rows].filter((row) => !orderedSet.has(row));
133
+ tbody.append(...ordered, ...extras);
134
+ };
55
135
 
56
136
  const colIndex = (th) => [...th.parentElement.children].indexOf(th);
57
137
  const cellText = (row, i) => row.children[i]?.textContent.trim() ?? '';
@@ -94,8 +174,10 @@ export function initTableSort({ root } = {}) {
94
174
  // Reset the OTHER sortable headers to `none` (not removed) so they keep
95
175
  // announcing sortability; only previously-sortable headers carry aria-sort.
96
176
  headers.forEach((h) => {
177
+ rememberHeaderState(h);
97
178
  if (h !== th && h.hasAttribute('aria-sort')) h.setAttribute('aria-sort', 'none');
98
179
  });
180
+ rememberHeaderState(th);
99
181
  th.setAttribute('aria-sort', dir);
100
182
  const i = colIndex(th);
101
183
  const sign = dir === 'ascending' ? 1 : -1;
@@ -110,9 +192,8 @@ export function initTableSort({ root } = {}) {
110
192
  return cmp * sign;
111
193
  });
112
194
  // Re-parent in document order: sorted data rows, then any empty/sentinel
113
- // row last. A single appendChild pass over the existing <tr> nodes (no
114
- // markup is created — these are trusted DOM elements being moved).
115
- for (const r of [...rows, ...emptyRows]) tbody.appendChild(r);
195
+ // row last. These are existing <tr> nodes being moved; no markup is parsed.
196
+ tbody.append(...rows, ...emptyRows);
116
197
  };
117
198
 
118
199
  const allBox = table.querySelector('[data-bronto-select-all]');
@@ -121,6 +202,7 @@ export function initTableSort({ root } = {}) {
121
202
  const boxes = rowBoxes();
122
203
  const on = boxes.filter((b) => b.checked).length;
123
204
  if (allBox) {
205
+ rememberCheckboxState(allBox);
124
206
  allBox.checked = on > 0 && on === boxes.length;
125
207
  allBox.indeterminate = on > 0 && on < boxes.length;
126
208
  }
@@ -129,8 +211,12 @@ export function initTableSort({ root } = {}) {
129
211
  );
130
212
  };
131
213
  const markRow = (box) => {
214
+ rememberCheckboxState(box);
132
215
  const tr = box.closest('tr');
133
- if (tr) tr.setAttribute('aria-selected', String(box.checked));
216
+ if (tr) {
217
+ rememberRowState(tr);
218
+ tr.setAttribute('aria-selected', String(box.checked));
219
+ }
134
220
  };
135
221
 
136
222
  const onClick = (e) => {
@@ -138,8 +224,9 @@ export function initTableSort({ root } = {}) {
138
224
  // keyboard-operable and carries the `::after` sort glyph. The bare
139
225
  // `th[data-sort]` path was mouse-only with no affordance, so it is gone
140
226
  // (C10); `data-sort="num"` is still read from the button or its th.
141
- const sorter = e.target.closest('.ui-table__sort');
227
+ const sorter = closestSafe(e.target, '.ui-table__sort');
142
228
  if (sorter && table.contains(sorter)) {
229
+ rememberSorterState(sorter);
143
230
  const th = sorter.closest('th');
144
231
  const numeric =
145
232
  (sorter.getAttribute('data-sort') || th.getAttribute('data-sort')) === 'num' ||
@@ -150,23 +237,28 @@ export function initTableSort({ root } = {}) {
150
237
  const onChange = (e) => {
151
238
  const t = e.target;
152
239
  if (t.matches?.('[data-bronto-select-all]')) {
240
+ rememberCheckboxState(t);
153
241
  rowBoxes().forEach((b) => {
242
+ rememberCheckboxState(b);
154
243
  b.checked = t.checked;
155
244
  markRow(b);
156
245
  });
157
246
  syncAll();
158
247
  } else if (t.matches?.('[data-bronto-select]')) {
248
+ rememberCheckboxState(t);
159
249
  markRow(t);
160
250
  syncAll();
161
251
  }
162
252
  };
163
253
 
164
254
  const bound = bindOnce(table, 'tableSort', () => {
255
+ const state = rememberTableState();
165
256
  table.addEventListener('click', onClick);
166
257
  table.addEventListener('change', onChange);
167
258
  return () => {
168
259
  table.removeEventListener('click', onClick);
169
260
  table.removeEventListener('change', onChange);
261
+ restoreTableState(state);
170
262
  };
171
263
  });
172
264
  cleanups.push(bound);
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAuF3C"}
1
+ {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;GAiBG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA+I3C"}
package/behaviors/tabs.js CHANGED
@@ -1,4 +1,12 @@
1
- import { hasDom, resolveHost, noop, bindOnce, nextFieldUid, collectHosts } from './internal.js';
1
+ import {
2
+ hasDom,
3
+ resolveHost,
4
+ noop,
5
+ bindOnce,
6
+ nextFieldUid,
7
+ collectHosts,
8
+ closestSafe,
9
+ } from './internal.js';
2
10
 
3
11
  /**
4
12
  * Wire `[data-bronto-tabs]` groups for full keyboard a11y. The framework
@@ -24,6 +32,23 @@ export function initTabs({ root } = {}) {
24
32
  if (!host) return noop;
25
33
  const cleanups = [];
26
34
  const groups = collectHosts(host, '[data-bronto-tabs]');
35
+ const snapshotAttrs = (el, names) => {
36
+ const out = {};
37
+ for (const name of names) {
38
+ out[name] = {
39
+ had: el.hasAttribute(name),
40
+ value: el.getAttribute(name),
41
+ };
42
+ }
43
+ return out;
44
+ };
45
+ const restoreAttrs = (el, attrs) => {
46
+ for (const [name, attr] of Object.entries(attrs)) {
47
+ if (attr.had) el.setAttribute(name, attr.value);
48
+ else el.removeAttribute(name);
49
+ }
50
+ };
51
+
27
52
  for (const group of groups) {
28
53
  // Own group only — a tab/panel inside a nested [data-bronto-tabs]
29
54
  // belongs to that inner group, not this one.
@@ -31,20 +56,43 @@ export function initTabs({ root } = {}) {
31
56
  const tabs = [...group.querySelectorAll('.ui-tab')].filter(owned);
32
57
  const panels = [...group.querySelectorAll('.ui-tabs__panel')].filter(owned);
33
58
  if (!tabs.length) continue;
34
- const list = group.querySelector('.ui-tabs__list');
35
- if (list) list.setAttribute('role', 'tablist');
36
-
37
- // APG: bind each tab to its panel (aria-controls) and back
38
- // (aria-labelledby), minting stable ids only where absent.
39
- for (const t of tabs) {
40
- const p = panels.find((x) => x.dataset.panel === t.dataset.tab);
41
- if (!p) continue;
42
- const n = nextFieldUid();
43
- if (!t.id) t.id = `bronto-tab-${n}`;
44
- if (!p.id) p.id = `bronto-tabpanel-${n}`;
45
- t.setAttribute('aria-controls', p.id);
46
- p.setAttribute('aria-labelledby', t.id);
47
- }
59
+ const list = [...group.querySelectorAll('.ui-tabs__list')].find(owned);
60
+ const rememberState = () => ({
61
+ list: list ? snapshotAttrs(list, ['role']) : null,
62
+ tabs: new Map(
63
+ tabs.map((tab) => [
64
+ tab,
65
+ {
66
+ active: tab.classList.contains('is-active'),
67
+ attrs: snapshotAttrs(tab, ['id', 'role', 'aria-selected', 'aria-controls', 'tabindex']),
68
+ },
69
+ ]),
70
+ ),
71
+ panels: new Map(
72
+ panels.map((panel) => [
73
+ panel,
74
+ {
75
+ hidden: panel.hidden,
76
+ attrs: snapshotAttrs(panel, ['id', 'role', 'aria-labelledby', 'tabindex']),
77
+ },
78
+ ]),
79
+ ),
80
+ });
81
+ const restoreState = (state) => {
82
+ if (list && state.list) restoreAttrs(list, state.list);
83
+ for (const tab of tabs) {
84
+ const tabState = state.tabs.get(tab);
85
+ if (!tabState) continue;
86
+ tab.classList.toggle('is-active', tabState.active);
87
+ restoreAttrs(tab, tabState.attrs);
88
+ }
89
+ for (const panel of panels) {
90
+ const panelState = state.panels.get(panel);
91
+ if (!panelState) continue;
92
+ panel.hidden = panelState.hidden;
93
+ restoreAttrs(panel, panelState.attrs);
94
+ }
95
+ };
48
96
 
49
97
  const select = (tab) => {
50
98
  for (const t of tabs) {
@@ -70,14 +118,15 @@ export function initTabs({ root } = {}) {
70
118
  const onClick = (e) => {
71
119
  // `tabs` is filtered to this group, so membership (not mere DOM
72
120
  // containment) is what isolates nested [data-bronto-tabs] groups.
73
- const tab = e.target.closest('.ui-tab');
121
+ const tab = closestSafe(e.target, '.ui-tab');
74
122
  if (tab && tabs.includes(tab)) {
123
+ e.preventDefault();
75
124
  select(tab);
76
125
  tab.focus();
77
126
  }
78
127
  };
79
128
  const onKey = (e) => {
80
- const i = tabs.indexOf(e.target.closest('.ui-tab'));
129
+ const i = tabs.indexOf(closestSafe(e.target, '.ui-tab'));
81
130
  if (i < 0) return;
82
131
  let n = i;
83
132
  if (e.key === 'ArrowRight' || e.key === 'ArrowDown') n = (i + 1) % tabs.length;
@@ -90,14 +139,29 @@ export function initTabs({ root } = {}) {
90
139
  select(tabs[n]);
91
140
  tabs[n].focus();
92
141
  };
93
- select(tabs.find((t) => t.classList.contains('is-active')) || tabs[0]);
94
142
  cleanups.push(
95
143
  bindOnce(group, 'tabs', () => {
144
+ const state = rememberState();
145
+ if (list) list.setAttribute('role', 'tablist');
146
+
147
+ // APG: bind each tab to its panel (aria-controls) and back
148
+ // (aria-labelledby), minting stable ids only where absent.
149
+ for (const t of tabs) {
150
+ const p = panels.find((x) => x.dataset.panel === t.dataset.tab);
151
+ if (!p) continue;
152
+ const n = nextFieldUid();
153
+ if (!t.id) t.id = `bronto-tab-${n}`;
154
+ if (!p.id) p.id = `bronto-tabpanel-${n}`;
155
+ t.setAttribute('aria-controls', p.id);
156
+ p.setAttribute('aria-labelledby', t.id);
157
+ }
158
+ select(tabs.find((t) => t.classList.contains('is-active')) || tabs[0]);
96
159
  group.addEventListener('click', onClick);
97
160
  group.addEventListener('keydown', onKey);
98
161
  return () => {
99
162
  group.removeEventListener('click', onClick);
100
163
  group.removeEventListener('keydown', onKey);
164
+ restoreState(state);
101
165
  };
102
166
  }),
103
167
  );
@@ -1 +1 @@
1
- {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["theme.js"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wDAHW,cAAc,GACZ,IAAI,CAahB;AAED;;;;;;;;;;;;;;GAcG;AACH,uDAHW,gBAAgB,GAAG,OAAO,eAAe,EAAE,YAAY,GACrD,OAAO,eAAe,EAAE,OAAO,CAmD3C;;;;;;;;;;6BA5FY,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;WAIpC,OAAO,GAAG,MAAM"}
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["theme.js"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wDAHW,cAAc,GACZ,IAAI,CAahB;AAED;;;;;;;;;;;;;;GAcG;AACH,uDAHW,gBAAgB,GAAG,OAAO,eAAe,EAAE,YAAY,GACrD,OAAO,eAAe,EAAE,OAAO,CAuE3C;;;;;;;;;;6BAhHY,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;WAIpC,OAAO,GAAG,MAAM"}
@@ -1,4 +1,4 @@
1
- import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
1
+ import { hasDom, resolveHost, noop, bindOnce, collectHosts, closestSafe } from './internal.js';
2
2
 
3
3
  const THEMES = ['light', 'dark'];
4
4
 
@@ -53,7 +53,18 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
53
53
  if (!hasDom()) return noop;
54
54
  const host = resolveHost(root);
55
55
  if (!host) return noop;
56
- const docEl = document.documentElement;
56
+ const doc = host.nodeType === 9 ? host : host.ownerDocument || document;
57
+ const docEl = doc.documentElement;
58
+ const toggleStates = new Map();
59
+
60
+ const rememberToggle = (el) => {
61
+ if (!toggleStates.has(el)) {
62
+ toggleStates.set(el, {
63
+ had: el.hasAttribute('aria-pressed'),
64
+ value: el.getAttribute('aria-pressed'),
65
+ });
66
+ }
67
+ };
57
68
 
58
69
  const prefersDark = () =>
59
70
  typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches;
@@ -66,7 +77,8 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
66
77
 
67
78
  const reflect = () => {
68
79
  const c = current();
69
- host.querySelectorAll('[data-bronto-theme-toggle]').forEach((el) => {
80
+ collectHosts(host, '[data-bronto-theme-toggle]').forEach((el) => {
81
+ rememberToggle(el);
70
82
  const forced = el.getAttribute('data-bronto-theme-toggle');
71
83
  // A forced control is "pressed" when its theme is the active one;
72
84
  // a plain toggle reflects whether dark is active.
@@ -76,8 +88,9 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
76
88
  };
77
89
 
78
90
  const onClick = (e) => {
79
- const trigger = e.target.closest('[data-bronto-theme-toggle]');
91
+ const trigger = closestSafe(e.target, '[data-bronto-theme-toggle]');
80
92
  if (!trigger || !host.contains(trigger)) return;
93
+ e.preventDefault();
81
94
  const forced = trigger.getAttribute('data-bronto-theme-toggle');
82
95
  const next = THEMES.includes(forced) ? forced : current() === 'dark' ? 'light' : 'dark';
83
96
  docEl.setAttribute('data-theme', next);
@@ -92,10 +105,17 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
92
105
  );
93
106
  };
94
107
 
95
- applyStoredTheme({ storageKey });
108
+ applyStoredTheme({ storageKey, root: docEl });
96
109
  reflect();
97
110
  return bindOnce(host, 'themeToggle', () => {
98
111
  host.addEventListener('click', onClick);
99
- return () => host.removeEventListener('click', onClick);
112
+ return () => {
113
+ host.removeEventListener('click', onClick);
114
+ for (const [el, state] of toggleStates) {
115
+ if (state.had) el.setAttribute('aria-pressed', state.value);
116
+ else el.removeAttribute('aria-pressed');
117
+ }
118
+ toggleStates.clear();
119
+ };
100
120
  });
101
121
  }