@wtfalch/design 0.8.0 → 0.9.0
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/README.md +18 -0
- package/dist/components/Button.js +8 -7
- package/dist/components/Callout.js +1 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +21 -4
- package/dist/components/Command.js +2 -2
- package/dist/components/Empty.js +22 -1
- package/dist/components/Input.js +4 -4
- package/dist/components/Kbd.js +11 -1
- package/dist/components/Menu.js +3 -3
- package/dist/components/Modal.js +2 -2
- package/dist/components/Pagination.js +6 -1
- package/dist/components/Progress.js +8 -1
- package/dist/components/Rows.d.ts +1 -1
- package/dist/components/Rows.js +1 -1
- package/dist/components/Select.js +2 -2
- package/dist/components/Shell.js +11 -1
- package/dist/components/SizeGrid.js +14 -2
- package/dist/components/Stat.js +11 -2
- package/dist/components/Table.js +7 -1
- package/dist/components/Toast.js +1 -1
- package/dist/components/Tooltip.js +16 -2
- package/dist/components/Tour.js +1 -1
- package/dist/styles/index.css +1028 -415
- package/dist/tf.css +1028 -415
- package/dist/valet.css +1028 -415
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -7,6 +7,24 @@ product can look like itself without forking the stylesheet.
|
|
|
7
7
|
pnpm add @wtfalch/design
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
+
## What is public
|
|
11
|
+
|
|
12
|
+
The components, and the tokens. Not the classes.
|
|
13
|
+
|
|
14
|
+
A class in this package is an implementation detail: `.card`, `.set-row`,
|
|
15
|
+
`.pill` and the rest may be renamed or deleted whenever the component that
|
|
16
|
+
draws them changes, and 0.9.0 did exactly that to several of them. If you
|
|
17
|
+
need a card, render `<Card>`; if you need a row of your own, write it in the
|
|
18
|
+
token vocabulary:
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
.app-row { display: flex; gap: var(--space-3); align-items: center; }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`tokens.css` ships separately for that, and every value a theme can change is
|
|
25
|
+
in it. An app that wants utilities should run Tailwind over its own source --
|
|
26
|
+
this package's stylesheet is compiled and does not need it.
|
|
27
|
+
|
|
10
28
|
```ts
|
|
11
29
|
import '@wtfalch/design/tokens.css' // the vocabulary and its base values
|
|
12
30
|
import '@wtfalch/design/styles.css' // the components
|
|
@@ -69,10 +69,16 @@ const blockActivation = (event) => {
|
|
|
69
69
|
export default function Button(props) {
|
|
70
70
|
const { kind = 'default', disabled, size = 'md', busy, block, iconOnly, className } = props;
|
|
71
71
|
const classes = [
|
|
72
|
+
/* Always, on both elements. Every rule that used to hang off the `button`
|
|
73
|
+
element now hangs off this class, so the styling belongs to this
|
|
74
|
+
component rather than to whatever happens to be a `<button>` -- which
|
|
75
|
+
is what let a tab, a row and a card inherit control styling and then
|
|
76
|
+
spend five declarations undoing it. */
|
|
77
|
+
'btn',
|
|
72
78
|
iconOnly ? 'icon-btn' : '',
|
|
73
79
|
kind === 'default' ? '' : kind,
|
|
74
80
|
size === 'md' ? '' : `size-${size}`,
|
|
75
|
-
block ? 'block' : '',
|
|
81
|
+
block ? 'ctl-block' : '',
|
|
76
82
|
className ?? '',
|
|
77
83
|
]
|
|
78
84
|
.filter(Boolean)
|
|
@@ -108,12 +114,7 @@ export default function Button(props) {
|
|
|
108
114
|
el.removeAttribute('aria-busy');
|
|
109
115
|
}, [busy]);
|
|
110
116
|
if (props.asChild) {
|
|
111
|
-
|
|
112
|
-
stylesheet is written against the `button` element, which an anchor is
|
|
113
|
-
not, so the slotted element needs a hook of its own. Adding it here
|
|
114
|
-
rather than to `classes` above keeps the button's own class list, and
|
|
115
|
-
therefore every committed baseline, exactly as it was. */
|
|
116
|
-
const slotClasses = ['btn', classes].filter(Boolean).join(' ');
|
|
117
|
+
const slotClasses = classes;
|
|
117
118
|
/* Narrowed by `props.asChild`, so what is left after the component's own
|
|
118
119
|
props is the DOM attribute set -- which is why an unknown `data-*`
|
|
119
120
|
reaches the child untouched. */
|
|
@@ -70,7 +70,7 @@ export default function Callout({ tone, children, icon, timed, onDismiss, classN
|
|
|
70
70
|
return (_jsxs("div", { className: `callout${tone ? ` callout-${tone}` : ''}${running ? ' callout-timed' : ''}${className ? ` ${className}` : ''}`,
|
|
71
71
|
/* An alert interrupts whatever is being read, which is right for bad news
|
|
72
72
|
and rude for "saved". */
|
|
73
|
-
role: tone === 'bad' ? 'alert' : 'status', onMouseEnter: () => running && setPaused(true), onMouseLeave: () => running && setPaused(false), onFocusCapture: () => running && setPaused(true), onBlurCapture: () => running && setPaused(false), children: [mark && (_jsx("span", { className: "callout-mark", "aria-hidden": "true", children: _jsx(Icon, { name: mark, size: 16 }) })), _jsx("div", { className: "grow", children: children }), running && (
|
|
73
|
+
role: tone === 'bad' ? 'alert' : 'status', onMouseEnter: () => running && setPaused(true), onMouseLeave: () => running && setPaused(false), onFocusCapture: () => running && setPaused(true), onBlurCapture: () => running && setPaused(false), children: [mark && (_jsx("span", { className: "callout-mark", "aria-hidden": "true", children: _jsx(Icon, { name: mark, size: 16 }) })), _jsx("div", { className: "ctl-grow", children: children }), running && (
|
|
74
74
|
/* Decoration: the time remaining is not information anybody can act on,
|
|
75
75
|
and a screen reader counting down a bar is noise on top of a message
|
|
76
76
|
it has already read out. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* A box with a reason to exist.
|
|
3
3
|
*
|
|
4
4
|
* `.card` was CSS and nothing else, so every caller assembled its own header:
|
|
5
|
-
* a `<div className="row">`, a `<div className="grow">`, a title in a
|
|
5
|
+
* a `<div className="row">`, a `<div className="ctl-grow">`, a title in a
|
|
6
6
|
* `truncate` span and a description in a `muted mono` div, with the spacing
|
|
7
7
|
* retyped each time. Four variants of that shape existed and no two agreed on
|
|
8
8
|
* the gap between the title and the line under it.
|
package/dist/components/Card.js
CHANGED
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
* A box with a reason to exist.
|
|
11
11
|
*
|
|
12
12
|
* `.card` was CSS and nothing else, so every caller assembled its own header:
|
|
13
|
-
* a `<div className="row">`, a `<div className="grow">`, a title in a
|
|
13
|
+
* a `<div className="row">`, a `<div className="ctl-grow">`, a title in a
|
|
14
14
|
* `truncate` span and a description in a `muted mono` div, with the spacing
|
|
15
15
|
* retyped each time. Four variants of that shape existed and no two agreed on
|
|
16
16
|
* the gap between the title and the line under it.
|
|
@@ -27,9 +27,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
27
27
|
*/
|
|
28
28
|
import Icon from './Icon.js';
|
|
29
29
|
export default function Card({ icon, title, description, action, children, tone, onClick, selected, className, }) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const body = children && _jsx("div", { className: "grid gap-3", children: children });
|
|
31
|
+
/* `mb-3` when something follows, which is what `.card-head:not(:last-child)`
|
|
32
|
+
was saying. The component knows whether there is a body; a structural
|
|
33
|
+
pseudo-class was working that out from the DOM. */
|
|
34
|
+
const head = (title || description || action) && (_jsxs("div", { className: `card-head flex items-start gap-3${body ? ' mb-3' : ''}`, children: [icon && (_jsx("span", { className: "card-icon", "aria-hidden": "true", children: _jsx(Icon, { name: icon, size: 20 }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [title && _jsx("strong", { className: "card-title", children: title }), description && _jsx("div", { className: "card-desc", children: description })] }), action && _jsx("div", { className: "flex-none self-center flex items-center", children: action })] }));
|
|
35
|
+
/* `card` stays as the hook: the tones, the pick and selected states, and
|
|
36
|
+
`:has(.card-icon)` indenting an inline row past the icon, are all either
|
|
37
|
+
a `color-mix` or a relationship between elements. What is here is the
|
|
38
|
+
card's own box -- and `display: block` with `width: 100%` matter, because
|
|
39
|
+
a pressable card is a `<button>`, which shrink-wraps: a row of cards
|
|
40
|
+
became a row of labels the moment one gained an `onClick`. */
|
|
41
|
+
const cls = [
|
|
42
|
+
'card',
|
|
43
|
+
onClick ? 'card-pick' : '',
|
|
44
|
+
selected ? 'card-on' : '',
|
|
45
|
+
tone ? `card-${tone}` : '',
|
|
46
|
+
className,
|
|
47
|
+
]
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join(' ');
|
|
33
50
|
if (onClick) {
|
|
34
51
|
return (_jsxs("button", { type: "button", className: cls, "aria-pressed": selected, onClick: onClick, children: [head, body] }));
|
|
35
52
|
}
|
|
@@ -34,11 +34,11 @@ export default function Command({ open, onOpenChange, groups, placeholder = 'Sea
|
|
|
34
34
|
}, isDismissable: true, children: _jsx(AriaModal, { className: `cmd${className ? ` ${className}` : ''}`, children: _jsx(Dialog, { className: "cmd-body", "aria-label": label, children: ({ close }) => (_jsxs(Autocomplete, { inputValue: query, onInputChange: setQuery, filter: () => true, children: [_jsxs(SearchField, { className: "cmd-field", "aria-label": label, autoFocus: true, children: [_jsx(Icon, { name: "chat", className: "cmd-mark" }), _jsx(Input, { className: "cmd-input", placeholder: placeholder })] }), empty ? (
|
|
35
35
|
/* The query, quoted. A palette that says "No results" to a
|
|
36
36
|
typo looks like a palette that is broken. */
|
|
37
|
-
_jsxs("p", { className: "cmd-empty", children: ["Nothing matches ", _jsx("strong", { children: query }), "."] })) : (_jsx(ListBox, { className: "cmd-list", "aria-label": label, selectionMode: "none", children: shown.map((group) => (_jsxs(ListBoxSection, {
|
|
37
|
+
_jsxs("p", { className: "cmd-empty", children: ["Nothing matches ", _jsx("strong", { children: query }), "."] })) : (_jsx(ListBox, { className: "cmd-list", "aria-label": label, selectionMode: "none", children: shown.map((group) => (_jsxs(ListBoxSection, { children: [_jsx(Header, { className: "cmd-group-title", children: group.title }), group.commands.map((command) => (_jsxs(ListBoxItem, { id: command.id, className: "cmd-item", textValue: command.label, isDisabled: command.disabled, onAction: () => {
|
|
38
38
|
command.onRun?.();
|
|
39
39
|
/* Closing is this component's job, not every
|
|
40
40
|
caller's. Forty `onRun`s that each remember to
|
|
41
41
|
close is thirty-nine chances to forget. */
|
|
42
42
|
close();
|
|
43
|
-
}, children: [command.icon && _jsx(Icon, { name: command.icon, className: "cmd-icon" }), _jsxs("span", { className: "cmd-text", children: [_jsx(Text, { slot: "label", className: "cmd-label", children: command.label }), command.description && (_jsx(Text, { slot: "description", className: "cmd-desc", children: command.description }))] }), command.shortcut && _jsx(Kbd, {
|
|
43
|
+
}, children: [command.icon && _jsx(Icon, { name: command.icon, className: "cmd-icon" }), _jsxs("span", { className: "cmd-text", children: [_jsx(Text, { slot: "label", className: "cmd-label", children: command.label }), command.description && (_jsx(Text, { slot: "description", className: "cmd-desc", children: command.description }))] }), command.shortcut && _jsx(Kbd, { children: command.shortcut })] }, command.id)))] }, group.title))) }))] })) }) }) }));
|
|
44
44
|
}
|
package/dist/components/Empty.js
CHANGED
|
@@ -31,5 +31,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
31
31
|
import Icon from './Icon.js';
|
|
32
32
|
import Illustration from './Illustration.js';
|
|
33
33
|
export default function Empty({ icon, illustration, children, action, className, }) {
|
|
34
|
-
return (_jsxs("div", { className:
|
|
34
|
+
return (_jsxs("div", { className: [
|
|
35
|
+
'nothing-surface-or-slot grid justify-items-center text-center rounded-md',
|
|
36
|
+
/* Dashed rather than solid: a solid border reads as a thing, and the
|
|
37
|
+
point of this box is that there is no thing -- it marks out where
|
|
38
|
+
the list will be. */
|
|
39
|
+
illustration
|
|
40
|
+
? /* A whole surface rather than a slot in a card. The outline goes,
|
|
41
|
+
because a window-sized dashed rectangle reads as a layout that
|
|
42
|
+
failed rather than a view with nothing on it, and the box
|
|
43
|
+
centres itself in whatever it was dropped into. */
|
|
44
|
+
'gap-3 py-10 px-4 m-auto'
|
|
45
|
+
: 'gap-2 py-5 px-4 border border-dashed border-border-strong',
|
|
46
|
+
className,
|
|
47
|
+
]
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join(' '), children: [illustration ? (
|
|
50
|
+
/* `currentColor` reaches the figure through the two rules the
|
|
51
|
+
illustrations ship with, so the colour is set here and the drawing
|
|
52
|
+
follows it -- the same grey as the sentence under it, in every
|
|
53
|
+
theme. `.illo` sets `color: inherit`, so this has to out-rank a
|
|
54
|
+
single class: `text-muted` on the element itself does. */
|
|
55
|
+
_jsx(Illustration, { name: illustration, size: 160, className: "nothing-figure" })) : (icon && (_jsx("span", { className: "text-muted leading-[0]", "aria-hidden": "true", children: _jsx(Icon, { name: icon, size: 20 }) }))), _jsx("p", { className: "m-0 text-muted text-sm leading-[1.5] max-w-[46ch]", children: children }), action && _jsx("div", { className: "mt-1", children: action })] }));
|
|
35
56
|
}
|
package/dist/components/Input.js
CHANGED
|
@@ -63,7 +63,7 @@ const Input = forwardRef(function Input({ size = 'md', mono, block, className, t
|
|
|
63
63
|
const classes = [
|
|
64
64
|
size === 'md' ? '' : `size-${size}`,
|
|
65
65
|
mono ? 'mono' : '',
|
|
66
|
-
block ? 'block' : '',
|
|
66
|
+
block ? 'ctl-block' : '',
|
|
67
67
|
className ?? '',
|
|
68
68
|
]
|
|
69
69
|
.filter(Boolean)
|
|
@@ -83,11 +83,11 @@ const Input = forwardRef(function Input({ size = 'md', mono, block, className, t
|
|
|
83
83
|
const filled = rest.value !== undefined
|
|
84
84
|
? String(rest.value).length > 0
|
|
85
85
|
: String(rest.defaultValue ?? '').length > 0;
|
|
86
|
-
return (_jsxs("span", { className: `adorned adorned-${size}${block ? ' block' : ''}${icon ? ' adorned-icon' : ''}`, children: [icon && _jsx(Icon, { name: icon, size: glyph, className: "adorned-mark", "aria-hidden": true }), box, (trailing || (onClear && filled)) && (_jsxs("span", { className: "adorned-end", children: [trailing, onClear && filled && (_jsx("button", { type: "button", className: "icon-btn
|
|
86
|
+
return (_jsxs("span", { className: `adorned adorned-${size}${block ? ' ctl-block' : ''}${icon ? ' adorned-icon' : ''}`, children: [icon && _jsx(Icon, { name: icon, size: glyph, className: "adorned-mark", "aria-hidden": true }), box, (trailing || (onClear && filled)) && (_jsxs("span", { className: "adorned-end", children: [trailing, onClear && filled && (_jsx("button", { type: "button", className: "icon-btn adorned-clear", "aria-label": "Clear", onClick: onClear, disabled: rest.disabled, children: _jsx(Icon, { name: "close", size: glyph }) }))] }))] }));
|
|
87
87
|
}
|
|
88
|
-
return (_jsxs("span", { className: `secret secret-${size}${block ? ' block' : ''}`, children: [_jsx("input", { ref: ref, type: shown ? 'text' : 'password', className: classes || undefined,
|
|
88
|
+
return (_jsxs("span", { className: `secret secret-${size}${block ? ' ctl-block' : ''}`, children: [_jsx("input", { ref: ref, type: shown ? 'text' : 'password', className: classes || undefined,
|
|
89
89
|
/* Only while revealed: as a password these are moot, and setting them
|
|
90
90
|
on a password field makes some browsers stop offering to fill it. */
|
|
91
|
-
autoCapitalize: shown ? 'off' : undefined, autoCorrect: shown ? 'off' : undefined, spellCheck: shown ? false : undefined, ...rest, ...wired }), _jsx(ToggleButton, { className: `icon-btn
|
|
91
|
+
autoCapitalize: shown ? 'off' : undefined, autoCorrect: shown ? 'off' : undefined, spellCheck: shown ? false : undefined, ...rest, ...wired }), _jsx(ToggleButton, { className: `icon-btn secret-eye${size === 'md' ? '' : ` size-${size}`}`, "aria-label": "Show password", isSelected: shown, onChange: setShown, isDisabled: rest.disabled, children: _jsx(Icon, { name: shown ? 'eye-off' : 'eye', size: glyph }) })] }));
|
|
92
92
|
});
|
|
93
93
|
export default Input;
|
package/dist/components/Kbd.js
CHANGED
|
@@ -23,6 +23,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
23
23
|
* carries the slot wiring that lets `Menu` and `Command` place it, so this is
|
|
24
24
|
* that component with the package's class on it rather than a new one.
|
|
25
25
|
*
|
|
26
|
+
* **Drawn with utilities.** Its five declarations were a `.kbd` rule that
|
|
27
|
+
* `Menu` and `Command` reached for by class name; they are on the element
|
|
28
|
+
* now, so the component carries its own appearance and nothing else has to
|
|
29
|
+
* know the class exists.
|
|
30
|
+
*
|
|
26
31
|
* **It draws the reminder; it does not bind anything.** Same contract as
|
|
27
32
|
* `Menu`'s `shortcut` and `Command`'s: what actually listens for the chord is
|
|
28
33
|
* the app's business, and a component that both drew a key and bound it would
|
|
@@ -30,5 +35,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
30
35
|
*/
|
|
31
36
|
import { Keyboard } from 'react-aria-components';
|
|
32
37
|
export default function Kbd({ children, className, }) {
|
|
33
|
-
|
|
38
|
+
/* Utilities rather than a `.kbd` rule. Five declarations, each of which
|
|
39
|
+
reads as what it is at the call site: it never shrinks in a flex row, it
|
|
40
|
+
is the theme's mono face at the smallest step, and it is quiet. */
|
|
41
|
+
return (_jsx(Keyboard, { className: ['flex-none font-mono text-2xs text-muted tracking-normal', className]
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join(' '), children: children }));
|
|
34
44
|
}
|
package/dist/components/Menu.js
CHANGED
|
@@ -17,12 +17,12 @@ function renderItem(item) {
|
|
|
17
17
|
/* Typeahead needs a string, and `label` may be a node. Without this,
|
|
18
18
|
typing the first letter of an item whose label is markup matches
|
|
19
19
|
nothing and the menu looks broken. */
|
|
20
|
-
textValue: typeof item.label === 'string' ? item.label : item.id, children: [item.icon && _jsx(Icon, { name: item.icon, className: "menu-icon" }), _jsxs("span", { className: "menu-text", children: [_jsx(Text, { slot: "label", className: "menu-label", children: item.label }), item.description && (_jsx(Text, { slot: "description", className: "menu-desc", children: item.description }))] }), item.shortcut && _jsx(Kbd, {
|
|
21
|
-
const body = item.items ? (_jsxs(SubmenuTrigger, { children: [row, _jsx(AriaPopover, { className: "menu-sheet", children: _jsx(AriaMenu, { className: "
|
|
20
|
+
textValue: typeof item.label === 'string' ? item.label : item.id, children: [item.icon && _jsx(Icon, { name: item.icon, className: "menu-icon" }), _jsxs("span", { className: "menu-text", children: [_jsx(Text, { slot: "label", className: "menu-label", children: item.label }), item.description && (_jsx(Text, { slot: "description", className: "menu-desc", children: item.description }))] }), item.shortcut && _jsx(Kbd, { children: item.shortcut }), item.items && (_jsx("svg", { className: "menu-more", viewBox: "0 0 24 24", width: "12", height: "12", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "m9 6 6 6-6 6" }) }))] }, item.id));
|
|
21
|
+
const body = item.items ? (_jsxs(SubmenuTrigger, { children: [row, _jsx(AriaPopover, { className: "menu-sheet surface-panel border border-border rounded-md min-w-[12rem] max-w-[min(20rem,calc(100vw-var(--space-6)))] max-h-[24rem] overflow-auto overscroll-contain z-[60]", children: _jsx(AriaMenu, { className: "p-1 outline-none grid gap-[var(--border-width)]", children: item.items.map(renderItem) }) })] }, item.id)) : (row);
|
|
22
22
|
if (!item.separated)
|
|
23
23
|
return body;
|
|
24
24
|
return [_jsx(Separator, { className: "menu-rule" }, `${item.id}-rule`), body];
|
|
25
25
|
}
|
|
26
26
|
export default function Menu({ trigger, items, placement = 'bottom start', label, className, }) {
|
|
27
|
-
return (_jsxs(MenuTrigger, { children: [trigger, _jsx(AriaPopover, { className: `menu-sheet${className ? ` ${className}` : ''}`, placement: placement, offset: 6, children: _jsx(AriaMenu, { className: "
|
|
27
|
+
return (_jsxs(MenuTrigger, { children: [trigger, _jsx(AriaPopover, { className: `menu-sheet${className ? ` ${className}` : ''}`, placement: placement, offset: 6, children: _jsx(AriaMenu, { className: "p-1 outline-none grid gap-[var(--border-width)]", "aria-label": label, children: items.map((entry) => isSection(entry) ? (_jsxs(MenuSection, { children: [_jsx(Header, { className: "menu-section-title", children: entry.title }), entry.items.map(renderItem)] }, entry.title)) : (renderItem(entry))) }) })] }));
|
|
28
28
|
}
|
package/dist/components/Modal.js
CHANGED
|
@@ -75,7 +75,7 @@ export default function Modal({ title, description, subtitle, head, children, fo
|
|
|
75
75
|
return (_jsx(ModalOverlay, { className: `backdrop${edge ? ` sheeted from-${edge}` : ''}`, isOpen: true, isDismissable: canClose && dismissOnScrim, isKeyboardDismissDisabled: !canClose, onOpenChange: (open) => {
|
|
76
76
|
if (!open && canClose)
|
|
77
77
|
onClose?.();
|
|
78
|
-
}, children: _jsx(AriaModal, { className: `modal${edge ? ` sheet from-${edge}` : ''}${className ? ` ${className}` : ''}`, style: width ? { width } : undefined, children: _jsxs(Dialog, { ref: dialogRef, className: "modal-dialog", "aria-labelledby": labelledBy ?? (title ? headingId : undefined), children: [(title || head || (onClose && closeButton)) && (_jsxs("header", { className: "modal-head", children: [title && (
|
|
78
|
+
}, children: _jsx(AriaModal, { className: `modal${edge ? ` sheet from-${edge}` : ''}${className ? ` ${className}` : ''}`, style: width ? { width } : undefined, children: _jsxs(Dialog, { ref: dialogRef, className: "modal-dialog flex flex-col min-h-0 flex-1", "aria-labelledby": labelledBy ?? (title ? headingId : undefined), children: [(title || head || (onClose && closeButton)) && (_jsxs("header", { className: "modal-head", children: [title && (
|
|
79
79
|
/* `Heading` is what React Aria labels the dialog by, and it
|
|
80
80
|
renders an `<h2>` -- which the browser gives its own margins
|
|
81
81
|
and its own size (17px above and below, 21px type). The old
|
|
@@ -83,5 +83,5 @@ export default function Modal({ title, description, subtitle, head, children, fo
|
|
|
83
83
|
takes them back off so the `<strong>` inside draws exactly as
|
|
84
84
|
it did; without it every window opened with its title
|
|
85
85
|
floating in a band twice the height of the head. */
|
|
86
|
-
_jsx(Heading, { slot: "title", id: headingId, level: 2, className: "modal-title", children: _jsx("strong", { children: title }) })), subtitle, head, onClose && closeButton && (_jsx(Button, { iconOnly: true, className: "x", "aria-label": "Close", isDisabled: closeDisabled, onPress: () => onClose(), children: _jsx(Icon, { name: "close", size: 16 }) }))] })), description && _jsx("p", { className: "
|
|
86
|
+
_jsx(Heading, { slot: "title", id: headingId, level: 2, className: "modal-title m-0 [font:inherit]", children: _jsx("strong", { children: title }) })), subtitle, head, onClose && closeButton && (_jsx(Button, { iconOnly: true, className: "x", "aria-label": "Close", isDisabled: closeDisabled, onPress: () => onClose(), children: _jsx(Icon, { name: "close", size: 16 }) }))] })), description && _jsx("p", { className: "m-0 px-4 pb-3 text-muted text-sm", children: description }), _jsx("div", { className: `modal-body${bodyClass ? ` ${bodyClass}` : ''}`, children: children }), footer && (_jsx("footer", { className: `set-actions${footerClass ? ` ${footerClass}` : ''}`, children: footer }))] }) }) }));
|
|
87
87
|
}
|
|
@@ -99,7 +99,12 @@ export default function Pagination({ position, limit, total, count, onChange, la
|
|
|
99
99
|
exactly once, on a list whose length is a multiple of the limit, and costs
|
|
100
100
|
one empty page rather than a hidden remainder. */
|
|
101
101
|
const canGoOn = pages === undefined ? shown >= limit : page < pages;
|
|
102
|
-
return (_jsxs("nav", { className:
|
|
102
|
+
return (_jsxs("nav", { className: [
|
|
103
|
+
'flex items-center justify-between gap-4 flex-wrap py-2 text-xs text-muted',
|
|
104
|
+
className,
|
|
105
|
+
]
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join(' '), "aria-label": label, children: [_jsx("p", { className: "pager-count m-0", children: shown === 0 ? (`No ${unit}`) : (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [group(first), "\u2013", group(last)] }), total === undefined ? ` ${unit}` : ` of ${group(total)} ${unit}`] })) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("button", { type: "button", className: "pager-step", onClick: () => onChange(Math.max(0, position - limit)), disabled: !canGoBack, "aria-label": "Previous page", children: _jsx(Chevron, { back: true }) }), pages !== undefined && (_jsx("ol", { className: "pager-pages", children: pageWindow(page, pages).map((slot, index, slots) => slot === 'gap' ? (
|
|
103
108
|
/* Keyed by the page it follows rather than by its index: a
|
|
104
109
|
window holds at most two gaps and they elide different runs,
|
|
105
110
|
and an index key makes React reuse the wrong one -- which
|
|
@@ -23,5 +23,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
23
23
|
export default function Progress({ value, max, label, detail, tone, className, }) {
|
|
24
24
|
const indeterminate = value === undefined || !max;
|
|
25
25
|
const pct = indeterminate ? 0 : Math.max(0, Math.min(100, (value / max) * 100));
|
|
26
|
-
return (
|
|
26
|
+
return (
|
|
27
|
+
/* `progress` stays as a hook: `card.css` spaces a bar that follows a
|
|
28
|
+
card's contents, which is a relationship between two elements. The rest
|
|
29
|
+
is this component's own shape and reads better here. */
|
|
30
|
+
_jsxs("div", { className: ['progress grid gap-1', className].filter(Boolean).join(' '), children: [(label || detail) && (
|
|
31
|
+
/* Baseline, not centre: the label and the detail are one line of text
|
|
32
|
+
with two weights, and centring them puts the smaller one adrift. */
|
|
33
|
+
_jsxs("div", { className: "flex items-baseline gap-2 text-sm", children: [_jsx("span", { children: label }), _jsx("span", { className: "ctl-grow" }), detail && _jsx("span", { className: "text-muted text-xs font-mono", children: detail })] })), _jsx("div", { className: `bar${tone ? ` bar-${tone}` : ''}${indeterminate ? ' bar-indeterminate' : ''}`, role: "progressbar", "aria-label": label, "aria-valuenow": indeterminate ? undefined : Math.round(pct), "aria-valuemin": indeterminate ? undefined : 0, "aria-valuemax": indeterminate ? undefined : 100, "aria-valuetext": detail, children: _jsx("i", { style: indeterminate ? undefined : { width: `${pct}%` } }) })] }));
|
|
27
34
|
}
|
package/dist/components/Rows.js
CHANGED
|
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
13
13
|
* hand:
|
|
14
14
|
*
|
|
15
15
|
* <div className="set-row">
|
|
16
|
-
* <span className="grow" style={{ minWidth: 0 }}>
|
|
16
|
+
* <span className="ctl-grow" style={{ minWidth: 0 }}>
|
|
17
17
|
* <span className="named">
|
|
18
18
|
* <span className="truncate">{name}</span>
|
|
19
19
|
* …pills…
|
|
@@ -78,8 +78,8 @@ export default function Select({ block = false, size, className, children, value
|
|
|
78
78
|
`sel-value` and the caret are the same markup as before, so the closed
|
|
79
79
|
control is pixel-identical; the list is the same markup with React Aria's
|
|
80
80
|
state attributes where the classes were. */
|
|
81
|
-
return (_jsxs(AriaSelect, { className: `sel${block ? ' block' : ''}${className ? ` ${className}` : ''}`, selectedKey: value !== undefined ? String(value) : undefined, defaultSelectedKey: defaultValue !== undefined ? String(defaultValue) : (options[0]?.value ?? undefined), onSelectionChange: (key) => {
|
|
81
|
+
return (_jsxs(AriaSelect, { className: `sel${block ? ' ctl-block' : ''}${className ? ` ${className}` : ''}`, selectedKey: value !== undefined ? String(value) : undefined, defaultSelectedKey: defaultValue !== undefined ? String(defaultValue) : (options[0]?.value ?? undefined), onSelectionChange: (key) => {
|
|
82
82
|
if (key !== null)
|
|
83
83
|
onChange?.({ target: { value: String(key) } });
|
|
84
|
-
}, isDisabled: disabled, name: name, form: form, "aria-label": ariaLabel, "aria-labelledby": wiredLabelledBy, children: [_jsxs(Button, { ref: control, id: wiredId, "aria-describedby": ariaDescribedBy ?? field?.['aria-describedby'], className: `sel-control${size ? ` size-${size}` : ''}`, children: [_jsx(SelectValue, { className: "sel-value", children: ({ selectedText, defaultChildren }) => selectedText ?? defaultChildren }), _jsx("svg", { className: "sel-caret", viewBox: "0 0 24 24", width: "14", height: "14", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "m6 9 6 6 6-6" }) })] }), _jsx(Popover, { className: "sel-list", placement: "bottom start", offset: 4, maxHeight: 280, children: _jsx(ListBox, {
|
|
84
|
+
}, isDisabled: disabled, name: name, form: form, "aria-label": ariaLabel, "aria-labelledby": wiredLabelledBy, children: [_jsxs(Button, { ref: control, id: wiredId, "aria-describedby": ariaDescribedBy ?? field?.['aria-describedby'], className: `sel-control${size ? ` size-${size}` : ''}`, children: [_jsx(SelectValue, { className: "sel-value flex-1 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", children: ({ selectedText, defaultChildren }) => selectedText ?? defaultChildren }), _jsx("svg", { className: "sel-caret flex-none opacity-60", viewBox: "0 0 24 24", width: "14", height: "14", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "m6 9 6 6 6-6" }) })] }), _jsx(Popover, { className: "sel-list z-[60] max-h-[280px] overflow-y-auto m-0 p-1 list-none border border-border-strong rounded-md surface-panel", placement: "bottom start", offset: 4, maxHeight: 280, children: _jsx(ListBox, { children: options.map((o) => (_jsxs(ListBoxItem, { id: o.value, textValue: o.text, isDisabled: o.disabled, className: "sel-item flex items-center gap-2 p-2 rounded-sm cursor-pointer whitespace-nowrap", children: [_jsx("span", { className: "flex-1 min-w-0 overflow-hidden text-ellipsis", children: o.label }), _jsx("svg", { className: "sel-tick flex-none opacity-0", viewBox: "0 0 24 24", width: "13", height: "13", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", d: "m5 13 4 4 10-10" }) }), aside?.(o.value)] }, o.value))) }) })] }));
|
|
85
85
|
}
|
package/dist/components/Shell.js
CHANGED
|
@@ -22,5 +22,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
22
22
|
* app's to pass and stay its own client boundaries.
|
|
23
23
|
*/
|
|
24
24
|
export default function Shell({ brand, who, nav, wide = false, children, className, }) {
|
|
25
|
-
|
|
25
|
+
/* Utilities: this component is layout and nothing else, which is where they
|
|
26
|
+
earn their place. The measure is the one thing that is not a utility --
|
|
27
|
+
it is a number two elements have to agree on, so it is a custom property
|
|
28
|
+
on the frame rather than a literal repeated in three class lists, which
|
|
29
|
+
is how manage ended up with `head-inner-wide` and `main-wide` as separate
|
|
30
|
+
classes and had to remember both. */
|
|
31
|
+
const measure = wide ? 'max-w-[var(--shell-measure-wide)]' : 'max-w-[var(--shell-measure)]';
|
|
32
|
+
const band = `w-full mx-auto px-4 ${measure}`;
|
|
33
|
+
return (_jsxs("div", { className: ['shell flex flex-col min-h-screen surface-bg text-text', className]
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join(' '), children: [_jsxs("header", { className: "flex-none border-b border-border surface-panel", children: [_jsxs("div", { className: `${band} flex items-center justify-between gap-4 py-3 min-w-0`, children: [brand, who && _jsx("div", { className: "flex items-center gap-3 min-w-0", children: who })] }), nav && _jsx("div", { className: `${band} pb-2`, children: nav })] }), _jsx("main", { className: `${band} flex-1 py-6 min-w-0`, children: children })] }));
|
|
26
36
|
}
|
|
@@ -41,8 +41,20 @@ export default function SizeGrid({ value, max = { cols: 3, rows: 3 }, disabled =
|
|
|
41
41
|
for (let col = 1; col <= extent.cols; col++) {
|
|
42
42
|
const on = col <= shown.cols && row <= shown.rows;
|
|
43
43
|
const chosen = col <= value.cols && row <= value.rows;
|
|
44
|
-
cells.push(_jsx("button", { type: "button",
|
|
44
|
+
cells.push(_jsx("button", { type: "button",
|
|
45
|
+
/* `size-cell` stays as the hook for the two states: `on` is a fill
|
|
46
|
+
from `color-mix`, which is not a utility. The rest -- a painted
|
|
47
|
+
cell with no contents, so it says it is a block rather than
|
|
48
|
+
taking a control's arrangement -- is here. */
|
|
49
|
+
className: [
|
|
50
|
+
'size-cell block w-[18px] h-[14px] p-0 rounded-sm border border-border surface-panel-2 cursor-pointer',
|
|
51
|
+
'disabled:cursor-default disabled:opacity-50',
|
|
52
|
+
on ? 'on' : '',
|
|
53
|
+
chosen && !hover ? 'border-accent' : '',
|
|
54
|
+
]
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(' '), disabled: disabled, "aria-label": `${col} by ${row}`, onMouseEnter: () => !disabled && setHover({ cols: col, rows: row }), onFocus: () => !disabled && setHover({ cols: col, rows: row }), onClick: () => onChange({ cols: col, rows: row }) }, `${col}x${row}`));
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
|
-
return (_jsxs("div", { className: "
|
|
59
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "grid gap-1", style: { gridTemplateColumns: `repeat(${extent.cols}, 1fr)` }, onMouseLeave: () => setHover(null), children: cells }), _jsxs("span", { className: "set-hint mono", children: [shown.cols, " \u00D7 ", shown.rows, (extent.cols === max.cols || extent.rows === max.rows) && (_jsxs("span", { className: "opacity-[0.55]", children: [' ', "\u00B7 max ", max.cols, "\u00D7", max.rows] }))] })] }));
|
|
48
60
|
}
|
package/dist/components/Stat.js
CHANGED
|
@@ -26,6 +26,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
26
26
|
* A bordered tile inside a bordered row is two boxes saying one thing.
|
|
27
27
|
*/
|
|
28
28
|
export default function Stat({ label, value, note, tone, look = 'tile', className, }) {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/* Utilities, so the tile's shape reads where it is drawn. `bare` states
|
|
30
|
+
the compact shape rather than adding a class that undoes the tile's --
|
|
31
|
+
which is the mistake the old element reset made across this package.
|
|
32
|
+
`stat` and `stat-value` stay as hooks: the tone is an attribute on the
|
|
33
|
+
tile colouring a child, which is a relationship, and a utility is a
|
|
34
|
+
property on one element. */
|
|
35
|
+
const tile = look === 'bare'
|
|
36
|
+
? 'grid gap-0 leading-[1.25]'
|
|
37
|
+
: 'grid gap-1 p-4 border border-border rounded-md surface-panel';
|
|
38
|
+
const classes = ['stat', tile, '[&>p]:m-0', className].filter(Boolean).join(' ');
|
|
39
|
+
return (_jsxs("div", { className: classes, "data-tone": tone, children: [_jsx("p", { className: "text-xs text-muted", children: label }), _jsx("p", { className: `stat-value font-strong tabular-nums ${look === 'bare' ? 'text-sm' : 'text-lg'}`, children: value }), note && _jsx("p", { className: "text-xs text-muted", children: note })] }));
|
|
31
40
|
}
|
package/dist/components/Table.js
CHANGED
|
@@ -3,7 +3,13 @@ export default function Table({ caption, columns, rows, keyOf, empty, className,
|
|
|
3
3
|
if (!rows.length) {
|
|
4
4
|
return empty ? _jsx("div", { className: "set-hint rows-empty", children: empty }) : null;
|
|
5
5
|
}
|
|
6
|
-
return (
|
|
6
|
+
return (
|
|
7
|
+
/* The one thing about a table that is a property of one element. Its
|
|
8
|
+
cells, headings and last-row rule are structural -- `th:first-child`,
|
|
9
|
+
`tbody tr:last-child td` -- and a structural pseudo-class is the right
|
|
10
|
+
tool for them: expressing the same thing with utilities means this
|
|
11
|
+
component doing index arithmetic to work out what CSS already knows. */
|
|
12
|
+
_jsx("div", { className: ['overflow-x-auto', className].filter(Boolean).join(' '), children: _jsxs("table", { className: "data-table", children: [_jsx("caption", { className: "sr-only", children: caption }), _jsx("thead", { children: _jsx("tr", { children: columns.map((c) => (_jsx("th", { scope: "col", style: { width: c.width, textAlign: c.align === 'end' ? 'right' : undefined }, children: _jsx("span", { className: c.quiet ? 'sr-only' : undefined, children: c.header }) }, c.header))) }) }), _jsx("tbody", { children: rows.map((item) => (_jsx("tr", { children: columns.map((c, i) => {
|
|
7
13
|
/* The first cell is the row's name, so it is a `<th scope="row">`
|
|
8
14
|
-- that is what lets a reader ask "which row am I in" and get
|
|
9
15
|
an answer instead of a cell index. */
|
package/dist/components/Toast.js
CHANGED
|
@@ -115,5 +115,5 @@ export function ToastHost({ children }) {
|
|
|
115
115
|
React error. */
|
|
116
116
|
if (outer)
|
|
117
117
|
return _jsx(_Fragment, { children: children });
|
|
118
|
-
return (_jsxs(Ctx.Provider, { value: push, children: [children, _jsx("div", { className: "sr-only", "aria-live": "polite", "aria-atomic": "true", children: polite }), _jsx("div", { className: "sr-only", "aria-live": "assertive", "aria-atomic": "true", children: assertive }), _jsx(ToastRegion, { queue: queue, className: "toasts", children: ({ toast }) => (_jsxs(AriaToast, { toast: toast, className: `toast toast-${toast.content.tone}`, children: [_jsx(Icon, { name: MARK[toast.content.tone], size: 16, className: "toast-mark" }), _jsx(ToastContent, { className: "grow", children: _jsx(Text, { slot: "title", children: toast.content.text }) }), _jsx(Button, { slot: "close", className: "
|
|
118
|
+
return (_jsxs(Ctx.Provider, { value: push, children: [children, _jsx("div", { className: "sr-only", "aria-live": "polite", "aria-atomic": "true", children: polite }), _jsx("div", { className: "sr-only", "aria-live": "assertive", "aria-atomic": "true", children: assertive }), _jsx(ToastRegion, { queue: queue, className: "toasts", children: ({ toast }) => (_jsxs(AriaToast, { toast: toast, className: `toast toast-${toast.content.tone}`, children: [_jsx(Icon, { name: MARK[toast.content.tone], size: 16, className: "toast-mark" }), _jsx(ToastContent, { className: "ctl-grow", children: _jsx(Text, { slot: "title", children: toast.content.text }) }), _jsx(Button, { slot: "close", className: "toast-close", "aria-label": "Dismiss", children: _jsx(Icon, { name: "close", size: 14 }) })] })) })] }));
|
|
119
119
|
}
|
|
@@ -58,12 +58,26 @@ export default function Tooltip({ label, align = 'right', mark, className, child
|
|
|
58
58
|
is also why `overflow: hidden` on any ancestor no longer clips it -- the
|
|
59
59
|
Permissions pane's horizontal scrollbar, found by hiding one class at a
|
|
60
60
|
time, was this box sitting in the scrollable overflow. */
|
|
61
|
-
return (_jsxs(TooltipTrigger, { delay: 0, closeDelay: 150, children: [_jsx(Focusable, { children: _jsx("span", {
|
|
61
|
+
return (_jsxs(TooltipTrigger, { delay: 0, closeDelay: 150, children: [_jsx(Focusable, { children: _jsx("span", {
|
|
62
|
+
/* `explain` stays as the hook: the mark brightens on hover and on
|
|
63
|
+
focus of this element, which is a parent state reaching a child.
|
|
64
|
+
The rest is this element's own. `text-transform` and
|
|
65
|
+
`letter-spacing` are reset because one of these sits inside a
|
|
66
|
+
heading, and headings here are uppercase and tracked out. */
|
|
67
|
+
className: [
|
|
68
|
+
'explain',
|
|
69
|
+
`explain-${align}`,
|
|
70
|
+
'relative flex-none inline-flex items-center ml-2 cursor-help',
|
|
71
|
+
'normal-case tracking-normal font-normal',
|
|
72
|
+
className,
|
|
73
|
+
]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join(' '),
|
|
62
76
|
/* A role, because a name on a role-less span is prohibited -- axe's
|
|
63
77
|
`aria-prohibited-attr`, found the first time this was scanned. `img`
|
|
64
78
|
rather than `button`: the mark is a glyph that reveals help, not a
|
|
65
79
|
control that does something, and `button` would promise an action
|
|
66
80
|
and -- inside the `<label>` rows these sit in -- invite a click that
|
|
67
81
|
the label forwards to the setting behind it. */
|
|
68
|
-
role: "img", "aria-label": mark ? label : `About ${label}`, children: mark ?? (_jsx("span", { className: "explain-mark", "aria-hidden": "true", children: "?" })) }) }), _jsx(AriaTooltip, { className: "explain-tip", placement: align === 'left' ? 'bottom end' : 'bottom start', offset: 8, crossOffset: align === 'left' ? 6 : -6, children: children })] }));
|
|
82
|
+
role: "img", "aria-label": mark ? label : `About ${label}`, children: mark ?? (_jsx("span", { className: "explain-mark w-[18px] h-[18px] rounded-full border border-border text-muted text-xs leading-[16px] text-center", "aria-hidden": "true", children: "?" })) }) }), _jsx(AriaTooltip, { className: "explain-tip", placement: align === 'left' ? 'bottom end' : 'bottom start', offset: 8, crossOffset: align === 'left' ? 6 : -6, children: children })] }));
|
|
69
83
|
}
|
package/dist/components/Tour.js
CHANGED
|
@@ -111,5 +111,5 @@ export default function Tour({ stops, onDone, storageKey, }) {
|
|
|
111
111
|
height: box.height + 12,
|
|
112
112
|
} }), _jsxs("div", { ref: card, className: "tour-card", style: style,
|
|
113
113
|
// biome-ignore lint/a11y/useSemanticElements: a <dialog> brings the top layer, ::backdrop and showModal() focus semantics; this is an anchored coach-mark card that positions itself and manages its own focus, so the role is the faithful choice.
|
|
114
|
-
role: "dialog", "aria-modal": "true", "aria-labelledby": "tour-title", tabIndex: -1, children: [_jsx("strong", { id: "tour-title", className: "tour-title", children: stop.title }), _jsx("p", { className: "tour-body", children: stop.body }), _jsxs("div", { className: "tour-actions", children: [_jsxs("span", { className: "tour-count mono", children: [at + 1, " of ", live.length] }), _jsx("span", { className: "grow" }), _jsx(Button, { kind: "ghost", size: "sm", onPress: finish, children: "Skip" }), _jsx(Button, { kind: "primary", size: "sm", onPress: next, children: at + 1 < live.length ? 'Next' : 'Done' })] })] })] }), document.body);
|
|
114
|
+
role: "dialog", "aria-modal": "true", "aria-labelledby": "tour-title", tabIndex: -1, children: [_jsx("strong", { id: "tour-title", className: "tour-title", children: stop.title }), _jsx("p", { className: "tour-body", children: stop.body }), _jsxs("div", { className: "tour-actions", children: [_jsxs("span", { className: "tour-count mono", children: [at + 1, " of ", live.length] }), _jsx("span", { className: "ctl-grow" }), _jsx(Button, { kind: "ghost", size: "sm", onPress: finish, children: "Skip" }), _jsx(Button, { kind: "primary", size: "sm", onPress: next, children: at + 1 < live.length ? 'Next' : 'Done' })] })] })] }), document.body);
|
|
115
115
|
}
|