@ponchia/ui 0.6.10 → 0.6.12

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 (149) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/README.md +38 -25
  3. package/annotations/index.d.ts +15 -15
  4. package/annotations/index.d.ts.map +1 -1
  5. package/annotations/index.js +52 -34
  6. package/behaviors/carousel.d.ts +7 -3
  7. package/behaviors/carousel.d.ts.map +1 -1
  8. package/behaviors/carousel.js +157 -27
  9. package/behaviors/combobox.d.ts +1 -1
  10. package/behaviors/combobox.d.ts.map +1 -1
  11. package/behaviors/combobox.js +46 -23
  12. package/behaviors/command.d.ts +1 -1
  13. package/behaviors/command.d.ts.map +1 -1
  14. package/behaviors/command.js +63 -23
  15. package/behaviors/connectors.d.ts.map +1 -1
  16. package/behaviors/connectors.js +126 -19
  17. package/behaviors/crosshair.d.ts.map +1 -1
  18. package/behaviors/crosshair.js +71 -8
  19. package/behaviors/dialog.d.ts.map +1 -1
  20. package/behaviors/dialog.js +20 -3
  21. package/behaviors/disclosure.d.ts.map +1 -1
  22. package/behaviors/disclosure.js +35 -6
  23. package/behaviors/dismissible.js +1 -1
  24. package/behaviors/forms.d.ts +23 -2
  25. package/behaviors/forms.d.ts.map +1 -1
  26. package/behaviors/forms.js +97 -9
  27. package/behaviors/glyph.d.ts.map +1 -1
  28. package/behaviors/glyph.js +56 -5
  29. package/behaviors/internal.d.ts.map +1 -1
  30. package/behaviors/internal.js +52 -5
  31. package/behaviors/menu.d.ts.map +1 -1
  32. package/behaviors/menu.js +2 -1
  33. package/behaviors/modal.d.ts +13 -13
  34. package/behaviors/modal.d.ts.map +1 -1
  35. package/behaviors/modal.js +283 -96
  36. package/behaviors/popover.d.ts.map +1 -1
  37. package/behaviors/popover.js +58 -8
  38. package/behaviors/sources.d.ts.map +1 -1
  39. package/behaviors/sources.js +24 -3
  40. package/behaviors/splitter.d.ts.map +1 -1
  41. package/behaviors/splitter.js +27 -6
  42. package/behaviors/table.d.ts.map +1 -1
  43. package/behaviors/table.js +44 -7
  44. package/behaviors/tabs.d.ts.map +1 -1
  45. package/behaviors/tabs.js +51 -14
  46. package/behaviors/theme.d.ts.map +1 -1
  47. package/behaviors/theme.js +64 -4
  48. package/behaviors/toast.d.ts +6 -1
  49. package/behaviors/toast.d.ts.map +1 -1
  50. package/behaviors/toast.js +48 -12
  51. package/classes/classes.json +24 -0
  52. package/classes/index.d.ts +3 -2
  53. package/classes/index.js +77 -39
  54. package/classes/vscode.css-custom-data.json +1 -1
  55. package/connectors/index.d.ts +4 -4
  56. package/connectors/index.d.ts.map +1 -1
  57. package/connectors/index.js +14 -12
  58. package/css/annotations.css +1 -0
  59. package/css/app.css +7 -0
  60. package/css/base.css +3 -0
  61. package/css/bullet.css +41 -7
  62. package/css/code.css +14 -0
  63. package/css/command.css +10 -0
  64. package/css/dataviz.css +27 -0
  65. package/css/diff.css +2 -0
  66. package/css/disclosure.css +8 -0
  67. package/css/dots.css +1 -1
  68. package/css/feedback.css +9 -0
  69. package/css/interval.css +20 -2
  70. package/css/legend.css +10 -9
  71. package/css/marks.css +1 -0
  72. package/css/motion.css +2 -0
  73. package/css/overlay.css +14 -2
  74. package/css/primitives.css +1 -1
  75. package/css/report.css +3 -0
  76. package/css/sources.css +4 -4
  77. package/css/spotlight.css +6 -0
  78. package/css/table.css +19 -0
  79. package/css/term.css +4 -1
  80. package/css/tokens.css +9 -14
  81. package/dist/bronto.css +1 -1
  82. package/dist/css/analytical.css +1 -1
  83. package/dist/css/app.css +1 -1
  84. package/dist/css/bullet.css +1 -1
  85. package/dist/css/code.css +1 -1
  86. package/dist/css/command.css +1 -1
  87. package/dist/css/dataviz.css +1 -1
  88. package/dist/css/diff.css +1 -1
  89. package/dist/css/disclosure.css +1 -1
  90. package/dist/css/dots.css +1 -1
  91. package/dist/css/feedback.css +1 -1
  92. package/dist/css/interval.css +1 -1
  93. package/dist/css/legend.css +1 -1
  94. package/dist/css/marks.css +1 -1
  95. package/dist/css/overlay.css +1 -1
  96. package/dist/css/primitives.css +1 -1
  97. package/dist/css/report-kit.css +1 -1
  98. package/dist/css/sources.css +1 -1
  99. package/dist/css/spotlight.css +1 -1
  100. package/dist/css/table.css +1 -1
  101. package/dist/css/term.css +1 -1
  102. package/dist/css/tokens.css +1 -1
  103. package/docs/architecture.md +6 -4
  104. package/docs/bullet.md +6 -1
  105. package/docs/clamp.md +5 -0
  106. package/docs/command.md +3 -2
  107. package/docs/contrast.md +3 -3
  108. package/docs/crosshair.md +6 -0
  109. package/docs/dots.md +10 -3
  110. package/docs/figure.md +7 -0
  111. package/docs/glyphs.md +14 -2
  112. package/docs/highlights.md +9 -0
  113. package/docs/interval.md +6 -0
  114. package/docs/mermaid.md +5 -3
  115. package/docs/package-contract.md +24 -1
  116. package/docs/reference.md +1 -1
  117. package/docs/reporting.md +8 -8
  118. package/docs/selection.md +9 -0
  119. package/docs/sources.md +5 -0
  120. package/docs/stability.md +31 -6
  121. package/docs/state.md +6 -0
  122. package/docs/textref.md +18 -13
  123. package/docs/theming.md +18 -8
  124. package/docs/toc.md +6 -0
  125. package/docs/tree.md +9 -2
  126. package/docs/usage.md +12 -2
  127. package/docs/vega.md +5 -3
  128. package/glyphs/glyphs.js +62 -8
  129. package/index.d.ts +1 -0
  130. package/llms.txt +18 -14
  131. package/package.json +98 -6
  132. package/qwik/index.d.ts +4 -3
  133. package/qwik/index.d.ts.map +1 -1
  134. package/qwik/index.js +7 -5
  135. package/react/index.d.ts +4 -3
  136. package/react/index.d.ts.map +1 -1
  137. package/react/index.js +3 -2
  138. package/solid/index.d.ts +7 -5
  139. package/solid/index.d.ts.map +1 -1
  140. package/solid/index.js +11 -7
  141. package/tokens/figma.variables.json +2 -2
  142. package/tokens/index.js +1 -1
  143. package/tokens/index.json +2 -2
  144. package/tokens/resolved.json +1 -1
  145. package/tokens/tokens.dtcg.json +1 -1
  146. package/tokens/vega.d.ts +1 -1
  147. package/tokens/vega.js +3 -2
  148. package/vue/index.d.ts.map +1 -1
  149. package/vue/index.js +37 -3
