@wtfalch/design 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/components/Button.d.ts +19 -27
  2. package/dist/components/Button.js +61 -1
  3. package/dist/components/Callout.d.ts +0 -21
  4. package/dist/components/Callout.js +7 -0
  5. package/dist/components/Card.js +7 -0
  6. package/dist/components/Checkbox.d.ts +0 -31
  7. package/dist/components/Checkbox.js +7 -0
  8. package/dist/components/Command.js +10 -2
  9. package/dist/components/DangerZone.js +7 -0
  10. package/dist/components/Dialog.d.ts +0 -21
  11. package/dist/components/Dialog.js +7 -0
  12. package/dist/components/Field.d.ts +0 -31
  13. package/dist/components/Field.js +7 -0
  14. package/dist/components/Identity.d.ts +0 -25
  15. package/dist/components/Identity.js +7 -0
  16. package/dist/components/Input.js +7 -0
  17. package/dist/components/Kbd.d.ts +7 -0
  18. package/dist/components/Kbd.js +34 -0
  19. package/dist/components/Markdown.js +7 -0
  20. package/dist/components/Menu.js +10 -2
  21. package/dist/components/Modal.d.ts +0 -33
  22. package/dist/components/Modal.js +7 -0
  23. package/dist/components/Pagination.d.ts +0 -33
  24. package/dist/components/Pagination.js +7 -0
  25. package/dist/components/Popover.js +7 -0
  26. package/dist/components/Rows.js +7 -0
  27. package/dist/components/ScrollArea.js +7 -0
  28. package/dist/components/Select.d.ts +6 -1
  29. package/dist/components/Select.js +9 -2
  30. package/dist/components/SizeGrid.js +7 -0
  31. package/dist/components/Slider.js +7 -0
  32. package/dist/components/SplitPane.js +7 -0
  33. package/dist/components/Stat.d.ts +45 -0
  34. package/dist/components/Stat.js +31 -0
  35. package/dist/components/Tabs.d.ts +21 -0
  36. package/dist/components/Tabs.js +8 -1
  37. package/dist/components/Toast.d.ts +24 -18
  38. package/dist/components/Toast.js +44 -3
  39. package/dist/components/Toggle.d.ts +0 -30
  40. package/dist/components/Toggle.js +7 -0
  41. package/dist/components/Tooltip.js +7 -0
  42. package/dist/components/Tour.d.ts +0 -23
  43. package/dist/components/Tour.js +7 -0
  44. package/dist/index.d.ts +13 -2
  45. package/dist/index.js +9 -1
  46. package/dist/styles/index.css +117 -13
  47. package/dist/tf.css +117 -13
  48. package/dist/valet.css +117 -13
  49. package/package.json +2 -1
@@ -1,29 +1,9 @@
1
- /**
2
- * A button. The most-copied markup in the app, finally a component.
3
- *
4
- * Four kinds that look like four things, three sizes, and a busy state that is
5
- * not a disabled one. None of that is new — all of it was in the stylesheet
6
- * already, and every call site assembled it by hand from
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 default function Button({ kind, disabled, size, busy, block, iconOnly, className, children, ...rest }: Props): import("react").JSX.Element;
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
- export default function Button({ kind = 'default', disabled, size = 'md', busy, block, iconOnly, className, children, ...rest }) {
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
  *
@@ -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, Keyboard, ListBox, ListBoxItem, ListBoxSection, ModalOverlay, SearchField, Text, useFilter, } from 'react-aria-components';
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 && (_jsx(Keyboard, { className: "cmd-key", children: command.shortcut }))] }, command.id)))] }, group.title))) }))] })) }) }) }));
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,34 +1,3 @@
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
1
  export interface FieldWiring {
33
2
  id: string;
34
3
  'aria-describedby': string | undefined;
@@ -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
  *
@@ -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,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
  *
@@ -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';
@@ -1,5 +1,13 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Menu as AriaMenu, Popover as AriaPopover, Header, Keyboard, MenuItem, MenuSection, MenuTrigger, Separator, SubmenuTrigger, Text, } from 'react-aria-components';
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
+ import { Menu as AriaMenu, Popover as AriaPopover, Header, MenuItem, MenuSection, MenuTrigger, Separator, SubmenuTrigger, Text, } from 'react-aria-components';
10
+ import Kbd from './Kbd.js';
3
11
  import Icon from './Icon.js';
4
12
  function isSection(entry) {
5
13
  return 'title' in entry && Array.isArray(entry.items);
@@ -9,7 +17,7 @@ function renderItem(item) {
9
17
  /* Typeahead needs a string, and `label` may be a node. Without this,
10
18
  typing the first letter of an item whose label is markup matches
11
19
  nothing and the menu looks broken. */
