@wtfalch/design 0.9.1 → 0.10.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 CHANGED
@@ -13,9 +13,12 @@ The components, and the tokens. Not the classes.
13
13
 
14
14
  A class in this package is an implementation detail: `.card`, `.set-row`,
15
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:
16
+ draws them changes. 0.9.0 did exactly that to several of them, and 0.10.0
17
+ deleted 41 more: the components that drew them draw themselves now, so the
18
+ rules had no reader left.
19
+
20
+ If you need a card, render `<Card>`; if you need a row of your own, write it
21
+ in the token vocabulary:
19
22
 
20
23
  ```css
21
24
  .app-row { display: flex; gap: var(--space-3); align-items: center; }
@@ -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: "ctl-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: "flex-none grid place-items-center mt-px opacity-90", "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. */
@@ -50,5 +50,5 @@ export default function Checkbox({ label, hint, meta, checked, defaultChecked, o
50
50
  the controlled and uncontrolled cases are the same call: pass both and
51
51
  let whichever was given decide. Passing `isSelected={false}` here
52
52
  instead would silently pin every uncontrolled box to off. */
53
- isSelected: checked, defaultSelected: defaultChecked, onChange: onChange, name: name, value: value, isDisabled: disabled, children: _jsxs("span", { className: "choice-body", children: [_jsx("span", { className: "choice-name", children: label }), hint && _jsx("span", { className: "choice-why", children: hint }), meta && _jsx("span", { className: "choice-meta", children: meta })] }) }));
53
+ isSelected: checked, defaultSelected: defaultChecked, onChange: onChange, name: name, value: value, isDisabled: disabled, children: _jsxs("span", { className: "choice-body", children: [_jsx("span", { className: "choice-name", children: label }), hint && _jsx("span", { className: "choice-why", children: hint }), meta && _jsx("span", { className: "text-muted text-xs font-mono", children: meta })] }) }));
54
54
  }
@@ -31,14 +31,14 @@ export default function Command({ open, onOpenChange, groups, placeholder = 'Sea
31
31
  delete before they can search for anything else. */
32
32
  if (!next)
33
33
  setQuery('');
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 ? (
34
+ }, isDismissable: true, children: _jsx(AriaModal, { className: `cmd${className ? ` ${className}` : ''}`, children: _jsx(Dialog, { className: "outline-none flex flex-col min-h-0", "aria-label": label, children: ({ close }) => (_jsxs(Autocomplete, { inputValue: query, onInputChange: setQuery, filter: () => true, children: [_jsxs(SearchField, { className: "flex items-center gap-2 py-3 px-4 border-b border-border", "aria-label": label, autoFocus: true, children: [_jsx(Icon, { name: "chat", className: "flex-none w-4 h-4 text-muted" }), _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, { 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: "p-2 overflow-auto overscroll-contain outline-none min-h-0", "aria-label": label, selectionMode: "none", children: shown.map((group) => (_jsxs(ListBoxSection, { children: [_jsx(Header, { className: "pt-2 px-2 pb-1 text-2xs uppercase tracking-normal text-muted", 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, { children: command.shortcut })] }, command.id)))] }, group.title))) }))] })) }) }) }));
43
+ }, children: [command.icon && _jsx(Icon, { name: command.icon, className: "cmd-icon" }), _jsxs("span", { className: "flex-[1_1_auto] min-w-0 grid", children: [_jsx(Text, { slot: "label", className: "overflow-hidden text-ellipsis whitespace-nowrap", 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
  }
@@ -53,5 +53,5 @@ match, label, busyLabel, busy = false, disabled = false, onConfirm,
53
53
  unavailable, }) {
54
54
  const [asking, setAsking] = useState(false);
55
55
  const [typed, setTyped] = useState('');
56
- return (_jsxs("div", { className: `danger-act${kind === 'plain' ? ' plain' : ''}`, children: [_jsx("h4", { children: title }), _jsx("div", { className: "set-hint", children: description }), unavailable ? (_jsx("div", { className: "set-hint danger-unavailable", children: unavailable })) : confirm === 'type' ? (_jsx(_Fragment, { children: _jsxs("div", { className: "row danger-row field-row", children: [_jsx(Field, { label: "Confirm", hint: _jsxs(_Fragment, { children: ["Type ", _jsx("code", { className: "mono danger-name", children: match }), " to confirm."] }), children: (f) => (_jsx(Input, { ...f, mono: true, value: typed, disabled: disabled || busy, autoComplete: "off", spellCheck: false, onChange: (e) => setTyped(e.target.value) })) }), _jsx(Button, { kind: "danger", isDisabled: disabled || busy || typed.trim() !== match, onPress: onConfirm, children: busy ? (busyLabel ?? '…') : label })] }) })) : confirm === 'none' ? (_jsx("div", { className: "row danger-row", children: _jsx(Button, { kind: kind === 'plain' ? 'primary' : 'danger', isDisabled: disabled || busy, onPress: onConfirm, children: busy ? (busyLabel ?? '…') : label }) })) : asking ? (_jsxs("div", { className: "row danger-row", children: [_jsx(Button, { kind: "danger", isDisabled: disabled || busy, onPress: onConfirm, children: busy ? (busyLabel ?? '…') : label }), _jsx(Button, { isDisabled: disabled || busy, onPress: () => setAsking(false), children: "Cancel" })] })) : (_jsx("div", { className: "row danger-row", children: _jsx(Button, { kind: "danger", isDisabled: disabled || busy, onPress: () => setAsking(true), children: label }) }))] }));
56
+ return (_jsxs("div", { className: `danger-act${kind === 'plain' ? ' plain' : ''}`, children: [_jsx("h4", { children: title }), _jsx("div", { className: "set-hint", children: description }), unavailable ? (_jsx("div", { className: "set-hint mt-2", children: unavailable })) : confirm === 'type' ? (_jsx(_Fragment, { children: _jsxs("div", { className: "row danger-row field-row", children: [_jsx(Field, { label: "Confirm", hint: _jsxs(_Fragment, { children: ["Type ", _jsx("code", { className: "mono danger-name", children: match }), " to confirm."] }), children: (f) => (_jsx(Input, { ...f, mono: true, value: typed, disabled: disabled || busy, autoComplete: "off", spellCheck: false, onChange: (e) => setTyped(e.target.value) })) }), _jsx(Button, { kind: "danger", isDisabled: disabled || busy || typed.trim() !== match, onPress: onConfirm, children: busy ? (busyLabel ?? '…') : label })] }) })) : confirm === 'none' ? (_jsx("div", { className: "row danger-row", children: _jsx(Button, { kind: kind === 'plain' ? 'primary' : 'danger', isDisabled: disabled || busy, onPress: onConfirm, children: busy ? (busyLabel ?? '…') : label }) })) : asking ? (_jsxs("div", { className: "row danger-row", children: [_jsx(Button, { kind: "danger", isDisabled: disabled || busy, onPress: onConfirm, children: busy ? (busyLabel ?? '…') : label }), _jsx(Button, { isDisabled: disabled || busy, onPress: () => setAsking(false), children: "Cancel" })] })) : (_jsx("div", { className: "row danger-row", children: _jsx(Button, { kind: "danger", isDisabled: disabled || busy, onPress: () => setAsking(true), children: label }) }))] }));
57
57
  }
@@ -50,7 +50,14 @@ export default function Empty({ icon, illustration, children, action, className,
50
50
  /* `currentColor` reaches the figure through the two rules the
51
51
  illustrations ship with, so the colour is set here and the drawing
52
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 })] }));
53
+ theme.
54
+
55
+ This wanted a rule of its own for a while. `.illo` sets
56
+ `color: inherit`, and while the package's sheets were unlayered an
57
+ unlayered declaration beat a layered utility whatever the
58
+ specificity, so `text-muted` lost silently and the figure came out
59
+ full text black beside a muted sentence. Wrapping the sheets in
60
+ `@layer components` put them *below* `@layer utilities` in the
61
+ declared order, and the utility has won ever since. */
62
+ _jsx(Illustration, { name: illustration, size: 160, className: "text-muted" })) : (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 })] }));
56
63
  }
@@ -53,5 +53,5 @@ export default function Field({ label, hint, error, required, children, labelHid
53
53
  'aria-describedby': [hint && hintId, error && errorId].filter(Boolean).join(' ') || undefined,
54
54
  'aria-invalid': error ? true : undefined,
55
55
  };
56
- return (_jsxs("div", { className: `field field-${layout}-layout${error ? ' field-bad' : ''}${className ? ` ${className}` : ''}`, children: [_jsxs("label", { id: labelId, className: labelHidden ? 'sr-only' : 'field-label', htmlFor: id, children: [label, required && (_jsxs("span", { className: "field-required", "aria-label": "required", children: [' ', "*"] }))] }), hint && (_jsx("p", { className: "field-hint", id: hintId, children: hint })), typeof children === 'function' ? (children(wiring)) : (_jsx(FieldWiringContext.Provider, { value: wiring, children: children })), error && (_jsx("p", { className: "field-error", id: errorId, role: "alert", children: error }))] }));
56
+ return (_jsxs("div", { className: `field field-${layout}-layout${error ? ' field-bad' : ''}${className ? ` ${className}` : ''}`, children: [_jsxs("label", { id: labelId, className: labelHidden ? 'sr-only' : 'field-label', htmlFor: id, children: [label, required && (_jsxs("span", { className: "text-muted", "aria-label": "required", children: [' ', "*"] }))] }), hint && (_jsx("p", { className: "field-hint", id: hintId, children: hint })), typeof children === 'function' ? (children(wiring)) : (_jsx(FieldWiringContext.Provider, { value: wiring, children: children })), error && (_jsx("p", { className: "field-error", id: errorId, role: "alert", children: error }))] }));
57
57
  }
@@ -39,5 +39,5 @@ export default function Identity({ name, address, kind = 'line', size = 'md', as
39
39
  /* The hue is a variable and the stylesheet decides what to do with it:
40
40
  a `background` written here would be a colour the theme cannot reach,
41
41
  and the discs would stay saturated on Paper. */
42
- style: { '--ident-hue': hueOf(address) }, children: [_jsx("span", { className: "ident-disc", "aria-hidden": "true", children: initials }), _jsxs("span", { className: "ident-text", children: [_jsx("span", { className: "ident-name", children: shown }), kind === 'full' && shown !== address && _jsx("span", { className: "ident-address", children: address })] }), aside && _jsx("span", { className: "ident-aside", children: aside }), onRemove && (_jsx("button", { type: "button", className: "ident-remove", onClick: onRemove, "aria-label": `Remove ${shown}`, children: _jsx("svg", { viewBox: "0 0 24 24", width: "12", height: "12", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", d: "M6 6l12 12M18 6L6 18" }) }) }))] }));
42
+ style: { '--ident-hue': hueOf(address) }, children: [_jsx("span", { className: "ident-disc", "aria-hidden": "true", children: initials }), _jsxs("span", { className: "grid min-w-0", children: [_jsx("span", { className: "overflow-hidden text-ellipsis whitespace-nowrap font-normal", children: shown }), kind === 'full' && shown !== address && (_jsx("span", { className: "overflow-hidden text-ellipsis whitespace-nowrap text-xs text-muted", children: address }))] }), aside && _jsx("span", { className: "flex-none text-muted text-xs", children: aside }), onRemove && (_jsx("button", { type: "button", className: "ident-remove", onClick: onRemove, "aria-label": `Remove ${shown}`, children: _jsx("svg", { viewBox: "0 0 24 24", width: "12", height: "12", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", d: "M6 6l12 12M18 6L6 18" }) }) }))] }));
43
43
  }
@@ -17,7 +17,7 @@ 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, { 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));
20
+ textValue: typeof item.label === 'string' ? item.label : item.id, children: [item.icon && _jsx(Icon, { name: item.icon, className: "menu-icon" }), _jsxs("span", { className: "flex-[1_1_auto] min-w-0 grid", children: [_jsx(Text, { slot: "label", className: "overflow-hidden text-ellipsis whitespace-nowrap", 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
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;
@@ -22,8 +22,7 @@
22
22
  * label from 4.7:1 to 3.9:1. The tones mix at 10% into the panel for that
23
23
  * reason and not for taste.
24
24
  *
25
- * This was the second-most-copied markup in the app — `<span className="pill
26
- * pill-info">` at every call site, and a gallery page teaching it. A page that
25
+ * This was the second-most-copied markup in the app — `<span className="pill * pill-info">` at every call site, and a gallery page teaching it. A page that
27
26
  * documents hand-written markup is a page that will be copied.
