@wtfalch/design 0.6.0 → 0.7.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/dist/components/Button.d.ts +19 -27
- package/dist/components/Button.js +61 -1
- package/dist/components/Callout.d.ts +0 -21
- package/dist/components/Callout.js +7 -0
- package/dist/components/Card.js +7 -0
- package/dist/components/Checkbox.d.ts +0 -31
- package/dist/components/Checkbox.js +7 -0
- package/dist/components/Command.js +10 -2
- package/dist/components/DangerZone.js +7 -0
- package/dist/components/Dialog.d.ts +0 -21
- package/dist/components/Dialog.js +7 -0
- package/dist/components/Field.d.ts +0 -31
- package/dist/components/Field.js +7 -0
- package/dist/components/Identity.d.ts +0 -25
- package/dist/components/Identity.js +7 -0
- package/dist/components/Input.js +7 -0
- package/dist/components/Kbd.d.ts +7 -0
- package/dist/components/Kbd.js +34 -0
- package/dist/components/Markdown.js +7 -0
- package/dist/components/Menu.js +10 -2
- package/dist/components/Modal.d.ts +0 -33
- package/dist/components/Modal.js +7 -0
- package/dist/components/Pagination.d.ts +0 -33
- package/dist/components/Pagination.js +7 -0
- package/dist/components/Popover.js +7 -0
- package/dist/components/Rows.js +7 -0
- package/dist/components/ScrollArea.js +7 -0
- package/dist/components/Select.d.ts +6 -1
- package/dist/components/Select.js +9 -2
- package/dist/components/SizeGrid.js +7 -0
- package/dist/components/Slider.js +7 -0
- package/dist/components/SplitPane.js +7 -0
- package/dist/components/Stat.d.ts +45 -0
- package/dist/components/Stat.js +31 -0
- package/dist/components/Tabs.d.ts +21 -0
- package/dist/components/Tabs.js +8 -1
- package/dist/components/Toast.d.ts +24 -18
- package/dist/components/Toast.js +44 -3
- package/dist/components/Toggle.d.ts +0 -30
- package/dist/components/Toggle.js +7 -0
- package/dist/components/Tooltip.js +7 -0
- package/dist/components/Tour.d.ts +0 -23
- package/dist/components/Tour.js +7 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +9 -1
- package/dist/styles/index.css +117 -13
- package/dist/tf.css +117 -13
- package/dist/valet.css +117 -13
- package/package.json +2 -1
package/dist/components/Rows.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* A list of things, each with a name, some qualifiers and something to do.
|
|
4
11
|
*
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
import { useCallback, useEffect, useRef } from 'react';
|
|
3
10
|
export default function ScrollArea({ axis = 'y', fade = true, hideBar = false, className, children, ref, label, }) {
|
|
4
11
|
const own = useRef(null);
|
|
@@ -12,6 +12,11 @@ interface Props {
|
|
|
12
12
|
'aria-label'?: string;
|
|
13
13
|
'aria-labelledby'?: string;
|
|
14
14
|
disabled?: boolean;
|
|
15
|
+
/** What the form submits this under. Without it there is nothing to post,
|
|
16
|
+
* which is what sent callers back to a mirrored hidden input. */
|
|
17
|
+
name?: string;
|
|
18
|
+
/** The form to submit with, when the control sits outside it. */
|
|
19
|
+
form?: string;
|
|
15
20
|
value?: string | number;
|
|
16
21
|
defaultValue?: string | number;
|
|
17
22
|
/** Native-select-shaped on purpose -- `e.target.value` -- because that is
|
|
@@ -24,5 +29,5 @@ interface Props {
|
|
|
24
29
|
/** Something to the right of an option -- a play button beside a voice. */
|
|
25
30
|
aside?: (value: string) => ReactNode;
|
|
26
31
|
}
|
|
27
|
-
export default function Select({ block, size, className, children, value, defaultValue, onChange, disabled, id, title, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, aside, }: Props): import("react").JSX.Element;
|
|
32
|
+
export default function Select({ block, size, className, children, value, defaultValue, onChange, disabled, name, form, id, title, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, aside, }: Props): import("react").JSX.Element;
|
|
28
33
|
export {};
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
import { Children, isValidElement, useEffect, useRef } from 'react';
|
|
3
10
|
/** Read `<option>` children into a list this can render itself. */
|
|
4
11
|
function readOptions(children) {
|
|
@@ -23,7 +30,7 @@ function readOptions(children) {
|
|
|
23
30
|
`<button>` -- `onCopy` alone is typed against a different element. Call sites
|
|
24
31
|
pass five things between them, so five is what this takes. */
|
|
25
32
|
import { Select as AriaSelect, Button, ListBox, ListBoxItem, Popover, SelectValue, } from 'react-aria-components';
|
|
26
|
-
export default function Select({ block = false, size, className, children, value, defaultValue, onChange, disabled, id, title, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, aside, }) {
|
|
33
|
+
export default function Select({ block = false, size, className, children, value, defaultValue, onChange, disabled, name, form, id, title, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, aside, }) {
|
|
27
34
|
const options = readOptions(children);
|
|
28
35
|
/* `title`, set on the element: React Aria's `Button` takes `id` and the
|
|
29
36
|
`aria-*` labelling props and filters the rest, the same `filterDOMProps`
|
|
@@ -52,5 +59,5 @@ export default function Select({ block = false, size, className, children, value
|
|
|
52
59
|
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) => {
|
|
53
60
|
if (key !== null)
|
|
54
61
|
onChange?.({ target: { value: String(key) } });
|
|
55
|
-
}, isDisabled: disabled, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: [_jsxs(Button, { ref: control, id: id, 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))) }) })] }));
|
|
62
|
+
}, isDisabled: disabled, name: name, form: form, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: [_jsxs(Button, { ref: control, id: id, 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))) }) })] }));
|
|
56
63
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
import { useState } from 'react';
|
|
3
10
|
/**
|
|
4
11
|
* Pick a tile size by sweeping a grid, the way a spreadsheet asks for table
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
import { useEffect, useId, useRef, useState } from 'react';
|
|
3
10
|
/**
|
|
4
11
|
* A number chosen from a range, by dragging.
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
3
10
|
function clamp(value, min, max) {
|
|
4
11
|
return Math.min(max, Math.max(min, value));
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One figure, with what it counts over it and what it means under it.
|
|
3
|
+
*
|
|
4
|
+
* **Drawn three times before it was a component.** valet's portal overview
|
|
5
|
+
* and its organisation page under `/admin` each had their own, and the same
|
|
6
|
+
* account's numbers rendered in two shapes on the two surfaces read as two
|
|
7
|
+
* products rather than as two views of one thing. Folding them into one local
|
|
8
|
+
* `Stat` fixed that inside valet and left the next app to rediscover it;
|
|
9
|
+
* valet's own docblock says so ("a candidate for the package, once a second
|
|
10
|
+
* product wants one"). The mail client's counts are the second product.
|
|
11
|
+
*
|
|
12
|
+
* **`tabular-nums`, and that is the reason this is not a `Card`.** Figures
|
|
13
|
+
* are read down a row, and proportional digits put the same magnitude in
|
|
14
|
+
* different places on every tile, so a column of numbers stops being
|
|
15
|
+
* comparable at a glance. A tile is not a card with a big number in it: it is
|
|
16
|
+
* a number with furniture, and the furniture exists to make the number
|
|
17
|
+
* legible next to the one beside it.
|
|
18
|
+
*
|
|
19
|
+
* **The label goes over the value.** A figure read before you know what it
|
|
20
|
+
* counts is a figure you read twice. This is the one place in the package
|
|
21
|
+
* where the caption leads.
|
|
22
|
+
*
|
|
23
|
+
* **`look="bare"` is the compact form**, for a figure inside a row that
|
|
24
|
+
* already has a border around it -- valet's `.v-figure`, in the admin table.
|
|
25
|
+
* A bordered tile inside a bordered row is two boxes saying one thing.
|
|
26
|
+
*/
|
|
27
|
+
export default function Stat({ label, value, note, tone, look, className, }: {
|
|
28
|
+
/** What is being counted. Over the value, not under it. */
|
|
29
|
+
label: React.ReactNode;
|
|
30
|
+
/** The figure. A string, because the formatting -- locale, unit, precision
|
|
31
|
+
* -- is the caller's: this component cannot know whether 1024 should read
|
|
32
|
+
* as `1,024`, `1.0k` or `1 KiB`. */
|
|
33
|
+
value: React.ReactNode;
|
|
34
|
+
/** The sentence under the figure. What it excludes, when it was measured,
|
|
35
|
+
* what it is a fraction of. */
|
|
36
|
+
note?: React.ReactNode;
|
|
37
|
+
/** Semantic colour on the value, the same four words as `Callout` and
|
|
38
|
+
* `Pill`. Left off, a figure is just a figure -- which is right for most
|
|
39
|
+
* of them, and a row where every tile is coloured says nothing. */
|
|
40
|
+
tone?: 'info' | 'good' | 'warn' | 'bad';
|
|
41
|
+
/** `tile` has a border and a panel behind it. `bare` is the figure alone,
|
|
42
|
+
* for a cell in something already bordered. */
|
|
43
|
+
look?: 'tile' | 'bare';
|
|
44
|
+
className?: string;
|
|
45
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* One figure, with what it counts over it and what it means under it.
|
|
4
|
+
*
|
|
5
|
+
* **Drawn three times before it was a component.** valet's portal overview
|
|
6
|
+
* and its organisation page under `/admin` each had their own, and the same
|
|
7
|
+
* account's numbers rendered in two shapes on the two surfaces read as two
|
|
8
|
+
* products rather than as two views of one thing. Folding them into one local
|
|
9
|
+
* `Stat` fixed that inside valet and left the next app to rediscover it;
|
|
10
|
+
* valet's own docblock says so ("a candidate for the package, once a second
|
|
11
|
+
* product wants one"). The mail client's counts are the second product.
|
|
12
|
+
*
|
|
13
|
+
* **`tabular-nums`, and that is the reason this is not a `Card`.** Figures
|
|
14
|
+
* are read down a row, and proportional digits put the same magnitude in
|
|
15
|
+
* different places on every tile, so a column of numbers stops being
|
|
16
|
+
* comparable at a glance. A tile is not a card with a big number in it: it is
|
|
17
|
+
* a number with furniture, and the furniture exists to make the number
|
|
18
|
+
* legible next to the one beside it.
|
|
19
|
+
*
|
|
20
|
+
* **The label goes over the value.** A figure read before you know what it
|
|
21
|
+
* counts is a figure you read twice. This is the one place in the package
|
|
22
|
+
* where the caption leads.
|
|
23
|
+
*
|
|
24
|
+
* **`look="bare"` is the compact form**, for a figure inside a row that
|
|
25
|
+
* already has a border around it -- valet's `.v-figure`, in the admin table.
|
|
26
|
+
* A bordered tile inside a bordered row is two boxes saying one thing.
|
|
27
|
+
*/
|
|
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 })] }));
|
|
31
|
+
}
|
|
@@ -39,6 +39,27 @@ export interface Tab {
|
|
|
39
39
|
label: string;
|
|
40
40
|
/** Shown after the label, for a count or a state. */
|
|
41
41
|
badge?: string;
|
|
42
|
+
/**
|
|
43
|
+
* What the badge means, on hover and to a screen reader.
|
|
44
|
+
*
|
|
45
|
+
* A badge is one or two characters by design, and a mark that terse either
|
|
46
|
+
* explains itself or does not. valet had two on its admin strip that did
|
|
47
|
+
* not, and moved them out of the badge into a visible line rather than ship
|
|
48
|
+
* a glyph nobody could resolve -- which is a page working around a
|
|
49
|
+
* component, not a page making a choice.
|
|
50
|
+
*
|
|
51
|
+
* On `title` plus `aria-label`, not `title` alone: `title` never appears on
|
|
52
|
+
* a touch screen and is inconsistently announced, so the accessible name is
|
|
53
|
+
* set explicitly. The badge becomes a labelled `<abbr>`-shaped thing rather
|
|
54
|
+
* than decoration, which is what it always was.
|
|
55
|
+
*
|
|
56
|
+
* Additive, deliberately. Without this the badge's own text stays part of
|
|
57
|
+
* the tab's accessible name, which is right for the count case the prop
|
|
58
|
+
* above was written for -- "Keys 3" is a useful thing to hear. Hiding an
|
|
59
|
+
* untitled badge would have been the tidier rule and would have taken that
|
|
60
|
+
* count away from everyone already relying on it.
|
|
61
|
+
*/
|
|
62
|
+
badgeTitle?: string;
|
|
42
63
|
/** A line under the label, for a rail with room for one. Ignored in a
|
|
43
64
|
* horizontal strip, where there is none, and under a group, where the
|
|
44
65
|
* group's hint is the context. */
|
package/dist/components/Tabs.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* One row of choices where only one is showing.
|
|
4
11
|
*
|
|
@@ -78,5 +85,5 @@ export default function Tabs({ tabs, value, onChange, label, orientation = 'hori
|
|
|
78
85
|
e.preventDefault();
|
|
79
86
|
move(tabs.length, -1);
|
|
80
87
|
}
|
|
81
|
-
}, children: [_jsx("span", { className: "tab-label", children: t.label }), t.badge && _jsx("span", { className: "tab-badge", 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: "tab-hint", children: t.hint }))] })] }, t.id))), _jsx("span", { className: "tabs-rule", "aria-hidden": "true" })] }));
|
|
82
89
|
}
|
|
@@ -1,27 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Something happened, and it does not need answering.
|
|
3
|
-
*
|
|
4
|
-
* There is no transient feedback in this app at all. Every outcome is either a
|
|
5
|
-
* `.callout` that stays until the page changes, or nothing -- so "saved",
|
|
6
|
-
* "copied", "model removed" and "conversation deleted" are all silent, and the
|
|
7
|
-
* only way to know a button worked is that something else moved.
|
|
8
|
-
*
|
|
9
|
-
* **A toast is for the outcome you would not have chased.** If the reader has to
|
|
10
|
-
* act on it, it is a callout and belongs on the page. If they have to decide, it
|
|
11
|
-
* is a dialog. This is the third case: it worked, you may carry on, and in four
|
|
12
|
-
* seconds there will be no trace.
|
|
13
|
-
*
|
|
14
|
-
* **`role="status"`, not `alert`.** An alert interrupts whatever a screen reader
|
|
15
|
-
* is saying, which is right for "the download failed" and rude for "copied".
|
|
16
|
-
* The failing case passes `tone="bad"`, which is the one that promotes itself.
|
|
17
|
-
*/
|
|
18
1
|
export interface Toast {
|
|
19
2
|
text: string;
|
|
20
3
|
/** The same four words `Callout` and `Pill` use. `warn` was missing here
|
|
21
4
|
* until 2026-09-05 -- three tones where the rest of the system has four. */
|
|
22
5
|
tone: 'info' | 'good' | 'warn' | 'bad';
|
|
23
6
|
}
|
|
24
|
-
|
|
7
|
+
type Push = (text: string, tone?: Toast['tone']) => void;
|
|
8
|
+
export declare function useToast(): Push;
|
|
9
|
+
/**
|
|
10
|
+
* Whether anything above will actually show a toast.
|
|
11
|
+
*
|
|
12
|
+
* For a component that has a second way of saying something and would rather
|
|
13
|
+
* use it than push into the void. `useToast` outside a host is a no-op that
|
|
14
|
+
* typechecks, which is the failure this exists to let you avoid.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useHasToastHost(): boolean;
|
|
25
17
|
/**
|
|
26
18
|
* The queue and the region, once, at the root.
|
|
27
19
|
*
|
|
@@ -37,7 +29,21 @@ export declare function useToast(): (text: string, tone?: Toast["tone"]) => void
|
|
|
37
29
|
* reachable; React Aria makes each toast an `alertdialog` inside a live
|
|
38
30
|
* `region` for that reason, so the announcement still happens and the button
|
|
39
31
|
* can still be reached.
|
|
32
|
+
*
|
|
33
|
+
* **Nesting one inside another is a no-op, and that is what makes this usable
|
|
34
|
+
* by a package.** `@wtfalch/email`'s mail client wanted to say "Archived."
|
|
35
|
+
* and could not: `useToast` needs a host above it, so the component would
|
|
36
|
+
* have had to wrap itself in one -- and then an application that already had
|
|
37
|
+
* a host would carry two regions announcing into the same page. It wrote its
|
|
38
|
+
* own `role="status"` strip instead and said so in a docblock, which is a
|
|
39
|
+
* package working around a package.
|
|
40
|
+
*
|
|
41
|
+
* So a `ToastHost` that finds one above it renders its children and steps
|
|
42
|
+
* aside. A library wraps itself unconditionally and gets the right answer
|
|
43
|
+
* both ways: its own region in an app that has none, and the app's region --
|
|
44
|
+
* one queue, one landmark, one place messages pile up -- in an app that does.
|
|
40
45
|
*/
|
|
41
46
|
export declare function ToastHost({ children }: {
|
|
42
47
|
children: React.ReactNode;
|
|
43
48
|
}): import("react").JSX.Element;
|
|
49
|
+
export {};
|
package/dist/components/Toast.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* Something happened, and it does not need answering.
|
|
4
11
|
*
|
|
@@ -29,9 +36,23 @@ const MARK = {
|
|
|
29
36
|
* them does not pile up -- and paused while the pointer or focus is on it,
|
|
30
37
|
* because a message that expires while you are reading it was never shown. */
|
|
31
38
|
const LINGER = 4000;
|
|
32
|
-
|
|
39
|
+
/* `null`, not a no-op, so `ToastHost` can tell "nobody above me" from "a host
|
|
40
|
+
above me whose push happens to do nothing". `useToast` still hands back a
|
|
41
|
+
no-op, which is the old behaviour. */
|
|
42
|
+
const Ctx = createContext(null);
|
|
43
|
+
const NOOP = () => { };
|
|
33
44
|
export function useToast() {
|
|
34
|
-
return useContext(Ctx);
|
|
45
|
+
return useContext(Ctx) ?? NOOP;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether anything above will actually show a toast.
|
|
49
|
+
*
|
|
50
|
+
* For a component that has a second way of saying something and would rather
|
|
51
|
+
* use it than push into the void. `useToast` outside a host is a no-op that
|
|
52
|
+
* typechecks, which is the failure this exists to let you avoid.
|
|
53
|
+
*/
|
|
54
|
+
export function useHasToastHost() {
|
|
55
|
+
return useContext(Ctx) !== null;
|
|
35
56
|
}
|
|
36
57
|
/**
|
|
37
58
|
* The queue and the region, once, at the root.
|
|
@@ -48,8 +69,22 @@ export function useToast() {
|
|
|
48
69
|
* reachable; React Aria makes each toast an `alertdialog` inside a live
|
|
49
70
|
* `region` for that reason, so the announcement still happens and the button
|
|
50
71
|
* can still be reached.
|
|
72
|
+
*
|
|
73
|
+
* **Nesting one inside another is a no-op, and that is what makes this usable
|
|
74
|
+
* by a package.** `@wtfalch/email`'s mail client wanted to say "Archived."
|
|
75
|
+
* and could not: `useToast` needs a host above it, so the component would
|
|
76
|
+
* have had to wrap itself in one -- and then an application that already had
|
|
77
|
+
* a host would carry two regions announcing into the same page. It wrote its
|
|
78
|
+
* own `role="status"` strip instead and said so in a docblock, which is a
|
|
79
|
+
* package working around a package.
|
|
80
|
+
*
|
|
81
|
+
* So a `ToastHost` that finds one above it renders its children and steps
|
|
82
|
+
* aside. A library wraps itself unconditionally and gets the right answer
|
|
83
|
+
* both ways: its own region in an app that has none, and the app's region --
|
|
84
|
+
* one queue, one landmark, one place messages pile up -- in an app that does.
|
|
51
85
|
*/
|
|
52
86
|
export function ToastHost({ children }) {
|
|
87
|
+
const outer = useContext(Ctx);
|
|
53
88
|
const queue = useMemo(() => new ToastQueue({ maxVisibleToasts: 4 }), []);
|
|
54
89
|
/* What gets read aloud, and it is not the toast.
|
|
55
90
|
|
|
@@ -74,5 +109,11 @@ export function ToastHost({ children }) {
|
|
|
74
109
|
say('');
|
|
75
110
|
requestAnimationFrame(() => say(text));
|
|
76
111
|
}, [queue]);
|
|
112
|
+
/* Somebody above owns the region. Render through: pushing into the outer
|
|
113
|
+
queue is what keeps one landmark and one pile of messages on the page.
|
|
114
|
+
After every hook, because a hook count that changes with context is a
|
|
115
|
+
React error. */
|
|
116
|
+
if (outer)
|
|
117
|
+
return _jsx(_Fragment, { children: children });
|
|
77
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 }) })] })) })] }));
|
|
78
119
|
}
|
|
@@ -1,33 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A setting that takes effect as it moves.
|
|
3
|
-
*
|
|
4
|
-
* **A checkbox and a switch are not the same control, and the difference is not
|
|
5
|
-
* visual.** A checkbox collects an answer: you tick it, and it applies when you
|
|
6
|
-
* press Save. A switch *is* the action — flipping it changes the thing, now,
|
|
7
|
-
* and there is nothing to confirm afterwards. That is why a switch reads as on
|
|
8
|
-
* or off rather than ticked or blank: the two states are both settled, and
|
|
9
|
-
* neither is a draft.
|
|
10
|
-
*
|
|
11
|
-
* Six settings in this app saved the moment they changed and were all drawn as
|
|
12
|
-
* tick boxes: whether an applet is enabled, whether it reaches the internet,
|
|
13
|
-
* whether a tool is granted, whether a server is trusted. Every one of them
|
|
14
|
-
* promised a Save button that does not
|
|
15
|
-
* exist. The header's status panel (gone since 2026-09-03) was the only one
|
|
16
|
-
* that had it right, in a comment nobody else read.
|
|
17
|
-
*
|
|
18
|
-
* **`role="switch"`, so it is announced as one.** A screen reader says "on" and
|
|
19
|
-
* "off" instead of "checked" and "not checked" — which is the same distinction
|
|
20
|
-
* in words, and the only signal a non-visual reader gets that pressing it does
|
|
21
|
-
* something immediately.
|
|
22
|
-
*
|
|
23
|
-
* **The label is the hit area.** A 30×18 target fails the minimum on its own,
|
|
24
|
-
* and reaching for the words is what people do anyway.
|
|
25
|
-
*
|
|
26
|
-
* **One switch, or several checkboxes.** A lone binary setting is a switch. A
|
|
27
|
-
* set you pick from is a list of checkboxes — fifteen switches in a column read
|
|
28
|
-
* as fifteen unrelated settings rather than one choice with fifteen parts. See
|
|
29
|
-
* `Checkbox`.
|
|
30
|
-
*/
|
|
31
1
|
export default function Toggle({ label, hint, checked, onChange, disabled, said, labelHidden, size, className, }: {
|
|
32
2
|
label: React.ReactNode;
|
|
33
3
|
/** What it does, or what turning it off costs. Under the label, in the same
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* A setting that takes effect as it moves.
|
|
4
11
|
*
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* A tooltip. It was called `Explain`, which named the intention rather than the
|
|
4
11
|
* thing -- so it sat in a catalogue beside Button and Callout as though it were
|
|
@@ -1,26 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A short walk round the chrome, once, after onboarding.
|
|
3
|
-
*
|
|
4
|
-
* Onboarding answers "what does this need to run". It says nothing about the
|
|
5
|
-
* four controls somebody is then left alone with: two floating buttons in one
|
|
6
|
-
* corner, a cog in another, and a `+` beside the board name. Every one of them
|
|
7
|
-
* is an icon with a `title`, which is a tooltip you have to already suspect is
|
|
8
|
-
* there to go looking for.
|
|
9
|
-
*
|
|
10
|
-
* **It points at the real thing.** Each step finds its target by selector and
|
|
11
|
-
* cuts a hole in the scrim over it, so the control being described is the
|
|
12
|
-
* control you can see — not a screenshot of one, which goes stale the first
|
|
13
|
-
* time the button moves.
|
|
14
|
-
*
|
|
15
|
-
* **A step whose target is missing is skipped, not shown empty.** The chat
|
|
16
|
-
* launcher is not on the page while the dock is open, and the studio button
|
|
17
|
-
* changes what it does when a session is minimised. A tour that insists on
|
|
18
|
-
* pointing at something that is not there would be describing a different app.
|
|
19
|
-
*
|
|
20
|
-
* **Escape ends it and it never comes back on its own.** This is the least
|
|
21
|
-
* important thing on the screen and it is in the way of everything else; the
|
|
22
|
-
* one unforgivable version is the one you cannot get out of.
|
|
23
|
-
*/
|
|
24
1
|
export interface TourStop {
|
|
25
2
|
/** Where it points. Missing from the page means the stop is skipped. */
|
|
26
3
|
target: string;
|
package/dist/components/Tour.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* A short walk round the chrome, once, after onboarding.
|
|
4
11
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,10 @@ export { default as Brand } from './components/Brand.js';
|
|
|
22
22
|
export { BRAND_MARKS, BRAND_NAMES } from './components/brandMarks.js';
|
|
23
23
|
export type { BrandName, FillMark, Mark, StrokeMark } from './components/brandMarks.js';
|
|
24
24
|
export { default as Button } from './components/Button.js';
|
|
25
|
-
|
|
25
|
+
/** The union: the button's props, or the DOM's when `asChild` hands the
|
|
26
|
+
* element to the caller. `ButtonOwnProps` is the button-only half, for a
|
|
27
|
+
* wrapper that never slots. */
|
|
28
|
+
export type { ButtonProps, Props as ButtonOwnProps } from './components/Button.js';
|
|
26
29
|
export { default as Callout } from './components/Callout.js';
|
|
27
30
|
export { default as Card } from './components/Card.js';
|
|
28
31
|
export { default as Checkbox } from './components/Checkbox.js';
|
|
@@ -45,6 +48,9 @@ export type { IconName } from './components/iconNames.js';
|
|
|
45
48
|
export { default as Illustration } from './components/Illustration.js';
|
|
46
49
|
export { default as Input } from './components/Input.js';
|
|
47
50
|
export type { Props as InputProps } from './components/Input.js';
|
|
51
|
+
/** A key, or a chord. The rule `Menu` and `Command` each had a private copy
|
|
52
|
+
* of, now one component a consumer can reach. */
|
|
53
|
+
export { default as Kbd } from './components/Kbd.js';
|
|
48
54
|
export { default as Markdown } from './components/Markdown.js';
|
|
49
55
|
export { default as Menu } from './components/Menu.js';
|
|
50
56
|
export type { Item as MenuItem, Section as MenuSection } from './components/Menu.js';
|
|
@@ -67,6 +73,9 @@ export type { Props as ScrollAreaProps } from './components/ScrollArea.js';
|
|
|
67
73
|
export { default as Select } from './components/Select.js';
|
|
68
74
|
export { default as SizeGrid } from './components/SizeGrid.js';
|
|
69
75
|
export { default as Skeleton } from './components/Skeleton.js';
|
|
76
|
+
/** One figure, with what it counts over it. Three apps drew this before it
|
|
77
|
+
* was here. */
|
|
78
|
+
export { default as Stat } from './components/Stat.js';
|
|
70
79
|
export { default as Slider } from './components/Slider.js';
|
|
71
80
|
/** Two panes and a handle between them, the handle being a real `separator`
|
|
72
81
|
* widget rather than a div with a mousedown listener. */
|
|
@@ -78,7 +87,9 @@ export { default as Tabs } from './components/Tabs.js';
|
|
|
78
87
|
export type { Tab, TabGroup } from './components/Tabs.js';
|
|
79
88
|
export { default as Textarea } from './components/Textarea.js';
|
|
80
89
|
export type { Props as TextareaProps } from './components/Textarea.js';
|
|
81
|
-
|
|
90
|
+
/** `ToastHost` nests: one inside another renders through, so a package can
|
|
91
|
+
* wrap itself and still leave an application with a single region. */
|
|
92
|
+
export { Toast, ToastHost, useHasToastHost, useToast } from './components/Toast.js';
|
|
82
93
|
export { default as Toggle } from './components/Toggle.js';
|
|
83
94
|
export { default as Tooltip } from './components/Tooltip.js';
|
|
84
95
|
export { default as Tour } from './components/Tour.js';
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,9 @@ export { hueOf, initialsOf } from './components/initials.js';
|
|
|
42
42
|
export { ICON_NAMES } from './components/iconNames.js';
|
|
43
43
|
export { default as Illustration } from './components/Illustration.js';
|
|
44
44
|
export { default as Input } from './components/Input.js';
|
|
45
|
+
/** A key, or a chord. The rule `Menu` and `Command` each had a private copy
|
|
46
|
+
* of, now one component a consumer can reach. */
|
|
47
|
+
export { default as Kbd } from './components/Kbd.js';
|
|
45
48
|
export { default as Markdown } from './components/Markdown.js';
|
|
46
49
|
export { default as Menu } from './components/Menu.js';
|
|
47
50
|
export { default as Modal } from './components/Modal.js';
|
|
@@ -62,6 +65,9 @@ export { default as ScrollArea } from './components/ScrollArea.js';
|
|
|
62
65
|
export { default as Select } from './components/Select.js';
|
|
63
66
|
export { default as SizeGrid } from './components/SizeGrid.js';
|
|
64
67
|
export { default as Skeleton } from './components/Skeleton.js';
|
|
68
|
+
/** One figure, with what it counts over it. Three apps drew this before it
|
|
69
|
+
* was here. */
|
|
70
|
+
export { default as Stat } from './components/Stat.js';
|
|
65
71
|
export { default as Slider } from './components/Slider.js';
|
|
66
72
|
/** Two panes and a handle between them, the handle being a real `separator`
|
|
67
73
|
* widget rather than a div with a mousedown listener. */
|
|
@@ -69,7 +75,9 @@ export { default as SplitPane } from './components/SplitPane.js';
|
|
|
69
75
|
export { default as Table } from './components/Table.js';
|
|
70
76
|
export { default as Tabs } from './components/Tabs.js';
|
|
71
77
|
export { default as Textarea } from './components/Textarea.js';
|
|
72
|
-
|
|
78
|
+
/** `ToastHost` nests: one inside another renders through, so a package can
|
|
79
|
+
* wrap itself and still leave an application with a single region. */
|
|
80
|
+
export { ToastHost, useHasToastHost, useToast } from './components/Toast.js';
|
|
73
81
|
export { default as Toggle } from './components/Toggle.js';
|
|
74
82
|
export { default as Tooltip } from './components/Tooltip.js';
|
|
75
83
|
export { default as Tour } from './components/Tour.js';
|