12
- textValue: typeof item.label === 'string' ? item.label : item.id, children: [item.icon && _jsx(Icon, { name: item.icon, className: "menu-icon" }), _jsxs("span", { className: "menu-text", children: [_jsx(Text, { slot: "label", className: "menu-label", children: item.label }), item.description && (_jsx(Text, { slot: "description", className: "menu-desc", children: item.description }))] }), item.shortcut && _jsx(Keyboard, { className: "menu-key", children: item.shortcut }), item.items && (_jsx("svg", { className: "menu-more", viewBox: "0 0 24 24", width: "12", height: "12", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "m9 6 6 6-6 6" }) }))] }, item.id));
20
+ textValue: typeof item.label === 'string' ? item.label : item.id, children: [item.icon && _jsx(Icon, { name: item.icon, className: "menu-icon" }), _jsxs("span", { className: "menu-text", children: [_jsx(Text, { slot: "label", className: "menu-label", children: item.label }), item.description && (_jsx(Text, { slot: "description", className: "menu-desc", children: item.description }))] }), item.shortcut && _jsx(Kbd, { className: "menu-key", children: item.shortcut }), item.items && (_jsx("svg", { className: "menu-more", viewBox: "0 0 24 24", width: "12", height: "12", "aria-hidden": "true", children: _jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "m9 6 6 6-6 6" }) }))] }, item.id));
13
21
  const body = item.items ? (_jsxs(SubmenuTrigger, { children: [row, _jsx(AriaPopover, { className: "menu-sheet", children: _jsx(AriaMenu, { className: "menu-list", children: item.items.map(renderItem) }) })] }, item.id)) : (row);
14
22
  if (!item.separated)
15
23
  return body;
