@ponchia/ui 0.6.0 → 0.6.4

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 (162) hide show
  1. package/CHANGELOG.md +82 -4
  2. package/README.md +1 -1
  3. package/annotations/index.d.ts.map +1 -1
  4. package/annotations/index.js +36 -33
  5. package/behaviors/carousel.d.ts +28 -0
  6. package/behaviors/carousel.d.ts.map +1 -0
  7. package/behaviors/carousel.js +3 -0
  8. package/behaviors/combobox.d.ts +40 -0
  9. package/behaviors/combobox.d.ts.map +1 -0
  10. package/behaviors/combobox.js +71 -20
  11. package/behaviors/command.d.ts +41 -0
  12. package/behaviors/command.d.ts.map +1 -0
  13. package/behaviors/command.js +9 -0
  14. package/behaviors/connectors.d.ts +17 -0
  15. package/behaviors/connectors.d.ts.map +1 -0
  16. package/behaviors/connectors.js +3 -0
  17. package/behaviors/crosshair.d.ts +42 -0
  18. package/behaviors/crosshair.d.ts.map +1 -0
  19. package/behaviors/crosshair.js +19 -1
  20. package/behaviors/dialog.d.ts +20 -0
  21. package/behaviors/dialog.d.ts.map +1 -0
  22. package/behaviors/dialog.js +3 -0
  23. package/behaviors/disclosure.d.ts +10 -0
  24. package/behaviors/disclosure.d.ts.map +1 -0
  25. package/behaviors/disclosure.js +3 -0
  26. package/behaviors/dismissible.d.ts +10 -0
  27. package/behaviors/dismissible.d.ts.map +1 -0
  28. package/behaviors/dismissible.js +3 -0
  29. package/behaviors/forms.d.ts +27 -0
  30. package/behaviors/forms.d.ts.map +1 -0
  31. package/behaviors/forms.js +18 -5
  32. package/behaviors/glyph.d.ts +21 -0
  33. package/behaviors/glyph.d.ts.map +1 -0
  34. package/behaviors/glyph.js +82 -4
  35. package/behaviors/index.d.ts +31 -237
  36. package/behaviors/index.d.ts.map +1 -0
  37. package/behaviors/index.js +17 -0
  38. package/behaviors/inert.d.ts +20 -0
  39. package/behaviors/inert.d.ts.map +1 -0
  40. package/behaviors/inert.js +46 -0
  41. package/behaviors/internal.d.ts +25 -0
  42. package/behaviors/internal.d.ts.map +1 -0
  43. package/behaviors/internal.js +30 -1
  44. package/behaviors/legend.d.ts +35 -0
  45. package/behaviors/legend.d.ts.map +1 -0
  46. package/behaviors/legend.js +9 -0
  47. package/behaviors/menu.d.ts +16 -0
  48. package/behaviors/menu.d.ts.map +1 -0
  49. package/behaviors/menu.js +3 -0
  50. package/behaviors/modal.d.ts +41 -0
  51. package/behaviors/modal.d.ts.map +1 -0
  52. package/behaviors/modal.js +124 -0
  53. package/behaviors/popover.d.ts +28 -0
  54. package/behaviors/popover.d.ts.map +1 -0
  55. package/behaviors/popover.js +17 -17
  56. package/behaviors/spotlight.d.ts +17 -0
  57. package/behaviors/spotlight.d.ts.map +1 -0
  58. package/behaviors/spotlight.js +3 -0
  59. package/behaviors/table.d.ts +36 -0
  60. package/behaviors/table.d.ts.map +1 -0
  61. package/behaviors/table.js +48 -8
  62. package/behaviors/tabs.d.ts +20 -0
  63. package/behaviors/tabs.d.ts.map +1 -0
  64. package/behaviors/tabs.js +3 -0
  65. package/behaviors/theme.d.ts +54 -0
  66. package/behaviors/theme.d.ts.map +1 -0
  67. package/behaviors/theme.js +17 -0
  68. package/behaviors/toast.d.ts +49 -0
  69. package/behaviors/toast.d.ts.map +1 -0
  70. package/behaviors/toast.js +34 -2
  71. package/classes/classes.json +747 -15
  72. package/classes/index.d.ts +118 -3
  73. package/classes/index.js +264 -66
  74. package/connectors/index.d.ts +12 -0
  75. package/connectors/index.d.ts.map +1 -1
  76. package/connectors/index.js +23 -2
  77. package/css/app.css +26 -0
  78. package/css/bullet.css +108 -0
  79. package/css/code.css +98 -0
  80. package/css/content.css +15 -2
  81. package/css/crosshair.css +7 -7
  82. package/css/diff.css +153 -0
  83. package/css/disclosure.css +18 -4
  84. package/css/dots.css +246 -9
  85. package/css/feedback.css +39 -7
  86. package/css/forms.css +71 -3
  87. package/css/legend.css +5 -2
  88. package/css/motion.css +79 -14
  89. package/css/overlay.css +59 -2
  90. package/css/primitives.css +67 -8
  91. package/css/report.css +43 -4
  92. package/css/sidenote.css +67 -0
  93. package/css/skins.css +9 -0
  94. package/css/spark.css +76 -0
  95. package/css/table.css +16 -3
  96. package/css/term.css +110 -0
  97. package/css/textref.css +63 -0
  98. package/css/toc.css +91 -0
  99. package/css/tokens.css +14 -1
  100. package/css/tree.css +134 -0
  101. package/dist/bronto.css +1 -1
  102. package/dist/css/analytical.css +1 -1
  103. package/dist/css/app.css +1 -1
  104. package/dist/css/bullet.css +1 -0
  105. package/dist/css/code.css +1 -0
  106. package/dist/css/content.css +1 -1
  107. package/dist/css/crosshair.css +1 -1
  108. package/dist/css/diff.css +1 -0
  109. package/dist/css/disclosure.css +1 -1
  110. package/dist/css/dots.css +1 -1
  111. package/dist/css/feedback.css +1 -1
  112. package/dist/css/forms.css +1 -1
  113. package/dist/css/legend.css +1 -1
  114. package/dist/css/motion.css +1 -1
  115. package/dist/css/overlay.css +1 -1
  116. package/dist/css/primitives.css +1 -1
  117. package/dist/css/report.css +1 -1
  118. package/dist/css/sidenote.css +1 -0
  119. package/dist/css/skins.css +1 -1
  120. package/dist/css/spark.css +1 -0
  121. package/dist/css/table.css +1 -1
  122. package/dist/css/term.css +1 -0
  123. package/dist/css/textref.css +1 -0
  124. package/dist/css/toc.css +1 -0
  125. package/dist/css/tokens.css +1 -1
  126. package/dist/css/tree.css +1 -0
  127. package/docs/annotations.md +39 -0
  128. package/docs/architecture.md +2 -3
  129. package/docs/bullet.md +78 -0
  130. package/docs/code.md +76 -0
  131. package/docs/d2.md +4 -3
  132. package/docs/diff.md +146 -0
  133. package/docs/dots.md +146 -0
  134. package/docs/glyphs.md +114 -0
  135. package/docs/legends.md +8 -4
  136. package/docs/mermaid.md +21 -4
  137. package/docs/reference.md +168 -8
  138. package/docs/reporting.md +49 -17
  139. package/docs/sidenote.md +64 -0
  140. package/docs/spark.md +78 -0
  141. package/docs/stability.md +1 -0
  142. package/docs/term.md +81 -0
  143. package/docs/textref.md +78 -0
  144. package/docs/theming.md +44 -5
  145. package/docs/toc.md +83 -0
  146. package/docs/tree.md +74 -0
  147. package/docs/usage.md +264 -23
  148. package/docs/vega.md +22 -3
  149. package/glyphs/glyphs.d.ts +61 -0
  150. package/glyphs/glyphs.js +600 -31
  151. package/llms.txt +169 -15
  152. package/package.json +51 -7
  153. package/qwik/index.d.ts +4 -2
  154. package/qwik/index.d.ts.map +1 -1
  155. package/qwik/index.js +10 -0
  156. package/react/index.d.ts +4 -2
  157. package/react/index.d.ts.map +1 -1
  158. package/react/index.js +6 -0
  159. package/solid/index.d.ts +6 -2
  160. package/solid/index.d.ts.map +1 -1
  161. package/solid/index.js +6 -0
  162. package/tokens/skins.js +22 -9
