@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/dist/components/Brand.d.ts +10 -0
  4. package/dist/components/Brand.js +212 -0
  5. package/dist/components/Button.d.ts +63 -0
  6. package/dist/components/Button.js +74 -0
  7. package/dist/components/Callout.d.ts +37 -0
  8. package/dist/components/Callout.js +71 -0
  9. package/dist/components/Card.d.ts +42 -0
  10. package/dist/components/Card.js +30 -0
  11. package/dist/components/Checkbox.d.ts +32 -0
  12. package/dist/components/Checkbox.js +31 -0
  13. package/dist/components/DangerZone.d.ts +59 -0
  14. package/dist/components/DangerZone.js +50 -0
  15. package/dist/components/Dialog.d.ts +28 -0
  16. package/dist/components/Dialog.js +29 -0
  17. package/dist/components/Empty.d.ts +45 -0
  18. package/dist/components/Empty.js +35 -0
  19. package/dist/components/Field.d.ts +58 -0
  20. package/dist/components/Field.js +46 -0
  21. package/dist/components/Icon.d.ts +64 -0
  22. package/dist/components/Icon.js +235 -0
  23. package/dist/components/Illustration.d.ts +36 -0
  24. package/dist/components/Illustration.js +48 -0
  25. package/dist/components/Input.d.ts +14 -0
  26. package/dist/components/Input.js +65 -0
  27. package/dist/components/Markdown.d.ts +21 -0
  28. package/dist/components/Markdown.js +29 -0
  29. package/dist/components/Modal.d.ts +59 -0
  30. package/dist/components/Modal.js +72 -0
  31. package/dist/components/Pill.d.ts +40 -0
  32. package/dist/components/Pill.js +41 -0
  33. package/dist/components/Progress.d.ts +35 -0
  34. package/dist/components/Progress.js +27 -0
  35. package/dist/components/Rows.d.ts +101 -0
  36. package/dist/components/Rows.js +55 -0
  37. package/dist/components/Select.d.ts +28 -0
  38. package/dist/components/Select.js +56 -0
  39. package/dist/components/SizeGrid.d.ts +34 -0
  40. package/dist/components/SizeGrid.js +41 -0
  41. package/dist/components/Skeleton.d.ts +45 -0
  42. package/dist/components/Skeleton.js +47 -0
  43. package/dist/components/Slider.d.ts +70 -0
  44. package/dist/components/Slider.js +100 -0
  45. package/dist/components/Table.d.ts +43 -0
  46. package/dist/components/Table.js +13 -0
  47. package/dist/components/Tabs.d.ts +72 -0
  48. package/dist/components/Tabs.js +82 -0
  49. package/dist/components/Textarea.d.ts +9 -0
  50. package/dist/components/Textarea.js +22 -0
  51. package/dist/components/Toast.d.ts +43 -0
  52. package/dist/components/Toast.js +78 -0
  53. package/dist/components/Toggle.d.ts +56 -0
  54. package/dist/components/Toggle.js +189 -0
  55. package/dist/components/Tooltip.d.ts +22 -0
  56. package/dist/components/Tooltip.js +62 -0
  57. package/dist/components/Tour.d.ts +33 -0
  58. package/dist/components/Tour.js +108 -0
  59. package/dist/components/iconNames.d.ts +18 -0
  60. package/dist/components/iconNames.js +60 -0
  61. package/dist/components/tourMarker.d.ts +29 -0
  62. package/dist/components/tourMarker.js +58 -0
  63. package/dist/contrast.d.ts +18 -0
  64. package/dist/contrast.js +27 -0
  65. package/dist/hooks/useTrapFocus.d.ts +24 -0
  66. package/dist/hooks/useTrapFocus.js +67 -0
  67. package/dist/illustrations.d.ts +11 -0
  68. package/dist/illustrations.js +55 -0
  69. package/dist/index.d.ts +72 -0
  70. package/dist/index.js +65 -0
  71. package/dist/styles/index.css +3124 -0
  72. package/dist/themes.d.ts +210 -0
  73. package/dist/themes.js +300 -0
  74. package/dist/tokens.css +251 -0
  75. 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;