@ponchia/ui 0.6.9 → 0.6.11

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 (145) hide show
  1. package/CHANGELOG.md +92 -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.map +1 -1
  34. package/behaviors/modal.js +25 -9
  35. package/behaviors/popover.d.ts.map +1 -1
  36. package/behaviors/popover.js +8 -6
  37. package/behaviors/sources.d.ts.map +1 -1
  38. package/behaviors/sources.js +24 -3
  39. package/behaviors/splitter.d.ts.map +1 -1
  40. package/behaviors/splitter.js +27 -6
  41. package/behaviors/table.d.ts.map +1 -1
  42. package/behaviors/table.js +44 -7
  43. package/behaviors/tabs.d.ts.map +1 -1
  44. package/behaviors/tabs.js +51 -14
  45. package/behaviors/theme.d.ts.map +1 -1
  46. package/behaviors/theme.js +64 -4
  47. package/behaviors/toast.d.ts +6 -1
  48. package/behaviors/toast.d.ts.map +1 -1
  49. package/behaviors/toast.js +48 -12
  50. package/classes/classes.json +57 -2
  51. package/classes/index.d.ts +13 -2
  52. package/classes/index.js +88 -40
  53. package/connectors/index.d.ts +4 -4
  54. package/connectors/index.d.ts.map +1 -1
  55. package/connectors/index.js +14 -12
  56. package/css/annotations.css +1 -0
  57. package/css/app.css +7 -0
  58. package/css/base.css +3 -0
  59. package/css/bullet.css +41 -7
  60. package/css/code.css +14 -0
  61. package/css/command.css +10 -0
  62. package/css/dataviz.css +27 -0
  63. package/css/diff.css +2 -0
  64. package/css/disclosure.css +8 -0
  65. package/css/dots.css +1 -1
  66. package/css/feedback.css +9 -0
  67. package/css/interval.css +20 -2
  68. package/css/legend.css +10 -9
  69. package/css/marks.css +1 -0
  70. package/css/motion.css +2 -0
  71. package/css/overlay.css +14 -2
  72. package/css/primitives.css +1 -1
  73. package/css/report.css +3 -0
  74. package/css/sources.css +4 -4
  75. package/css/spotlight.css +6 -0
  76. package/css/table.css +19 -0
  77. package/css/term.css +4 -1
  78. package/css/tokens.css +8 -13
  79. package/css/workbench.css +128 -0
  80. package/dist/bronto.css +1 -1
  81. package/dist/css/analytical.css +1 -1
  82. package/dist/css/app.css +1 -1
  83. package/dist/css/bullet.css +1 -1
  84. package/dist/css/code.css +1 -1
  85. package/dist/css/command.css +1 -1
  86. package/dist/css/dataviz.css +1 -1
  87. package/dist/css/diff.css +1 -1
  88. package/dist/css/disclosure.css +1 -1
  89. package/dist/css/dots.css +1 -1
  90. package/dist/css/feedback.css +1 -1
  91. package/dist/css/interval.css +1 -1
  92. package/dist/css/legend.css +1 -1
  93. package/dist/css/marks.css +1 -1
  94. package/dist/css/overlay.css +1 -1
  95. package/dist/css/primitives.css +1 -1
  96. package/dist/css/report-kit.css +1 -1
  97. package/dist/css/sources.css +1 -1
  98. package/dist/css/spotlight.css +1 -1
  99. package/dist/css/table.css +1 -1
  100. package/dist/css/term.css +1 -1
  101. package/dist/css/tokens.css +1 -1
  102. package/dist/css/workbench.css +1 -1
  103. package/docs/annotations.md +27 -0
  104. package/docs/architecture.md +5 -3
  105. package/docs/bullet.md +6 -1
  106. package/docs/clamp.md +5 -0
  107. package/docs/command.md +3 -2
  108. package/docs/contrast.md +3 -3
  109. package/docs/crosshair.md +6 -0
  110. package/docs/dots.md +10 -3
  111. package/docs/figure.md +7 -0
  112. package/docs/glyphs.md +14 -2
  113. package/docs/highlights.md +9 -0
  114. package/docs/interval.md +6 -0
  115. package/docs/mermaid.md +5 -3
  116. package/docs/package-contract.md +24 -1
  117. package/docs/reference.md +21 -1
  118. package/docs/reporting.md +8 -8
  119. package/docs/selection.md +9 -0
  120. package/docs/sources.md +5 -0
  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 +2 -2
  127. package/docs/vega.md +5 -3
  128. package/docs/workbench.md +56 -9
  129. package/glyphs/glyphs.js +62 -8
  130. package/index.d.ts +1 -0
  131. package/llms.txt +18 -14
  132. package/package.json +98 -6
  133. package/qwik/index.d.ts +4 -3
  134. package/qwik/index.d.ts.map +1 -1
  135. package/qwik/index.js +7 -5
  136. package/react/index.d.ts +4 -3
  137. package/react/index.d.ts.map +1 -1
  138. package/react/index.js +3 -2
  139. package/solid/index.d.ts +7 -5
  140. package/solid/index.d.ts.map +1 -1
  141. package/solid/index.js +11 -7
  142. package/tokens/vega.d.ts +1 -1
  143. package/tokens/vega.js +3 -2
  144. package/vue/index.d.ts.map +1 -1
  145. package/vue/index.js +37 -3