28
27
  */
29
28
  export default function Pill({ tone, quiet, inRow, className, children, ...rest }: {
@@ -23,8 +23,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
23
23
  * label from 4.7:1 to 3.9:1. The tones mix at 10% into the panel for that
24
24
  * reason and not for taste.
25
25
  *
26
- * This was the second-most-copied markup in the app — `<span className="pill
27
- * pill-info">` at every call site, and a gallery page teaching it. A page that
26
+ * This was the second-most-copied markup in the app — `<span className="pill * pill-info">` at every call site, and a gallery page teaching it. A page that
28
27
  * documents hand-written markup is a page that will be copied.
29
28
  */
30
29
  export default function Pill({ tone, quiet, inRow, className, children, ...rest }) {
@@ -8,5 +8,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  line at the package's front door. */
9
9
  import { Popover as AriaPopover, Dialog, DialogTrigger, } from 'react-aria-components';
10
10
  export default function Popover({ trigger, children, placement = 'bottom start', label, offset = 6, open, onOpenChange, className, }) {
11
- return (_jsxs(DialogTrigger, { isOpen: open, onOpenChange: onOpenChange, children: [trigger, _jsx(AriaPopover, { className: `pop${className ? ` ${className}` : ''}`, placement: placement, offset: offset, children: _jsx(Dialog, { className: "pop-body", "aria-label": label, children: ({ close }) => (typeof children === 'function' ? children(close) : children) }) })] }));
11
+ return (_jsxs(DialogTrigger, { isOpen: open, onOpenChange: onOpenChange, children: [trigger, _jsx(AriaPopover, { className: `pop${className ? ` ${className}` : ''}`, placement: placement, offset: offset, children: _jsx(Dialog, { className: "p-3 outline-none text-text text-sm", "aria-label": label, children: ({ close }) => (typeof children === 'function' ? children(close) : children) }) })] }));
12
12
  }
@@ -45,14 +45,14 @@ export function Rows({ children, empty, label, look = 'list', className, }) {
45
45
  const items = Array.isArray(children) ? children.filter(Boolean) : children;
46
46
  const none = Array.isArray(items) ? items.length === 0 : !items;
47
47
  if (none) {
48
- return empty ? _jsx("div", { className: "set-hint rows-empty", children: empty }) : null;
48
+ return empty ? _jsx("div", { className: "set-hint py-3", children: empty }) : null;
49
49
  }
50
50
  return (_jsx("div", { className: `rows${look === 'pick' ? ' rows-pick' : ''}${className ? ` ${className}` : ''}`,
51
51
  // biome-ignore lint/a11y/useSemanticElements: valid ARIA on a div; a <ul> brings the browser's list reset and the stylesheet and 204 baselines key on `div.rows`.
52
52
  role: "list", "aria-label": label, children: items }));
53
53
  }
54
54
  export function Row({ name, pills, hint, trail, actions, below, onClick, tone, picked, loading, waiting, align = 'center', className, }) {
55
- const body = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "row-subject", children: [_jsxs("span", { className: "named", children: [_jsx("span", { className: "truncate", children: name }), pills] }), hint && _jsx("div", { className: "set-hint", children: hint })] }), trail && _jsx("span", { className: "row-trail set-hint mono", children: trail })] }));
55
+ const body = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "row-subject", children: [_jsxs("span", { className: "named", children: [_jsx("span", { className: "truncate", children: name }), pills] }), hint && _jsx("div", { className: "set-hint", children: hint })] }), trail && _jsx("span", { className: "flex-none ml-auto whitespace-nowrap set-hint mono", children: trail })] }));
56
56
  const rowClass = `set-row rows-row${tone ? ` rows-${tone}` : ''}` +