@@ -19,9 +19,20 @@ import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js
19
19
  *
20
20
  * SSR-safe, idempotent per table; returns a cleanup function.
21
21
  *
22
- * The numeric sort parses each cell as display text (strips non-[0-9.-] chars),
23
- * so it is locale-naive group/decimal separators beyond `.`/`-` are not
24
- * interpreted. It is a client-side convenience sorter, not a data grid.
22
+ * The numeric sort parses each cell's display text after normalizing the
23
+ * common report shapes: a Unicode minus (U+2212) and en/em dashes count as a
24
+ * sign (so a "−5" loss sorts BELOW a "5" gain, not above it), accounting
25
+ * parentheses `(1,234)` read as negative, and `,` thousands separators are
26
+ * dropped. Note the consequence: a bare `,` in the *display text* is read as a
27
+ * thousands separator, so a European decimal "3,5" sorts as 35, not 3.5 — for a
28
+ * European decimal comma (or mixed units, or any ambiguous text) put the
29
+ * canonical number in a `data-sort-value` attribute on the cell. That escape
30
+ * hatch wins over the parsed text and accepts either a dot ("3.5") or a single
31
+ * decimal comma ("3,5"). It is a client-side convenience sorter, not a data
32
+ * grid. (component audit C3/C5.)
33
+ *
34
+ * @param {import('./internal.js').DelegateOpts} [opts]
35
+ * @returns {import('./internal.js').Cleanup}
25
36
  */