@@ -68,13 +68,59 @@ export function initPopover({ root } = {}) {
68
68
  if (!hasDom()) return noop;
69
69
  const host = resolveHost(root);
70
70
  if (!host) return noop;
71
- const view = document.defaultView;
71
+ const doc = host.nodeType === 9 ? host : host.ownerDocument;
72
+ if (!doc) return noop;
73
+ const view = doc.defaultView;
72
74
  const GAP = 8;
73
75
  let openPanel = null;
74
76
  let openTrigger = null;
77
+ let pendingFocusObserver = null;
75
78
  const triggerStates = new Map();
76
79
  const panelStates = new Map();
77
80
 
81
+ const cancelPendingFocus = () => {
82
+ pendingFocusObserver?.disconnect();
83
+ pendingFocusObserver = null;
84
+ };
85
+
86
+ const focusWhenInteractive = (panel) => {
87
+ cancelPendingFocus();
88
+ if (!panel.inert) {
89
+ focusInto(panel);
90
+ return;
91
+ }
92
+
93
+ // A panel portaled out of a controlled modal starts in the trapped
94
+ // background. Wait for initModal to admit that specific panel instead of
95
+ // guessing how many microtasks its MutationObserver reconciliation needs.
96
+ const Observer = view?.MutationObserver;
97
+ if (!Observer) {
98
+ queueMicrotask(() => {
99
+ if (openPanel === panel && !panel.inert) focusInto(panel);
100
+ });
101
+ return;
102
+ }
103
+ const focusIfReady = () => {
104
+ if (openPanel !== panel) {
105
+ cancelPendingFocus();
106
+ return;
107
+ }
108
+ if (panel.inert) return;
109
+ cancelPendingFocus();
110
+ focusInto(panel);
111
+ };
112
+ pendingFocusObserver = new Observer(focusIfReady);
113
+ pendingFocusObserver.observe(panel, { attributes: true, attributeFilter: ['inert'] });
114
+ // Some DOM implementations model `inert` as a property without reflecting
115
+ // its attribute, so no observer record is delivered. Cover that test-DOM
116
+ // path across the modal reconciler's queued microtask; real browsers still
117
+ // use the attribute observer as the source of truth.
118
+ queueMicrotask(() => {
119
+ focusIfReady();
120
+ if (openPanel === panel && panel.inert) queueMicrotask(focusIfReady);
121
+ });
122
+ };
123
+
78
124
  const rememberTrigger = (trigger) => {
79
125
  if (!triggerStates.has(trigger)) {
80
126
  triggerStates.set(
@@ -121,13 +167,14 @@ export function initPopover({ root } = {}) {
121
167
  };
122
168
 
123
169
  const close = () => {
170
+ cancelPendingFocus();
124
171
  if (!openPanel) return;
125
172
  const panel = openPanel;
126
173
  const trigger = openTrigger;
127
174
  // Only steal focus back to the trigger when focus is still inside the panel
128
175
  // (Escape / programmatic re-toggle). An outside-click leaves focus where the
129
176
  // click landed — deliberate intent to move on, per the doc contract.
130
- const focusWasInside = panel.contains(document.activeElement);
177
+ const focusWasInside = panel.contains(doc.activeElement);
131
178
  openPanel = openTrigger = null;
132
179
  if (panel.hasAttribute('popover') && typeof panel.hidePopover === 'function') {
133
180
  try {
@@ -164,7 +211,7 @@ export function initPopover({ root } = {}) {
164
211
  openPanel = panel;
165
212
  openTrigger = trigger;
166
213
  place(trigger, panel);
167
- focusInto(panel);
214
+ focusWhenInteractive(panel);
168
215
  };
169
216
 
170
217
  const onClick = (e) => {
@@ -225,7 +272,10 @@ export function initPopover({ root } = {}) {
225
272
  const onToggle = (e) => {
226
273
  const isOpen = e.newState === 'open';
227
274
  trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
228
- if (!isOpen && openPanel === panel) openPanel = openTrigger = null;
275
+ if (!isOpen && openPanel === panel) {
276
+ cancelPendingFocus();
277
+ openPanel = openTrigger = null;
278
+ }
229
279
  };
230
280
  panel.addEventListener('toggle', onToggle);
231
281
  seedTeardowns.push(() => panel.removeEventListener('toggle', onToggle));
@@ -235,8 +285,8 @@ export function initPopover({ root } = {}) {
235
285
 
236
286
  return bindOnce(host, 'popover', () => {
237
287
  seed();
238
- document.addEventListener('click', onClick);
239
- document.addEventListener('keydown', onKey);
288
+ doc.addEventListener('click', onClick);
289
+ doc.addEventListener('keydown', onKey);
240
290
  view?.addEventListener('scroll', onReflow, true);
241
291
  view?.addEventListener('resize', onReflow);
242
292
  return () => {
@@ -250,8 +300,8 @@ export function initPopover({ root } = {}) {
250
300
  restoreStyle(panel, state.style);
251
301
  }
252
302
  panelStates.clear();
253
- document.removeEventListener('click', onClick);
254
- document.removeEventListener('keydown', onKey);
303
+ doc.removeEventListener('click', onClick);
304
+ doc.removeEventListener('keydown', onKey);
255
305
  view?.removeEventListener('scroll', onReflow, true);
256
306
  view?.removeEventListener('resize', onReflow);
257
307
  };
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["sources.js"],"names":[],"mappings":"AAqDA;;;;;;;;;;;GAWG;AACH,uCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA+H3C;;;;;QAlLa,MAAM;;;;cACN,OAAO;;;;YACP,OAAO"}
@@ -3,7 +3,6 @@ import {
3
3
  resolveHost,
4
4
  noop,
5
5
  bindOnce,
6
- byIdInHost,
7
6
  collectHosts,
8
7
  scrollIntoViewSafe,
9
8
  closestSafe,
@@ -42,6 +41,16 @@ function sourcePreview(source) {
42
41
  return [title, meta, excerpt].filter(Boolean).join(' — ');
43
42
  }
44
43
 
44
+ function idMapFor(island) {
45
+ const ids = new Map();
46
+ const add = (el) => {
47
+ if (el?.id && !ids.has(el.id)) ids.set(el.id, el);
48
+ };
49
+ add(island);
50
+ for (const el of island.querySelectorAll?.('[id]') || []) add(el);
51
+ return ids;
52
+ }
53
+
45
54
  /**
46
55
  * Source/citation affordances for the `sources.css` trust layer. The behavior
47
56
  * is deliberately small: within each `[data-bronto-sources]` island it resolves
@@ -66,11 +75,23 @@ export function initSources({ root } = {}) {
66
75
  const timers = new Set();
67
76
  const seeded = [];
68
77
  const activeSources = new Set();
78
+ const sourcesById = idMapFor(island);
79
+ const previewBySource = new WeakMap();
69
80
 
70
81
  const targetFor = (ref) => {
71
82
  const id = sourceId(ref);
72
83
  if (!id) return null;
73
- return byIdInHost(island, id);
84
+ const source = sourcesById.get(id) || null;
85
+ return source && island.contains(source) ? source : null;
86
+ };
87
+
88
+ const previewFor = (source) => {
89
+ let preview = previewBySource.get(source);
90
+ if (preview === undefined) {
91
+ preview = sourcePreview(source);
92
+ previewBySource.set(source, preview);
93
+ }
94
+ return preview;
74
95
  };
75
96
 
76
97
  const seed = () => {
@@ -81,7 +102,7 @@ export function initSources({ root } = {}) {
81
102
  const describedBy = ref.getAttribute('aria-describedby') || '';
82
103
  const describedIds = describedBy.split(/\s+/).filter(Boolean);
83
104
  const title = ref.getAttribute('title');
84
- const preview = sourcePreview(source);
105
+ const preview = previewFor(source);
85
106
  const prior = {
86
107
  ref,
87
108
  describedBy,
@@ -1 +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"}
1
+ {"version":3,"file":"splitter.d.ts","sourceRoot":"","sources":["splitter.js"],"names":[],"mappings":"AA6OA;;;;;;;;;;;;;GAaG;AACH,wCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAU3C;;;;;WAvPa,MAAM;;;;iBACN,UAAU,GAAG,YAAY"}
@@ -14,7 +14,16 @@ const LARGE_STEP = 10;
14
14
  * @property {'vertical' | 'horizontal'} orientation Splitter orientation.
15
15
  */
16
16
 
17
+ const NUMBER_RE = /^[-+]?(?:(?:\d+\.?\d*)|\.\d+)(?:e[-+]?\d+)?$/i;
18
+
17
19
  const num = (v, fallback) => {
20
+ const value = String(v ?? '').trim();
21
+ if (!NUMBER_RE.test(value)) return fallback;
22
+ const n = Number(value);
23
+ return Number.isFinite(n) ? n : fallback;
24
+ };
25
+
26
+ const cssNum = (v, fallback) => {
18
27
  const n = Number.parseFloat(String(v ?? '').trim());
19
28
  return Number.isFinite(n) ? n : fallback;
20
29
  };
@@ -26,7 +35,7 @@ const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
26
35
  const readCssValue = (splitter) => splitter.style.getPropertyValue('--splitter-pos');
27
36
 
28
37
  const readOrientation = (splitter, handle) => {
29
- const data = splitter.getAttribute('data-bronto-splitter');
38
+ const data = splitter.getAttribute('data-bronto-splitter')?.trim();
30
39
  if (data === 'horizontal' || data === 'vertical') return data;
31
40
  if (splitter.classList.contains('ui-splitter--horizontal')) return 'horizontal';
32
41
  if (splitter.classList.contains('ui-splitter--vertical')) return 'vertical';
@@ -87,13 +96,25 @@ function wireSplitter(splitter) {
87
96
  ]);
88
97
  const splitterPos = snapshotStyleProp(splitter, '--splitter-pos');
89
98
  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));
99
+ const minAttr = handle.getAttribute('aria-valuemin');
100
+ const maxAttr = handle.getAttribute('aria-valuemax');
101
+ const min = num(minAttr, DEFAULT_MIN);
102
+ const max = Math.max(min, num(maxAttr, DEFAULT_MAX));
92
103
  let value = clamp(
93
- num(handle.getAttribute('aria-valuenow'), num(readCssValue(splitter), DEFAULT_VALUE)),
104
+ num(handle.getAttribute('aria-valuenow'), cssNum(readCssValue(splitter), DEFAULT_VALUE)),
94
105
  min,
95
106
  max,
96
107
  );
108
+ const syncRangeAttr = (name, authored, normalized) => {
109
+ const parsed = num(authored, Number.NaN);
110
+ if (
111
+ authored === null ||
112
+ !Number.isFinite(parsed) ||
113
+ String(authored).trim() !== fmt(normalized)
114
+ ) {
115
+ handle.setAttribute(name, fmt(normalized));
116
+ }
117
+ };
97
118
  let activePointer = null;
98
119
 
99
120
  const apply = (next, { emit = true } = {}) => {
@@ -108,8 +129,8 @@ function wireSplitter(splitter) {
108
129
  if (!handle.hasAttribute('tabindex')) handle.tabIndex = 0;
109
130
  if (!handle.hasAttribute('aria-orientation'))
110
131
  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));
132
+ syncRangeAttr('aria-valuemin', minAttr, min);
133
+ syncRangeAttr('aria-valuemax', maxAttr, max);
113
134
  apply(value, { emit: false });
114
135
 
115
136
  const fromPointer = (event) => {
@@ -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,CAwO3C"}
1
+ {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["table.js"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA0P3C"}
@@ -1,5 +1,24 @@
1
1
  import { hasDom, resolveHost, noop, bindOnce, collectHosts, closestSafe } from './internal.js';
2
2
 
3
+ const localeOf = (el) => {
4
+ const locale =
5
+ closestSafe(el, '[lang]')?.getAttribute('lang')?.trim() ||
6
+ el?.ownerDocument?.documentElement?.getAttribute('lang')?.trim();
7
+ return locale || undefined;
8
+ };
9
+
10
+ const textComparatorFor = (el) => {
11
+ const options = { sensitivity: 'base', numeric: false };
12
+ if (typeof Intl === 'undefined' || typeof Intl.Collator !== 'function') {
13
+ return (a, b) => a.localeCompare(b);
14
+ }
15
+ try {
16
+ return new Intl.Collator(localeOf(el), options).compare;
17
+ } catch {
18
+ return new Intl.Collator(undefined, options).compare;
19
+ }
20
+ };
21
+
3
22
  /**
4
23
  * Client-side sortable + selectable data table. Wires
5
24
  * `[data-bronto-sortable]`:
@@ -71,6 +90,8 @@ export function initTableSort({ root } = {}) {
71
90
  const rows = [];
72
91
  const checkboxStates = new WeakMap();
73
92
  const checkboxes = [];
93
+ const misplacedSorters = new WeakSet();
94
+ const compareText = textComparatorFor(table);
74
95
 
75
96
  const rememberHeaderState = (th) => {
76
97
  if (!th || headerStates.has(th)) return;
@@ -134,6 +155,15 @@ export function initTableSort({ root } = {}) {
134
155
 
135
156
  const colIndex = (th) => [...th.parentElement.children].indexOf(th);
136
157
  const cellText = (row, i) => row.children[i]?.textContent.trim() ?? '';
158
+ const warnMisplacedSorter = (sorter) => {
159
+ if (misplacedSorters.has(sorter)) return;
160
+ misplacedSorters.add(sorter);
161
+ if (typeof console !== 'undefined') {
162
+ console.warn(
163
+ '[bronto] initTableSort(): .ui-table__sort must be placed inside a <th>; ignoring this sorter.',
164
+ );
165
+ }
166
+ };
137
167
  // Numeric value of a cell for sorting. A `data-sort-value` attribute is the
138
168
  // authoritative escape hatch; otherwise normalize the display text so the
139
169
  // sign survives (U+2212 / en-em dashes → minus, accounting parens →
@@ -143,7 +173,7 @@ export function initTableSort({ root } = {}) {
143
173
  const cell = row.children[i];
144
174
  const explicit = cell?.getAttribute?.('data-sort-value');
145
175
  if (explicit != null && explicit.trim() !== '') {
146
- const raw = explicit.trim();
176
+ const raw = explicit.trim().replace(/[−–—]/g, '-');
147
177
  let v = Number(raw);
148
178
  // The escape hatch must actually handle the case the doc names it for: a
149
179
  // European decimal comma. `Number("3,5")` is NaN, which silently fell
@@ -184,15 +214,18 @@ export function initTableSort({ root } = {}) {
184
214
  // or after a sort they float above the real rows.
185
215
  const emptyRows = [...tbody.rows].filter((r) => r.classList.contains('ui-table__empty'));
186
216
  const rows = [...tbody.rows].filter((r) => !r.classList.contains('ui-table__empty'));
187
- rows.sort((a, b) => {
188
- const cmp = numeric
189
- ? cellNum(a, i) - cellNum(b, i)
190
- : cellText(a, i).localeCompare(cellText(b, i));
191
- return cmp * sign;
217
+ const keyedRows = rows.map((row, index) => ({
218
+ row,
219
+ index,
220
+ key: numeric ? cellNum(row, i) : cellText(row, i),
221
+ }));
222
+ keyedRows.sort((a, b) => {
223
+ const cmp = numeric ? a.key - b.key : compareText(a.key, b.key);
224
+ return cmp === 0 ? a.index - b.index : cmp * sign;
192
225
  });
193
226
  // Re-parent in document order: sorted data rows, then any empty/sentinel
194
227
  // row last. These are existing <tr> nodes being moved; no markup is parsed.
195
- tbody.append(...rows, ...emptyRows);
228
+ tbody.append(...keyedRows.map(({ row }) => row), ...emptyRows);
196
229
  };
197
230
 
198
231
  const allBox = table.querySelector('[data-bronto-select-all]');
@@ -227,6 +260,10 @@ export function initTableSort({ root } = {}) {
227
260
  if (sorter && table.contains(sorter)) {
228
261
  rememberSorterState(sorter);
229
262
  const th = sorter.closest('th');
263
+ if (!th) {
264
+ warnMisplacedSorter(sorter);
265
+ return;
266
+ }
230
267
  const numeric =
231
268
  (sorter.getAttribute('data-sort') || th.getAttribute('data-sort')) === 'num' ||
232
269
  th.classList.contains('is-num');
@@ -1 +1 @@
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"}
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,CAoL3C"}
package/behaviors/tabs.js CHANGED
@@ -57,6 +57,17 @@ export function initTabs({ root } = {}) {
57
57
  const panels = [...group.querySelectorAll('.ui-tabs__panel')].filter(owned);
58
58
  if (!tabs.length) continue;
59
59
  const list = [...group.querySelectorAll('.ui-tabs__list')].find(owned);
60
+ const isNativeDisabled = (tab) => {
61
+ try {
62
+ if (tab.matches?.(':disabled')) return true;
63
+ } catch {
64
+ /* fall through to the native property */
65
+ }
66
+ return Boolean('disabled' in tab && tab.disabled);
67
+ };
68
+ const isReachable = (tab) => !tab.hidden && !isNativeDisabled(tab);
69
+ const isAriaDisabled = (tab) => tab.getAttribute('aria-disabled') === 'true';
70
+ const reachableTabs = () => tabs.filter(isReachable);
60
71
  const rememberState = () => ({
61
72
  list: list ? snapshotAttrs(list, ['role']) : null,
62
73
  tabs: new Map(
@@ -94,17 +105,31 @@ export function initTabs({ root } = {}) {
94
105
  }
95
106
  };
96
107
 
97
- const select = (tab) => {
108
+ let selectedTab = null;
109
+ const syncTabs = (selected, tabStop) => {
98
110
  for (const t of tabs) {
99
- const on = t === tab;
111
+ const on = t === selected;
100
112
  t.classList.toggle('is-active', on);
101
113
  t.setAttribute('role', 'tab');
102
114
  t.setAttribute('aria-selected', String(on));
103
- t.tabIndex = on ? 0 : -1;
115
+ t.tabIndex = t === tabStop ? 0 : -1;
104
116
  }
117
+ };
118
+ const moveTabStop = (tab) => {
119
+ const candidates = reachableTabs();
120
+ const next = candidates.includes(tab) ? tab : candidates[0] || null;
121
+ if (!next) return false;
122
+ syncTabs(selectedTab, next);
123
+ return true;
124
+ };
125
+ const select = (tab) => {
126
+ const candidates = reachableTabs();
127
+ if (!candidates.includes(tab) || isAriaDisabled(tab)) return false;
128
+ selectedTab = tab;
129
+ syncTabs(tab, tab);
105
130
  // Only retarget panels when this tab actually controls one. A panel-less
106
131
  // tab must not hide every panel; leave the prior panel visible.
107
- if (!panels.some((p) => p.dataset.panel === tab.dataset.tab)) return;
132
+ if (!panels.some((p) => p.dataset.panel === tab.dataset.tab)) return true;
108
133
  for (const p of panels) {
109
134
  p.setAttribute('role', 'tabpanel');
110
135
  const shown = p.dataset.panel === tab.dataset.tab;
@@ -114,6 +139,7 @@ export function initTabs({ root } = {}) {
114
139
  if (shown) p.tabIndex = 0;
115
140
  else p.removeAttribute('tabindex');
116
141
  }
142
+ return true;
117
143
  };
118
144
  const onClick = (e) => {
119
145
  // `tabs` is filtered to this group, so membership (not mere DOM
@@ -121,23 +147,28 @@ export function initTabs({ root } = {}) {
121
147
  const tab = closestSafe(e.target, '.ui-tab');
122
148
  if (tab && tabs.includes(tab)) {
123
149
  e.preventDefault();
124
- select(tab);
125
- tab.focus();
150
+ const handled = select(tab) || moveTabStop(tab);
151
+ if (handled) tab.focus();
126
152
  }
127
153
  };
128
154
  const onKey = (e) => {
129
- const i = tabs.indexOf(closestSafe(e.target, '.ui-tab'));
155
+ const candidates = reachableTabs();
156
+ const i = candidates.indexOf(closestSafe(e.target, '.ui-tab'));
130
157
  if (i < 0) return;
158
+ const orientation =
159
+ list?.getAttribute('aria-orientation') === 'vertical' ? 'vertical' : 'horizontal';
160
+ const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
161
+ const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
131
162
  let n = i;
132
- if (e.key === 'ArrowRight' || e.key === 'ArrowDown') n = (i + 1) % tabs.length;
133
- else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp')
134
- n = (i - 1 + tabs.length) % tabs.length;
163
+ if (e.key === nextKey) n = (i + 1) % candidates.length;
164
+ else if (e.key === prevKey) n = (i - 1 + candidates.length) % candidates.length;
135
165
  else if (e.key === 'Home') n = 0;
136
- else if (e.key === 'End') n = tabs.length - 1;
166
+ else if (e.key === 'End') n = candidates.length - 1;
137
167
  else return;
138
168
  e.preventDefault();
139
- select(tabs[n]);
140
- tabs[n].focus();
169
+ const next = candidates[n];
170
+ if (!select(next)) moveTabStop(next);
171
+ next.focus();
141
172
  };
142
173
  cleanups.push(
143
174
  bindOnce(group, 'tabs', () => {
@@ -155,7 +186,13 @@ export function initTabs({ root } = {}) {
155
186
  t.setAttribute('aria-controls', p.id);
156
187
  p.setAttribute('aria-labelledby', t.id);
157
188
  }
158
- select(tabs.find((t) => t.classList.contains('is-active')) || tabs[0]);
189
+ const candidates = reachableTabs();
190
+ const initial =
191
+ candidates.find((t) => t.classList.contains('is-active') && !isAriaDisabled(t)) ||
192
+ candidates.find((t) => !isAriaDisabled(t)) ||
193
+ candidates[0];
194
+ if (initial && !select(initial)) moveTabStop(initial);
195
+ else if (!initial) syncTabs(null, null);
159
196
  group.addEventListener('click', onClick);
160
197
  group.addEventListener('keydown', onKey);
161
198
  return () => {
@@ -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,CAuE3C;;;;;;;;;;6BAhHY,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":"AAuBA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wDAHW,cAAc,GACZ,IAAI,CAahB;AAED;;;;;;;;;;;;;;GAcG;AACH,uDAHW,gBAAgB,GAAG,OAAO,eAAe,EAAE,YAAY,GACrD,OAAO,eAAe,EAAE,OAAO,CAgH3C;;;;;;;;;;6BAzJY,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;WAIpC,OAAO,GAAG,MAAM"}
@@ -1,6 +1,25 @@
1
1
  import { hasDom, resolveHost, noop, bindOnce, collectHosts, closestSafe } from './internal.js';
2
2
 
3
3
  const THEMES = ['light', 'dark'];
4
+ const DARK_SCHEME_QUERY = '(prefers-color-scheme: dark)';
5
+
6
+ const colorSchemeQuery = () =>
7
+ typeof matchMedia === 'function' ? matchMedia(DARK_SCHEME_QUERY) : null;
8
+
9
+ function onColorSchemeChange(query, listener) {
10
+ if (
11
+ typeof query?.addEventListener === 'function' &&
12
+ typeof query.removeEventListener === 'function'
13
+ ) {
14
+ query.addEventListener('change', listener);
15
+ return () => query.removeEventListener('change', listener);
16
+ }
17
+ if (typeof query?.addListener === 'function' && typeof query.removeListener === 'function') {
18
+ query.addListener(listener);
19
+ return () => query.removeListener(listener);
20
+ }
21
+ return null;
22
+ }
4
23
 
5
24
  /**
6
25
  * @typedef {object} ThemeStorageOpts
@@ -66,8 +85,12 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
66
85
  }
67
86
  };
68
87
 
69
- const prefersDark = () =>
70
- typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches;
88
+ let schemeQuery = null;
89
+ let removeSchemeListener = noop;
90
+
91
+ const hasExplicitTheme = () => THEMES.includes(docEl.getAttribute('data-theme'));
92
+
93
+ const prefersDark = () => (schemeQuery || colorSchemeQuery())?.matches === true;
71
94
 
72
95
  const current = () => {
73
96
  const attr = docEl.getAttribute('data-theme');
@@ -87,6 +110,38 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
87
110
  });
88
111
  };
89
112
 
113
+ const clearSchemeListener = () => {
114
+ removeSchemeListener();
115
+ removeSchemeListener = noop;
116
+ schemeQuery = null;
117
+ };
118
+
119
+ const syncSchemeListener = () => {
120
+ if (hasExplicitTheme()) {
121
+ clearSchemeListener();
122
+ return;
123
+ }
124
+ if (schemeQuery) return;
125
+ const query = colorSchemeQuery();
126
+ const cleanup = onColorSchemeChange(query, onSchemeChange);
127
+ if (!cleanup) return;
128
+ schemeQuery = query;
129
+ removeSchemeListener = cleanup;
130
+ };
131
+
132
+ function onSchemeChange() {
133
+ if (hasExplicitTheme()) {
134
+ clearSchemeListener();
135
+ return;
136
+ }
137
+ reflect();
138
+ }
139
+
140
+ const onThemeChange = () => {
141
+ reflect();
142
+ syncSchemeListener();
143
+ };
144
+
90
145
  const onClick = (e) => {
91
146
  const trigger = closestSafe(e.target, '[data-bronto-theme-toggle]');
92
147
  if (!trigger || !host.contains(trigger)) return;
@@ -94,6 +149,7 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
94
149
  const forced = trigger.getAttribute('data-bronto-theme-toggle');
95
150
  const next = THEMES.includes(forced) ? forced : current() === 'dark' ? 'light' : 'dark';
96
151
  docEl.setAttribute('data-theme', next);
152
+ clearSchemeListener();
97
153
  try {
98
154
  localStorage.setItem(storageKey, next);
99
155
  } catch {
@@ -105,12 +161,16 @@ export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
105
161
  );
106
162
  };
107
163
 
108
- applyStoredTheme({ storageKey, root: docEl });
109
- reflect();
110
164
  return bindOnce(host, 'themeToggle', () => {
165
+ applyStoredTheme({ storageKey, root: docEl });
166
+ reflect();
167
+ syncSchemeListener();
168
+ docEl.addEventListener('bronto:themechange', onThemeChange);
111
169
  host.addEventListener('click', onClick);
112
170
  return () => {
171
+ docEl.removeEventListener('bronto:themechange', onThemeChange);
113
172
  host.removeEventListener('click', onClick);
173
+ clearSchemeListener();
114
174
  for (const [el, state] of toggleStates) {
115
175
  if (state.had) el.setAttribute('aria-pressed', state.value);
116
176
  else el.removeAttribute('aria-pressed');
@@ -5,6 +5,7 @@
5
5
  * @property {number} [duration] Auto-dismiss delay in ms. `0` keeps it until dismissed. Default: `4000`.
6
6
  * @property {boolean} [assertive] Route to the assertive live region so AT interrupts immediately. Defaults to `true` when `tone === 'danger'`.
7
7
  * @property {boolean} [closable] Render a dismiss button on the toast.
8
+ * @property {string} [dismissLabel] Accessible label for the generated dismiss button. Default: `Dismiss`.
8
9
  */
9
10
  /**
10
11
  * Push a transient toast into a shared, screen-anchored stack. The stack
@@ -23,7 +24,7 @@
23
24
  * @param {ToastOpts} [opts]
24
25
  * @returns {import('./internal.js').Cleanup}
25
26
  */
26
- export function toast(message: string, { tone, title, duration, assertive, closable }?: ToastOpts): import("./internal.js").Cleanup;
27
+ export function toast(message: string, { tone, title, duration, assertive, closable, dismissLabel }?: ToastOpts): import("./internal.js").Cleanup;
27
28
  export type ToastOpts = {
28
29
  /**
29
30
  * Status tone — maps to `ui-toast--<tone>`.
@@ -45,5 +46,9 @@ export type ToastOpts = {
45
46
  * Render a dismiss button on the toast.
46
47
  */
47
48
  closable?: boolean | undefined;
49
+ /**
50
+ * Accessible label for the generated dismiss button. Default: `Dismiss`.
51
+ */
52
+ dismissLabel?: string | undefined;
48
53
  };
49
54
  //# sourceMappingURL=toast.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["toast.js"],"names":[],"mappings":"AA2HA;;;;;;;GAOG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,+BAJW,MAAM,mDACN,SAAS,GACP,OAAO,eAAe,EAAE,OAAO,CAgD3C"}
1
+ {"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["toast.js"],"names":[],"mappings":"AAwJA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,+BAJW,MAAM,iEACN,SAAS,GACP,OAAO,eAAe,EAAE,OAAO,CAsD3C"}