@@ -30,11 +30,62 @@ function rememberCleanup(el, cleanups, cleanup) {
30
30
  cleanups.push(wrapped);
31
31
  }
32
32
 
33
- // `dot`/`gap`/`size` land in inline CSS, so allow only length/calc syntax —
34
- // drop anything with a `;`/`{` that could open a second declaration (mirrors
35
- // glyphs.js cssLen). Used for the mask path's --icon-size.
33
+ const CSS_LENGTH_UNITS =
34
+ '(?:px|r?em|ch|ex|cap|ic|lh|rlh|vw|vh|vi|vb|vmin|vmax|' +
35
+ 'svw|svh|svi|svb|svmin|svmax|lvw|lvh|lvi|lvb|lvmin|lvmax|' +
36
+ 'dvw|dvh|dvi|dvb|dvmin|dvmax|cm|mm|q|in|pc|pt|%)';
37
+ const CSS_NUMBER = '[-+]?(?:\\d*\\.\\d+|\\d+)';
38
+ const CSS_LENGTH_RE = new RegExp(`^(?:0|${CSS_NUMBER}${CSS_LENGTH_UNITS})$`, 'i');
39
+ const CSS_CALC_VALUE_RE = new RegExp(`^${CSS_NUMBER}(?:${CSS_LENGTH_UNITS})?`, 'i');
40
+
41
+ function isCalcLength(expr) {
42
+ let rest = expr.trim();
43
+ let depth = 0;
44
+ let sawLength = false;
45
+ let expectValue = true;
46
+
47
+ while (rest) {
48
+ if (expectValue) {
49
+ if (rest[0] === '(') {
50
+ depth += 1;
51
+ rest = rest.slice(1).trim();
52
+ continue;
53
+ }
54
+ const token = rest.match(CSS_CALC_VALUE_RE)?.[0];
55
+ if (!token) return false;
56
+ if (CSS_LENGTH_RE.test(token)) sawLength = true;
57
+ rest = rest.slice(token.length).trim();
58
+ expectValue = false;
59
+ continue;
60
+ }
61
+
62
+ if (rest[0] === ')') {
63
+ depth -= 1;
64
+ if (depth < 0) return false;
65
+ rest = rest.slice(1).trim();
66
+ continue;
67
+ }
68
+
69
+ const op = rest.match(/^[+\-*/]/)?.[0];
70
+ if (!op) return false;
71
+ rest = rest.slice(op.length).trim();
72
+ expectValue = true;
73
+ }
74
+
75
+ if (!sawLength) return false;
76
+ if (expectValue) return false;
77
+ return depth === 0;
78
+ }
79
+
80
+ // `size` lands in inline CSS, so accept only concrete CSS lengths or calc()
81
+ // expressions (mirrors glyphs.js cssLen). Used for the mask path's --icon-size.
36
82
  function cssLen(v) {
37
- return v && /^[\w.%+\-*/()\s,]+$/.test(v) ? v : '';
83
+ const value = String(v ?? '').trim();
84
+ if (CSS_LENGTH_RE.test(value)) return value;
85
+ if (value.toLowerCase().startsWith('calc(') && value.endsWith(')')) {
86
+ return isCalcLength(value.slice(5, -1)) ? value : '';
87
+ }
88
+ return '';
38
89
  }
