@wtfalch/design 0.1.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/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/components/Brand.d.ts +10 -0
- package/dist/components/Brand.js +212 -0
- package/dist/components/Button.d.ts +63 -0
- package/dist/components/Button.js +74 -0
- package/dist/components/Callout.d.ts +37 -0
- package/dist/components/Callout.js +71 -0
- package/dist/components/Card.d.ts +42 -0
- package/dist/components/Card.js +30 -0
- package/dist/components/Checkbox.d.ts +32 -0
- package/dist/components/Checkbox.js +31 -0
- package/dist/components/DangerZone.d.ts +59 -0
- package/dist/components/DangerZone.js +50 -0
- package/dist/components/Dialog.d.ts +28 -0
- package/dist/components/Dialog.js +29 -0
- package/dist/components/Empty.d.ts +45 -0
- package/dist/components/Empty.js +35 -0
- package/dist/components/Field.d.ts +58 -0
- package/dist/components/Field.js +46 -0
- package/dist/components/Icon.d.ts +64 -0
- package/dist/components/Icon.js +235 -0
- package/dist/components/Illustration.d.ts +36 -0
- package/dist/components/Illustration.js +48 -0
- package/dist/components/Input.d.ts +14 -0
- package/dist/components/Input.js +65 -0
- package/dist/components/Markdown.d.ts +21 -0
- package/dist/components/Markdown.js +29 -0
- package/dist/components/Modal.d.ts +59 -0
- package/dist/components/Modal.js +72 -0
- package/dist/components/Pill.d.ts +40 -0
- package/dist/components/Pill.js +41 -0
- package/dist/components/Progress.d.ts +35 -0
- package/dist/components/Progress.js +27 -0
- package/dist/components/Rows.d.ts +101 -0
- package/dist/components/Rows.js +55 -0
- package/dist/components/Select.d.ts +28 -0
- package/dist/components/Select.js +56 -0
- package/dist/components/SizeGrid.d.ts +34 -0
- package/dist/components/SizeGrid.js +41 -0
- package/dist/components/Skeleton.d.ts +45 -0
- package/dist/components/Skeleton.js +47 -0
- package/dist/components/Slider.d.ts +70 -0
- package/dist/components/Slider.js +100 -0
- package/dist/components/Table.d.ts +43 -0
- package/dist/components/Table.js +13 -0
- package/dist/components/Tabs.d.ts +72 -0
- package/dist/components/Tabs.js +82 -0
- package/dist/components/Textarea.d.ts +9 -0
- package/dist/components/Textarea.js +22 -0
- package/dist/components/Toast.d.ts +43 -0
- package/dist/components/Toast.js +78 -0
- package/dist/components/Toggle.d.ts +56 -0
- package/dist/components/Toggle.js +189 -0
- package/dist/components/Tooltip.d.ts +22 -0
- package/dist/components/Tooltip.js +62 -0
- package/dist/components/Tour.d.ts +33 -0
- package/dist/components/Tour.js +108 -0
- package/dist/components/iconNames.d.ts +18 -0
- package/dist/components/iconNames.js +60 -0
- package/dist/components/tourMarker.d.ts +29 -0
- package/dist/components/tourMarker.js +58 -0
- package/dist/contrast.d.ts +18 -0
- package/dist/contrast.js +27 -0
- package/dist/hooks/useTrapFocus.d.ts +24 -0
- package/dist/hooks/useTrapFocus.js +67 -0
- package/dist/illustrations.d.ts +11 -0
- package/dist/illustrations.js +55 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +65 -0
- package/dist/styles/index.css +3124 -0
- package/dist/themes.d.ts +210 -0
- package/dist/themes.js +300 -0
- package/dist/tokens.css +251 -0
- package/package.json +74 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A box with a reason to exist.
|
|
3
|
+
*
|
|
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
|
|
6
|
+
* `truncate` span and a description in a `muted mono` div, with the spacing
|
|
7
|
+
* retyped each time. Four variants of that shape existed and no two agreed on
|
|
8
|
+
* the gap between the title and the line under it.
|
|
9
|
+
*
|
|
10
|
+
* **`title` and `description` are props, so the parts are named.** The same
|
|
11
|
+
* argument as `Field`: a component that takes a heading and a sentence cannot
|
|
12
|
+
* put them in the wrong order or forget to associate them, and a caller that
|
|
13
|
+
* only has a heading does not have to remember which wrapper it goes in.
|
|
14
|
+
*
|
|
15
|
+
* **The icon is optional and it is decoration.** `aria-hidden`, always — a card
|
|
16
|
+
* with `icon="image"` beside a title that says "A model that draws" is the same
|
|
17
|
+
* word twice to a screen reader. It is there to make a page of cards scannable
|
|
18
|
+
* by shape, which is a thing eyes do and readers do not.
|
|
19
|
+
*/
|
|
20
|
+
import { type IconName } from './Icon';
|
|
21
|
+
export default function Card({ icon, title, description, action, children, tone, onClick, selected, className, }: {
|
|
22
|
+
icon?: IconName;
|
|
23
|
+
title?: React.ReactNode;
|
|
24
|
+
/** One or two sentences under the title. What this is for, or what pressing
|
|
25
|
+
* the thing inside it will do. */
|
|
26
|
+
description?: React.ReactNode;
|
|
27
|
+
/** The control the card is about — a switch, usually. Far right, centred
|
|
28
|
+
* against the whole heading block rather than against the title, so it does
|
|
29
|
+
* not drift up when the description runs to two lines. */
|
|
30
|
+
action?: React.ReactNode;
|
|
31
|
+
children?: React.ReactNode;
|
|
32
|
+
/** `bad` for the one that failed — a card in a list, not a whole screen.
|
|
33
|
+
* `warn` for a card whose contents want reading before touching: the
|
|
34
|
+
* same colours `Callout` uses for the tone, so the card reads as the
|
|
35
|
+
* callout it replaces. */
|
|
36
|
+
tone?: 'bad' | 'warn';
|
|
37
|
+
/** Makes the card itself the choice. Rendered as a button, so it has a
|
|
38
|
+
* keyboard and a focus ring rather than a hover state and nothing else. */
|
|
39
|
+
onClick?: () => void;
|
|
40
|
+
selected?: boolean;
|
|
41
|
+
className?: string;
|
|
42
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A box with a reason to exist.
|
|
4
|
+
*
|
|
5
|
+
* `.card` was CSS and nothing else, so every caller assembled its own header:
|
|
6
|
+
* a `<div className="row">`, a `<div className="grow">`, a title in a
|
|
7
|
+
* `truncate` span and a description in a `muted mono` div, with the spacing
|
|
8
|
+
* retyped each time. Four variants of that shape existed and no two agreed on
|
|
9
|
+
* the gap between the title and the line under it.
|
|
10
|
+
*
|
|
11
|
+
* **`title` and `description` are props, so the parts are named.** The same
|
|
12
|
+
* argument as `Field`: a component that takes a heading and a sentence cannot
|
|
13
|
+
* put them in the wrong order or forget to associate them, and a caller that
|
|
14
|
+
* only has a heading does not have to remember which wrapper it goes in.
|
|
15
|
+
*
|
|
16
|
+
* **The icon is optional and it is decoration.** `aria-hidden`, always — a card
|
|
17
|
+
* with `icon="image"` beside a title that says "A model that draws" is the same
|
|
18
|
+
* word twice to a screen reader. It is there to make a page of cards scannable
|
|
19
|
+
* by shape, which is a thing eyes do and readers do not.
|
|
20
|
+
*/
|
|
21
|
+
import Icon from './Icon';
|
|
22
|
+
export default function Card({ icon, title, description, action, children, tone, onClick, selected, className, }) {
|
|
23
|
+
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 })] }));
|
|
24
|
+
const cls = `card${onClick ? ' card-pick' : ''}${selected ? ' card-on' : ''}${tone ? ` card-${tone}` : ''}${className ? ` ${className}` : ''}`;
|
|
25
|
+
const body = children && _jsx("div", { className: "card-body", children: children });
|
|
26
|
+
if (onClick) {
|
|
27
|
+
return (_jsxs("button", { type: "button", className: cls, "aria-pressed": selected, onClick: onClick, children: [head, body] }));
|
|
28
|
+
}
|
|
29
|
+
return (_jsxs("div", { className: cls, children: [head, body] }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choose some of these, then press the button that applies them.
|
|
3
|
+
*
|
|
4
|
+
* The fourth component the gallery documented that nobody could import — and
|
|
5
|
+
* this one is worse than the others, because the markup it documents was
|
|
6
|
+
* *designed* to replace the native tick box and then never used anywhere. Four
|
|
7
|
+
* places went on rendering `<input type="checkbox">` in a `.set-row` while
|
|
8
|
+
* `.choice` sat in the stylesheet with a page of its own.
|
|
9
|
+
*
|
|
10
|
+
* **The native box is hidden, not styled.** A checkbox styles down to a square
|
|
11
|
+
* and no further; the tick inside it is the operating system's, in its shape,
|
|
12
|
+
* at its size. So the input is present and off-screen — it is what a keyboard
|
|
13
|
+
* reaches and what a screen reader reads — and the ring and tick are drawn by
|
|
14
|
+
* the row. Clicking anywhere in the row toggles it, because the row is the
|
|
15
|
+
* `<label>`.
|
|
16
|
+
*
|
|
17
|
+
* **This is not a `Toggle`.** A switch is the action and applies as it moves; a
|
|
18
|
+
* checkbox is an answer that applies when something else is pressed. If there
|
|
19
|
+
* is no Save at the end of it, this is the wrong control — see `Toggle`.
|
|
20
|
+
*/
|
|
21
|
+
export default function Checkbox({ label, hint, meta, checked, onChange, disabled, className, }: {
|
|
22
|
+
label: React.ReactNode;
|
|
23
|
+
/** What choosing it means, or what it costs. Under the name. `hint`, the
|
|
24
|
+
* word `Toggle`, `Slider` and `Field` use for the same line; it was `why`. */
|
|
25
|
+
hint?: React.ReactNode;
|
|
26
|
+
/** A quieter third line — a path, a size, an id. */
|
|
27
|
+
meta?: React.ReactNode;
|
|
28
|
+
checked: boolean;
|
|
29
|
+
onChange: (on: boolean) => void;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
className?: string;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Choose some of these, then press the button that applies them.
|
|
4
|
+
*
|
|
5
|
+
* The fourth component the gallery documented that nobody could import — and
|
|
6
|
+
* this one is worse than the others, because the markup it documents was
|
|
7
|
+
* *designed* to replace the native tick box and then never used anywhere. Four
|
|
8
|
+
* places went on rendering `<input type="checkbox">` in a `.set-row` while
|
|
9
|
+
* `.choice` sat in the stylesheet with a page of its own.
|
|
10
|
+
*
|
|
11
|
+
* **The native box is hidden, not styled.** A checkbox styles down to a square
|
|
12
|
+
* and no further; the tick inside it is the operating system's, in its shape,
|
|
13
|
+
* at its size. So the input is present and off-screen — it is what a keyboard
|
|
14
|
+
* reaches and what a screen reader reads — and the ring and tick are drawn by
|
|
15
|
+
* the row. Clicking anywhere in the row toggles it, because the row is the
|
|
16
|
+
* `<label>`.
|
|
17
|
+
*
|
|
18
|
+
* **This is not a `Toggle`.** A switch is the action and applies as it moves; a
|
|
19
|
+
* checkbox is an answer that applies when something else is pressed. If there
|
|
20
|
+
* is no Save at the end of it, this is the wrong control — see `Toggle`.
|
|
21
|
+
*/
|
|
22
|
+
import { Checkbox as AriaCheckbox } from 'react-aria-components';
|
|
23
|
+
export default function Checkbox({ label, hint, meta, checked, onChange, disabled, className, }) {
|
|
24
|
+
/* React Aria's `Checkbox` is the `<label>`. It keeps the real input in the
|
|
25
|
+
markup and visually hidden -- exactly the arrangement this component
|
|
26
|
+
already had by hand -- and stamps `data-selected`, `data-focus-visible`
|
|
27
|
+
and `data-disabled` on the row. The ring and the tick are still drawn by
|
|
28
|
+
the row's `::after` and `::before`; `checkbox.css` now reads the state off
|
|
29
|
+
those attributes instead of `:has(input:checked)`. */
|
|
30
|
+
return (_jsx(AriaCheckbox, { className: `choice${className ? ` ${className}` : ''}`, isSelected: checked, onChange: onChange, 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 })] }) }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The bottom of a settings panel, where the things that cannot be undone live.
|
|
4
|
+
*
|
|
5
|
+
* One bordered section with its actions divided inside it, rather than a box
|
|
6
|
+
* per action. Three tiles of red furniture stacked down a pane read as three
|
|
7
|
+
* warnings; one section with three things in it reads as a place — which is
|
|
8
|
+
* the point, because a place is somewhere you have to go.
|
|
9
|
+
*
|
|
10
|
+
* The same shape as the one in valet, and deliberately: these are the same
|
|
11
|
+
* decision in two products, and a person who has learned to be careful in one
|
|
12
|
+
* should not have to learn it again in the other.
|
|
13
|
+
*
|
|
14
|
+
* What differs is what a confirmation costs. Typing a name is the right price
|
|
15
|
+
* for something with no undo and the wrong one for something with an obvious
|
|
16
|
+
* undo — so the section says up front which of its actions are which, and each
|
|
17
|
+
* asks for what it is worth.
|
|
18
|
+
*/
|
|
19
|
+
export default function DangerZone({
|
|
20
|
+
/** Which of these can be taken back, said before anything is pressed. */
|
|
21
|
+
note, children, className, }: {
|
|
22
|
+
note: string;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
className?: string;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
26
|
+
/**
|
|
27
|
+
* One thing that can be done here, and what it costs.
|
|
28
|
+
*
|
|
29
|
+
* `confirm` is the whole of the difference between them:
|
|
30
|
+
*
|
|
31
|
+
* - `click` — a second press, for something with a way back. The button says
|
|
32
|
+
* what it will do rather than "Are you sure?", because a person reading
|
|
33
|
+
* "Remove" twice has read what it removes.
|
|
34
|
+
* - `type` — the name, for something with no way back. A second click in the
|
|
35
|
+
* same place as the first is a reflex; writing the thing out is the only
|
|
36
|
+
* confirmation that requires having read what it is.
|
|
37
|
+
*/
|
|
38
|
+
export declare function DangerAction({ title, description,
|
|
39
|
+
/** Not destructive at all — rename, edit. No confirmation, no red. */
|
|
40
|
+
kind, confirm,
|
|
41
|
+
/** What must be typed, when `confirm` is `type`. */
|
|
42
|
+
match, label, busyLabel, busy, disabled, onConfirm,
|
|
43
|
+
/** Rendered instead of the controls, saying why this cannot be done. */
|
|
44
|
+
unavailable, }: {
|
|
45
|
+
/** `title` and `description`, the words `Card`, `Modal` and `Dialog` use;
|
|
46
|
+
* they were `heading` and `body`. */
|
|
47
|
+
title: string;
|
|
48
|
+
description: ReactNode;
|
|
49
|
+
/** `kind`, like `Button`: a role, not a colour. */
|
|
50
|
+
kind?: 'destructive' | 'plain';
|
|
51
|
+
confirm?: 'click' | 'type' | 'none';
|
|
52
|
+
match?: string;
|
|
53
|
+
label: string;
|
|
54
|
+
busyLabel?: string;
|
|
55
|
+
busy?: boolean;
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
onConfirm: () => void;
|
|
58
|
+
unavailable?: ReactNode;
|
|
59
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import Button from './Button';
|
|
4
|
+
import Field from './Field';
|
|
5
|
+
import Input from './Input';
|
|
6
|
+
/**
|
|
7
|
+
* The bottom of a settings panel, where the things that cannot be undone live.
|
|
8
|
+
*
|
|
9
|
+
* One bordered section with its actions divided inside it, rather than a box
|
|
10
|
+
* per action. Three tiles of red furniture stacked down a pane read as three
|
|
11
|
+
* warnings; one section with three things in it reads as a place — which is
|
|
12
|
+
* the point, because a place is somewhere you have to go.
|
|
13
|
+
*
|
|
14
|
+
* The same shape as the one in valet, and deliberately: these are the same
|
|
15
|
+
* decision in two products, and a person who has learned to be careful in one
|
|
16
|
+
* should not have to learn it again in the other.
|
|
17
|
+
*
|
|
18
|
+
* What differs is what a confirmation costs. Typing a name is the right price
|
|
19
|
+
* for something with no undo and the wrong one for something with an obvious
|
|
20
|
+
* undo — so the section says up front which of its actions are which, and each
|
|
21
|
+
* asks for what it is worth.
|
|
22
|
+
*/
|
|
23
|
+
export default function DangerZone({
|
|
24
|
+
/** Which of these can be taken back, said before anything is pressed. */
|
|
25
|
+
note, children, className, }) {
|
|
26
|
+
return (_jsxs("section", { className: `danger-zone${className ? ` ${className}` : ''}`, children: [_jsxs("div", { className: "danger-zone-head", children: [_jsx("h3", { children: "Danger zone" }), _jsx("p", { className: "set-hint", children: note })] }), children] }));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One thing that can be done here, and what it costs.
|
|
30
|
+
*
|
|
31
|
+
* `confirm` is the whole of the difference between them:
|
|
32
|
+
*
|
|
33
|
+
* - `click` — a second press, for something with a way back. The button says
|
|
34
|
+
* what it will do rather than "Are you sure?", because a person reading
|
|
35
|
+
* "Remove" twice has read what it removes.
|
|
36
|
+
* - `type` — the name, for something with no way back. A second click in the
|
|
37
|
+
* same place as the first is a reflex; writing the thing out is the only
|
|
38
|
+
* confirmation that requires having read what it is.
|
|
39
|
+
*/
|
|
40
|
+
export function DangerAction({ title, description,
|
|
41
|
+
/** Not destructive at all — rename, edit. No confirmation, no red. */
|
|
42
|
+
kind = 'destructive', confirm = 'click',
|
|
43
|
+
/** What must be typed, when `confirm` is `type`. */
|
|
44
|
+
match, label, busyLabel, busy = false, disabled = false, onConfirm,
|
|
45
|
+
/** Rendered instead of the controls, saying why this cannot be done. */
|
|
46
|
+
unavailable, }) {
|
|
47
|
+
const [asking, setAsking] = useState(false);
|
|
48
|
+
const [typed, setTyped] = useState('');
|
|
49
|
+
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 }) }))] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A question that stops what you were doing until it is answered.
|
|
3
|
+
*
|
|
4
|
+
* `Modal` with three things changed, and each of them is a decision rather than
|
|
5
|
+
* a size:
|
|
6
|
+
*
|
|
7
|
+
* **Narrow, and centred.** `Modal` is sized for Settings -- wide, and starting
|
|
8
|
+
* near the top of the screen, because eleven panes of preferences need the room
|
|
9
|
+
* and want to begin where the eye already is. A two-button question inheriting
|
|
10
|
+
* that becomes a grey band across the top: the shape says "here is somewhere to
|
|
11
|
+
* work" while the content says "answer this and go".
|
|
12
|
+
*
|
|
13
|
+
* **The scrim decides nothing.** It is the easiest thing on screen to hit by
|
|
14
|
+
* accident, and for "may this applet write to your files" the accident would be
|
|
15
|
+
* an answer. Closing a workspace by mistake costs a click; closing a question
|
|
16
|
+
* by mistake means it was answered without being read.
|
|
17
|
+
*
|
|
18
|
+
* **Escape is optional.** Omit `onCancel` when there is no safe default -- a
|
|
19
|
+
* decision that must be made has no Escape key, because dismissing it would be
|
|
20
|
+
* choosing on the reader's behalf.
|
|
21
|
+
*/
|
|
22
|
+
export default function Dialog({ title, children, onCancel, actions, tone, }: {
|
|
23
|
+
title: string;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
onCancel?: () => void;
|
|
26
|
+
actions: React.ReactNode;
|
|
27
|
+
tone?: 'bad';
|
|
28
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A question that stops what you were doing until it is answered.
|
|
4
|
+
*
|
|
5
|
+
* `Modal` with three things changed, and each of them is a decision rather than
|
|
6
|
+
* a size:
|
|
7
|
+
*
|
|
8
|
+
* **Narrow, and centred.** `Modal` is sized for Settings -- wide, and starting
|
|
9
|
+
* near the top of the screen, because eleven panes of preferences need the room
|
|
10
|
+
* and want to begin where the eye already is. A two-button question inheriting
|
|
11
|
+
* that becomes a grey band across the top: the shape says "here is somewhere to
|
|
12
|
+
* work" while the content says "answer this and go".
|
|
13
|
+
*
|
|
14
|
+
* **The scrim decides nothing.** It is the easiest thing on screen to hit by
|
|
15
|
+
* accident, and for "may this applet write to your files" the accident would be
|
|
16
|
+
* an answer. Closing a workspace by mistake costs a click; closing a question
|
|
17
|
+
* by mistake means it was answered without being read.
|
|
18
|
+
*
|
|
19
|
+
* **Escape is optional.** Omit `onCancel` when there is no safe default -- a
|
|
20
|
+
* decision that must be made has no Escape key, because dismissing it would be
|
|
21
|
+
* choosing on the reader's behalf.
|
|
22
|
+
*/
|
|
23
|
+
import Modal from './Modal';
|
|
24
|
+
export default function Dialog({ title, children, onCancel, actions, tone, }) {
|
|
25
|
+
return (_jsx(Modal, { title: title, className: `dialog${tone ? ` dialog-${tone}` : ''}`, onClose: onCancel, dismissOnScrim: false,
|
|
26
|
+
/* The answers are the buttons. A ✕ beside them is a third answer that
|
|
27
|
+
means nothing. */
|
|
28
|
+
closeButton: false, footer: actions, children: children }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A list with nothing in it, said properly.
|
|
3
|
+
*
|
|
4
|
+
* There were two conventions and neither was a component. `.empty` — centred,
|
|
5
|
+
* generously padded — was used by five files; the settings panes used a bare
|
|
6
|
+
* `.set-hint`, which is the same 12px muted line that explains a heading. So
|
|
7
|
+
* "None configured." rendered as a fragment of caption floating under a card
|
|
8
|
+
* title, indistinguishable from a description of the thing above it, and a
|
|
9
|
+
* pane with nothing in it looked like a pane that had failed to finish drawing.
|
|
10
|
+
*
|
|
11
|
+
* **An empty state has three jobs and the old one did none of them.** Say the
|
|
12
|
+
* list is empty rather than leaving it ambiguous with loading. Say why, if
|
|
13
|
+
* there is a why. Offer the thing you would do next, if there is one.
|
|
14
|
+
*
|
|
15
|
+
* **The icon is decoration and is marked as such.** It is there so a pane of
|
|
16
|
+
* cards is scannable by shape — the same argument as `Card`'s. A screen reader
|
|
17
|
+
* gets the sentence, which is the part that carries the meaning.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately quieter than a full-screen empty state: these sit inside a card
|
|
20
|
+
* that already has a heading, so a second large heading here would compete with
|
|
21
|
+
* the one above it. One line, optionally one button.
|
|
22
|
+
*
|
|
23
|
+
* **`illustration` is the same box given a whole surface.** A view with no
|
|
24
|
+
* applets on it is not a slot in a card -- it is the window, and the dashed
|
|
25
|
+
* outline that marks out where a list will be reads as a broken layout at that
|
|
26
|
+
* size. So the figure replaces the icon, the outline goes, and the box centres
|
|
27
|
+
* itself in whatever it was put in. Same component, because it is the same
|
|
28
|
+
* three jobs: say it is empty, say why, offer the next thing.
|
|
29
|
+
*/
|
|
30
|
+
import { type IconName } from './Icon';
|
|
31
|
+
import { type IllustrationName } from './Illustration';
|
|
32
|
+
export default function Empty({ icon, illustration, children, action, className, }: {
|
|
33
|
+
/** Decoration. Omit it where the surrounding card already carries one. */
|
|
34
|
+
icon?: IconName;
|
|
35
|
+
/** For an empty *surface* rather than an empty list: a figure instead of the
|
|
36
|
+
* small mark, and the box loses its outline and centres itself. Wins over
|
|
37
|
+
* `icon` where both are given -- two drawings saying the same thing is the
|
|
38
|
+
* noise this is meant to avoid. */
|
|
39
|
+
illustration?: IllustrationName;
|
|
40
|
+
/** What is empty, and why if that helps. A sentence, not a label. */
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
/** The obvious next thing, where there is one. */
|
|
43
|
+
action?: React.ReactNode;
|
|
44
|
+
className?: string;
|
|
45
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A list with nothing in it, said properly.
|
|
4
|
+
*
|
|
5
|
+
* There were two conventions and neither was a component. `.empty` — centred,
|
|
6
|
+
* generously padded — was used by five files; the settings panes used a bare
|
|
7
|
+
* `.set-hint`, which is the same 12px muted line that explains a heading. So
|
|
8
|
+
* "None configured." rendered as a fragment of caption floating under a card
|
|
9
|
+
* title, indistinguishable from a description of the thing above it, and a
|
|
10
|
+
* pane with nothing in it looked like a pane that had failed to finish drawing.
|
|
11
|
+
*
|
|
12
|
+
* **An empty state has three jobs and the old one did none of them.** Say the
|
|
13
|
+
* list is empty rather than leaving it ambiguous with loading. Say why, if
|
|
14
|
+
* there is a why. Offer the thing you would do next, if there is one.
|
|
15
|
+
*
|
|
16
|
+
* **The icon is decoration and is marked as such.** It is there so a pane of
|
|
17
|
+
* cards is scannable by shape — the same argument as `Card`'s. A screen reader
|
|
18
|
+
* gets the sentence, which is the part that carries the meaning.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately quieter than a full-screen empty state: these sit inside a card
|
|
21
|
+
* that already has a heading, so a second large heading here would compete with
|
|
22
|
+
* the one above it. One line, optionally one button.
|
|
23
|
+
*
|
|
24
|
+
* **`illustration` is the same box given a whole surface.** A view with no
|
|
25
|
+
* applets on it is not a slot in a card -- it is the window, and the dashed
|
|
26
|
+
* outline that marks out where a list will be reads as a broken layout at that
|
|
27
|
+
* size. So the figure replaces the icon, the outline goes, and the box centres
|
|
28
|
+
* itself in whatever it was put in. Same component, because it is the same
|
|
29
|
+
* three jobs: say it is empty, say why, offer the next thing.
|
|
30
|
+
*/
|
|
31
|
+
import Icon from './Icon';
|
|
32
|
+
import Illustration from './Illustration';
|
|
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 })] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A labelled input, its explanation, and what went wrong with it.
|
|
3
|
+
*
|
|
4
|
+
* **Almost every input in this app is labelled by its placeholder.**
|
|
5
|
+
* `placeholder="name"`, `placeholder="command"`,
|
|
6
|
+
* `placeholder="https://mcp.example.com/sse"` -- and a placeholder is not a
|
|
7
|
+
* label. It disappears the moment somebody types, so the one time you most want
|
|
8
|
+
* to know what a field was for is the one time it is gone: reviewing what you
|
|
9
|
+
* filled in. Half-finished forms are full of fields nobody can identify without
|
|
10
|
+
* clearing them first. Assistive technology treats it as a last-resort fallback
|
|
11
|
+
* for the same reason.
|
|
12
|
+
*
|
|
13
|
+
* **And thirty errors are red text sitting near a field, associated with
|
|
14
|
+
* nothing.** `<div className="set-hint" style={{ color: 'var(--bad)' }}>` says
|
|
15
|
+
* something is wrong to anyone looking directly at it, and says nothing at all
|
|
16
|
+
* to a screen reader on the input itself: no `aria-invalid`, no
|
|
17
|
+
* `aria-describedby`, so you can tab into a field the form has rejected and be
|
|
18
|
+
* told only its name. Colour alone carrying the meaning is the same failure in
|
|
19
|
+
* the other direction.
|
|
20
|
+
*
|
|
21
|
+
* So: the label is an element, the description and the error are wired to the
|
|
22
|
+
* input with `aria-describedby`, and a field in error says so with
|
|
23
|
+
* `aria-invalid` as well as with red.
|
|
24
|
+
*
|
|
25
|
+
* **The description sits above the input and the error below it**, in the order
|
|
26
|
+
* they are wanted. The description is what you read before typing -- under the
|
|
27
|
+
* field it is behind the cursor, arriving after the decision it was meant to
|
|
28
|
+
* inform. The error is the reply to what you typed, and a reply belongs after
|
|
29
|
+
* the thing it answers. Both can show at once: the rule still holds while you
|
|
30
|
+
* are breaking it.
|
|
31
|
+
*/
|
|
32
|
+
export interface FieldWiring {
|
|
33
|
+
id: string;
|
|
34
|
+
'aria-describedby': string | undefined;
|
|
35
|
+
'aria-invalid': boolean | undefined;
|
|
36
|
+
}
|
|
37
|
+
export default function Field({ label, hint, error, required, children, labelHidden, layout, className, }: {
|
|
38
|
+
/** What the field is. Always given -- there is no unlabelled case, only
|
|
39
|
+
* fields whose label is hidden. */
|
|
40
|
+
label: string;
|
|
41
|
+
/** What to put in it, or what it will do. Read before typing, so it is drawn
|
|
42
|
+
* above the input. */
|
|
43
|
+
hint?: React.ReactNode;
|
|
44
|
+
/** What is wrong. Truthy switches the field into its invalid state. */
|
|
45
|
+
error?: React.ReactNode;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
/** For a field whose surroundings already name it -- a search box under a
|
|
48
|
+
* heading that says Search. Still announced, just not drawn. */
|
|
49
|
+
labelHidden?: boolean;
|
|
50
|
+
/** `row` puts the label beside the control instead of above it, which is the
|
|
51
|
+
* shape a settings pane wants: a column of names down the left and their
|
|
52
|
+
* controls down the right, scannable as a list of what is set rather than a
|
|
53
|
+
* form to fill in. Same element, same wiring, same guarantees -- the only
|
|
54
|
+
* thing that changes is where the label sits. */
|
|
55
|
+
layout?: 'stack' | 'row';
|
|
56
|
+
children: (field: FieldWiring) => React.ReactNode;
|
|
57
|
+
className?: string;
|
|
58
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A labelled input, its explanation, and what went wrong with it.
|
|
4
|
+
*
|
|
5
|
+
* **Almost every input in this app is labelled by its placeholder.**
|
|
6
|
+
* `placeholder="name"`, `placeholder="command"`,
|
|
7
|
+
* `placeholder="https://mcp.example.com/sse"` -- and a placeholder is not a
|
|
8
|
+
* label. It disappears the moment somebody types, so the one time you most want
|
|
9
|
+
* to know what a field was for is the one time it is gone: reviewing what you
|
|
10
|
+
* filled in. Half-finished forms are full of fields nobody can identify without
|
|
11
|
+
* clearing them first. Assistive technology treats it as a last-resort fallback
|
|
12
|
+
* for the same reason.
|
|
13
|
+
*
|
|
14
|
+
* **And thirty errors are red text sitting near a field, associated with
|
|
15
|
+
* nothing.** `<div className="set-hint" style={{ color: 'var(--bad)' }}>` says
|
|
16
|
+
* something is wrong to anyone looking directly at it, and says nothing at all
|
|
17
|
+
* to a screen reader on the input itself: no `aria-invalid`, no
|
|
18
|
+
* `aria-describedby`, so you can tab into a field the form has rejected and be
|
|
19
|
+
* told only its name. Colour alone carrying the meaning is the same failure in
|
|
20
|
+
* the other direction.
|
|
21
|
+
*
|
|
22
|
+
* So: the label is an element, the description and the error are wired to the
|
|
23
|
+
* input with `aria-describedby`, and a field in error says so with
|
|
24
|
+
* `aria-invalid` as well as with red.
|
|
25
|
+
*
|
|
26
|
+
* **The description sits above the input and the error below it**, in the order
|
|
27
|
+
* they are wanted. The description is what you read before typing -- under the
|
|
28
|
+
* field it is behind the cursor, arriving after the decision it was meant to
|
|
29
|
+
* inform. The error is the reply to what you typed, and a reply belongs after
|
|
30
|
+
* the thing it answers. Both can show at once: the rule still holds while you
|
|
31
|
+
* are breaking it.
|
|
32
|
+
*/
|
|
33
|
+
import { useId } from 'react';
|
|
34
|
+
export default function Field({ label, hint, error, required, children, labelHidden, layout = 'stack', className, }) {
|
|
35
|
+
const id = useId();
|
|
36
|
+
const hintId = `${id}-hint`;
|
|
37
|
+
const errorId = `${id}-error`;
|
|
38
|
+
return (_jsxs("div", { className: `field field-${layout}-layout${error ? ' field-bad' : ''}${className ? ` ${className}` : ''}`, children: [_jsxs("label", { 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 })), children({
|
|
39
|
+
id,
|
|
40
|
+
/* Both, in reading order, when both are there. A field that has a rule
|
|
41
|
+
and has broken it needs to say the rule too -- "must be a URL" on its
|
|
42
|
+
own does not tell you what shape of URL. */
|
|
43
|
+
'aria-describedby': [hint && hintId, error && errorId].filter(Boolean).join(' ') || undefined,
|
|
44
|
+
'aria-invalid': error ? true : undefined,
|
|
45
|
+
}), error && (_jsx("p", { className: "field-error", id: errorId, role: "alert", children: error }))] }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icon set. One component, one library, named glyphs.
|
|
3
|
+
*
|
|
4
|
+
* There were six hand-drawn icon components -- `ChatIcon`, `GearIcon`,
|
|
5
|
+
* `RefreshIcon`, `MinimizeIcon`, `StudioIcon`, `ConnectivityIcon` -- each a file
|
|
6
|
+
* exporting one path, each drawn to its own weight and its own optical size. Six
|
|
7
|
+
* hands, six sizes, and no way to add a seventh except by drawing it. That is
|
|
8
|
+
* how an interface ends up with a 14px gear beside a 16px chevron and nobody
|
|
9
|
+
* able to say which is wrong.
|
|
10
|
+
*
|
|
11
|
+
* These are **Pepicons Pencil**, hand-drawn, to sit with the Open Peeps
|
|
12
|
+
* illustrations rather than against them. Material Symbols was the set before,
|
|
13
|
+
* and the mismatch was not a matter of taste: Material is a *filled* set at ~27%
|
|
14
|
+
* ink coverage, Open Peeps is pure outline. Two drawings that share a screen
|
|
15
|
+
* should share a technique.
|
|
16
|
+
*
|
|
17
|
+
* **Attribution is a licence condition, not a courtesy.** Pepicons is CC BY 4.0
|
|
18
|
+
* (Open Peeps was CC0, which is why nothing like this line existed before).
|
|
19
|
+
* Credit: Pepicons by Christoph Kuehl -- https://pepicons.com. This comment is
|
|
20
|
+
* not enough on its own; the credit has to be somewhere a user can reach.
|
|
21
|
+
*
|
|
22
|
+
* **Inlined rather than installed.** The npm package ships the whole set, and a
|
|
23
|
+
* font or a bundle has to load before anything draws -- in an app that opens
|
|
24
|
+
* offline, on a machine that has just downloaded it, that is a flash of missing
|
|
25
|
+
* chrome on the one screen that has to inspire confidence. Nineteen glyphs cost
|
|
26
|
+
* nothing and cannot fail to arrive.
|
|
27
|
+
*
|
|
28
|
+
* `currentColor` throughout, so an icon takes the colour of the thing it sits
|
|
29
|
+
* in and never needs a variant.
|
|
30
|
+
*
|
|
31
|
+
* Source names, if you need to add a twentieth from https://pepicons.com:
|
|
32
|
+
* chat text-bubble
|
|
33
|
+
* settings gear
|
|
34
|
+
* refresh arrows-spin
|
|
35
|
+
* minimize minus
|
|
36
|
+
* code code
|
|
37
|
+
* wifi wifi
|
|
38
|
+
* wifi-off wifi-off
|
|
39
|
+
* check checkmark
|
|
40
|
+
* close times
|
|
41
|
+
* info info-circle
|
|
42
|
+
* warning exclamation-circle
|
|
43
|
+
* error no-entry
|
|
44
|
+
* expand expand
|
|
45
|
+
* download cloud-down
|
|
46
|
+
* image photo
|
|
47
|
+
* bolt electricity
|
|
48
|
+
* speaker speaker-high
|
|
49
|
+
* mic microphone
|
|
50
|
+
* back arrow-left
|
|
51
|
+
* spinner arrow-spin
|
|
52
|
+
file file
|
|
53
|
+
*/
|
|
54
|
+
import type { IconName } from './iconNames';
|
|
55
|
+
export type { IconName };
|
|
56
|
+
export default function Icon({ name, size, className, title, }: {
|
|
57
|
+
name: IconName;
|
|
58
|
+
/** One number, because these are square and always have been. */
|
|
59
|
+
size?: number;
|
|
60
|
+
className?: string;
|
|
61
|
+
/** Given only when the icon is the whole message. An icon beside a label it
|
|
62
|
+
* repeats is decoration, and decoration announced twice is noise. */
|
|
63
|
+
title?: string;
|
|
64
|
+
}): import("react").JSX.Element;
|