@ponchia/ui 0.5.0 → 0.6.3
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.
- package/CHANGELOG.md +386 -4
- package/MIGRATIONS.json +14 -0
- package/README.md +29 -6
- package/annotations/index.d.ts +398 -276
- package/annotations/index.d.ts.map +1 -0
- package/annotations/index.js +350 -77
- package/behaviors/carousel.d.ts +28 -0
- package/behaviors/carousel.d.ts.map +1 -0
- package/behaviors/carousel.js +20 -16
- package/behaviors/combobox.d.ts +40 -0
- package/behaviors/combobox.d.ts.map +1 -0
- package/behaviors/combobox.js +111 -29
- package/behaviors/command.d.ts +41 -0
- package/behaviors/command.d.ts.map +1 -0
- package/behaviors/command.js +27 -15
- package/behaviors/connectors.d.ts +17 -0
- package/behaviors/connectors.d.ts.map +1 -0
- package/behaviors/connectors.js +7 -5
- package/behaviors/crosshair.d.ts +42 -0
- package/behaviors/crosshair.d.ts.map +1 -0
- package/behaviors/crosshair.js +23 -6
- package/behaviors/dialog.d.ts +20 -0
- package/behaviors/dialog.d.ts.map +1 -0
- package/behaviors/dialog.js +6 -2
- package/behaviors/disclosure.d.ts +10 -0
- package/behaviors/disclosure.d.ts.map +1 -0
- package/behaviors/disclosure.js +6 -2
- package/behaviors/dismissible.d.ts +10 -0
- package/behaviors/dismissible.d.ts.map +1 -0
- package/behaviors/dismissible.js +6 -2
- package/behaviors/forms.d.ts +27 -0
- package/behaviors/forms.d.ts.map +1 -0
- package/behaviors/forms.js +54 -13
- package/behaviors/glyph.d.ts +14 -0
- package/behaviors/glyph.d.ts.map +1 -0
- package/behaviors/glyph.js +28 -5
- package/behaviors/index.d.ts +31 -237
- package/behaviors/index.d.ts.map +1 -0
- package/behaviors/index.js +17 -0
- package/behaviors/inert.d.ts +20 -0
- package/behaviors/inert.d.ts.map +1 -0
- package/behaviors/inert.js +46 -0
- package/behaviors/internal.d.ts +25 -0
- package/behaviors/internal.d.ts.map +1 -0
- package/behaviors/internal.js +77 -1
- package/behaviors/legend.d.ts +35 -0
- package/behaviors/legend.d.ts.map +1 -0
- package/behaviors/legend.js +32 -2
- package/behaviors/menu.d.ts +16 -0
- package/behaviors/menu.d.ts.map +1 -0
- package/behaviors/menu.js +6 -2
- package/behaviors/modal.d.ts +41 -0
- package/behaviors/modal.d.ts.map +1 -0
- package/behaviors/modal.js +124 -0
- package/behaviors/popover.d.ts +28 -0
- package/behaviors/popover.d.ts.map +1 -0
- package/behaviors/popover.js +78 -7
- package/behaviors/spotlight.d.ts +17 -0
- package/behaviors/spotlight.d.ts.map +1 -0
- package/behaviors/spotlight.js +7 -5
- package/behaviors/table.d.ts +36 -0
- package/behaviors/table.d.ts.map +1 -0
- package/behaviors/table.js +84 -17
- package/behaviors/tabs.d.ts +20 -0
- package/behaviors/tabs.d.ts.map +1 -0
- package/behaviors/tabs.js +17 -14
- package/behaviors/theme.d.ts +54 -0
- package/behaviors/theme.d.ts.map +1 -0
- package/behaviors/theme.js +22 -3
- package/behaviors/toast.d.ts +49 -0
- package/behaviors/toast.d.ts.map +1 -0
- package/behaviors/toast.js +47 -3
- package/classes/classes.json +2527 -0
- package/classes/index.d.ts +134 -15
- package/classes/index.js +280 -80
- package/classes/vscode.css-custom-data.json +12 -0
- package/connectors/index.d.ts +201 -69
- package/connectors/index.d.ts.map +1 -0
- package/connectors/index.js +142 -25
- package/css/app.css +69 -13
- package/css/base.css +15 -10
- package/css/bullet.css +108 -0
- package/css/code.css +98 -0
- package/css/connectors.css +17 -0
- package/css/content.css +22 -3
- package/css/crosshair.css +7 -7
- package/css/dataviz.css +5 -1
- package/css/diff.css +153 -0
- package/css/disclosure.css +53 -7
- package/css/dots.css +94 -7
- package/css/feedback.css +97 -7
- package/css/forms.css +113 -4
- package/css/legend.css +16 -9
- package/css/marks.css +38 -8
- package/css/motion.css +98 -53
- package/css/navigation.css +7 -0
- package/css/overlay.css +90 -3
- package/css/primitives.css +158 -13
- package/css/report.css +73 -56
- package/css/sidenote.css +67 -0
- package/css/site.css +16 -2
- package/css/sources.css +43 -1
- package/css/spark.css +62 -0
- package/css/spotlight.css +1 -1
- package/css/table.css +9 -2
- package/css/term.css +110 -0
- package/css/textref.css +63 -0
- package/css/toc.css +91 -0
- package/css/tokens.css +49 -1
- package/css/tree.css +134 -0
- package/css/workbench.css +1 -1
- package/dist/bronto.css +1 -1
- package/dist/css/analytical.css +1 -1
- package/dist/css/app.css +1 -1
- package/dist/css/base.css +1 -1
- package/dist/css/bullet.css +1 -0
- package/dist/css/code.css +1 -0
- package/dist/css/connectors.css +1 -1
- package/dist/css/content.css +1 -1
- package/dist/css/crosshair.css +1 -1
- package/dist/css/diff.css +1 -0
- package/dist/css/disclosure.css +1 -1
- package/dist/css/dots.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/forms.css +1 -1
- package/dist/css/legend.css +1 -1
- package/dist/css/marks.css +1 -1
- package/dist/css/motion.css +1 -1
- package/dist/css/navigation.css +1 -1
- package/dist/css/overlay.css +1 -1
- package/dist/css/primitives.css +1 -1
- package/dist/css/report.css +1 -1
- package/dist/css/sidenote.css +1 -0
- package/dist/css/site.css +1 -1
- package/dist/css/sources.css +1 -1
- package/dist/css/spark.css +1 -0
- package/dist/css/spotlight.css +1 -1
- package/dist/css/table.css +1 -1
- package/dist/css/term.css +1 -0
- package/dist/css/textref.css +1 -0
- package/dist/css/toc.css +1 -0
- package/dist/css/tokens.css +1 -1
- package/dist/css/tree.css +1 -0
- package/dist/css/workbench.css +1 -1
- package/docs/adr/0003-theme-model.md +1 -1
- package/docs/annotations.md +133 -14
- package/docs/architecture.md +49 -6
- package/docs/bullet.md +78 -0
- package/docs/code.md +76 -0
- package/docs/contrast.md +116 -92
- package/docs/d2.md +196 -0
- package/docs/diff.md +146 -0
- package/docs/legends.md +23 -3
- package/docs/marks.md +9 -2
- package/docs/mermaid.md +169 -0
- package/docs/reference.md +201 -26
- package/docs/reporting.md +416 -57
- package/docs/sidenote.md +64 -0
- package/docs/sources.md +27 -0
- package/docs/spark.md +78 -0
- package/docs/stability.md +10 -2
- package/docs/term.md +81 -0
- package/docs/textref.md +78 -0
- package/docs/theming.md +44 -5
- package/docs/toc.md +83 -0
- package/docs/tree.md +74 -0
- package/docs/usage.md +354 -16
- package/docs/vega.md +244 -0
- package/docs/workbench.md +7 -1
- package/glyphs/glyphs.js +13 -5
- package/llms.txt +285 -14
- package/package.json +95 -17
- package/qwik/index.d.ts +44 -59
- package/qwik/index.d.ts.map +1 -0
- package/qwik/index.js +65 -3
- package/react/index.d.ts +41 -61
- package/react/index.d.ts.map +1 -0
- package/react/index.js +63 -3
- package/solid/index.d.ts +68 -61
- package/solid/index.d.ts.map +1 -0
- package/solid/index.js +66 -3
- package/tokens/d2.d.ts +38 -0
- package/tokens/d2.js +71 -0
- package/tokens/d2.json +43 -0
- package/tokens/index.d.ts +5 -5
- package/tokens/index.js +15 -1
- package/tokens/index.json +9 -0
- package/tokens/mermaid.d.ts +23 -0
- package/tokens/mermaid.js +181 -0
- package/tokens/mermaid.json +163 -0
- package/tokens/resolved.json +45 -1
- package/tokens/skins.js +3 -2
- package/tokens/tokens.dtcg.json +26 -0
- package/tokens/vega.d.ts +34 -0
- package/tokens/vega.js +155 -0
- package/tokens/vega.json +179 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position a spotlight cutout over a target element. Each
|
|
3
|
+
* `[data-bronto-spotlight]` is a `.ui-spotlight` overlay; `data-target` is the
|
|
4
|
+
* id of the element to highlight. The behavior measures the target and sets
|
|
5
|
+
* `--spot-x/y/w/h` (viewport coordinates) on the overlay, re-placing on
|
|
6
|
+
* resize/scroll and whenever `data-target` changes.
|
|
7
|
+
*
|
|
8
|
+
* Bronto owns only positioning + the visual language. It is NOT a tour engine:
|
|
9
|
+
* the host decides which target is current, when to advance, and whether to
|
|
10
|
+
* show/hide the overlay — just update `data-target` (or toggle `hidden`) and
|
|
11
|
+
* the cutout follows. SSR-safe, idempotent per host; returns a cleanup.
|
|
12
|
+
*
|
|
13
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
14
|
+
* @returns {import('./internal.js').Cleanup}
|
|
15
|
+
*/
|
|
16
|
+
export function initSpotlight({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
17
|
+
//# sourceMappingURL=spotlight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spotlight.d.ts","sourceRoot":"","sources":["spotlight.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CAuC3C"}
|
package/behaviors/spotlight.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hasDom, noop, bindOnce, byIdInHost } from './internal.js';
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce, byIdInHost, collectHosts } from './internal.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Position a spotlight cutout over a target element. Each
|
|
@@ -11,13 +11,15 @@ import { hasDom, noop, bindOnce, byIdInHost } from './internal.js';
|
|
|
11
11
|
* the host decides which target is current, when to advance, and whether to
|
|
12
12
|
* show/hide the overlay — just update `data-target` (or toggle `hidden`) and
|
|
13
13
|
* the cutout follows. SSR-safe, idempotent per host; returns a cleanup.
|
|
14
|
+
*
|
|
15
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
16
|
+
* @returns {import('./internal.js').Cleanup}
|
|
14
17
|
*/
|
|
15
18
|
export function initSpotlight({ root } = {}) {
|
|
16
19
|
if (!hasDom()) return noop;
|
|
17
|
-
const host = root
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
spots.push(...host.querySelectorAll('[data-bronto-spotlight]'));
|
|
20
|
+
const host = resolveHost(root);
|
|
21
|
+
if (!host) return noop;
|
|
22
|
+
const spots = collectHosts(host, '[data-bronto-spotlight]');
|
|
21
23
|
if (!spots.length) return noop;
|
|
22
24
|
|
|
23
25
|
const place = () => {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side sortable + selectable data table. Wires
|
|
3
|
+
* `[data-bronto-sortable]`:
|
|
4
|
+
*
|
|
5
|
+
* - clicking a header's `.ui-table__sort` button sorts the tbody by
|
|
6
|
+
* that column. Sortable headers are seeded `aria-sort="none"`; a
|
|
7
|
+
* click toggles that header ascending ⇄ descending (first click =
|
|
8
|
+
* ascending) and resets the other sortable headers to `none`.
|
|
9
|
+
* Numeric columns (`data-sort="num"` or `.is-num` cells) sort
|
|
10
|
+
* numerically; everything else, locale string compare. Any
|
|
11
|
+
* `.ui-table__empty` sentinel row is kept last after a sort.
|
|
12
|
+
* - a `[data-bronto-select-all]` checkbox toggles every
|
|
13
|
+
* `[data-bronto-select]` row checkbox and the rows'
|
|
14
|
+
* `aria-selected`; toggling a row keeps the header checkbox's
|
|
15
|
+
* checked/indeterminate state in sync. Emits `bronto:selectionchange`
|
|
16
|
+
* ({ detail: { count } }) on the table.
|
|
17
|
+
*
|
|
18
|
+
* SSR-safe, idempotent per table; returns a cleanup function.
|
|
19
|
+
*
|
|
20
|
+
* The numeric sort parses each cell's display text after normalizing the
|
|
21
|
+
* common report shapes: a Unicode minus (U+2212) and en/em dashes count as a
|
|
22
|
+
* sign (so a "−5" loss sorts BELOW a "5" gain, not above it), accounting
|
|
23
|
+
* parentheses `(1,234)` read as negative, and `,` thousands separators are
|
|
24
|
+
* dropped. Note the consequence: a bare `,` in the *display text* is read as a
|
|
25
|
+
* thousands separator, so a European decimal "3,5" sorts as 35, not 3.5 — for a
|
|
26
|
+
* European decimal comma (or mixed units, or any ambiguous text) put the
|
|
27
|
+
* canonical number in a `data-sort-value` attribute on the cell. That escape
|
|
28
|
+
* hatch wins over the parsed text and accepts either a dot ("3.5") or a single
|
|
29
|
+
* decimal comma ("3,5"). It is a client-side convenience sorter, not a data
|
|
30
|
+
* grid. (component audit C3/C5.)
|
|
31
|
+
*
|
|
32
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
33
|
+
* @returns {import('./internal.js').Cleanup}
|
|
34
|
+
*/
|
|
35
|
+
export function initTableSort({ root }?: import("./internal.js").DelegateOpts): import("./internal.js").Cleanup;
|
|
36
|
+
//# sourceMappingURL=table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["table.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,yCAHW,OAAO,eAAe,EAAE,YAAY,GAClC,OAAO,eAAe,EAAE,OAAO,CA6I3C"}
|
package/behaviors/table.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { hasDom, noop, bindOnce } from './internal.js';
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce, collectHosts } from './internal.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Client-side sortable + selectable data table. Wires
|
|
5
5
|
* `[data-bronto-sortable]`:
|
|
6
6
|
*
|
|
7
|
-
* - clicking a header's `.ui-table__sort`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* - clicking a header's `.ui-table__sort` button sorts the tbody by
|
|
8
|
+
* that column. Sortable headers are seeded `aria-sort="none"`; a
|
|
9
|
+
* click toggles that header ascending ⇄ descending (first click =
|
|
10
|
+
* ascending) and resets the other sortable headers to `none`.
|
|
10
11
|
* Numeric columns (`data-sort="num"` or `.is-num` cells) sort
|
|
11
|
-
* numerically; everything else, locale string compare.
|
|
12
|
+
* numerically; everything else, locale string compare. Any
|
|
13
|
+
* `.ui-table__empty` sentinel row is kept last after a sort.
|
|
12
14
|
* - a `[data-bronto-select-all]` checkbox toggles every
|
|
13
15
|
* `[data-bronto-select]` row checkbox and the rows'
|
|
14
16
|
* `aria-selected`; toggling a row keeps the header checkbox's
|
|
@@ -16,40 +18,101 @@ import { hasDom, noop, bindOnce } from './internal.js';
|
|
|
16
18
|
* ({ detail: { count } }) on the table.
|
|
17
19
|
*
|
|
18
20
|
* SSR-safe, idempotent per table; returns a cleanup function.
|
|
21
|
+
*
|
|
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}
|
|
19
36
|
*/
|
|
20
37
|
export function initTableSort({ root } = {}) {
|
|
21
38
|
if (!hasDom()) return noop;
|
|
22
|
-
const host = root
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
tables.push(...(host.querySelectorAll?.('[data-bronto-sortable]') ?? []));
|
|
39
|
+
const host = resolveHost(root);
|
|
40
|
+
if (!host) return noop;
|
|
41
|
+
const tables = collectHosts(host, '[data-bronto-sortable]');
|
|
26
42
|
const cleanups = [];
|
|
27
43
|
|
|
28
44
|
for (const table of tables) {
|
|
29
45
|
const tbody = table.tBodies[0];
|
|
30
46
|
if (!tbody) continue;
|
|
31
47
|
|
|
48
|
+
// Seed the resting `aria-sort="none"` on every sortable header so AT
|
|
49
|
+
// announces the column as sortable from the start (it was unset until the
|
|
50
|
+
// first click — C10).
|
|
51
|
+
for (const sort of table.querySelectorAll('.ui-table__sort')) {
|
|
52
|
+
const th = sort.closest('th');
|
|
53
|
+
if (th && !th.hasAttribute('aria-sort')) th.setAttribute('aria-sort', 'none');
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
const colIndex = (th) => [...th.parentElement.children].indexOf(th);
|
|
33
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
|
+
};
|
|
34
90
|
|
|
35
91
|
const sortBy = (th, numeric) => {
|
|
36
92
|
const headers = th.closest('tr').querySelectorAll('th');
|
|
37
93
|
const dir = th.getAttribute('aria-sort') === 'ascending' ? 'descending' : 'ascending';
|
|
38
|
-
headers
|
|
94
|
+
// Reset the OTHER sortable headers to `none` (not removed) so they keep
|
|
95
|
+
// announcing sortability; only previously-sortable headers carry aria-sort.
|
|
96
|
+
headers.forEach((h) => {
|
|
97
|
+
if (h !== th && h.hasAttribute('aria-sort')) h.setAttribute('aria-sort', 'none');
|
|
98
|
+
});
|
|
39
99
|
th.setAttribute('aria-sort', dir);
|
|
40
100
|
const i = colIndex(th);
|
|
41
101
|
const sign = dir === 'ascending' ? 1 : -1;
|
|
102
|
+
// Empty/sentinel rows sort out of the data set AND must re-append LAST,
|
|
103
|
+
// or after a sort they float above the real rows (C29).
|
|
104
|
+
const emptyRows = [...tbody.rows].filter((r) => r.classList.contains('ui-table__empty'));
|
|
42
105
|
const rows = [...tbody.rows].filter((r) => !r.classList.contains('ui-table__empty'));
|
|
43
106
|
rows.sort((a, b) => {
|
|
44
|
-
const x = cellText(a, i);
|
|
45
|
-
const y = cellText(b, i);
|
|
46
107
|
const cmp = numeric
|
|
47
|
-
? (
|
|
48
|
-
|
|
49
|
-
: x.localeCompare(y);
|
|
108
|
+
? cellNum(a, i) - cellNum(b, i)
|
|
109
|
+
: cellText(a, i).localeCompare(cellText(b, i));
|
|
50
110
|
return cmp * sign;
|
|
51
111
|
});
|
|
52
|
-
rows
|
|
112
|
+
// Re-parent in document order: sorted data rows, then any empty/sentinel
|
|
113
|
+
// row last. A single appendChild pass over the existing <tr> nodes (no
|
|
114
|
+
// markup is created — these are trusted DOM elements being moved).
|
|
115
|
+
for (const r of [...rows, ...emptyRows]) tbody.appendChild(r);
|
|
53
116
|
};
|
|
54
117
|
|
|
55
118
|
const allBox = table.querySelector('[data-bronto-select-all]');
|
|
@@ -71,7 +134,11 @@ export function initTableSort({ root } = {}) {
|
|
|
71
134
|
};
|
|
72
135
|
|
|
73
136
|
const onClick = (e) => {
|
|
74
|
-
|
|
137
|
+
// Only the focusable `.ui-table__sort` button is a sort trigger — it is
|
|
138
|
+
// keyboard-operable and carries the `::after` sort glyph. The bare
|
|
139
|
+
// `th[data-sort]` path was mouse-only with no affordance, so it is gone
|
|
140
|
+
// (C10); `data-sort="num"` is still read from the button or its th.
|
|
141
|
+
const sorter = e.target.closest('.ui-table__sort');
|
|
75
142
|
if (sorter && table.contains(sorter)) {
|
|
76
143
|
const th = sorter.closest('th');
|
|
77
144
|
const numeric =
|
|
@@ -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
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import { hasDom, noop, bindOnce } from './internal.js';
|
|
2
|
-
|
|
3
|
-
// Module-global so tab ids stay unique across *every* initTabs() call.
|
|
4
|
-
// A per-call counter makes separate islands/roots all mint `bronto-tab-1`,
|
|
5
|
-
// which collides aria-controls/aria-labelledby across the document.
|
|
6
|
-
let tabUid = 0;
|
|
1
|
+
import { hasDom, resolveHost, noop, bindOnce, nextFieldUid, collectHosts } from './internal.js';
|
|
7
2
|
|
|
8
3
|
/**
|
|
9
4
|
* Wire `[data-bronto-tabs]` groups for full keyboard a11y. The framework
|
|
@@ -19,16 +14,16 @@ let tabUid = 0;
|
|
|
19
14
|
* `initTabs` is guaranteed to run client-side — without it the panels
|
|
20
15
|
* stay hidden with no keyboard/pointer way to reveal them. Prefer
|
|
21
16
|
* authoring all panels visible and letting `initTabs` add `hidden`.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('./internal.js').DelegateOpts} [opts]
|
|
19
|
+
* @returns {import('./internal.js').Cleanup}
|
|
22
20
|
*/
|
|
23
21
|
export function initTabs({ root } = {}) {
|
|
24
22
|
if (!hasDom()) return noop;
|
|
25
|
-
const host = root
|
|
23
|
+
const host = resolveHost(root);
|
|
24
|
+
if (!host) return noop;
|
|
26
25
|
const cleanups = [];
|
|
27
|
-
|
|
28
|
-
// tab group would be skipped — include it explicitly.
|
|
29
|
-
const groups = [];
|
|
30
|
-
if (host !== document && host.matches?.('[data-bronto-tabs]')) groups.push(host);
|
|
31
|
-
groups.push(...host.querySelectorAll('[data-bronto-tabs]'));
|
|
26
|
+
const groups = collectHosts(host, '[data-bronto-tabs]');
|
|
32
27
|
for (const group of groups) {
|
|
33
28
|
// Own group only — a tab/panel inside a nested [data-bronto-tabs]
|
|
34
29
|
// belongs to that inner group, not this one.
|
|
@@ -44,7 +39,7 @@ export function initTabs({ root } = {}) {
|
|
|
44
39
|
for (const t of tabs) {
|
|
45
40
|
const p = panels.find((x) => x.dataset.panel === t.dataset.tab);
|
|
46
41
|
if (!p) continue;
|
|
47
|
-
const n =
|
|
42
|
+
const n = nextFieldUid();
|
|
48
43
|
if (!t.id) t.id = `bronto-tab-${n}`;
|
|
49
44
|
if (!p.id) p.id = `bronto-tabpanel-${n}`;
|
|
50
45
|
t.setAttribute('aria-controls', p.id);
|
|
@@ -59,9 +54,17 @@ export function initTabs({ root } = {}) {
|
|
|
59
54
|
t.setAttribute('aria-selected', String(on));
|
|
60
55
|
t.tabIndex = on ? 0 : -1;
|
|
61
56
|
}
|
|
57
|
+
// Only retarget panels when this tab actually controls one. A panel-less
|
|
58
|
+
// tab must NOT hide every panel — leave the prior panel visible (C30).
|
|
59
|
+
if (!panels.some((p) => p.dataset.panel === tab.dataset.tab)) return;
|
|
62
60
|
for (const p of panels) {
|
|
63
61
|
p.setAttribute('role', 'tabpanel');
|
|
64
|
-
|
|
62
|
+
const shown = p.dataset.panel === tab.dataset.tab;
|
|
63
|
+
p.hidden = !shown;
|
|
64
|
+
// APG: a tabpanel is focusable so keyboard users can reach a text-only
|
|
65
|
+
// panel; hidden panels drop out of the tab order (C30).
|
|
66
|
+
if (shown) p.tabIndex = 0;
|
|
67
|
+
else p.removeAttribute('tabindex');
|
|
65
68
|
}
|
|
66
69
|
};
|
|
67
70
|
const onClick = (e) => {
|
|
@@ -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"}
|
package/behaviors/theme.js
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
|
-
import { hasDom, noop, bindOnce } from './internal.js';
|
|
1
|
+
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;
|
|
12
|
-
const el = root
|
|
26
|
+
const el = resolveHost(root, document.documentElement);
|
|
27
|
+
if (!el) return;
|
|
13
28
|
let stored = null;
|
|
14
29
|
try {
|
|
15
30
|
stored = localStorage.getItem(storageKey);
|
|
@@ -30,10 +45,14 @@ export function applyStoredTheme({ storageKey = 'bronto-theme', root } = {}) {
|
|
|
30
45
|
*
|
|
31
46
|
* `root` scopes event delegation and which controls are queried/reflected
|
|
32
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}
|
|
33
51
|
*/
|
|
34
52
|
export function initThemeToggle({ storageKey = 'bronto-theme', root } = {}) {
|
|
35
53
|
if (!hasDom()) return noop;
|
|
36
|
-
const host = root
|
|
54
|
+
const host = resolveHost(root);
|
|
55
|
+
if (!host) return noop;
|
|
37
56
|
const docEl = document.documentElement;
|
|
38
57
|
|
|
39
58
|
const prefersDark = () =>
|
|
@@ -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"}
|
package/behaviors/toast.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { hasDom, noop } from './internal.js';
|
|
2
2
|
|
|
3
|
+
// The tones that have a `.ui-toast--*` rule. The TS type already unions these,
|
|
4
|
+
// but a plain-JS / LLM caller can pass any string — and an unknown tone built a
|
|
5
|
+
// `.ui-toast--error` class that matches no CSS, yielding a silent neutral toast.
|
|
6
|
+
// Validate so an unknown tone degrades to neutral *and warns*, never lies. (C16)
|
|
7
|
+
const TOAST_TONES = new Set(['accent', 'success', 'warning', 'danger', 'info']);
|
|
8
|
+
|
|
3
9
|
// First-toast deferral queue. The very first toast on a brand-new stack
|
|
4
10
|
// is appended next frame so AT observes the empty aria-live region
|
|
5
11
|
// before its first child. Any further toasts created *before* that frame
|
|
@@ -19,7 +25,13 @@ function toastStack(isAssertive) {
|
|
|
19
25
|
stack = document.createElement('div');
|
|
20
26
|
stack.className = isAssertive ? 'ui-toast-stack ui-toast-stack--assertive' : 'ui-toast-stack';
|
|
21
27
|
stack.setAttribute('aria-live', isAssertive ? 'assertive' : 'polite');
|
|
22
|
-
if (isAssertive)
|
|
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
|
+
}
|
|
23
35
|
document.body.appendChild(stack);
|
|
24
36
|
}
|
|
25
37
|
return { stack, fresh };
|
|
@@ -43,9 +55,20 @@ function enqueueToast(place, freshStack) {
|
|
|
43
55
|
|
|
44
56
|
function toastElement(message, { tone, title }) {
|
|
45
57
|
const el = document.createElement('div');
|
|
46
|
-
|
|
58
|
+
const validTone = tone && TOAST_TONES.has(tone) ? tone : null;
|
|
59
|
+
if (tone && !validTone && typeof console !== 'undefined') {
|
|
60
|
+
console.warn(
|
|
61
|
+
`[bronto] toast(): unknown tone "${tone}" — expected ${[...TOAST_TONES].join('/')}. Rendering neutral.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
el.className = validTone ? `ui-toast ui-toast--${validTone}` : 'ui-toast';
|
|
47
65
|
// No per-item role: the stack itself is the live region; a nested
|
|
48
|
-
// 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');
|
|
49
72
|
if (title) {
|
|
50
73
|
const t = document.createElement('p');
|
|
51
74
|
t.className = 'ui-toast__title';
|
|
@@ -98,6 +121,15 @@ function addToastClose(el, dismiss) {
|
|
|
98
121
|
el.appendChild(close);
|
|
99
122
|
}
|
|
100
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
|
+
|
|
101
133
|
/**
|
|
102
134
|
* Push a transient toast into a shared, screen-anchored stack. The stack
|
|
103
135
|
* is the `aria-live="polite"` region: it is created once, appended to
|
|
@@ -110,6 +142,10 @@ function addToastClose(el, dismiss) {
|
|
|
110
142
|
* optional uppercase label; `duration` ms before auto-dismiss (0 keeps
|
|
111
143
|
* it until dismissed). Returns a function that dismisses the toast
|
|
112
144
|
* early. SSR-safe (no-op).
|
|
145
|
+
*
|
|
146
|
+
* @param {string} message
|
|
147
|
+
* @param {ToastOpts} [opts]
|
|
148
|
+
* @returns {import('./internal.js').Cleanup}
|
|
113
149
|
*/
|
|
114
150
|
export function toast(message, { tone, title, duration = 4000, assertive, closable } = {}) {
|
|
115
151
|
if (!hasDom()) return noop;
|
|
@@ -146,6 +182,14 @@ export function toast(message, { tone, title, duration = 4000, assertive, closab
|
|
|
146
182
|
// it gets a dismiss affordance by default; any toast can opt in via
|
|
147
183
|
// `closable`. The button carries no text node (glyph is a CSS
|
|
148
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
|
+
}
|
|
149
193
|
if (closable ?? duration === 0) addToastClose(el, dismiss);
|
|
150
194
|
if (duration > 0) timer = setTimeout(dismiss, duration);
|
|
151
195
|
return dismiss;
|