57
57
  `${align === 'start' ? ' rows-top' : ''}${picked ? ' is-picked' : ''}` +
58
58
  `${loading ? ' is-loading' : ''}${waiting ? ' is-waiting' : ''}` +
@@ -88,7 +88,7 @@ export default function Slider({ label, hint, value, min, max, step, onChange, f
88
88
  const risk = danger
89
89
  ? Math.max(0, Math.min(1, (live - danger.from) / ((danger.to ?? max) - danger.from || 1)))
90
90
  : 0;
91
- return (_jsxs("div", { className: `slider-row slider-${size}${disabled ? ' is-disabled' : ''}${className ? ` ${className}` : ''}`, children: [_jsxs("label", { htmlFor: id, className: labelHidden ? 'sr-only' : 'slider-body', children: [_jsx("span", { className: "slider-label", children: label }), hint && _jsx("span", { className: "slider-hint", children: hint })] }), _jsxs("div", { className: "slider-control", children: [_jsxs("span", { className: "slider-box", children: [_jsx("span", { className: "slider-track", style: {
91
+ return (_jsxs("div", { className: `slider-row slider-${size}${disabled ? ' is-disabled' : ''}${className ? ` ${className}` : ''}`, children: [_jsxs("label", { htmlFor: id, className: labelHidden ? 'sr-only' : 'flex-[1_1_260px] min-w-0 grid gap-px', children: [_jsx("span", { className: "text-text", children: label }), hint && _jsx("span", { className: "slider-hint", children: hint })] }), _jsxs("div", { className: "slider-control", children: [_jsxs("span", { className: "slider-box", children: [_jsx("span", { className: "slider-track", style: {
92
92
  '--slider-f': String(fill / 100),
93
93
  '--slider-risk': `${Math.round(risk * 100)}%`,
94
94
  }, children: _jsx("input", { id: id, type: "range", className: "slider", min: min, max: max, step: step ?? 'any', value: live, disabled: disabled, "aria-valuetext": format(live), ref: input, onKeyDown: () => {
@@ -102,9 +102,9 @@ export default function SplitPane({ children, direction = 'row', defaultSize = 3
102
102
  return;
103
103
  event.preventDefault();
104
104
  };
105
- return (_jsxs("div", { ref: frame, className: `split dir-${direction}${className ? ` ${className}` : ''}`, style: { '--split': `${size}%` }, children: [_jsx("div", { className: "split-pane split-first", id: firstPaneId, children: first }), _jsx("div", { role: "separator", tabIndex: 0, "aria-label": label, "aria-orientation": direction === 'row' ? 'vertical' : 'horizontal', "aria-controls": firstPaneId, "aria-valuenow": Math.round(size), "aria-valuemin": min, "aria-valuemax": max, className: "split-handle", onPointerDown: onPointerDown, onKeyDown: onKeyDown,
105
+ return (_jsxs("div", { ref: frame, className: `split dir-${direction}${className ? ` ${className}` : ''}`, style: { '--split': `${size}%` }, children: [_jsx("div", { className: "min-w-0 min-h-0 overflow-hidden flex flex-col flex-[0_0_var(--split)]", id: firstPaneId, children: first }), _jsx("div", { role: "separator", tabIndex: 0, "aria-label": label, "aria-orientation": direction === 'row' ? 'vertical' : 'horizontal', "aria-controls": firstPaneId, "aria-valuenow": Math.round(size), "aria-valuemin": min, "aria-valuemax": max, className: "split-handle", onPointerDown: onPointerDown, onKeyDown: onKeyDown,
106
106
  /* Back to where it started. A drag has no undo, and the size is
107
107
  remembered, so without this a mis-drag is permanent until you get it
108
108
  right by hand. */
109
- onDoubleClick: () => move(defaultSize), children: _jsx("span", { className: "split-grip", "aria-hidden": "true" }) }), _jsx("div", { className: "split-pane split-second", children: second })] }));
109
+ onDoubleClick: () => move(defaultSize), children: _jsx("span", { className: "split-grip", "aria-hidden": "true" }) }), _jsx("div", { className: "min-w-0 min-h-0 overflow-hidden flex flex-col flex-[1_1_0]", children: second })] }));
110
110
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  export default function Table({ caption, columns, rows, keyOf, empty, className, }) {
3
3
  if (!rows.length) {
4
- return empty ? _jsx("div", { className: "set-hint rows-empty", children: empty }) : null;
4
+ return empty ? _jsx("div", { className: "set-hint py-3", children: empty }) : null;
5
5
  }
6
6
  return (
7
7
  /* The one thing about a table that is a property of one element. Its
@@ -64,7 +64,7 @@ export default function Tabs({ tabs, value, onChange, label, orientation = 'hori
64
64
  tabs[i - 1]?.group !== t.group &&
65
65
  (() => {
66
66
  const g = groups.find((x) => x.id === t.group);
67
- return g ? (_jsxs("div", { className: "tab-group", role: "presentation", children: [_jsx("span", { className: "tab-group-label", children: g.label }), g.hint && _jsx("span", { className: "tab-hint", children: g.hint })] })) : null;
67
+ return g ? (_jsxs("div", { className: "tab-group", role: "presentation", children: [_jsx("span", { className: "block text-sm font-strong text-text", children: g.label }), g.hint && (_jsx("span", { className: "block mt-px text-xs text-muted leading-[1.35]", children: g.hint }))] })) : null;
68
68
  })(), _jsxs("button", { role: "tab", type: "button", className: `tab${t.id === value ? ' on' : ''}${grouped && t.group ? ' in-group' : ''}`, "aria-selected": t.id === value, disabled: t.disabled,
69
69
  /* One tab stop for the whole strip: everything but the current tab is
70
70
  taken out of the tab order, and the arrows move between them. */
@@ -85,5 +85,5 @@ export default function Tabs({ tabs, value, onChange, label, orientation = 'hori
85
85
  e.preventDefault();
86
86
  move(tabs.length, -1);
87
87
  }
88
- }, children: [_jsx("span", { className: "tab-label", children: t.label }), t.badge && (_jsx("span", { className: "tab-badge", title: t.badgeTitle, "aria-label": t.badgeTitle, children: t.badge })), t.hint && orientation === 'vertical' && !(grouped && t.group) && (_jsx("span", { className: "tab-hint", children: t.hint }))] })] }, t.id))), _jsx("span", { className: "tabs-rule", "aria-hidden": "true" })] }));
88
+ }, children: [_jsx("span", { className: "tab-label", children: t.label }), t.badge && (_jsx("span", { className: "tab-badge", title: t.badgeTitle, "aria-label": t.badgeTitle, children: t.badge })), t.hint && orientation === 'vertical' && !(grouped && t.group) && (_jsx("span", { className: "block mt-px text-xs text-muted leading-[1.35]", children: t.hint }))] })] }, t.id))), _jsx("span", { className: "tabs-rule", "aria-hidden": "true" })] }));
89
89
  }
@@ -158,7 +158,7 @@ export default function Toggle({ label, hint, checked, onChange, disabled, said,
158
158
  /* Read-only, not disabled, while a request is out: focus stays where it
159
159
  is and the row does not dim, it just refuses a second answer until
160
160
  the first has been taken. */
161
- isReadOnly: pending, children: [_jsxs("span", { className: labelHidden ? 'sr-only' : 'switch-body', children: [_jsx("span", { className: "switch-label", children: label }), hint && _jsx("span", { className: "switch-hint", children: hint })] }), said && _jsx("span", { className: "switch-said mono", children: said }), _jsx("span", { className: "toggle", "aria-hidden": "true", "data-held": held || undefined, "data-dragging": knob === null ? undefined : true, style: knob === null ? undefined : { '--knob-x': knob }, onPointerDown: (e) => {
161
+ isReadOnly: pending, children: [_jsxs("span", { className: labelHidden ? 'sr-only' : 'switch-body', children: [_jsx("span", { className: "text-text", children: label }), hint && _jsx("span", { className: "switch-hint", children: hint })] }), said && _jsx("span", { className: "flex-none text-xs text-muted mono", children: said }), _jsx("span", { className: "toggle", "aria-hidden": "true", "data-held": held || undefined, "data-dragging": knob === null ? undefined : true, style: knob === null ? undefined : { '--knob-x': knob }, onPointerDown: (e) => {
162
162
  if (disabled || pending || e.button !== 0)
163
163
  return;
164
164
  e.stopPropagation();
@@ -99,7 +99,11 @@ export default function Tour({ stops, onDone, storageKey, }) {
99
99
  bottom: below ? undefined : window.innerHeight - box.top + 14,
100
100
  left: Math.max(16, Math.min(box.left + box.width / 2 - 170, window.innerWidth - 356)),
101
101
  };
102
- return createPortal(_jsxs("div", { className: "tour", onKeyDown: (e) => {
102
+ return createPortal(_jsxs("div", {
103
+ /* Over everything and catching nothing: the scrim is painted by the
104
+ hole's own shadow, so there is no full-screen element here to swallow
105
+ a click meant for the page underneath. */
106
+ className: "fixed inset-0 z-[60] pointer-events-none", onKeyDown: (e) => {
103
107
  if (e.key === 'Escape')
104
108
  finish();
105
109
  if (e.key === 'Enter' || e.key === 'ArrowRight')
@@ -111,5 +115,5 @@ export default function Tour({ stops, onDone, storageKey, }) {
111
115
  height: box.height + 12,
112
116
  } }), _jsxs("div", { ref: card, className: "tour-card", style: style,
113
117
  // 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: "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);
118
+ role: "dialog", "aria-modal": "true", "aria-labelledby": "tour-title", tabIndex: -1, children: [_jsx("strong", { id: "tour-title", className: "text-base", 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
119
  }