@wtfalch/design 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Button.d.ts +19 -27
- package/dist/components/Button.js +61 -1
- package/dist/components/Callout.d.ts +0 -21
- package/dist/components/Callout.js +7 -0
- package/dist/components/Card.js +7 -0
- package/dist/components/Checkbox.d.ts +0 -31
- package/dist/components/Checkbox.js +7 -0
- package/dist/components/Command.js +10 -2
- package/dist/components/DangerZone.js +7 -0
- package/dist/components/Dialog.d.ts +0 -21
- package/dist/components/Dialog.js +7 -0
- package/dist/components/Field.d.ts +22 -50
- package/dist/components/Field.js +18 -9
- package/dist/components/Identity.d.ts +0 -25
- package/dist/components/Identity.js +7 -0
- package/dist/components/Input.d.ts +10 -0
- package/dist/components/Input.js +31 -3
- package/dist/components/Kbd.d.ts +7 -0
- package/dist/components/Kbd.js +34 -0
- package/dist/components/Markdown.js +7 -0
- package/dist/components/Menu.js +10 -2
- package/dist/components/Modal.d.ts +0 -33
- package/dist/components/Modal.js +7 -0
- package/dist/components/Pagination.d.ts +0 -33
- package/dist/components/Pagination.js +7 -0
- package/dist/components/Popover.js +7 -0
- package/dist/components/Rows.js +7 -0
- package/dist/components/ScrollArea.js +7 -0
- package/dist/components/Select.d.ts +8 -1
- package/dist/components/Select.js +34 -5
- package/dist/components/Shell.d.ts +35 -0
- package/dist/components/Shell.js +26 -0
- package/dist/components/SizeGrid.js +7 -0
- package/dist/components/Slider.js +7 -0
- package/dist/components/SplitPane.js +7 -0
- package/dist/components/Stat.d.ts +45 -0
- package/dist/components/Stat.js +31 -0
- package/dist/components/Tabs.d.ts +21 -0
- package/dist/components/Tabs.js +8 -1
- package/dist/components/Textarea.js +14 -4
- package/dist/components/ThemeSwitch.d.ts +19 -0
- package/dist/components/ThemeSwitch.js +65 -0
- package/dist/components/Toast.d.ts +24 -18
- package/dist/components/Toast.js +44 -3
- package/dist/components/Toggle.d.ts +0 -30
- package/dist/components/Toggle.js +7 -0
- package/dist/components/Tooltip.js +7 -0
- package/dist/components/Tour.d.ts +0 -23
- package/dist/components/Tour.js +7 -0
- package/dist/components/fieldWiring.d.ts +44 -0
- package/dist/components/fieldWiring.js +30 -0
- package/dist/index.d.ts +23 -3
- package/dist/index.js +17 -1
- package/dist/styles/index.css +259 -13
- package/dist/tf.css +259 -13
- package/dist/themes/choice.d.ts +71 -0
- package/dist/themes/choice.js +81 -0
- package/dist/valet.css +259 -13
- package/package.json +2 -1
|
@@ -1,29 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
* `<button className="primary size-sm">`. A page in the gallery documented that
|
|
8
|
-
* markup, which is how a catalogue teaches people to copy rather than import.
|
|
9
|
-
*
|
|
10
|
-
* **`type="button"` is the default, and that is the whole reason this exists
|
|
11
|
-
* rather than a class.** A `<button>` with no type is `type="submit"`: drop one
|
|
12
|
-
* inside a `<form>` and pressing it submits the form and reloads the page. That
|
|
13
|
-
* is not a thing anybody writes on purpose, it is a thing everybody forgets —
|
|
14
|
-
* fifty-eight of them here, found by a linter rather than by a person. A
|
|
15
|
-
* default cannot be forgotten. `type="submit"` is still available, and now it
|
|
16
|
-
* has to be asked for, which is the right way round.
|
|
17
|
-
*
|
|
18
|
-
* **Behaviour comes from React Aria.** Press handling that works with a mouse,
|
|
19
|
-
* a touch, a pen and a keyboard is more than `onClick`: it is pointer capture,
|
|
20
|
-
* the difference between a press that ends on the button and one that drags
|
|
21
|
-
* off it, and not firing twice on a touch screen. `data-pressed`,
|
|
22
|
-
* `data-hovered` and `data-focus-visible` land on the element, so the
|
|
23
|
-
* stylesheet keeps describing states rather than tracking them.
|
|
24
|
-
*/
|
|
25
|
-
import { type ButtonProps } from 'react-aria-components';
|
|
26
|
-
export interface Props extends Omit<ButtonProps, 'className' | 'style' | 'children'> {
|
|
1
|
+
import { type ButtonProps as AriaButtonProps } from 'react-aria-components';
|
|
2
|
+
/** What this component adds, on either element. Kept separate from React
|
|
3
|
+
* Aria's set so the `asChild` half of `ButtonProps` can have these without
|
|
4
|
+
* the button-only ones -- `className` lives here, and React Aria's own
|
|
5
|
+
* `className` is a function-or-string this package does not want. */
|
|
6
|
+
export interface OwnProps {
|
|
27
7
|
children?: React.ReactNode;
|
|
28
8
|
/** The same word every other control in the package uses. React Aria spells
|
|
29
9
|
* it `isDisabled`, and that still works; this one exists so a consumer does
|
|
@@ -60,4 +40,16 @@ export interface Props extends Omit<ButtonProps, 'className' | 'style' | 'childr
|
|
|
60
40
|
iconOnly?: boolean;
|
|
61
41
|
className?: string;
|
|
62
42
|
}
|
|
63
|
-
export
|
|
43
|
+
export interface Props extends OwnProps, Omit<AriaButtonProps, 'className' | 'style' | 'children'> {
|
|
44
|
+
}
|
|
45
|
+
/** The button's own props, plus the slot switch. When `asChild` is set the
|
|
46
|
+
* element is the caller's, so what may be passed alongside is the DOM's
|
|
47
|
+
* attribute set rather than React Aria's — `onPress` and `type` have nothing
|
|
48
|
+
* to attach to on an `<a>`. */
|
|
49
|
+
export type ButtonProps = (Props & {
|
|
50
|
+
asChild?: false;
|
|
51
|
+
}) | (OwnProps & Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style' | 'children'> & {
|
|
52
|
+
asChild: true;
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
});
|
|
55
|
+
export default function Button(props: ButtonProps): import("react").JSX.Element;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* A button. The most-copied markup in the app, finally a component.
|
|
4
11
|
*
|
|
@@ -22,10 +29,45 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
22
29
|
* off it, and not firing twice on a touch screen. `data-pressed`,
|
|
23
30
|
* `data-hovered` and `data-focus-visible` land on the element, so the
|
|
24
31
|
* stylesheet keeps describing states rather than tracking them.
|
|
32
|
+
*
|
|
33
|
+
* **`asChild` puts the styling on somebody else's element, and that is how a
|
|
34
|
+
* link becomes a button.** A control that takes you somewhere has to be an
|
|
35
|
+
* anchor: middle-click, cmd-click, "copy link address" and a screen reader's
|
|
36
|
+
* list of links all come from the element, not from what it looks like. Every
|
|
37
|
+
* consumer had drawn its own instead — valet's `.v-link-button`, manage's and
|
|
38
|
+
* app-template's `.app-link` — three hand-styled anchors chasing one button's
|
|
39
|
+
* appearance, which is the drift this package exists to stop.
|
|
40
|
+
*
|
|
41
|
+
* The alternative was an `href` prop rendering React Aria's `Link`, and
|
|
42
|
+
* `asChild` beats it on the case that actually occurs: in a Next app the
|
|
43
|
+
* anchor has to be `next/link`, or the whole page reloads. `href` would have
|
|
44
|
+
* meant every app wrapping its tree in a `RouterProvider` and remembering to;
|
|
45
|
+
* `asChild` lets the caller hand over the element they already wanted:
|
|
46
|
+
*
|
|
47
|
+
* <Button asChild kind="primary"><Link href="/manage">Manage</Link></Button>
|
|
48
|
+
*
|
|
49
|
+
* It is the same shape chef-monorepo's `Button` uses, for the same reason.
|
|
50
|
+
*
|
|
51
|
+
* Two things do not survive the swap, both because the slotted element is not
|
|
52
|
+
* a `<button>`. `busy` is ignored: its countdown bar is drawn on the element
|
|
53
|
+
* this component would have rendered, and the child owns its own contents.
|
|
54
|
+
* And `disabled` cannot use the native attribute, which an anchor ignores, so
|
|
55
|
+
* it becomes `aria-disabled` plus a capture-phase block — `Slot` runs the
|
|
56
|
+
* child's own `onClick` before ours, so the capture phase is the only place
|
|
57
|
+
* left to stop it, and stopping propagation there is what a native disabled
|
|
58
|
+
* button does anyway: it emits no click at all.
|
|
25
59
|
*/
|
|
60
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
26
61
|
import { useEffect, useRef } from 'react';
|
|
27
62
|
import { Button as AriaButton } from 'react-aria-components';
|
|
28
|
-
|
|
63
|
+
/* An anchor ignores `disabled`, and `Slot` runs the child's `onClick` before
|
|
64
|
+
ours, so the capture phase is the only place left to block it. */
|
|
65
|
+
const blockActivation = (event) => {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
event.stopPropagation();
|
|
68
|
+
};
|
|
69
|
+
export default function Button(props) {
|
|
70
|
+
const { kind = 'default', disabled, size = 'md', busy, block, iconOnly, className } = props;
|
|
29
71
|
const classes = [
|
|
30
72
|
iconOnly ? 'icon-btn' : '',
|
|
31
73
|
kind === 'default' ? '' : kind,
|
|
@@ -50,6 +92,10 @@ export default function Button({ kind = 'default', disabled, size = 'md', busy,
|
|
|
50
92
|
* *not* disabled -- "Working, not disabled", and the label stays because it
|
|
51
93
|
* is the only thing saying what is taking so long. Swapping the semantics to
|
|
52
94
|
* get a tidier call site would be changing behaviour nobody asked to change.
|
|
95
|
+
*
|
|
96
|
+
* Declared before the `asChild` branch below, because a hook cannot sit
|
|
97
|
+
* after a conditional return. It does nothing on that path: the ref is
|
|
98
|
+
* never attached, so there is no element to stamp.
|
|
53
99
|
*/
|
|
54
100
|
const ref = useRef(null);
|
|
55
101
|
useEffect(() => {
|
|
@@ -61,6 +107,20 @@ export default function Button({ kind = 'default', disabled, size = 'md', busy,
|
|
|
61
107
|
else
|
|
62
108
|
el.removeAttribute('aria-busy');
|
|
63
109
|
}, [busy]);
|
|
110
|
+
if (props.asChild) {
|
|
111
|
+
/* The one class the button form does not carry. Every rule in the
|
|
112
|
+
stylesheet is written against the `button` element, which an anchor is
|
|
113
|
+
not, so the slotted element needs a hook of its own. Adding it here
|
|
114
|
+
rather than to `classes` above keeps the button's own class list, and
|
|
115
|
+
therefore every committed baseline, exactly as it was. */
|
|
116
|
+
const slotClasses = ['btn', classes].filter(Boolean).join(' ');
|
|
117
|
+
/* Narrowed by `props.asChild`, so what is left after the component's own
|
|
118
|
+
props is the DOM attribute set -- which is why an unknown `data-*`
|
|
119
|
+
reaches the child untouched. */
|
|
120
|
+
const { asChild: _asChild, kind: _kind, disabled: _disabled, size: _size, busy: _busy, block: _block, iconOnly: _iconOnly, className: _className, children, ...slotted } = props;
|
|
121
|
+
return (_jsx(Slot, { ...slotted, "aria-disabled": disabled || slotted['aria-disabled'], onClickCapture: disabled ? blockActivation : slotted.onClickCapture, className: slotClasses, children: children }));
|
|
122
|
+
}
|
|
123
|
+
const { asChild: _asChild, kind: _kind, disabled: _disabled, size: _size, busy: _busy, block: _block, iconOnly: _iconOnly, className: _className, children, ...rest } = props;
|
|
64
124
|
return (_jsx(AriaButton, { ref: ref,
|
|
65
125
|
/* The default, and the point of the component. React Aria sets this too;
|
|
66
126
|
stating it here means the prop is visible in the signature rather than
|
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Something the page needs to say, in the place it applies to.
|
|
3
|
-
*
|
|
4
|
-
* The third component the gallery documented and nobody could import — 31 call
|
|
5
|
-
* sites assembling `className="callout callout-bad"` by hand, and a `-mark`
|
|
6
|
-
* that was a letter somebody typed (`i`, `!`, `✓`) rather than the icon set the
|
|
7
|
-
* rest of the app draws from.
|
|
8
|
-
*
|
|
9
|
-
* **A Callout is not a Toast.** A toast floats over the page and takes no
|
|
10
|
-
* space; this sits in the flow, next to the thing it is about, and pushes what
|
|
11
|
-
* follows down. That is the point of it: "this server is not answering" belongs
|
|
12
|
-
* beside the server, not in the corner.
|
|
13
|
-
*
|
|
14
|
-
* **`timed` makes it a toast that stayed home.** A countdown bar, five seconds,
|
|
15
|
-
* then gone. The rule that comes with it: **anything that disappears on a timer
|
|
16
|
-
* must be safe to have missed.** "Saved" qualifies. "This download failed" does
|
|
17
|
-
* not, and neither does anything carrying a button — if the reader has to act,
|
|
18
|
-
* the message waits for them. The countdown pauses on hover for the same
|
|
19
|
-
* reason, because a message that expires while you are reading it was never
|
|
20
|
-
* really shown.
|
|
21
|
-
*/
|
|
22
1
|
import { type IconName } from './Icon.js';
|
|
23
2
|
export default function Callout({ tone, children, icon, timed, onDismiss, className, }: {
|
|
24
3
|
tone?: 'info' | 'good' | 'warn' | 'bad';
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* Something the page needs to say, in the place it applies to.
|
|
4
11
|
*
|
package/dist/components/Card.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* A box with a reason to exist.
|
|
4
11
|
*
|
|
@@ -1,34 +1,3 @@
|
|
|
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
|
-
* **It can post itself.** `checked`/`onChange` were required and there was no
|
|
22
|
-
* `name`, which made this component unusable in a plain `<form action=…>`:
|
|
23
|
-
* with nothing to submit under, a caller wanting one boolean in a Server
|
|
24
|
-
* Action had to render `<input type="checkbox" name="x">` by hand and wrap it
|
|
25
|
-
* in its own `<label>`. That is exactly the native tick box this component
|
|
26
|
-
* exists to replace, and it reappeared the moment the form was uncontrolled —
|
|
27
|
-
* manage's break-glass form carried one, with a comment explaining why it had
|
|
28
|
-
* to. So `name` and `value` are passed through, and `checked` is optional:
|
|
29
|
-
* give it `checked` and `onChange` for a controlled box, `defaultChecked` (or
|
|
30
|
-
* neither) for one the form reads at submit.
|
|
31
|
-
*/
|
|
32
1
|
export default function Checkbox({ label, hint, meta, checked, defaultChecked, onChange, name, value, disabled, className, }: {
|
|
33
2
|
label: React.ReactNode;
|
|
34
3
|
/** What choosing it means, or what it costs. Under the name. `hint`, the
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* Choose some of these, then press the button that applies them.
|
|
4
11
|
*
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
import { useState } from 'react';
|
|
3
|
-
import { Modal as AriaModal, Autocomplete, Dialog, Header, Input,
|
|
10
|
+
import { Modal as AriaModal, Autocomplete, Dialog, Header, Input, ListBox, ListBoxItem, ListBoxSection, ModalOverlay, SearchField, Text, useFilter, } from 'react-aria-components';
|
|
11
|
+
import Kbd from './Kbd.js';
|
|
4
12
|
import Icon from './Icon.js';
|
|
5
13
|
export default function Command({ open, onOpenChange, groups, placeholder = 'Search commands…', label = 'Command palette', className, }) {
|
|
6
14
|
const [query, setQuery] = useState('');
|
|
@@ -32,5 +40,5 @@ export default function Command({ open, onOpenChange, groups, placeholder = 'Sea
|
|
|
32
40
|
caller's. Forty `onRun`s that each remember to
|
|
33
41
|
close is thirty-nine chances to forget. */
|
|
34
42
|
close();
|
|
35
|
-
}, children: [command.icon && _jsx(Icon, { name: command.icon, className: "cmd-icon" }), _jsxs("span", { className: "cmd-text", children: [_jsx(Text, { slot: "label", className: "cmd-label", children: command.label }), command.description && (_jsx(Text, { slot: "description", className: "cmd-desc", children: command.description }))] }), command.shortcut &&
|
|
43
|
+
}, children: [command.icon && _jsx(Icon, { name: command.icon, className: "cmd-icon" }), _jsxs("span", { className: "cmd-text", children: [_jsx(Text, { slot: "label", className: "cmd-label", children: command.label }), command.description && (_jsx(Text, { slot: "description", className: "cmd-desc", children: command.description }))] }), command.shortcut && _jsx(Kbd, { className: "cmd-key", children: command.shortcut })] }, command.id)))] }, group.title))) }))] })) }) }) }));
|
|
36
44
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
import { useState } from 'react';
|
|
3
10
|
import Button from './Button.js';
|
|
4
11
|
import Field from './Field.js';
|
|
@@ -1,24 +1,3 @@
|
|
|
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
1
|
export default function Dialog({ title, children, onCancel, actions, tone, }: {
|
|
23
2
|
title: string;
|
|
24
3
|
children: React.ReactNode;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* A question that stops what you were doing until it is answered.
|
|
4
11
|
*
|
|
@@ -1,52 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
/** The label element's own id, for a control a `<label for>` cannot name.
|
|
37
|
-
*
|
|
38
|
-
* `htmlFor` is enough for an `<input>`, which is what almost every caller
|
|
39
|
-
* wraps. It is not enough for `Select`, or for anything else built on a
|
|
40
|
-
* `<button>`: a button takes its accessible name from its *contents*, and
|
|
41
|
-
* a `<label for>` pointing at one is ignored by the name computation. A
|
|
42
|
-
* caller that put a `Select` in a `Field` got a label on screen and a
|
|
43
|
-
* control announcing only its current value — and the workaround was an
|
|
44
|
-
* `aria-label` repeating the label string, which is two literals and two
|
|
45
|
-
* chances to drift apart. That drift is exactly the bug this component
|
|
46
|
-
* exists to prevent, so: pass this as `aria-labelledby` and there is one
|
|
47
|
-
* string in one place. */
|
|
48
|
-
labelId: string;
|
|
49
|
-
}
|
|
1
|
+
import { type FieldWiring } from './fieldWiring.js';
|
|
2
|
+
export type { FieldWiring } from './fieldWiring.js';
|
|
50
3
|
export default function Field({ label, hint, error, required, children, labelHidden, layout, className, }: {
|
|
51
4
|
/** What the field is. Always given -- there is no unlabelled case, only
|
|
52
5
|
* fields whose label is hidden. */
|
|
@@ -66,6 +19,25 @@ export default function Field({ label, hint, error, required, children, labelHid
|
|
|
66
19
|
* form to fill in. Same element, same wiring, same guarantees -- the only
|
|
67
20
|
* thing that changes is where the label sits. */
|
|
68
21
|
layout?: 'stack' | 'row';
|
|
69
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The control.
|
|
24
|
+
*
|
|
25
|
+
* **Elements, or a function.** Elements are the shape to reach for: a
|
|
26
|
+
* function cannot cross the server boundary, so a render prop made every
|
|
27
|
+
* page with a form a client component whether or not it needed to be, and
|
|
28
|
+
* all three apps' `design.ts` say so in the same sentence. Plain children
|
|
29
|
+
* read the wiring from context instead, and the package's own controls
|
|
30
|
+
* apply it when the caller has not named an `id`:
|
|
31
|
+
*
|
|
32
|
+
* <Field label="Region" hint="Cannot be changed later">
|
|
33
|
+
* <Select name="region" defaultValue="sg">…</Select>
|
|
34
|
+
* </Field>
|
|
35
|
+
*
|
|
36
|
+
* The function form stays, unchanged and not deprecated. It is still the
|
|
37
|
+
* answer for a control the package does not own, or for a caller that
|
|
38
|
+
* needs the ids for something else -- a `<datalist>` to point at, a label
|
|
39
|
+
* rendered somewhere the provider does not reach.
|
|
40
|
+
*/
|
|
41
|
+
children: React.ReactNode | ((field: FieldWiring) => React.ReactNode);
|
|
70
42
|
className?: string;
|
|
71
43
|
}): import("react").JSX.Element;
|
package/dist/components/Field.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* A labelled input, its explanation, and what went wrong with it.
|
|
4
11
|
*
|
|
@@ -31,18 +38,20 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
31
38
|
* are breaking it.
|
|
32
39
|
*/
|
|
33
40
|
import { useId } from 'react';
|
|
41
|
+
import { FieldWiringContext } from './fieldWiring.js';
|
|
34
42
|
export default function Field({ label, hint, error, required, children, labelHidden, layout = 'stack', className, }) {
|
|
35
43
|
const id = useId();
|
|
36
44
|
const hintId = `${id}-hint`;
|
|
37
45
|
const errorId = `${id}-error`;
|
|
38
46
|
const labelId = `${id}-label`;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const wiring = {
|
|
48
|
+
id,
|
|
49
|
+
labelId,
|
|
50
|
+
/* Both, in reading order, when both are there. A field that has a rule
|
|
51
|
+
and has broken it needs to say the rule too -- "must be a URL" on its
|
|
52
|
+
own does not tell you what shape of URL. */
|
|
53
|
+
'aria-describedby': [hint && hintId, error && errorId].filter(Boolean).join(' ') || undefined,
|
|
54
|
+
'aria-invalid': error ? true : undefined,
|
|
55
|
+
};
|
|
56
|
+
return (_jsxs("div", { className: `field field-${layout}-layout${error ? ' field-bad' : ''}${className ? ` ${className}` : ''}`, children: [_jsxs("label", { id: labelId, className: labelHidden ? 'sr-only' : 'field-label', htmlFor: id, children: [label, required && (_jsxs("span", { className: "field-required", "aria-label": "required", children: [' ', "*"] }))] }), hint && (_jsx("p", { className: "field-hint", id: hintId, children: hint })), typeof children === 'function' ? (children(wiring)) : (_jsx(FieldWiringContext.Provider, { value: wiring, children: children })), error && (_jsx("p", { className: "field-error", id: errorId, role: "alert", children: error }))] }));
|
|
48
57
|
}
|
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A person, said in one line.
|
|
3
|
-
*
|
|
4
|
-
* A name, an address and a coloured disc with their initials. It is a mail
|
|
5
|
-
* client's most repeated object -- every row of a thread list, every header of
|
|
6
|
-
* a message, every chip in a composer's To field -- and until there is one of
|
|
7
|
-
* these it is written by hand at each of those, slightly differently, and the
|
|
8
|
-
* initials are wrong in a different way at every site.
|
|
9
|
-
*
|
|
10
|
-
* **The colour is derived from the address, not chosen.** The same person is
|
|
11
|
-
* the same colour in the list, in the header and in the composer, across
|
|
12
|
-
* reloads and across machines, with nothing stored. That consistency is the
|
|
13
|
-
* only thing the colour is for: it is a second, weaker cue that two rows are
|
|
14
|
-
* from the same sender, which is worth something when scanning and worth
|
|
15
|
-
* nothing if it changes.
|
|
16
|
-
*
|
|
17
|
-
* **The disc is `aria-hidden` and the initials are never read out.** "A L" is
|
|
18
|
-
* not a name, and a screen reader that announces it before the name has made
|
|
19
|
-
* every row of the list longer to listen to for no information at all.
|
|
20
|
-
*
|
|
21
|
-
* **Initials come from the name when there is one, and from the address when
|
|
22
|
-
* there is not.** A contact with no display name is common -- most machine
|
|
23
|
-
* senders have none -- and falling through to the first letter of the local
|
|
24
|
-
* part beats an empty disc or a `?`.
|
|
25
|
-
*/
|
|
26
1
|
export interface Props {
|
|
27
2
|
/** The display name, when the message carried one. */
|
|
28
3
|
name?: string | null;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* A person, said in one line.
|
|
4
11
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type IconName } from './Icon.js';
|
|
1
2
|
export interface Props extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'className'> {
|
|
2
3
|
/** The same three every control takes, so a row of mixed controls lines up
|
|
3
4
|
* without anyone measuring. */
|
|
@@ -8,6 +9,15 @@ export interface Props extends Omit<React.InputHTMLAttributes<HTMLInputElement>,
|
|
|
8
9
|
/** Full width of whatever holds it. Inputs already are, by default; this is
|
|
9
10
|
* for the `size`d ones, which are not. */
|
|
10
11
|
block?: boolean;
|
|
12
|
+
/** A glyph inside the left edge. Decorative: it is `aria-hidden`, because
|
|
13
|
+
* a magnifier beside a field called "Search" is the label said twice. */
|
|
14
|
+
icon?: IconName;
|
|
15
|
+
/** Empty it. Given, a clear button appears inside the right edge whenever
|
|
16
|
+
* the field has a value; the caller owns the value and does the clearing. */
|
|
17
|
+
onClear?: () => void;
|
|
18
|
+
/** Inside the right edge, before the clear button. A `Kbd` saying what
|
|
19
|
+
* opens this, which is what the mail client's `.mail-search-key` was. */
|
|
20
|
+
trailing?: React.ReactNode;
|
|
11
21
|
className?: string;
|
|
12
22
|
}
|
|
13
23
|
declare const Input: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLInputElement>>;
|
package/dist/components/Input.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
/**
|
|
3
10
|
* The bare control `Field` wraps.
|
|
4
11
|
*
|
|
@@ -41,7 +48,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
41
48
|
import { forwardRef, useState } from 'react';
|
|
42
49
|
import { ToggleButton } from 'react-aria-components';
|
|
43
50
|
import Icon from './Icon.js';
|
|
44
|
-
|
|
51
|
+
import { useFieldWiring } from './fieldWiring.js';
|
|
52
|
+
const Input = forwardRef(function Input({ size = 'md', mono, block, className, type = 'text', icon, onClear, trailing, ...rest }, ref) {
|
|
53
|
+
/* What the `Field` above wired, when the caller did not thread it by hand.
|
|
54
|
+
Explicit props win: a caller that named an `id` meant that id, and a
|
|
55
|
+
render-prop `Field` spreading its wiring is passing the same values in
|
|
56
|
+
anyway. This is the fallback, not an override. */
|
|
57
|
+
const field = useFieldWiring();
|
|
58
|
+
const wired = {
|
|
59
|
+
id: rest.id ?? field?.id,
|
|
60
|
+
'aria-describedby': rest['aria-describedby'] ?? field?.['aria-describedby'],
|
|
61
|
+
'aria-invalid': rest['aria-invalid'] ?? field?.['aria-invalid'],
|
|
62
|
+
};
|
|
45
63
|
const classes = [
|
|
46
64
|
size === 'md' ? '' : `size-${size}`,
|
|
47
65
|
mono ? 'mono' : '',
|
|
@@ -54,12 +72,22 @@ const Input = forwardRef(function Input({ size = 'md', mono, block, className, t
|
|
|
54
72
|
run time does not remount it -- a hook count that changes with a prop is a
|
|
55
73
|
React error, and a remount would drop the caret. */
|
|
56
74
|
const [shown, setShown] = useState(false);
|
|
75
|
+
const glyph = { sm: 14, md: 16, lg: 18 }[size];
|
|
57
76
|
if (type !== 'password') {
|
|
58
|
-
|
|
77
|
+
const box = (_jsx("input", { ref: ref, type: type, className: classes || undefined, ...rest, ...wired }));
|
|
78
|
+
if (!icon && !onClear && !trailing)
|
|
79
|
+
return box;
|
|
80
|
+
/* Something to clear, rather than something that could be cleared: a
|
|
81
|
+
clear button over an empty field is a control that does nothing. Both
|
|
82
|
+
shapes of value are checked because either may be the caller's. */
|
|
83
|
+
const filled = rest.value !== undefined
|
|
84
|
+
? String(rest.value).length > 0
|
|
85
|
+
: String(rest.defaultValue ?? '').length > 0;
|
|
86
|
+
return (_jsxs("span", { className: `adorned adorned-${size}${block ? ' block' : ''}${icon ? ' adorned-icon' : ''}`, children: [icon && _jsx(Icon, { name: icon, size: glyph, className: "adorned-mark", "aria-hidden": true }), box, (trailing || (onClear && filled)) && (_jsxs("span", { className: "adorned-end", children: [trailing, onClear && filled && (_jsx("button", { type: "button", className: "icon-btn ghost adorned-clear", "aria-label": "Clear", onClick: onClear, disabled: rest.disabled, children: _jsx(Icon, { name: "close", size: glyph }) }))] }))] }));
|
|
59
87
|
}
|
|
60
88
|
return (_jsxs("span", { className: `secret secret-${size}${block ? ' block' : ''}`, children: [_jsx("input", { ref: ref, type: shown ? 'text' : 'password', className: classes || undefined,
|
|
61
89
|
/* Only while revealed: as a password these are moot, and setting them
|
|
62
90
|
on a password field makes some browsers stop offering to fill it. */
|
|
63
|
-
autoCapitalize: shown ? 'off' : undefined, autoCorrect: shown ? 'off' : undefined, spellCheck: shown ? false : undefined, ...rest }), _jsx(ToggleButton, { className: `icon-btn ghost secret-eye${size === 'md' ? '' : ` size-${size}`}`, "aria-label": "Show password", isSelected: shown, onChange: setShown, isDisabled: rest.disabled, children: _jsx(Icon, { name: shown ? 'eye-off' : 'eye', size:
|
|
91
|
+
autoCapitalize: shown ? 'off' : undefined, autoCorrect: shown ? 'off' : undefined, spellCheck: shown ? false : undefined, ...rest, ...wired }), _jsx(ToggleButton, { className: `icon-btn ghost secret-eye${size === 'md' ? '' : ` size-${size}`}`, "aria-label": "Show password", isSelected: shown, onChange: setShown, isDisabled: rest.disabled, children: _jsx(Icon, { name: shown ? 'eye-off' : 'eye', size: glyph }) })] }));
|
|
64
92
|
});
|
|
65
93
|
export default Input;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export default function Kbd({ children, className, }: {
|
|
2
|
+
/** The chord as it should read. `⌘K`, `Ctrl+K`, `Esc`. The package does not
|
|
3
|
+
* translate between platforms: which modifier this machine calls what is a
|
|
4
|
+
* fact the app knows and this component does not. */
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
9
|
+
/**
|
|
10
|
+
* A key, or a chord, drawn the way the app draws one.
|
|
11
|
+
*
|
|
12
|
+
* The rule already existed twice. `Menu` had `.menu-key` and `Command` had
|
|
13
|
+
* `.cmd-key`, and the two declaration blocks were identical down to the
|
|
14
|
+
* property order -- the same mono face, the same `--text-2xs`, the same
|
|
15
|
+
* `--muted`, the same tracking. Neither was reachable: `Keyboard` came from
|
|
16
|
+
* React Aria and the class was internal, so a consumer wanting to say "press
|
|
17
|
+
* ⌘K" beside its own search box had nothing to import and drew a third one.
|
|
18
|
+
* The mail client's `.keys` and `.mail-search-key` are that third one.
|
|
19
|
+
*
|
|
20
|
+
* **It is `<kbd>`, which is the whole point.** A styled `<span>` reads as a
|
|
21
|
+
* word; `<kbd>` says "this is something you press", and a screen reader can
|
|
22
|
+
* treat it accordingly. React Aria's `Keyboard` renders the element and
|
|
23
|
+
* carries the slot wiring that lets `Menu` and `Command` place it, so this is
|
|
24
|
+
* that component with the package's class on it rather than a new one.
|
|
25
|
+
*
|
|
26
|
+
* **It draws the reminder; it does not bind anything.** Same contract as
|
|
27
|
+
* `Menu`'s `shortcut` and `Command`'s: what actually listens for the chord is
|
|
28
|
+
* the app's business, and a component that both drew a key and bound it would
|
|
29
|
+
* be two things. Nothing here reads the keyboard.
|
|
30
|
+
*/
|
|
31
|
+
import { Keyboard } from 'react-aria-components';
|
|
32
|
+
export default function Kbd({ children, className, }) {
|
|
33
|
+
return _jsx(Keyboard, { className: ['kbd', className].filter(Boolean).join(' '), children: children });
|
|
34
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/* Client, because this module's own JSX attaches handlers or calls hooks. A
|
|
4
|
+
server component may still import it -- that is the point -- it simply
|
|
5
|
+
renders on the client. The ones without this line (Brand, Empty, Icon,
|
|
6
|
+
Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
|
|
7
|
+
the server, which is why the directive is per component rather than one
|
|
8
|
+
line at the package's front door. */
|
|
2
9
|
import DOMPurify from 'dompurify';
|
|
3
10
|
import { marked } from 'marked';
|
|
4
11
|
import { useEffect, useMemo, useState } from 'react';
|