@ponchia/ui 0.6.10 → 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 (140) hide show
  1. package/CHANGELOG.md +72 -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 +24 -0
  51. package/classes/index.d.ts +3 -2
  52. package/classes/index.js +77 -39
  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/dist/bronto.css +1 -1
  80. package/dist/css/analytical.css +1 -1
  81. package/dist/css/app.css +1 -1
  82. package/dist/css/bullet.css +1 -1
  83. package/dist/css/code.css +1 -1
  84. package/dist/css/command.css +1 -1
  85. package/dist/css/dataviz.css +1 -1
  86. package/dist/css/diff.css +1 -1
  87. package/dist/css/disclosure.css +1 -1
  88. package/dist/css/dots.css +1 -1
  89. package/dist/css/feedback.css +1 -1
  90. package/dist/css/interval.css +1 -1
  91. package/dist/css/legend.css +1 -1
  92. package/dist/css/marks.css +1 -1
  93. package/dist/css/overlay.css +1 -1
  94. package/dist/css/primitives.css +1 -1
  95. package/dist/css/report-kit.css +1 -1
  96. package/dist/css/sources.css +1 -1
  97. package/dist/css/spotlight.css +1 -1
  98. package/dist/css/table.css +1 -1
  99. package/dist/css/term.css +1 -1
  100. package/dist/css/tokens.css +1 -1
  101. package/docs/architecture.md +5 -3
  102. package/docs/bullet.md +6 -1
  103. package/docs/clamp.md +5 -0
  104. package/docs/command.md +3 -2
  105. package/docs/contrast.md +3 -3
  106. package/docs/crosshair.md +6 -0
  107. package/docs/dots.md +10 -3
  108. package/docs/figure.md +7 -0
  109. package/docs/glyphs.md +14 -2
  110. package/docs/highlights.md +9 -0
  111. package/docs/interval.md +6 -0
  112. package/docs/mermaid.md +5 -3
  113. package/docs/package-contract.md +24 -1
  114. package/docs/reporting.md +8 -8
  115. package/docs/selection.md +9 -0
  116. package/docs/sources.md +5 -0
  117. package/docs/state.md +6 -0
  118. package/docs/textref.md +18 -13
  119. package/docs/theming.md +18 -8
  120. package/docs/toc.md +6 -0
  121. package/docs/tree.md +9 -2
  122. package/docs/usage.md +2 -2
  123. package/docs/vega.md +5 -3
  124. package/glyphs/glyphs.js +62 -8
  125. package/index.d.ts +1 -0
  126. package/llms.txt +18 -14
  127. package/package.json +98 -6
  128. package/qwik/index.d.ts +4 -3
  129. package/qwik/index.d.ts.map +1 -1
  130. package/qwik/index.js +7 -5
  131. package/react/index.d.ts +4 -3
  132. package/react/index.d.ts.map +1 -1
  133. package/react/index.js +3 -2
  134. package/solid/index.d.ts +7 -5
  135. package/solid/index.d.ts.map +1 -1
  136. package/solid/index.js +11 -7
  137. package/tokens/vega.d.ts +1 -1
  138. package/tokens/vega.js +3 -2
  139. package/vue/index.d.ts.map +1 -1
  140. package/vue/index.js +37 -3
@@ -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"}
@@ -27,10 +27,10 @@ function toastStack(isAssertive) {
27
27
  stack.setAttribute('aria-live', isAssertive ? 'assertive' : 'polite');
28
28
  if (isAssertive) {
29
29
  stack.setAttribute('role', 'alert');
30
- // The assertive region carries one error at a time and must be read whole;
31
- // aria-atomic ensures the full toast (title + message) announces, not just
32
- // the changed fragment.
33
- stack.setAttribute('aria-atomic', 'true');
30
+ // The assertive stack may hold more than one sticky danger toast. Keep the
31
+ // container non-atomic so adding a new toast announces that toast only,
32
+ // while the per-toast aria-atomic below still reads its title + message.
33
+ stack.setAttribute('aria-atomic', 'false');
34
34
  }
35
35
  document.body.appendChild(stack);
36
36
  }
@@ -102,25 +102,54 @@ function removeToast(el) {
102
102
  }
103
103
  el.classList.add('is-leaving');
104
104
  let done = false;