@@ -1,36 +1,3 @@
1
- /**
2
- * A window over the app, and the behaviour every one of them was missing.
3
- *
4
- * There are eight of these -- Settings, FirstRun, ViewSettings, DownloadModal,
5
- * ModelSettings, AppletSettings, AppletReview, the studio -- and all eight were
6
- * built by hand from `.backdrop` and `.modal`. Not one of them trapped focus.
7
- *
8
- * **That is not a detail.** Everything behind a modal is still in the tab
9
- * order: still focusable, still clickable by a keyboard, and completely
10
- * invisible under the scrim. Tab past the last button in Settings and you are
11
- * somewhere on the dashboard you cannot see, operating controls you cannot
12
- * read. Shift-Tab from the first does the same going the other way. The mouse
13
- * never finds this, which is why it survived eight implementations.
14
- *
15
- * So: focus moves in, is kept in, and goes back where it came from when the
16
- * window closes -- to the button that opened it, not to the top of the page.
17
- * `role="dialog"` and `aria-modal` say the same thing to a screen reader, which
18
- * otherwise reads the page underneath as though it were still there.
19
- *
20
- * **This is the workspace shape.** A title, a body, optionally a footer, sized
21
- * to its content. `Dialog` is the narrow two-answer version and is built on
22
- * this -- same trap, same restore, stricter about the scrim, because for "may
23
- * this applet write to your files" a stray click on the background is a way of
24
- * answering by accident.
25
- *
26
- * **A sheet is this with `edge` set, not a second component.** A drawer from
27
- * the side of the window differs from a window in the middle of it by where it
28
- * is anchored and which way it slides -- and in nothing else. Same focus trap,
29
- * same restore, same scrim, same header, body and footer. Writing a `Sheet`
30
- * that duplicates all of that to change two CSS properties is how a design
31
- * system ends up with two windows that drift: one of them gets the fix and
32
- * nobody notices which.
33
- */
34
1
  export default function Modal({ title, description, subtitle, head, children, footer, footerClass, onClose, closeDisabled, width, edge, bodyClass, className, dismissOnScrim, closeButton, labelledBy, }: {
35
2
  /** The window's name. Rendered as the heading and announced on open. */
36
3
  title?: React.ReactNode;
@@ -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 window over the app, and the behaviour every one of them was missing.
4
11
  *
@@ -1,36 +1,3 @@
1
- /**
2
- * Moving through a list that does not fit.
3
- *
4
- * **It counts in items, not in pages**, because that is what the server
5
- * answers and what the reader asks. A JMAP query returns a position, a limit
6
- * and a total; a mailbox is "51–100 of 1,284", not "page 2 of 26". Page
7
- * numbers are this component's arithmetic, done once here rather than at every
8
- * call site -- which is where the off-by-one lives, and where it becomes an
9
- * empty last page.
10
- *
11
- * **The count is the point, and it is said out loud.** "51–100 of 1,284" tells
12
- * you how far in you are and how much is left; two arrows tell you neither. A
13
- * pager with no count is a pager you navigate by feel.
14
- *
15
- * **A total is optional, because a server may refuse to count.** JMAP's
16
- * `calculateTotal` is a request, not a promise, and a large mailbox is exactly
17
- * where it gets declined. Without one this shows the range and keeps Next
18
- * enabled while a full page came back, which is the only honest thing it can
19
- * do: a page shorter than the limit is the end.
20
- *
21
- * **The ellipsis is a field, not punctuation.** It stands for the pages you
22
- * cannot see, so it is where you say which one you want: type a number, press
23
- * Enter, and you are there. Drawn as a ghost -- no border, no background, the
24
- * `…` as its placeholder -- so at rest the row looks exactly like a row of
25
- * page buttons with an elision in it, and it becomes a control when you touch
26
- * it. Without this, reaching page 17 of 26 is eleven presses of Next, and the
27
- * middle of a row of live buttons is a dead spot.
28
- *
29
- * **`nav` with a name**, so a screen reader can jump to it and so two pagers
30
- * on a page are distinguishable. The current page's button is
31
- * `aria-current="page"`, which is what tells a reader where they are without
32
- * relying on the colour that says it visually.
33
- */
34
1
  export interface Props {
35
2
  /** Index of the first item shown, counting from zero -- the same number the
36
3
  * query was given, so caller and component never disagree about the origin. */
@@ -1,4 +1,11 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /* Client, because this module's own JSX attaches handlers or calls hooks. A
4
+ server component may still import it -- that is the point -- it simply
5
+ renders on the client. The ones without this line (Brand, Empty, Icon,
6
+ Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
7
+ the server, which is why the directive is per component rather than one
8
+ line at the package's front door. */
2
9
  /**
3
10
  * Moving through a list that does not fit.
4
11
  *
@@ -1,4 +1,11 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /* Client, because this module's own JSX attaches handlers or calls hooks. A
4
+ server component may still import it -- that is the point -- it simply
5
+ renders on the client. The ones without this line (Brand, Empty, Icon,
6
+ Illustration, Pill, Progress, Skeleton, Stat, Textarea, Table) render on
7
+ the server, which is why the directive is per component rather than one
8
+ line at the package's front door. */
2
9
  import { Popover as AriaPopover, Dialog, DialogTrigger, } from 'react-aria-components';
3
10
  export default function Popover({ trigger, children, placement = 'bottom start', label, offset = 6, open, onOpenChange, className, }) {
4
11
  return (_jsxs(DialogTrigger, { isOpen: open, onOpenChange: onOpenChange, children: [trigger, _jsx(AriaPopover, { className: `pop${className ? ` ${className}` : ''}`, placement: placement, offset: offset, children: _jsx(Dialog, { className: "pop-body", "aria-label": label, children: ({ close }) => (typeof children === 'function' ? children(close) : children) }) })] }));