@wtfalch/design 0.8.0 → 0.9.1

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 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
- /* The one class the button form does not carry. Every rule in the
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.
@@ -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 head = (title || description || action) && (_jsxs("div", { className: "card-head", children: [icon && (_jsx("span", { className: "card-icon", "aria-hidden": "true", children: _jsx(Icon, { name: icon, size: 20 }) })), _jsxs("div", { className: "card-headings", children: [title && _jsx("strong", { className: "card-title", children: title }), description && _jsx("div", { className: "card-desc", children: description })] }), action && _jsx("div", { className: "card-action", children: action })] }));
31
- const cls = `card${onClick ? ' card-pick' : ''}${selected ? ' card-on' : ''}${tone ? ` card-${tone}` : ''}${className ? ` ${className}` : ''}`;
32
- const body = children && _jsx("div", { className: "card-body", children: children });
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, { className: "cmd-group", 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: () => {
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, { className: "cmd-key", children: command.shortcut })] }, command.id)))] }, group.title))) }))] })) }) }) }));
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
  }
@@ -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: `nothing${illustration ? ' nothing-surface' : ''}${className ? ` ${className}` : ''}`, children: [illustration ? (_jsx(Illustration, { name: illustration, size: 160, className: "nothing-figure" })) : (icon && (_jsx("span", { className: "nothing-mark", "aria-hidden": "true", children: _jsx(Icon, { name: icon, size: 20 }) }))), _jsx("p", { className: "nothing-said", children: children }), action && _jsx("div", { className: "nothing-act", children: action })] }));
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
  }
@@ -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 ghost adorned-clear", "aria-label": "Clear", onClick: onClear, disabled: rest.disabled, children: _jsx(Icon, { name: "close", size: glyph }) }))] }))] }));
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 ghost 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 }) })] }));
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;
@@ -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
- return _jsx(Keyboard, { className: ['kbd', className].filter(Boolean).join(' '), children: children });
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
  }
@@ -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, { className: "menu-key", 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", children: _jsx(AriaMenu, { className: "menu-list", children: item.items.map(renderItem) }) })] }, item.id)) : (row);
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: "menu-list", "aria-label": label, children: items.map((entry) => isSection(entry) ? (_jsxs(MenuSection, { className: "menu-section", children: [_jsx(Header, { className: "menu-section-title", children: entry.title }), entry.items.map(renderItem)] }, entry.title)) : (renderItem(entry))) }) })] }));
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
  }
@@ -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: "modal-desc", children: description }), _jsx("div", { className: `modal-body${bodyClass ? ` ${bodyClass}` : ''}`, children: children }), footer && (_jsx("footer", { className: `set-actions${footerClass ? ` ${footerClass}` : ''}`, children: footer }))] }) }) }));
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: `pager${className ? ` ${className}` : ''}`, "aria-label": label, children: [_jsx("p", { className: "pager-count", 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: "pager-controls", 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' ? (
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 (_jsxs("div", { className: `progress${className ? ` ${className}` : ''}`, children: [(label || detail) && (_jsxs("div", { className: "progress-head", children: [_jsx("span", { className: "progress-label", children: label }), _jsx("span", { className: "grow" }), detail && _jsx("span", { className: "progress-detail 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}%` } }) })] }));
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
  }
@@ -5,7 +5,7 @@
5
5
  * hand:
6
6
  *
7
7
  * <div className="set-row">
8
- * <span className="grow" style={{ minWidth: 0 }}>
8
+ * <span className="ctl-grow" style={{ minWidth: 0 }}>
9
9
  * <span className="named">
10
10
  * <span className="truncate">{name}</span>
11
11
  * …pills…
@@ -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, { className: "sel-listbox", children: options.map((o) => (_jsxs(ListBoxItem, { id: o.value, textValue: o.text, isDisabled: o.disabled, className: "sel-item", children: [_jsx("span", { className: "sel-item-label", children: o.label }), _jsx("svg", { className: "sel-tick", 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))) }) })] }));
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", 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
  }
@@ -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
- return (_jsxs("div", { className: ['shell', className].filter(Boolean).join(' '), children: [_jsxs("header", { className: "shell-head", children: [_jsxs("div", { className: `shell-head-inner${wide ? ' shell-wide' : ''}`, children: [brand, who && _jsx("div", { className: "shell-who", children: who })] }), nav && _jsx("div", { className: `shell-nav${wide ? ' shell-wide' : ''}`, children: nav })] }), _jsx("main", { className: `shell-main${wide ? ' shell-wide' : ''}`, children: children })] }));
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,24 @@ 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", className: `size-cell${on ? ' on' : ''}${chosen && !hover ? ' chosen' : ''}`, 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}`));
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
+ /* Geometry here; colour in `sizegrid.css`. The cell's resting border and
51
+ surface are the thing `on` and `chosen` override, and a utility
52
+ beats a component rule once the sheets are layered -- so stated
53
+ here they would win permanently and no cell would ever light up. */
54
+ 'size-cell block w-[18px] h-[14px] p-0 rounded-sm cursor-pointer',
55
+ 'disabled:cursor-default disabled:opacity-50',
56
+ on ? 'on' : '',
57
+ chosen && !hover ? 'chosen' : '',
58
+ ]
59
+ .filter(Boolean)
60
+ .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
61
  }
46
62
  }
47
- return (_jsxs("div", { className: "size-grid-wrap", children: [_jsx("div", { className: "size-grid", 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: "size-ceiling", children: [' ', "\u00B7 max ", max.cols, "\u00D7", max.rows] }))] })] }));
63
+ 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
64
  }
@@ -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
- const classes = ['stat', look === 'bare' ? 'stat-bare' : '', className].filter(Boolean).join(' ');
30
- return (_jsxs("div", { className: classes, "data-tone": tone, children: [_jsx("p", { className: "stat-label", children: label }), _jsx("p", { className: "stat-value", children: value }), note && _jsx("p", { className: "stat-note", children: note })] }));
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
  }
@@ -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 (_jsx("div", { className: `table-scroll${className ? ` ${className}` : ''}`, children: _jsxs("table", { className: "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) => {
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. */
@@ -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: "ghost size-sm", "aria-label": "Dismiss", children: _jsx(Icon, { name: "close", size: 14 }) })] })) })] }));
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", { className: `explain explain-${align}${className ? ` ${className}` : ''}`,
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
  }
@@ -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
  }