105
+ let timer;
105
106
  const finish = () => {
106
107
  if (done) return;
107
108
  done = true;
109
+ el.removeEventListener('transitionend', finish);
110
+ if (timer !== undefined) clearTimeout(timer);
108
111
  el.remove();
109
112
  };
110
- el.addEventListener('transitionend', finish, { once: true });
111
- const timer = setTimeout(finish, dur * 1000 + 120);
113
+ el.addEventListener('transitionend', finish);
114
+ timer = setTimeout(finish, dur * 1000 + 120);
112
115
  timer?.unref?.(); // don't keep a Node test process alive
113
116
  }
114
117
 
115
- function addToastClose(el, dismiss) {
118
+ function addToastClose(el, dismiss, label = 'Dismiss') {
116
119
  const close = document.createElement('button');
117
120
  close.type = 'button';
118
121
  close.className = 'ui-toast__close';
119
- close.setAttribute('aria-label', 'Dismiss');
122
+ close.setAttribute('aria-label', label);
120
123
  close.addEventListener('click', dismiss);
121
124
  el.appendChild(close);
122
125
  }
123
126
 
127
+ function normalizeToastDuration(duration) {
128
+ let value;
129
+ try {
130
+ value = Number(duration);
131
+ } catch {
132
+ value = NaN;
133
+ }
134
+ if (Number.isFinite(value) && value >= 0) return value;
135
+ if (typeof console !== 'undefined') {
136
+ console.warn(
137
+ '[bronto] toast(): duration must be a finite non-negative number. Rendering a sticky, closable toast instead.',
138
+ );
139
+ }
140
+ return 0;
141
+ }
142
+
143
+ function toastDismissLabel(stack, dismissLabel) {
144
+ return (
145
+ (typeof dismissLabel === 'string' && dismissLabel.trim()) ||
146
+ stack.getAttribute('data-bronto-toast-dismiss-label')?.trim() ||
147
+ document.body?.getAttribute('data-bronto-toast-dismiss-label')?.trim() ||
148
+ document.documentElement?.getAttribute('data-bronto-toast-dismiss-label')?.trim() ||
149
+ 'Dismiss'
150
+ );
151
+ }
152
+
124
153
  /**
125
154
  * @typedef {object} ToastOpts
126
155
  * @property {'accent' | 'success' | 'warning' | 'danger' | 'info'} [tone] Status tone — maps to `ui-toast--<tone>`.
@@ -128,6 +157,7 @@ function addToastClose(el, dismiss) {
128
157
  * @property {number} [duration] Auto-dismiss delay in ms. `0` keeps it until dismissed. Default: `4000`.
129
158
  * @property {boolean} [assertive] Route to the assertive live region so AT interrupts immediately. Defaults to `true` when `tone === 'danger'`.
130
159
  * @property {boolean} [closable] Render a dismiss button on the toast.
160
+ * @property {string} [dismissLabel] Accessible label for the generated dismiss button. Default: `Dismiss`.
131
161
  */
132
162
 
133
163
  /**
@@ -147,7 +177,10 @@ function addToastClose(el, dismiss) {
147
177
  * @param {ToastOpts} [opts]
148
178
  * @returns {import('./internal.js').Cleanup}
149
179
  */
150
- export function toast(message, { tone, title, duration = 4000, assertive, closable } = {}) {
180
+ export function toast(
181
+ message,
182
+ { tone, title, duration = 4000, assertive, closable, dismissLabel } = {},
183
+ ) {
151
184
  if (!hasDom()) return noop;
152
185
  // Errors must interrupt: danger toasts (or an explicit `assertive`)
153
186
  // go to a SEPARATE assertive region so they announce immediately,
@@ -157,6 +190,7 @@ export function toast(message, { tone, title, duration = 4000, assertive, closab
157
190
  const isAssertive = assertive ?? tone === 'danger';
158
191
  const { stack, fresh: freshStack } = toastStack(isAssertive);
159
192
  const el = toastElement(message, { tone, title });
193
+ const normalizedDuration = normalizeToastDuration(duration);
160
194
  // Append after a frame the *first* time so the empty live region is
161
195
  // observed by AT before its first child arrives; once the region has
162
196
  // been observed, later toasts append synchronously.
@@ -185,12 +219,14 @@ export function toast(message, { tone, title, duration = 4000, assertive, closab
185
219
  // Explicitly opting OUT of the close button on a sticky toast strands it with
186
220
  // no in-UI dismissal; warn that the caller must retain and call the returned
187
221
  // dismiss function.
188
- if (duration === 0 && closable === false && typeof console !== 'undefined') {
222
+ if (normalizedDuration === 0 && closable === false && typeof console !== 'undefined') {
189
223
  console.warn(
190
224
  '[bronto] toast(): duration:0 + closable:false has no in-UI dismissal — keep the returned dismiss() and call it yourself, or set closable:true.',
191
225
  );
192
226
  }
193
- if (closable ?? duration === 0) addToastClose(el, dismiss);
194
- if (duration > 0) timer = setTimeout(dismiss, duration);
227
+ if (closable ?? normalizedDuration === 0) {
228
+ addToastClose(el, dismiss, toastDismissLabel(stack, dismissLabel));
229
+ }
230
+ if (normalizedDuration > 0) timer = setTimeout(dismiss, normalizedDuration);
195
231
  return dismiss;
196
232
  }
@@ -2754,6 +2754,30 @@
2754
2754
  "behavior": "initCommand",
2755
2755
  "note": "wires the filter input + active-option keyboard model"
2756
2756
  },
2757
+ {
2758
+ "name": "data-bronto-carousel-roledescription",
2759
+ "on": "a .ui-carousel host",
2760
+ "behavior": "initCarousel",
2761
+ "note": "localized override for the carousel aria-roledescription; the behavior applies its English default only when this attribute and any authored aria-roledescription are absent."
2762
+ },
2763
+ {
2764
+ "name": "data-bronto-carousel-slide-roledescription",
2765
+ "on": "a .ui-carousel host (applies to each slide)",
2766
+ "behavior": "initCarousel",
2767
+ "note": "localized override for the per-slide aria-roledescription; an authored slide aria-roledescription is preserved and the English \"slide\" default applies only when both are absent."
2768
+ },
2769
+ {
2770
+ "name": "data-bronto-error-summary-title",
2771
+ "on": "a form/error-summary host enhanced by initFormValidation",
2772
+ "behavior": "initFormValidation",
2773
+ "note": "localized heading for the generated validation error summary; an authored title or this attribute is preserved, English is only the fallback."
2774
+ },
2775
+ {
2776
+ "name": "data-bronto-toast-dismiss-label",
2777
+ "on": "the toast host (or via toast() options)",
2778
+ "behavior": "toast",
2779
+ "note": "localized aria-label for the generated toast dismiss button; defaults to the English 'Dismiss' only when absent."
2780
+ },
2757
2781
  {
2758
2782
  "name": "data-bronto-sources",
2759
2783
  "on": "a source/citation island",
@@ -10,7 +10,7 @@ export type ClassValue =
10
10
  | boolean
11
11
  | null
12
12
  | undefined
13
- | ClassValue[];
13
+ | readonly ClassValue[];
14
14
 
15
15
  /** The flat registry of every class @ponchia/ui defines (literal). */
16
16
  export declare const cls: {
@@ -661,7 +661,7 @@ export declare const cls: {
661
661
  };
662
662
 
663
663
  /** classnames-style joiner: skips falsy, flattens arrays. */
664
- export declare function cx(...parts: ClassValue[]): string;
664
+ export declare function cx(...parts: readonly ClassValue[]): string;
665
665
 
666
666
  export interface ButtonOpts {
667
667
  variant?: 'ghost' | 'subtle' | 'danger';
@@ -715,6 +715,7 @@ export interface DotgridOpts {
715
715
  export interface TableOpts {
716
716
  density?: 'dense' | 'comfortable';
717
717
  lined?: boolean;
718
+ breakAnywhere?: boolean;
718
719
  }
719
720
  export interface EyebrowOpts {
720
721
  muted?: boolean;
package/classes/index.js CHANGED
@@ -703,50 +703,70 @@ export const cls = Object.freeze({
703
703
  themetoggleThumb: 'ui-themetoggle__thumb',
704
704
  });
705
705
 
706
- /** classnames-style joiner: skips falsy, flattens nested arrays of any depth. */
706
+ /**
707
+ * Mirrors clsx's permissive input: `number`/`boolean` accepted so guarded class
708
+ * expressions type-check; readonly arrays/tuples flatten the same as mutable ones.
709
+ * @typedef {string | number | boolean | null | undefined | ReadonlyArray<ClassValue>} ClassValue
710
+ */
711
+
712
+ /**
713
+ * classnames-style joiner: skips falsy, flattens nested arrays of any depth.
714
+ * @param {...ClassValue} parts
715
+ * @returns {string}
716
+ */
707
717
  export function cx(...parts) {
708
718
  const out = [];
709
- for (const p of parts.flat(Infinity)) if (p) out.push(p);
719
+ for (const p of parts.flat(Infinity)) if (p && typeof p !== 'boolean') out.push(p);
710
720
  return out.join(' ');
711
721
  }
712
722
 
713
723
  const j = (...p) => p.filter(Boolean).join(' ');
724
+ const valueClass = (map, value) => (value == null || !Object.hasOwn(map, value) ? '' : map[value]);
714
725
 
715
726
  // Lifecycle state → canonical tone class.
716
727
  const stateTone = (state) =>
717
- ({
718
- saving: cls.stateSaving,
719
- saved: cls.stateSaved,
720
- queued: cls.stateQueued,
721
- offline: cls.stateOffline,
722
- stale: cls.stateStale,
723
- conflict: cls.stateConflict,
724
- error: cls.stateError,
725
- locked: cls.stateLocked,
726
- reviewed: cls.stateReviewed,
727
- 'needs-review': cls.stateNeedsReview,
728
- })[state] || '';
728
+ valueClass(
729
+ {
730
+ saving: cls.stateSaving,
731
+ saved: cls.stateSaved,
732
+ queued: cls.stateQueued,
733
+ offline: cls.stateOffline,
734
+ stale: cls.stateStale,
735
+ conflict: cls.stateConflict,
736
+ error: cls.stateError,
737
+ locked: cls.stateLocked,
738
+ reviewed: cls.stateReviewed,
739
+ 'needs-review': cls.stateNeedsReview,
740
+ },
741
+ state,
742
+ );
729
743
 
730
744
  const jobTone = (state) =>
731
- ({
732
- queued: cls.jobQueued,
733
- running: cls.jobRunning,
734
- blocked: cls.jobBlocked,
735
- failed: cls.jobFailed,
736
- complete: cls.jobComplete,
737
- })[state] || '';
745
+ valueClass(
746
+ {
747
+ queued: cls.jobQueued,
748
+ running: cls.jobRunning,
749
+ blocked: cls.jobBlocked,
750
+ failed: cls.jobFailed,
751
+ complete: cls.jobComplete,
752
+ },
753
+ state,
754
+ );
738
755
 
739
756
  // Trust-state → tone class, shared by the source/citation/provenance recipes.
740
- // Object-literal lookup to match stateTone above (shorter, greppable, one idiom).
757
+ // Own-property object-literal lookup to match stateTone above.
741
758
  const srcTone = (state) =>
742
- ({
743
- verified: cls.srcVerified,
744
- unverified: cls.srcUnverified,
745
- generated: cls.srcGenerated,
746
- reviewed: cls.srcReviewed,
747
- stale: cls.srcStale,
748
- conflict: cls.srcConflict,
749
- })[state] || '';
759
+ valueClass(
760
+ {
761
+ verified: cls.srcVerified,
762
+ unverified: cls.srcUnverified,
763
+ generated: cls.srcGenerated,
764
+ reviewed: cls.srcReviewed,
765
+ stale: cls.srcStale,
766
+ conflict: cls.srcConflict,
767
+ },
768
+ state,
769
+ );
750
770
 
751
771
  // Component tone → modifier class. Same object-literal idiom as srcTone/stateTone
752
772
  // while keeping modifier classes grep-friendly.
@@ -759,13 +779,13 @@ const srcTone = (state) =>
759
779
  // is fine and returns no modifier.
760
780
  const toneClass = (component, map, tone) => {
761
781
  if (tone == null) return '';
762
- const hit = map[tone];
782
+ const hit = valueClass(map, tone);
763
783
  if (!hit && typeof console !== 'undefined') {
764
784
  console.warn(
765
785
  `[bronto] ui.${component}(): "${tone}" is not a ${component} tone (use one of: ${Object.keys(map).join(', ')}).`,
766
786
  );
767
787
  }
768
- return hit || '';
788
+ return hit;
769
789
  };
770
790
 
771
791
  const badgeTone = (tone) =>
@@ -862,8 +882,6 @@ const claimStatus = (status) =>
862
882
  status,
863
883
  );
864
884
 
865
- const valueClass = (map, value) => (value == null ? '' : map[value] || '');
866
-
867
885
  const annotationVariants = Object.freeze({
868
886
  label: cls.annotationLabelVariant,
869
887
  callout: cls.annotationCallout,
@@ -899,6 +917,13 @@ const annotationMotions = Object.freeze({
899
917
  focus: cls.annotationFocus,
900
918
  });
901
919
 
920
+ /**
921
+ * @typedef {object} TableOpts
922
+ * @property {'dense' | 'comfortable'} [density] Density modifier.
923
+ * @property {boolean} [lined] Add row separator lines.
924
+ * @property {boolean} [breakAnywhere] Allow long unspaced cell content to wrap instead of overflowing.
925
+ */
926
+
902
927
  export const ui = {
903
928
  button: ({ variant, icon, size } = {}) =>
904
929
  j(
@@ -930,6 +955,7 @@ export const ui = {
930
955
  dot: ({ tone, live } = {}) => j(cls.dot, dotTone(tone), live && cls.dotLive),
931
956
  dotgrid: ({ accent, dense } = {}) =>
932
957
  j(cls.dotgrid, accent && cls.dotgridAccent, dense && cls.dotgridDense),
958
+ /** @type {(opts?: TableOpts) => string} */
933
959
  table: ({ density, lined, breakAnywhere } = {}) =>
934
960
  j(
935
961
  cls.table,
@@ -1078,9 +1104,19 @@ export const ui = {
1078
1104
  // unknown; a meter is never indeterminate so it passes false. (Kept a boolean
1079
1105
  // flag rather than testing the role string, so check:recipe-types doesn't read it
1080
1106
  // as a recipe option literal.)
1081
- const valueAttrs = (role, value, min, max, busyWhenIndeterminate) => {
1107
+ const valueAttrs = (component, role, value, min, max, busyWhenIndeterminate) => {
1082
1108
  const lo = Number(min);
1083
1109
  const hi = Number(max);
1110
+ const validRange = Number.isFinite(lo) && Number.isFinite(hi) && hi > lo;
1111
+ if (!validRange) {
1112
+ if (typeof console !== 'undefined') {
1113
+ console.warn(
1114
+ `[bronto] attrs.${component}(): invalid range (expected finite min/max with max > min); omitting value ARIA.`,
1115
+ );
1116
+ }
1117
+ return busyWhenIndeterminate ? { role, 'aria-busy': 'true' } : { role };
1118
+ }
1119
+
1084
1120
  const raw = Number(value);
1085
1121
  // Indeterminate: an omitted/unknown value (attrs.progress() with no argument).
1086
1122
  // ARIA requires aria-valuenow be OMITTED here — emitting 0 announces "0%",
@@ -1093,7 +1129,7 @@ const valueAttrs = (role, value, min, max, busyWhenIndeterminate) => {
1093
1129
  return busyWhenIndeterminate ? { role, 'aria-busy': 'true' } : { role };
1094
1130
  }
1095
1131
  const now = Math.min(hi, Math.max(lo, raw));
1096
- const pct = hi > lo ? ((now - lo) / (hi - lo)) * 100 : 0;
1132
+ const pct = ((now - lo) / (hi - lo)) * 100;
1097
1133
  return {
1098
1134
  role,
1099
1135
  'aria-valuenow': now,
@@ -1119,10 +1155,12 @@ const valueAttrs = (role, value, min, max, busyWhenIndeterminate) => {
1119
1155
  * call with no value for the indeterminate sweep.
1120
1156
  */
1121
1157
  export const attrs = Object.freeze({
1122
- meter: (value, { min = 0, max = 100 } = {}) => valueAttrs('meter', value, min, max, false),
1158
+ meter: (value, { min = 0, max = 100 } = {}) =>
1159
+ valueAttrs('meter', 'meter', value, min, max, false),
1123
1160
  progress: (value, { min = 0, max = 100 } = {}) =>
1124
- valueAttrs('progressbar', value, min, max, true),
1125
- dotbar: (value, { min = 0, max = 100 } = {}) => valueAttrs('progressbar', value, min, max, true),
1161
+ valueAttrs('progress', 'progressbar', value, min, max, true),
1162
+ dotbar: (value, { min = 0, max = 100 } = {}) =>
1163
+ valueAttrs('dotbar', 'progressbar', value, min, max, true),
1126
1164
  });
1127
1165
 
1128
1166
  export default ui;
@@ -80,10 +80,10 @@ export function curvePath(from: Point, to: Point, opts?: {
80
80
  }): string;
81
81
  /**
82
82
  * Build a path between two points by `shape` (`straight` | `elbow` | `curve`).
83
- * @param {ConnectorPathOptions} [opts]
83
+ * @param {ConnectorPathOptions} opts
84
84
  * @returns {string}
85
85
  */
86
- export function connectorPath(opts?: ConnectorPathOptions): string;
86
+ export function connectorPath(opts: ConnectorPathOptions): string;
87
87
  /**
88
88
  * A filled triangle arrowhead at `p`, pointing along `angle` (radians).
89
89
  * @param {Point} p
@@ -136,10 +136,10 @@ export function endTangentAngle(from: Point, to: Point, shape?: ConnectorShape):
136
136
  * Connect two rects. Resolves anchor points (explicit `fromSide`/`toSide`, else
137
137
  * auto), builds the path, and returns `{ d, from, to, angle }` so the caller can
138
138
  * place an arrowhead/dot at `to` rotated by `angle`.
139
- * @param {ConnectRectsOptions} [opts]
139
+ * @param {ConnectRectsOptions} opts
140
140
  * @returns {ConnectRectsResult}
141
141
  */
142
- export function connectRects(opts?: ConnectRectsOptions): ConnectRectsResult;
142
+ export function connectRects(opts: ConnectRectsOptions): ConnectRectsResult;
143
143
  /**
144
144
  * @ponchia/ui/connectors — dependency-free SVG geometry for connecting two
145
145
  * elements (or two points) with a leader line.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"AAiDA;;;;;;GAMG;AACH,6BALW,MAAM,SACN,MAAM,GAAG,IAAI,GAAG,SAAS,aACzB,MAAM,GAAG,IAAI,GAAG,SAAS,GACvB,MAAM,CAMlB;AAED;;;;;;GAMG;AACH,gCALW,MAAM,SACN,MAAM,GAAG,IAAI,GAAG,SAAS,aACzB,MAAM,GAAG,IAAI,GAAG,SAAS,GACvB,MAAM,CAMlB;AAID;;;GAGG;AACH,mCAHW,MAAM,GACJ,MAAM,CAKlB;AAED;;;GAGG;AACH,2BAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,yBAJW,MAAM,KACN,MAAM,GACJ,MAAM,CAIlB;AAGD;;;;;GAKG;AACH,6BALW,MAAM,OACN,MAAM,OACN,MAAM,GACJ,MAAM,CAKlB;AAqBD;;;;;GAKG;AACH,kCAJW,IAAI,SACJ,IAAI,GACF,KAAK,CAoBjB;AAED;;;;;GAKG;AACH,mCAJW,KAAK,MACL,KAAK,GACH,MAAM,CAOlB;AAED;;;;;GAKG;AACH,mCAJW,KAAK,MACL,KAAK,GACH,MAAM,CAOlB;AAED;;;;;;GAMG;AACH,gCALW,KAAK,MACL,KAAK,SACL;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACd,MAAM,CAgBlB;AAED;;;;;;GAMG;AACH,gCALW,KAAK,MACL,KAAK,SACL;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACpB,MAAM,CAclB;AAED;;;;GAIG;AACH,qCAHW,oBAAoB,GAClB,MAAM,CAQlB;AAED;;;;;;;;GAQG;AACH,6BAPW,KAAK,SACL,MAAM,SACN,MAAM,WACN,MAAM,GAEJ,MAAM,CAalB;AAED;;;;;GAKG;AACH,2BAJW,KAAK,WACL,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;GASG;AACH,+BANW,MAAM,OACN,MAAM,SACN,MAAM,UACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;;GAKG;AACH,oCAJW,IAAI,UACJ,IAAI,GACF;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,CAWpC;AAED;;;;;;;;GAQG;AACH,sCALW,KAAK,MACL,KAAK,UACL,cAAc,GACZ,MAAM,CASlB;AAED;;;;;;GAMG;AACH,oCAHW,mBAAmB,GACjB,kBAAkB,CAe9B;AA9VD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAKH,wBAAyB,IAAI,CAAC;oBA/BjB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE;mBACxB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;mBACvD,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ;6BAC9C,UAAU,GAAG,OAAO,GAAG,OAAO;;UAG7B,KAAK;QACL,KAAK;;;;;;;;;;;;cAML,IAAI;YACJ,IAAI;;;;;;;;;;;;;;OAQJ,MAAM;UACN,KAAK;QACL,KAAK;;;;WACL,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":"AAiDA;;;;;;GAMG;AACH,6BALW,MAAM,SACN,MAAM,GAAG,IAAI,GAAG,SAAS,aACzB,MAAM,GAAG,IAAI,GAAG,SAAS,GACvB,MAAM,CAMlB;AAED;;;;;;GAMG;AACH,gCALW,MAAM,SACN,MAAM,GAAG,IAAI,GAAG,SAAS,aACzB,MAAM,GAAG,IAAI,GAAG,SAAS,GACvB,MAAM,CAMlB;AAID;;;GAGG;AACH,mCAHW,MAAM,GACJ,MAAM,CAKlB;AAED;;;GAGG;AACH,2BAHW,MAAM,GACJ,MAAM,CAIlB;AAED;;;;GAIG;AACH,yBAJW,MAAM,KACN,MAAM,GACJ,MAAM,CAIlB;AAGD;;;;;GAKG;AACH,6BALW,MAAM,OACN,MAAM,OACN,MAAM,GACJ,MAAM,CAKlB;AAqBD;;;;;GAKG;AACH,kCAJW,IAAI,SACJ,IAAI,GACF,KAAK,CAoBjB;AAED;;;;;GAKG;AACH,mCAJW,KAAK,MACL,KAAK,GACH,MAAM,CAOlB;AAED;;;;;GAKG;AACH,mCAJW,KAAK,MACL,KAAK,GACH,MAAM,CAOlB;AAED;;;;;;GAMG;AACH,gCALW,KAAK,MACL,KAAK,SACL;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACd,MAAM,CAgBlB;AAED;;;;;;GAMG;AACH,gCALW,KAAK,MACL,KAAK,SACL;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACpB,MAAM,CAclB;AAED;;;;GAIG;AACH,oCAHW,oBAAoB,GAClB,MAAM,CASlB;AAED;;;;;;;;GAQG;AACH,6BAPW,KAAK,SACL,MAAM,SACN,MAAM,WACN,MAAM,GAEJ,MAAM,CAalB;AAED;;;;;GAKG;AACH,2BAJW,KAAK,WACL,MAAM,GACJ,MAAM,CAUlB;AAED;;;;;;;;;GASG;AACH,+BANW,MAAM,OACN,MAAM,SACN,MAAM,UACN,MAAM,GACJ,MAAM,CAIlB;AAED;;;;;GAKG;AACH,oCAJW,IAAI,UACJ,IAAI,GACF;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,CAWpC;AAED;;;;;;;;GAQG;AACH,sCALW,KAAK,MACL,KAAK,UACL,cAAc,GACZ,MAAM,CASlB;AAED;;;;;;GAMG;AACH,mCAHW,mBAAmB,GACjB,kBAAkB,CAgB9B;AAhWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAKH,wBAAyB,IAAI,CAAC;oBA/BjB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE;mBACxB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;mBACvD,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ;6BAC9C,UAAU,GAAG,OAAO,GAAG,OAAO;;UAG7B,KAAK;QACL,KAAK;;;;;;;;;;;;cAML,IAAI;YACJ,IAAI;;;;;;;;;;;;;;OAQJ,MAAM;UACN,KAAK;QACL,KAAK;;;;WACL,MAAM"}
@@ -230,14 +230,15 @@ export function curvePath(from, to, opts = {}) {
230
230
 
231
231
  /**
232
232
  * Build a path between two points by `shape` (`straight` | `elbow` | `curve`).
233
- * @param {ConnectorPathOptions} [opts]
233
+ * @param {ConnectorPathOptions} opts
234
234
  * @returns {string}
235
235
  */
236
- export function connectorPath(opts = {}) {
237
- const { from, to } = opts;
238
- const shape = connectorShape(opts.shape);
239
- if (shape === 'elbow') return elbowPath(from, to, opts);
240
- if (shape === 'curve') return curvePath(from, to, opts);
236
+ export function connectorPath(opts) {
237
+ const options = opts ?? {};
238
+ const { from, to } = options;
239
+ const shape = connectorShape(options.shape);
240
+ if (shape === 'elbow') return elbowPath(from, to, options);
241
+ if (shape === 'curve') return curvePath(from, to, options);
241
242
  return straightPath(from, to);
242
243
  }
243
244
 
@@ -332,17 +333,18 @@ export function endTangentAngle(from, to, shape = 'straight') {
332
333
  * Connect two rects. Resolves anchor points (explicit `fromSide`/`toSide`, else
333
334
  * auto), builds the path, and returns `{ d, from, to, angle }` so the caller can
334
335
  * place an arrowhead/dot at `to` rotated by `angle`.
335
- * @param {ConnectRectsOptions} [opts]
336
+ * @param {ConnectRectsOptions} opts
336
337
  * @returns {ConnectRectsResult}
337
338
  */
338
- export function connectRects(opts = {}) {
339
- const { fromRect, toRect, curvature, mid } = opts;
340
- const shape = connectorShape(opts.shape);
339
+ export function connectRects(opts) {
340
+ const options = opts ?? {};
341
+ const { fromRect, toRect, curvature, mid } = options;
342
+ const shape = connectorShape(options.shape);
341
343
  // Honor each side override independently; auto-pick whichever is unset.
342
344
  const auto = autoSides(fromRect, toRect);
343
345
  const sides = {
344
- from: opts.fromSide == null ? auto.from : sideValue(opts.fromSide),
345
- to: opts.toSide == null ? auto.to : sideValue(opts.toSide),
346
+ from: options.fromSide == null ? auto.from : sideValue(options.fromSide),
347
+ to: options.toSide == null ? auto.to : sideValue(options.toSide),
346
348
  };
347
349
  const from = anchorPoint(fromRect, sides.from);
348
350
  const to = anchorPoint(toRect, sides.to);
@@ -275,6 +275,7 @@
275
275
  .ui-annotation__note,
276
276
  .ui-annotation__note-line,
277
277
  .ui-annotation__badge {
278
+ /* Intentional !important: print must freeze opt-in draw/reveal animations. */
278
279
  animation: none !important;
279
280
  opacity: 1;
280
281
  stroke-dashoffset: 0;
package/css/app.css CHANGED
@@ -184,6 +184,13 @@
184
184
  z-index: var(--z-raised);
185
185
  }
186
186
 
187
+ @media (prefers-reduced-transparency: reduce) {
188
+ .ui-app-topbar {
189
+ backdrop-filter: none;
190
+ background: var(--surface-1);
191
+ }
192
+ }
193
+
187
194
  .ui-app-topbar__title {
188
195
  color: var(--text);
189
196
  font-family: var(--display);
package/css/base.css CHANGED
@@ -267,6 +267,7 @@ textarea:focus-visible,
267
267
  .ui-dotloader,
268
268
  .ui-dotspinner,
269
269
  dialog.ui-modal::backdrop {
270
+ /* Intentional !important: print chrome must stay hidden across component display rules. */
270
271
  display: none !important;
271
272
  }
272
273
 
@@ -274,6 +275,7 @@ textarea:focus-visible,
274
275
  *,
275
276
  *::before,
276
277
  *::after {
278
+ /* Intentional !important: print output drops decorative shadows globally. */
277
279
  box-shadow: none !important;
278
280
  text-shadow: none !important;
279
281
  }
@@ -291,6 +293,7 @@ textarea:focus-visible,
291
293
  .ui-prose table,
292
294
  .ui-tabs__list,
293
295
  pre {
296
+ /* Intentional !important: print must spill scroll containers even if component rules clip. */
294
297
  overflow: visible !important;
295
298
  }
296
299