26
37
  export function initTableSort({ root } = {}) {
27
38
  if (!hasDom()) return noop;
@@ -44,6 +55,38 @@ export function initTableSort({ root } = {}) {
44
55
 
45
56
  const colIndex = (th) => [...th.parentElement.children].indexOf(th);
46
57
  const cellText = (row, i) => row.children[i]?.textContent.trim() ?? '';
58
+ // Numeric value of a cell for sorting. A `data-sort-value` attribute is the
59
+ // authoritative escape hatch; otherwise normalize the display text so the
60
+ // sign survives (U+2212 / en-em dashes → minus, accounting parens →
61
+ // negative) and `,` grouping is dropped. Returns 0 for unparseable cells so
62
+ // they cluster rather than scatter. (component audit C3.)
63
+ const cellNum = (row, i) => {
64
+ const cell = row.children[i];
65
+ const explicit = cell?.getAttribute?.('data-sort-value');
66
+ if (explicit != null && explicit.trim() !== '') {
67
+ const raw = explicit.trim();
68
+ let v = Number(raw);
69
+ // The escape hatch must actually handle the case the doc names it for: a
70
+ // European decimal comma. `Number("3,5")` is NaN, which silently fell
71
+ // back to parsing the display text (where `,` is dropped as a thousands
72
+ // separator → "35"). A lone comma with no dot is a decimal point here.
73
+ if (!Number.isFinite(v) && /^[+-]?\d+,\d+$/.test(raw)) v = Number(raw.replace(',', '.'));
74
+ if (Number.isFinite(v)) return v;
75
+ }
76
+ let s = (cell?.textContent ?? '').trim();
77
+ if (!s) return 0;
78
+ let sign = 1;
79
+ const paren = /^\((.*)\)$/.exec(s); // accounting negative
80
+ if (paren) {
81
+ sign = -1;
82
+ s = paren[1];
83
+ }
84
+ s = s.replace(/[−–—]/g, '-'); // minus / en / em dash → '-'
85
+ if (/-/.test(s)) sign *= -1;
86
+ s = s.replace(/,/g, ''); // drop thousands separators
87
+ const v = parseFloat(s.replace(/[^\d.]/g, '')); // magnitude
88
+ return Number.isFinite(v) ? sign * v : 0;
89
+ };
47
90
 
48
91
  const sortBy = (th, numeric) => {
49
92
  const headers = th.closest('tr').querySelectorAll('th');
@@ -61,12 +104,9 @@ export function initTableSort({ root } = {}) {
61
104
  const emptyRows = [...tbody.rows].filter((r) => r.classList.contains('ui-table__empty'));
62
105
  const rows = [...tbody.rows].filter((r) => !r.classList.contains('ui-table__empty'));
63
106
  rows.sort((a, b) => {
64
- const x = cellText(a, i);
65
- const y = cellText(b, i);
66
107
  const cmp = numeric
67
- ? (parseFloat(x.replace(/[^\d.-]/g, '')) || 0) -
68
- (parseFloat(y.replace(/[^\d.-]/g, '')) || 0)
69
- : x.localeCompare(y);
108
+ ? cellNum(a, i) - cellNum(b, i)
109
+ : cellText(a, i).localeCompare(cellText(b, i));
70
110
  return cmp * sign;
71
111
  });
72
112
  // Re-parent in document order: sorted data rows, then any empty/sentinel
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Wire `[data-bronto-tabs]` groups for full keyboard a11y. The framework
3
+ * ships the look + the ARIA/`.is-active` contract; this adds the WAI-ARIA
4
+ * Tabs pattern: roving `tabindex`, `aria-selected`, Arrow/Home/End
5
+ * navigation with automatic activation, and panel `hidden` sync. Tabs are
6
+ * `.ui-tab[data-tab]`; panels are `.ui-tabs__panel[data-panel]` with
7
+ * matching values. SSR-safe and idempotent (re-init replaces, never
8
+ * stacks, the per-group listeners); returns a cleanup function.
9
+ *
10
+ * Accessibility caveat: this is what makes tabs operable. Do **not**
11
+ * author `hidden` on `.ui-tabs__panel` in server-rendered markup unless
12
+ * `initTabs` is guaranteed to run client-side — without it the panels
13
+ * stay hidden with no keyboard/pointer way to reveal them. Prefer
14
+ * authoring all panels visible and letting `initTabs` add `hidden`.
15
+ *
16
+ * @param {import('./internal.js').DelegateOpts} [opts]
17
+ * @returns {import('./internal.js').Cleanup}
18
+ */
19
+ export function initTabs({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
20
+ //# sourceMappingURL=tabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,oCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAuF3C"}
package/behaviors/tabs.js CHANGED
@@ -14,6 +14,9 @@ import { hasDom, resolveHost, noop, bindOnce, nextFieldUid, collectHosts } from
14
14
  * `initTabs` is guaranteed to run client-side — without it the panels
15
15
  * stay hidden with no keyboard/pointer way to reveal them. Prefer
16
16
  * authoring all panels visible and letting `initTabs` add `hidden`.
17
+ *
18
+ * @param {import('./internal.js').DelegateOpts} [opts]
19
+ * @returns {import('./internal.js').Cleanup}
17
20
  */
18
21
  export function initTabs({ root } = {}) {
19
22
  if (!hasDom()) return noop;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @typedef {object} ThemeStorageOpts
3
+ * @property {string} [storageKey] localStorage key for the persisted theme. Default: `"bronto-theme"`.
4
+ *
5
+ * @typedef {ThemeStorageOpts & { root?: Element }} ApplyThemeOpts
6
+ * `root` is the element to set `data-theme` on. Default: `<html>`.
7
+ *
8
+ * @typedef {object} ThemeChangeDetail
9
+ * @property {'light' | 'dark'} theme `bronto:themechange` CustomEvent detail.
10
+ */
11
+ /**
12
+ * Apply the persisted theme to <html data-theme>. Call as early as
13
+ * possible (an inline module in <head>) to avoid a flash before the
14
+ * toggle wires up. No stored value → leaves prefers-color-scheme to act.
15
+ *
16
+ * @param {ApplyThemeOpts} [opts]
17
+ * @returns {void}
18
+ */
19
+ export function applyStoredTheme({ storageKey, root }?: ApplyThemeOpts): void;
20
+ /**
21
+ * Wire `[data-bronto-theme-toggle]` controls. Click toggles light/dark,
22
+ * persists to localStorage, and **always** sets `data-theme` on <html>
23
+ * (a theme is document-global). State is reflected via `aria-pressed`
24
+ * and a `bronto:themechange` CustomEvent ({ detail: { theme } }) is
25
+ * dispatched on <html> so consumers can sync their own UI without
26
+ * racing the click handler. A control may set
27
+ * `data-bronto-theme-toggle="dark"` to force a specific theme.
28
+ *
29
+ * `root` scopes event delegation and which controls are queried/reflected
30
+ * (default `document`); it does not change where the theme is applied.
31
+ *
32
+ * @param {ThemeStorageOpts & import('./internal.js').DelegateOpts} [opts]
33
+ * @returns {import('./internal.js').Cleanup}
34
+ */
35
+ export function initThemeToggle({ storageKey, root }?: ThemeStorageOpts & import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
36
+ export type ThemeStorageOpts = {
37
+ /**
38
+ * localStorage key for the persisted theme. Default: `"bronto-theme"`.
39
+ */
40
+ storageKey?: string | undefined;
41
+ };
42
+ /**
43
+ * `root` is the element to set `data-theme` on. Default: `<html>`.
44
+ */
45
+ export type ApplyThemeOpts = ThemeStorageOpts & {
46
+ root?: Element;
47
+ };
48
+ export type ThemeChangeDetail = {
49
+ /**
50
+ * `bronto:themechange` CustomEvent detail.
51
+ */
52
+ theme: "light" | "dark";
53
+ };
54
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["theme.js"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH;;;;;;;GAOG;AACH,wDAHW,cAAc,GACZ,IAAI,CAahB;AAED;;;;;;;;;;;;;;GAcG;AACH,uDAHW,gBAAgB,GAAG,OAAO,eAAe,EAAE,YAAY,GACrD,OAAO,eAAe,EAAE,OAAO,CAmD3C;;;;;;;;;;6BA5FY,gBAAgB,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;WAIpC,OAAO,GAAG,MAAM"}
@@ -2,10 +2,24 @@ import { hasDom, resolveHost, noop, bindOnce } from './internal.js';
2
2
 
3
3
  const THEMES = ['light', 'dark'];
4
4
 
5
+ /**
6
+ * @typedef {object} ThemeStorageOpts
7
+ * @property {string} [storageKey] localStorage key for the persisted theme. Default: `"bronto-theme"`.
8
+ *
9
+ * @typedef {ThemeStorageOpts & { root?: Element }} ApplyThemeOpts
10
+ * `root` is the element to set `data-theme` on. Default: `<html>`.
11
+ *
12
+ * @typedef {object} ThemeChangeDetail
13
+ * @property {'light' | 'dark'} theme `bronto:themechange` CustomEvent detail.
14
+ */
15
+
5
16
  /**
6
17
  * Apply the persisted theme to <html data-theme>. Call as early as
7
18
  * possible (an inline module in <head>) to avoid a flash before the
8
19
  * toggle wires up. No stored value → leaves prefers-color-scheme to act.
20
+ *
21
+ * @param {ApplyThemeOpts} [opts]
22
+ * @returns {void}
9
23
  */
10
24
  export function applyStoredTheme({ storageKey = 'bronto-theme', root } = {}) {
11
25
  if (!hasDom()) return;
@@ -31,6 +45,9 @@ export function applyStoredTheme({ storageKey = 'bronto-theme', root } = {}) {
31
45
  *
32
46
  * `root` scopes event delegation and which controls are queried/reflected
33
47
  * (default `document`); it does not change where the theme is applied.
48
+ *
49
+ * @param {ThemeStorageOpts & import('./internal.js').DelegateOpts} [opts]
50
+ * @returns {import('./internal.js').Cleanup}
34
51
  */
35
52
  export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
36
53
  if (!hasDom()) return noop;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @typedef {object} ToastOpts
3
+ * @property {'accent' | 'success' | 'warning' | 'danger' | 'info'} [tone] Status tone — maps to `ui-toast--<tone>`.
4
+ * @property {string} [title] Optional uppercase label rendered above the message.
5
+ * @property {number} [duration] Auto-dismiss delay in ms. `0` keeps it until dismissed. Default: `4000`.
6
+ * @property {boolean} [assertive] Route to the assertive live region so AT interrupts immediately. Defaults to `true` when `tone === 'danger'`.
7
+ * @property {boolean} [closable] Render a dismiss button on the toast.
8
+ */
9
+ /**
10
+ * Push a transient toast into a shared, screen-anchored stack. The stack
11
+ * is the `aria-live="polite"` region: it is created once, appended to
12
+ * <body>, and **kept resident even when empty** so the live region is
13
+ * always present before content is inserted (a freshly created region
14
+ * that receives its first child in the same tick is not reliably
15
+ * announced by VoiceOver/NVDA). On first creation the empty region is
16
+ * inserted and the toast is appended on the next frame for the same
17
+ * reason. `tone` is accent/success/warning/danger/info; `title` is an
18
+ * optional uppercase label; `duration` ms before auto-dismiss (0 keeps
19
+ * it until dismissed). Returns a function that dismisses the toast
20
+ * early. SSR-safe (no-op).
21
+ *
22
+ * @param {string} message
23
+ * @param {ToastOpts} [opts]
24
+ * @returns {import('./internal.js').Cleanup}
25
+ */
26
+ export function toast(message: string, { tone, title, duration, assertive, closable }?: ToastOpts): import("./internal.js").Cleanup;
27
+ export type ToastOpts = {
28
+ /**
29
+ * Status tone — maps to `ui-toast--<tone>`.
30
+ */
31
+ tone?: "accent" | "success" | "warning" | "danger" | "info" | undefined;
32
+ /**
33
+ * Optional uppercase label rendered above the message.
34
+ */
35
+ title?: string | undefined;
36
+ /**
37
+ * Auto-dismiss delay in ms. `0` keeps it until dismissed. Default: `4000`.
38
+ */
39
+ duration?: number | undefined;
40
+ /**
41
+ * Route to the assertive live region so AT interrupts immediately. Defaults to `true` when `tone === 'danger'`.
42
+ */
43
+ assertive?: boolean | undefined;
44
+ /**
45
+ * Render a dismiss button on the toast.
46
+ */
47
+ closable?: boolean | undefined;
48
+ };
49
+ //# sourceMappingURL=toast.d.ts.map
@@ -0,0 +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"}
@@ -25,7 +25,13 @@ function toastStack(isAssertive) {
25
25
  stack = document.createElement('div');
26
26
  stack.className = isAssertive ? 'ui-toast-stack ui-toast-stack--assertive' : 'ui-toast-stack';
27
27
  stack.setAttribute('aria-live', isAssertive ? 'assertive' : 'polite');
28
- if (isAssertive) stack.setAttribute('role', 'alert');
28
+ if (isAssertive) {
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 diff. (component audit C38.)
33
+ stack.setAttribute('aria-atomic', 'true');
34
+ }
29
35
  document.body.appendChild(stack);
30
36
  }
31
37
  return { stack, fresh };
@@ -57,7 +63,12 @@ function toastElement(message, { tone, title }) {
57
63
  }
58
64
  el.className = validTone ? `ui-toast ui-toast--${validTone}` : 'ui-toast';
59
65
  // No per-item role: the stack itself is the live region; a nested
60
- // live region risks double announcement in some SRs.
66
+ // live region risks double announcement in some SRs. But mark the toast
67
+ // aria-atomic so a *titled* toast announces title + message as one unit, not
68
+ // disjointly — and unlike aria-atomic on the polite STACK (which would re-read
69
+ // every resident toast on each new one), scoping it to the toast keeps sibling
70
+ // toasts out of the announcement. (component audit C23.)
71
+ el.setAttribute('aria-atomic', 'true');
61
72
  if (title) {
62
73
  const t = document.createElement('p');
63
74
  t.className = 'ui-toast__title';
@@ -110,6 +121,15 @@ function addToastClose(el, dismiss) {
110
121
  el.appendChild(close);
111
122
  }
112
123
 
124
+ /**
125
+ * @typedef {object} ToastOpts
126
+ * @property {'accent' | 'success' | 'warning' | 'danger' | 'info'} [tone] Status tone — maps to `ui-toast--<tone>`.
127
+ * @property {string} [title] Optional uppercase label rendered above the message.
128
+ * @property {number} [duration] Auto-dismiss delay in ms. `0` keeps it until dismissed. Default: `4000`.
129
+ * @property {boolean} [assertive] Route to the assertive live region so AT interrupts immediately. Defaults to `true` when `tone === 'danger'`.
130
+ * @property {boolean} [closable] Render a dismiss button on the toast.
131
+ */
132
+
113
133
  /**
114
134
  * Push a transient toast into a shared, screen-anchored stack. The stack
115
135
  * is the `aria-live="polite"` region: it is created once, appended to
@@ -122,6 +142,10 @@ function addToastClose(el, dismiss) {
122
142
  * optional uppercase label; `duration` ms before auto-dismiss (0 keeps
123
143
  * it until dismissed). Returns a function that dismisses the toast
124
144
  * early. SSR-safe (no-op).
145
+ *
146
+ * @param {string} message
147
+ * @param {ToastOpts} [opts]
148
+ * @returns {import('./internal.js').Cleanup}
125
149
  */
126
150
  export function toast(message, { tone, title, duration = 4000, assertive, closable } = {}) {
127
151
  if (!hasDom()) return noop;
@@ -158,6 +182,14 @@ export function toast(message, { tone, title, duration = 4000, assertive, closab
158
182
  // it gets a dismiss affordance by default; any toast can opt in via
159
183
  // `closable`. The button carries no text node (glyph is a CSS
160
184
  // ::before) so the toast's announced/textContent stays the message.
185
+ // Explicitly opting OUT of the close button on a sticky toast strands it with
186
+ // no in-UI dismissal — warn that the caller must retain and call the returned
187
+ // dismiss fn. (component audit C37.)
188
+ if (duration === 0 && closable === false && typeof console !== 'undefined') {
189
+ console.warn(
190
+ '[bronto] toast(): duration:0 + closable:false has no in-UI dismissal — keep the returned dismiss() and call it yourself, or set closable:true.',
191
+ );
192
+ }
161
193
  if (closable ?? duration === 0) addToastClose(el, dismiss);
162
194
  if (duration > 0) timer = setTimeout(dismiss, duration);
163
195
  return dismiss;