@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,41 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * A word attached to a thing.
4
+ *
5
+ * Three jobs, three weights, and the weight is the whole design: a
6
+ * *measurement* is quiet, because there is nothing to act on and it should
7
+ * read as text that happens to sit in a row; a *capability* is a plain chip,
8
+ * because it is one of a set scanned across and none may be louder than the
9
+ * others; a *state* is the only one that takes colour, because it is the only
10
+ * one that can change.
11
+ *
12
+ * The four state tones are the ones `Callout` and `Toast` use, so one
13
+ * vocabulary covers the app: `warn` is a caution — the thing works, mind how —
14
+ * and `bad` is something that is actually not working.
15
+ *
16
+ * **Colour is never the only signal.** A state pill has a tint, a border and a
17
+ * word. The tint alone was 1.67:1 on Paper for weeks and looked fine on every
18
+ * dark theme anybody tested, which is why `pillTones.test.ts` measures every
19
+ * tone against every theme rather than trusting anyone's eye.
20
+ *
21
+ * **Tint into `--panel`, not `--panel-2`.** Mixing a mid tone into the darker
22
+ * surface moves the background towards the text; on a light theme that drops a
23
+ * label from 4.7:1 to 3.9:1. The tones mix at 10% into the panel for that
24
+ * reason and not for taste.
25
+ *
26
+ * This was the second-most-copied markup in the app — `<span className="pill
27
+ * pill-info">` at every call site, and a gallery page teaching it. A page that
28
+ * documents hand-written markup is a page that will be copied.
29
+ */
30
+ export default function Pill({ tone, quiet, inRow, className, children, ...rest }) {
31
+ const classes = [
32
+ 'pill',
33
+ quiet ? 'pill-quiet' : '',
34
+ tone ? `pill-${tone}` : '',
35
+ inRow ? 'row-pill' : '',
36
+ className ?? '',
37
+ ]
38
+ .filter(Boolean)
39
+ .join(' ');
40
+ return (_jsx("span", { className: classes, ...rest, children: children }));
41
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * How far along something is.
3
+ *
4
+ * There was a `.bar` -- six pixels of `--panel-2` with an accent `<i>` inside --
5
+ * and every caller did the arithmetic, the formatting and the conditional
6
+ * itself. That was survivable while one screen had a download in it. It is not
7
+ * now: the engine install, the model pull, the image runtime and the weights
8
+ * fetch all report progress, and three of them have a phase where the total is
9
+ * not known yet.
10
+ *
11
+ * **The indeterminate case is the reason this exists.** `.bar` could only draw a
12
+ * fraction, so a fetch that had not yet read `content-length` drew nothing --
13
+ * an empty track, indistinguishable from stalled, for however long the server
14
+ * took to answer. A thing that is working and a thing that is stuck must not
15
+ * look alike.
16
+ *
17
+ * **It announces itself.** A bar is a picture of a number, and a picture of a
18
+ * number is nothing at all to somebody who cannot see it. `role="progressbar"`
19
+ * with a `valuetext`, because "62%" is what the bar shows and "412 MB of 660 MB"
20
+ * is what it means.
21
+ */
22
+ export default function Progress({ value, max, label, detail, tone, className, }: {
23
+ /** How far. Omit for indeterminate — the work has started, the size has not
24
+ * arrived. */
25
+ value?: number;
26
+ max?: number;
27
+ /** What is progressing. Required: an unlabelled bar is a rectangle. */
28
+ label: string;
29
+ /** The human sentence, shown beside the bar and announced instead of the
30
+ * percentage. */
31
+ detail?: string;
32
+ /** A subset of the tone words; the default fill is the accent and is not a tone. */
33
+ tone?: 'good' | 'bad';
34
+ className?: string;
35
+ }): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * How far along something is.
4
+ *
5
+ * There was a `.bar` -- six pixels of `--panel-2` with an accent `<i>` inside --
6
+ * and every caller did the arithmetic, the formatting and the conditional
7
+ * itself. That was survivable while one screen had a download in it. It is not
8
+ * now: the engine install, the model pull, the image runtime and the weights
9
+ * fetch all report progress, and three of them have a phase where the total is
10
+ * not known yet.
11
+ *
12
+ * **The indeterminate case is the reason this exists.** `.bar` could only draw a
13
+ * fraction, so a fetch that had not yet read `content-length` drew nothing --
14
+ * an empty track, indistinguishable from stalled, for however long the server
15
+ * took to answer. A thing that is working and a thing that is stuck must not
16
+ * look alike.
17
+ *
18
+ * **It announces itself.** A bar is a picture of a number, and a picture of a
19
+ * number is nothing at all to somebody who cannot see it. `role="progressbar"`
20
+ * with a `valuetext`, because "62%" is what the bar shows and "412 MB of 660 MB"
21
+ * is what it means.
22
+ */
23
+ export default function Progress({ value, max, label, detail, tone, className, }) {
24
+ const indeterminate = value === undefined || !max;
25
+ const pct = indeterminate ? 0 : Math.max(0, Math.min(100, (value / max) * 100));
26
+ return (_jsxs("div", { className: `progress${className ? ` ${className}` : ''}`, children: [(label || detail) && (_jsxs("div", { className: "progress-head", children: [_jsx("span", { className: "progress-label", children: label }), _jsx("span", { className: "grow" }), detail && _jsx("span", { className: "progress-detail mono", children: detail })] })), _jsx("div", { className: `bar${tone ? ` bar-${tone}` : ''}${indeterminate ? ' bar-indeterminate' : ''}`, role: "progressbar", "aria-label": label, "aria-valuenow": indeterminate ? undefined : Math.round(pct), "aria-valuemin": indeterminate ? undefined : 0, "aria-valuemax": indeterminate ? undefined : 100, "aria-valuetext": detail, children: _jsx("i", { style: indeterminate ? undefined : { width: `${pct}%` } }) })] }));
27
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * A list of things, each with a name, some qualifiers and something to do.
3
+ *
4
+ * There are thirty-eight of these in the app and every one is assembled by
5
+ * hand:
6
+ *
7
+ * <div className="set-row">
8
+ * <span className="grow" style={{ minWidth: 0 }}>
9
+ * <span className="named">
10
+ * <span className="truncate">{name}</span>
11
+ * …pills…
12
+ * </span>
13
+ * <div className="set-hint">{meta}</div>
14
+ * </span>
15
+ * …buttons…
16
+ * </div>
17
+ *
18
+ * Six nested elements, two of which exist only to stop the name eating the
19
+ * pills, and `minWidth: 0` inline on one of them because a flex item will not
20
+ * shrink below its content without it. Miss that and the row looks fine until
21
+ * somebody installs a model with a sixty-character name, at which point the
22
+ * tags saying what it can do get ellipsised away and the name -- the one part
23
+ * you could have guessed -- takes the whole row. That has been fixed twice.
24
+ *
25
+ * **The parts are named, so the layout is not the caller's problem.** `name`
26
+ * truncates, `pills` never do, `hint` is the line underneath, `trail` is the
27
+ * right-aligned column that lines up down the list, `actions` are the buttons.
28
+ * Every row in the app is some subset of those five.
29
+ *
30
+ * **`Rows` owns the dividers.** A rule between rows and none after the last one
31
+ * -- which sounds trivial and was not: `:last-of-type` matches by element type,
32
+ * so an actions bar rendered after the list meant the final row kept its border
33
+ * and every settings pane ended in two parallel lines. The container knows what
34
+ * a row is and the CSS asks it, rather than guessing from the DOM.
35
+ */
36
+ export declare function Rows({ children, empty, label, look, className, }: {
37
+ /** Absent is the empty case too: a caller mapping over nothing renders
38
+ * nothing, and that has to reach `empty` rather than an empty bordered box. */
39
+ children?: React.ReactNode;
40
+ /** Shown instead of the list when there is nothing. Every caller writes one
41
+ * of these by hand today and half of them forget. */
42
+ empty?: React.ReactNode;
43
+ /** What the list is, for a screen reader. A list of rows with no name is a
44
+ * stack of unrelated sentences. */
45
+ label?: string;
46
+ /**
47
+ * How the list is drawn.
48
+ *
49
+ * - `list` — rows separated by a hairline. Right for things you are reading:
50
+ * downloads, activity, applets.
51
+ * - `pick` — each row its own bordered box, spaced. Right for things you are
52
+ * *choosing between*: the rule-of-thumb elsewhere in this app is that
53
+ * several options are choice rows, and a ruled list reads as a table of
54
+ * records rather than as a set of alternatives one of which is yours.
55
+ *
56
+ * A variant rather than a second component: the slots, the truncation rule
57
+ * and the `min-width: 0` that makes the pills survive are the same either
58
+ * way, and only the surface differs.
59
+ */
60
+ look?: 'list' | 'pick';
61
+ className?: string;
62
+ }): import("react").JSX.Element | null;
63
+ export declare function Row({ name, pills, hint, trail, actions, below, onClick, tone, picked, loading, waiting, align, className, }: {
64
+ /** The subject. Truncates -- it is the only part that may. */
65
+ name: React.ReactNode;
66
+ /** Qualifiers, on the name's line. Never truncated: a badge that is
67
+ * ellipsised is a badge that has stopped saying anything. */
68
+ pills?: React.ReactNode;
69
+ /** The line underneath: sizes, counts, whatever is true but secondary. */
70
+ hint?: React.ReactNode;
71
+ /** A right-aligned column before the actions -- a timestamp, a size. Lines up
72
+ * down the list, which is the whole reason it is a slot and not another
73
+ * pill. */
74
+ trail?: React.ReactNode;
75
+ actions?: React.ReactNode;
76
+ /** Something that opens underneath this row -- a confirmation, a report, the
77
+ * detail of the thing named above. A sibling rather than a child, because it
78
+ * is the full width of the list and not part of the row's own layout.
79
+ *
80
+ * This exists so callers do not wrap the row in a div of their own: doing
81
+ * that puts a plain element between `role="list"` and `role="listitem"`,
82
+ * which breaks the relationship, and it hides the row from the container's
83
+ * "no rule after the last one" rule. */
84
+ below?: React.ReactNode;
85
+ /** Makes the whole row activate. Rendered as a button, so it has a keyboard
86
+ * and a focus ring, which a `<div onClick>` has neither of. */
87
+ onClick?: () => void;
88
+ tone?: 'bad';
89
+ /** The one that is chosen, in a `pick` list. A second signal beside whatever
90
+ * the row already says in words -- colour is never the only one. */
91
+ picked?: boolean;
92
+ /** This row is the thing currently working. */
93
+ loading?: boolean;
94
+ /** Something else is working, so this one cannot be chosen yet. */
95
+ waiting?: boolean;
96
+ /** `start` when the row has enough text that vertically centred buttons drift
97
+ * away from the name they belong to. */
98
+ align?: 'center' | 'start';
99
+ className?: string;
100
+ }): import("react").JSX.Element;
101
+ export default Rows;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * A list of things, each with a name, some qualifiers and something to do.
4
+ *
5
+ * There are thirty-eight of these in the app and every one is assembled by
6
+ * hand:
7
+ *
8
+ * <div className="set-row">
9
+ * <span className="grow" style={{ minWidth: 0 }}>
10
+ * <span className="named">
11
+ * <span className="truncate">{name}</span>
12
+ * …pills…
13
+ * </span>
14
+ * <div className="set-hint">{meta}</div>
15
+ * </span>
16
+ * …buttons…
17
+ * </div>
18
+ *
19
+ * Six nested elements, two of which exist only to stop the name eating the
20
+ * pills, and `minWidth: 0` inline on one of them because a flex item will not
21
+ * shrink below its content without it. Miss that and the row looks fine until
22
+ * somebody installs a model with a sixty-character name, at which point the
23
+ * tags saying what it can do get ellipsised away and the name -- the one part
24
+ * you could have guessed -- takes the whole row. That has been fixed twice.
25
+ *
26
+ * **The parts are named, so the layout is not the caller's problem.** `name`
27
+ * truncates, `pills` never do, `hint` is the line underneath, `trail` is the
28
+ * right-aligned column that lines up down the list, `actions` are the buttons.
29
+ * Every row in the app is some subset of those five.
30
+ *
31
+ * **`Rows` owns the dividers.** A rule between rows and none after the last one
32
+ * -- which sounds trivial and was not: `:last-of-type` matches by element type,
33
+ * so an actions bar rendered after the list meant the final row kept its border
34
+ * and every settings pane ended in two parallel lines. The container knows what
35
+ * a row is and the CSS asks it, rather than guessing from the DOM.
36
+ */
37
+ export function Rows({ children, empty, label, look = 'list', className, }) {
38
+ const items = Array.isArray(children) ? children.filter(Boolean) : children;
39
+ const none = Array.isArray(items) ? items.length === 0 : !items;
40
+ if (none) {
41
+ return empty ? _jsx("div", { className: "set-hint rows-empty", children: empty }) : null;
42
+ }
43
+ return (_jsx("div", { className: `rows${look === 'pick' ? ' rows-pick' : ''}${className ? ` ${className}` : ''}`,
44
+ // biome-ignore lint/a11y/useSemanticElements: valid ARIA on a div; a <ul> brings the browser's list reset and the stylesheet and 204 baselines key on `div.rows`.
45
+ role: "list", "aria-label": label, children: items }));
46
+ }
47
+ export function Row({ name, pills, hint, trail, actions, below, onClick, tone, picked, loading, waiting, align = 'center', className, }) {
48
+ const body = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "row-subject", children: [_jsxs("span", { className: "named", children: [_jsx("span", { className: "truncate", children: name }), pills] }), hint && _jsx("div", { className: "set-hint", children: hint })] }), trail && _jsx("span", { className: "row-trail set-hint mono", children: trail })] }));
49
+ const rowClass = `set-row rows-row${tone ? ` rows-${tone}` : ''}` +
50
+ `${align === 'start' ? ' rows-top' : ''}${picked ? ' is-picked' : ''}` +
51
+ `${loading ? ' is-loading' : ''}${waiting ? ' is-waiting' : ''}` +
52
+ `${className ? ` ${className}` : ''}`;
53
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: rowClass, role: "listitem", children: [onClick ? (_jsx("button", { type: "button", className: "row-hit", disabled: waiting, onClick: onClick, children: body })) : (body), actions] }), below] }));
54
+ }
55
+ export default Rows;
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from 'react';
2
+ interface Props {
3
+ block?: boolean;
4
+ size?: 'sm' | 'md' | 'lg';
5
+ className?: string;
6
+ /** `<option>`s, exactly as a native select takes them. The children are the
7
+ * API and React Aria's items are the implementation; `readOptions` is the
8
+ * seam between them. */
9
+ children?: ReactNode;
10
+ id?: string;
11
+ title?: string;
12
+ 'aria-label'?: string;
13
+ 'aria-labelledby'?: string;
14
+ disabled?: boolean;
15
+ value?: string | number;
16
+ defaultValue?: string | number;
17
+ /** Native-select-shaped on purpose -- `e.target.value` -- because that is
18
+ * what every call site was written against when this *was* a `<select>`. */
19
+ onChange?: (event: {
20
+ target: {
21
+ value: string;
22
+ };
23
+ }) => void;
24
+ /** Something to the right of an option -- a play button beside a voice. */
25
+ aside?: (value: string) => ReactNode;
26
+ }
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;
28
+ export {};
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, isValidElement, useEffect, useRef } from 'react';
3
+ /** Read `<option>` children into a list this can render itself. */
4
+ function readOptions(children) {
5
+ const out = [];
6
+ Children.forEach(children, (child) => {
7
+ if (!isValidElement(child))
8
+ return;
9
+ const props = child.props;
10
+ const label = props.children;
11
+ out.push({
12
+ value: String(props.value ?? ''),
13
+ label,
14
+ text: typeof label === 'string' ? label : String(props.value ?? ''),
15
+ disabled: props.disabled,
16
+ });
17
+ });
18
+ return out;
19
+ }
20
+ /* Named rather than inherited from `SelectHTMLAttributes`.
21
+ The old signature spread every select attribute onto the element, which was
22
+ free when that element was a `<select>` and is wrong now that it is a
23
+ `<button>` -- `onCopy` alone is typed against a different element. Call sites
24
+ pass five things between them, so five is what this takes. */
25
+ 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, }) {
27
+ const options = readOptions(children);
28
+ /* `title`, set on the element: React Aria's `Button` takes `id` and the
29
+ `aria-*` labelling props and filters the rest, the same `filterDOMProps`
30
+ that dropped `aria-busy` on `Button` and `aria-modal` on `Modal`. */
31
+ const control = useRef(null);
32
+ useEffect(() => {
33
+ const el = control.current;
34
+ if (!el)
35
+ return;
36
+ if (title)
37
+ el.title = title;
38
+ else
39
+ el.removeAttribute('title');
40
+ }, [title]);
41
+ /* React Aria owns what was 13 KB of hand-rolled behaviour: the popover is
42
+ positioned against the button and flips when the edge is near, which the
43
+ measured-rectangle `top`/`left` could not; typeahead, Home and End, the
44
+ arrows, Escape, outside-press and focus restore are all its. What stays
45
+ is the seam -- `<option>` children in, `{target: {value}}` out -- because
46
+ the call sites were written against a native select and there is no
47
+ reason to make them care that it is not one any more.
48
+
49
+ `sel-value` and the caret are the same markup as before, so the closed
50
+ control is pixel-identical; the list is the same markup with React Aria's
51
+ state attributes where the classes were. */
52
+ 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
+ if (key !== null)
54
+ 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))) }) })] }));
56
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pick a tile size by sweeping a grid, the way a spreadsheet asks for table
3
+ * dimensions.
4
+ *
5
+ * Six preset buttons and a `2×1` dropdown both said the same thing in a
6
+ * notation you have to decode. A tile is a rectangle on a grid, so the control
7
+ * is that rectangle: hover to see the shape you would get, click to take it.
8
+ * The label underneath keeps the numbers for anyone who wants them.
9
+ *
10
+ * It grows as you reach its edge, the way a spreadsheet's does: showing every
11
+ * cell up to the ceiling would be a wall of squares you have to aim in, and
12
+ * showing a fixed few would put the larger sizes behind a number box. Reaching
13
+ * the last column offers one more, and only then.
14
+ *
15
+ * `max` is the ceiling, and it means something: for an applet it is the
16
+ * dashboard grid you configured, so the control cannot offer a tile the layout
17
+ * has no room for.
18
+ */
19
+ export default function SizeGrid({ value, max, disabled, onChange, }: {
20
+ value: {
21
+ cols: number;
22
+ rows: number;
23
+ };
24
+ /** The furthest this can go. The grid never offers past it. */
25
+ max?: {
26
+ cols: number;
27
+ rows: number;
28
+ };
29
+ disabled?: boolean;
30
+ onChange: (size: {
31
+ cols: number;
32
+ rows: number;
33
+ }) => void;
34
+ }): import("react").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ /**
4
+ * Pick a tile size by sweeping a grid, the way a spreadsheet asks for table
5
+ * dimensions.
6
+ *
7
+ * Six preset buttons and a `2×1` dropdown both said the same thing in a
8
+ * notation you have to decode. A tile is a rectangle on a grid, so the control
9
+ * is that rectangle: hover to see the shape you would get, click to take it.
10
+ * The label underneath keeps the numbers for anyone who wants them.
11
+ *
12
+ * It grows as you reach its edge, the way a spreadsheet's does: showing every
13
+ * cell up to the ceiling would be a wall of squares you have to aim in, and
14
+ * showing a fixed few would put the larger sizes behind a number box. Reaching
15
+ * the last column offers one more, and only then.
16
+ *
17
+ * `max` is the ceiling, and it means something: for an applet it is the
18
+ * dashboard grid you configured, so the control cannot offer a tile the layout
19
+ * has no room for.
20
+ */
21
+ export default function SizeGrid({ value, max = { cols: 3, rows: 3 }, disabled = false, onChange, }) {
22
+ // What the pointer is over, which is what gets shown. Falls back to the real
23
+ // value on leave, so the control never lies about what is selected.
24
+ const [hover, setHover] = useState(null);
25
+ const shown = hover ?? value;
26
+ // One spare row and column beyond whatever is reached, so there is always
27
+ // somewhere further to go until the ceiling says otherwise.
28
+ const extent = {
29
+ cols: Math.min(max.cols, Math.max(value.cols, shown.cols) + 1),
30
+ rows: Math.min(max.rows, Math.max(value.rows, shown.rows) + 1),
31
+ };
32
+ const cells = [];
33
+ for (let row = 1; row <= extent.rows; row++) {
34
+ for (let col = 1; col <= extent.cols; col++) {
35
+ const on = col <= shown.cols && row <= shown.rows;
36
+ const chosen = col <= value.cols && row <= value.rows;
37
+ cells.push(_jsx("button", { type: "button", className: `size-cell${on ? ' on' : ''}${chosen && !hover ? ' chosen' : ''}`, disabled: disabled, "aria-label": `${col} by ${row}`, onMouseEnter: () => !disabled && setHover({ cols: col, rows: row }), onFocus: () => !disabled && setHover({ cols: col, rows: row }), onClick: () => onChange({ cols: col, rows: row }) }, `${col}x${row}`));
38
+ }
39
+ }
40
+ return (_jsxs("div", { className: "size-grid-wrap", children: [_jsx("div", { className: "size-grid", style: { gridTemplateColumns: `repeat(${extent.cols}, 1fr)` }, onMouseLeave: () => setHover(null), children: cells }), _jsxs("span", { className: "set-hint mono", children: [shown.cols, " \u00D7 ", shown.rows, (extent.cols === max.cols || extent.rows === max.rows) && (_jsxs("span", { className: "size-ceiling", children: [' ', "\u00B7 max ", max.cols, "\u00D7", max.rows] }))] })] }));
41
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The shape of what has not arrived yet.
3
+ *
4
+ * Ported from chef-monorepo's `Skeleton`, keeping its API — `variant`,
5
+ * `surface`, `animation`, and the convention that a bare number is a spacing
6
+ * step, so `height={5}` is 20px. That convention lands exactly on this app's
7
+ * four-pixel grid, which is why it survived the port unchanged. What did not
8
+ * survive is `cva` and `clsx`: this project keeps four runtime dependencies on
9
+ * purpose, and two of them would have been for string concatenation.
10
+ *
11
+ * **The colour is `currentColor`, not a named surface.** chef's version picks
12
+ * `bg-surface-muted` or `bg-surface-elevated`, and the first place this landed
13
+ * was a card inside a modal — where the card is already that exact token, so
14
+ * six skeletons rendered invisible on an identical background. Any scheme that
15
+ * names the colour has that in it: the caller has to know what it is being
16
+ * drawn on, and is wrong the first time a surface moves. Ink at low alpha is
17
+ * right on every surface in every theme without anybody deciding.
18
+ *
19
+ * **A skeleton is a promise about layout, not a loading noise.** Its whole job
20
+ * is to occupy the space the real thing will occupy, so nothing jumps when the
21
+ * content lands. A grey box of the wrong size is worse than no box at all: it
22
+ * says "something this shape is coming", and then something a different shape
23
+ * arrives and pushes the page around. If you cannot say what shape is coming,
24
+ * the honest control is `Progress`, which claims nothing about layout.
25
+ *
26
+ * **Invisible to a screen reader.** These are pictures of absent text; read
27
+ * aloud they are nothing at all. Mark the region `aria-busy` and let the
28
+ * announcement come from the content when it arrives — a reader should be told
29
+ * "loading" once, not handed six shapes.
30
+ */
31
+ export default function Skeleton({ variant, surface, animation, width, height, lines, className, }: {
32
+ variant?: 'rectangular' | 'rounded' | 'circular';
33
+ /** How strongly it reads. Named for the surface in the original because it
34
+ * chose a colour there; here the colour comes from `currentColor`, so this
35
+ * is only a strength — `elevated` for the flatter surfaces where a faint
36
+ * bar disappears. */
37
+ surface?: 'muted' | 'elevated';
38
+ animation?: 'pulse' | 'none';
39
+ width?: number | string;
40
+ height?: number | string;
41
+ /** A paragraph rather than a bar. The last line is short, because real
42
+ * paragraphs end mid-line and a block of equal bars reads as a table. */
43
+ lines?: number;
44
+ className?: string;
45
+ }): import("react").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * The shape of what has not arrived yet.
4
+ *
5
+ * Ported from chef-monorepo's `Skeleton`, keeping its API — `variant`,
6
+ * `surface`, `animation`, and the convention that a bare number is a spacing
7
+ * step, so `height={5}` is 20px. That convention lands exactly on this app's
8
+ * four-pixel grid, which is why it survived the port unchanged. What did not
9
+ * survive is `cva` and `clsx`: this project keeps four runtime dependencies on
10
+ * purpose, and two of them would have been for string concatenation.
11
+ *
12
+ * **The colour is `currentColor`, not a named surface.** chef's version picks
13
+ * `bg-surface-muted` or `bg-surface-elevated`, and the first place this landed
14
+ * was a card inside a modal — where the card is already that exact token, so
15
+ * six skeletons rendered invisible on an identical background. Any scheme that
16
+ * names the colour has that in it: the caller has to know what it is being
17
+ * drawn on, and is wrong the first time a surface moves. Ink at low alpha is
18
+ * right on every surface in every theme without anybody deciding.
19
+ *
20
+ * **A skeleton is a promise about layout, not a loading noise.** Its whole job
21
+ * is to occupy the space the real thing will occupy, so nothing jumps when the
22
+ * content lands. A grey box of the wrong size is worse than no box at all: it
23
+ * says "something this shape is coming", and then something a different shape
24
+ * arrives and pushes the page around. If you cannot say what shape is coming,
25
+ * the honest control is `Progress`, which claims nothing about layout.
26
+ *
27
+ * **Invisible to a screen reader.** These are pictures of absent text; read
28
+ * aloud they are nothing at all. Mark the region `aria-busy` and let the
29
+ * announcement come from the content when it arrives — a reader should be told
30
+ * "loading" once, not handed six shapes.
31
+ */
32
+ const STEP = 4;
33
+ /** A bare number is a spacing step; a string is a length, used as given. */
34
+ const length = (value, fallback) => {
35
+ const v = value ?? fallback;
36
+ return typeof v === 'number' ? `${v * STEP}px` : v;
37
+ };
38
+ export default function Skeleton({ variant = 'rectangular', surface = 'muted', animation = 'pulse', width = '100%', height = 2, lines, className, }) {
39
+ const cls = `skel skel-${variant} skel-on-${surface}${animation === 'none' ? '' : ' skel-pulse'}${className ? ` ${className}` : ''}`;
40
+ if (lines && lines > 1) {
41
+ return (_jsx("span", { className: "skel-lines", "aria-hidden": "true", children: Array.from({ length: lines }, (_, i) => (_jsx("span", { className: cls, style: {
42
+ height: length(height, 2),
43
+ width: i === lines - 1 ? '62%' : length(width, '100%'),
44
+ } }, i))) }));
45
+ }
46
+ return (_jsx("span", { className: cls, "aria-hidden": "true", style: { height: length(height, 2), width: length(width, '100%') } }));
47
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * A number chosen from a range, by dragging.
3
+ *
4
+ * For a setting whose answer is "about this much" rather than one of a list:
5
+ * how much memory tf may take, say. A `Select` with eight sizes on it makes
6
+ * the person pick the nearest wrong one; a box you type a number into asks
7
+ * them to know the ceiling. A slider shows the ceiling, the floor, and where
8
+ * between them they are.
9
+ *
10
+ * **Bounded, always.** `min` and `max` are required rather than defaulted,
11
+ * because a slider with an invented ceiling lies about what the machine can
12
+ * do. **Stepped, optionally.** `step` given, the knob snaps to that interval
13
+ * -- half a gigabyte, five minutes -- and the value is always one of the
14
+ * marks. Without it the range is continuous (`step="any"`) and the value is
15
+ * wherever the knob stopped, which is right for a quantity nobody counts in
16
+ * units.
17
+ *
18
+ * **Applies on release, shows while dragging.** A setting that saved on
19
+ * every pixel would put a hundred writes behind one gesture, so `onChange`
20
+ * fires when the knob is let go and the value beside the knob follows the
21
+ * drag live. That is the *native* `change` event, listened for directly:
22
+ * React's `onChange` on an input is its `input` event under another name
23
+ * and fires per pixel, which is exactly what the first version did -- every
24
+ * pixel saved, every save handed the value back, and the knob snapped to
25
+ * wherever the last reply said while the pointer was still down. Nobody
26
+ * could drag it. The keyboard is the one place even the native event is too
27
+ * eager: an arrow key fires `change` per press, and the same replies landed
28
+ * between presses (measured: twenty-two presses from the floor landed at
29
+ * 2.5 rather than 12). So key presses settle for a third of a second before
30
+ * they are said, and while a drag or a settle is under way the prop is not
31
+ * allowed to overwrite what is on screen.
32
+ *
33
+ * A native `<input type="range">`, for the same reason `Toggle` is a native
34
+ * checkbox: the keyboard, the focus ring and the announcement come free, and
35
+ * `aria-valuetext` says the value in the caller's words ("12.0 GB") rather
36
+ * than as a bare number.
37
+ */
38
+ export default function Slider({ label, hint, value, min, max, step, onChange, format, disabled, labelHidden, size, danger, className, }: {
39
+ label: React.ReactNode;
40
+ /** What it decides, or what the ends of it mean. */
41
+ hint?: React.ReactNode;
42
+ value: number;
43
+ min: number;
44
+ max: number;
45
+ /** The interval the knob snaps to. Absent, the range is continuous. */
46
+ step?: number;
47
+ /** The value settled on: knob released, or a key pressed. */
48
+ onChange: (value: number) => void;
49
+ /** The value in words, beside the knob and to a screen reader. */
50
+ format?: (value: number) => string;
51
+ disabled?: boolean;
52
+ labelHidden?: boolean;
53
+ /** The same three names every other control takes, and the same three
54
+ * boxes: `sm`, `md` and `lg` are `.size-sm`, `.size-md` and `.size-lg`'s
55
+ * padding and type, so a slider beside a button or a select of the same
56
+ * size is the same height. Widths 120, 150 and 180. */
57
+ size?: 'sm' | 'md' | 'lg';
58
+ /**
59
+ * Where the range starts to be a risk. The fill is the accent up to
60
+ * `from` and drifts, step by step, to the danger colour at `to` (the
61
+ * maximum when omitted) -- so a value past the safe mark reads as such
62
+ * without a word beside it, and the further past, the more so. For a
63
+ * ceiling somebody may raise past what was measured.
64
+ */
65
+ danger?: {
66
+ from: number;
67
+ to?: number;
68
+ };
69
+ className?: string;
70
+ }): import("react").JSX.Element;