39
90
 
40
91
  function applyGlyphA11y(el, label) {
@@ -214,7 +265,7 @@ export function initDotGlyph({ root } = {}) {
214
265
  const label = el.getAttribute('data-bronto-glyph-label');
215
266
 
216
267
  // One-node mask path — the icon-at-scale counterpart to the 256-cell grid.
217
- if (el.getAttribute('data-bronto-glyph-render') === 'mask') {
268
+ if (el.getAttribute('data-bronto-glyph-render')?.trim() === 'mask') {
218
269
  expandMaskGlyph(el, name, label, cleanups);
219
270
  continue;
220
271
  }
@@ -1 +1 @@
1
- {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AAiDA,iEAIC;AAeD,sEAUC;AAED,oDAQC;AAED,yDAOC;AAMD,8DAIC;AAID;;SAMC;AAUD,gDAQC;AAMD,+DAKC;AAlIM,6BAAqB;AAErB,kCAAoD;AAyCpD,uCAAqC;;;;;sBArD/B,MAAM,IAAI"}
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["internal.js"],"names":[],"mappings":"AAiDA,iEAIC;AAeD,sEAaC;AAED,oDAUC;AAED,yDAOC;AAMD,8DAIC;AAID;;SAMC;AAkDD,gDAUC;AAMD,+DAKC;AAjLM,6BAAqB;AAErB,kCAAoD;AAyCpD,uCAAqC;;;;;sBArD/B,MAAM,IAAI"}
@@ -70,7 +70,10 @@ export function bindOnce(target, key, add) {
70
70
  const reg = target[BOUND] || (target[BOUND] = Object.create(null));
71
71
  if (reg[key]) reg[key]();
72
72
  const remove = add();
73
+ let done = false;
73
74
  const cleanup = () => {
75
+ if (done) return;
76
+ done = true;
74
77
  remove();
75
78
  if (reg[key] === cleanup) delete reg[key];
76
79
  };
@@ -80,11 +83,13 @@ export function bindOnce(target, key, add) {
80
83
 
81
84
  export function byIdInHost(host, id) {
82
85
  if (!id) return null;
83
- if (host === document) return document.getElementById(id);
86
+ const doc = host.nodeType === 9 ? host : host.ownerDocument;
87
+ if (host === doc) return doc.getElementById(id);
84
88
  if (host.id === id) return host;
85
89
  return (
86
90
  Array.from(host.querySelectorAll?.('[id]') || []).find((el) => el.id === id) ||
87
- document.getElementById(id)
91
+ doc?.getElementById(id) ||
92
+ null
88
93
  );
89
94
  }
90
95
 
@@ -122,11 +127,53 @@ export function scrollIntoViewSafe(el, opts = { block: 'nearest' }) {
122
127
  // itself on open). Focus the first focusable descendant, else make the
123
128
  // container programmatically focusable and focus it, so a content-only
124
129
  // panel/modal still receives focus.
125
- const FOCUSABLE =
126
- 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
130
+ const FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]';
131
+
132
+ function isInsideDisabledFieldset(el) {
133
+ return Boolean(el.closest?.('fieldset[disabled]'));
134
+ }
135
+
136
+ function isRendered(el) {
137
+ try {
138
+ if (
139
+ typeof el.checkVisibility === 'function' &&
140
+ !el.checkVisibility({ checkVisibilityCSS: true, visibilityProperty: true })
141
+ ) {
142
+ return false;
143
+ }
144
+ } catch {
145
+ /* fall through to conservative checks */
146
+ }
147
+
148
+ const view = el.ownerDocument?.defaultView;
149
+ const style = view?.getComputedStyle?.(el);
150
+ if (style?.display === 'none') return false;
151
+ if (style?.visibility === 'hidden' || style?.visibility === 'collapse') return false;
152
+
153
+ const docHasLayout = Boolean(el.ownerDocument?.documentElement?.getClientRects?.().length);
154
+ if (
155
+ docHasLayout &&
156
+ 'offsetParent' in el &&
157
+ el.offsetParent === null &&
158
+ el.getClientRects?.().length === 0
159
+ ) {
160
+ return false;
161
+ }
162
+ return true;
163
+ }
164
+
165
+ function isFocusableCandidate(el) {
166
+ if (el.closest?.('[hidden], [inert]')) return false;
167
+ if ('disabled' in el && el.disabled) return false;
168
+ if (isInsideDisabledFieldset(el)) return false;
169
+ if (el.tabIndex < 0) return false;
170
+ return isRendered(el);
171
+ }
127
172
 
128
173
  export function focusInto(container) {
129
- const first = container.querySelector(FOCUSABLE);
174
+ const first = Array.from(container.querySelectorAll?.(FOCUSABLE) || []).find(
175
+ isFocusableCandidate,
176
+ );
130
177
  if (first) {
131
178
  first.focus?.();
132
179
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["menu.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAwC3C"}
1
+ {"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["menu.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAyC3C"}
package/behaviors/menu.js CHANGED
@@ -18,7 +18,8 @@ export function initMenu({ root } = {}) {
18
18
  if (!hasDom()) return noop;
19
19
  const host = resolveHost(root);
20
20
  if (!host) return noop;
21
- const doc = host.nodeType === 9 ? host : host.ownerDocument || document;
21
+ const doc = host.nodeType === 9 ? host : host.ownerDocument;
22
+ if (!doc) return noop;
22
23
  const openMenus = () => collectHosts(host, '[data-bronto-menu][open]');
23
24
  const shut = (menu) => {
24
25
  if (!menu || !menu.open) return;
@@ -1 +1 @@
1
- {"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"AAsDA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA+F3C;;;;;YA7Ha,QAAQ"}
1
+ {"version":3,"file":"modal.d.ts","sourceRoot":"","sources":["modal.js"],"names":[],"mappings":"AAsDA;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA+G3C;;;;;YA7Ia,QAAQ"}
@@ -94,6 +94,10 @@ export function initModal({ root } = {}) {
94
94
  const cleanups = [];
95
95
 
96
96
  for (const modal of modals) {
97
+ const doc = modal.ownerDocument;
98
+ if (!doc) continue;
99
+ const view = doc.defaultView;
100
+ const isNativeDialog = modal.localName === 'dialog';
97
101
  let opener = null;
98
102
  let inerted = [];
99
103
 
@@ -103,10 +107,10 @@ export function initModal({ root } = {}) {
103
107
  // app inerted for its own reasons.
104
108
  const trap = () => {
105
109
  if (opener) return; // already trapped
106
- opener = document.activeElement;
110
+ opener = doc.activeElement;
107
111
  pushActiveModal(modal);
108
112
  let el = modal;
109
- while (el && el.parentElement && el !== document.body) {
113
+ while (el && el.parentElement && el !== doc.body) {
110
114
  for (const sib of el.parentElement.children) {
111
115
  if (sib !== el && !sib.inert) {
112
116
  sib.inert = true;
@@ -128,14 +132,23 @@ export function initModal({ root } = {}) {
128
132
  if (back?.isConnected && typeof back.focus === 'function') back.focus();
129
133
  };
130
134
 
131
- const sync = () => (modal.classList.contains('is-open') ? trap() : release());
135
+ const sync = () => {
136
+ if (modal.classList.contains('is-open')) {
137
+ if (!isNativeDialog) modal.hidden = false;
138
+ trap();
139
+ return;
140
+ }
141
+ release();
142
+ if (!isNativeDialog) modal.hidden = true;
143
+ };
132
144
 
133
145
  const onKey = (e) => {
134
146
  if (e.key === 'Escape' && opener) {
135
147
  if (activeModals.at(-1) !== modal) return;
136
148
  if (insideOpenPopover(e.target, modal)) return;
149
+ const ModalCloseEvent = view?.CustomEvent ?? CustomEvent;
137
150
  modal.dispatchEvent(
138
- new CustomEvent('bronto:modal:close', {
151
+ new ModalCloseEvent('bronto:modal:close', {
139
152
  detail: { reason: 'escape' },
140
153
  bubbles: true,
141
154
  cancelable: true,
@@ -146,7 +159,7 @@ export function initModal({ root } = {}) {
146
159
 
147
160
  cleanups.push(
148
161
  bindOnce(modal, 'modal', () => {
149
- const attrs = snapshotAttrs(modal, ['role', 'aria-modal', 'tabindex']);
162
+ const attrs = snapshotAttrs(modal, ['role', 'aria-modal', 'tabindex', 'hidden']);
150
163
 
151
164
  // A controlled modal must announce AS a modal dialog, not a generic group —
152
165
  // parity with initPopover. Apply a dialog role + aria-modal (unless the
@@ -164,13 +177,16 @@ export function initModal({ root } = {}) {
164
177
  );
165
178
  }
166
179
 
167
- const observer = typeof MutationObserver === 'function' ? new MutationObserver(sync) : null;
180
+ const Observer =
181
+ view?.MutationObserver ??
182
+ (typeof MutationObserver === 'function' ? MutationObserver : null);
183
+ const observer = Observer ? new Observer(sync) : null;
168
184
  observer?.observe(modal, { attributes: true, attributeFilter: ['class'] });
169
- document.addEventListener('keydown', onKey, true);
170
- if (modal.classList.contains('is-open')) trap(); // already open at init
185
+ doc.addEventListener('keydown', onKey, true);
186
+ sync();
171
187
  return () => {
172
188
  observer?.disconnect();
173
- document.removeEventListener('keydown', onKey, true);
189
+ doc.removeEventListener('keydown', onKey, true);
174
190
  release();
175
191
  restoreAttrs(modal, attrs);
176
192
  };
@@ -1 +1 @@
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
+ {"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,CAoM3C"}
@@ -68,7 +68,9 @@ 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;
@@ -127,7 +129,7 @@ export function initPopover({ root } = {}) {
127
129
  // Only steal focus back to the trigger when focus is still inside the panel
128
130
  // (Escape / programmatic re-toggle). An outside-click leaves focus where the
129
131
  // click landed — deliberate intent to move on, per the doc contract.
130
- const focusWasInside = panel.contains(document.activeElement);
132
+ const focusWasInside = panel.contains(doc.activeElement);
131
133
  openPanel = openTrigger = null;
132
134
  if (panel.hasAttribute('popover') && typeof panel.hidePopover === 'function') {
133
135
  try {
@@ -235,8 +237,8 @@ export function initPopover({ root } = {}) {
235
237
 
236
238
  return bindOnce(host, 'popover', () => {
237
239
  seed();
238
- document.addEventListener('click', onClick);
239
- document.addEventListener('keydown', onKey);
240
+ doc.addEventListener('click', onClick);
241
+ doc.addEventListener('keydown', onKey);
240
242
  view?.addEventListener('scroll', onReflow, true);
241
243
  view?.addEventListener('resize', onReflow);
242
244
  return () => {
@@ -250,8 +252,8 @@ export function initPopover({ root } = {}) {
250
252
  restoreStyle(panel, state.style);
251
253
  }
252
254
  panelStates.clear();
253
- document.removeEventListener('click', onClick);
254
- document.removeEventListener('keydown', onKey);
255
+ doc.removeEventListener('click', onClick);
256
+ doc.removeEventListener('keydown', onKey);
255
257
  view?.removeEventListener('scroll', onReflow, true);
256
258
  view?.removeEventListener('resize', onReflow);
257
259
  };
@@ -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 () => {