@wtfalch/design 0.7.0 → 0.9.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/README.md +18 -0
- package/dist/components/Button.js +8 -7
- package/dist/components/Callout.js +1 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +21 -4
- package/dist/components/Command.js +2 -2
- package/dist/components/Empty.js +22 -1
- package/dist/components/Field.d.ts +22 -19
- package/dist/components/Field.js +11 -9
- package/dist/components/Input.d.ts +10 -0
- package/dist/components/Input.js +26 -5
- package/dist/components/Kbd.js +11 -1
- package/dist/components/Menu.js +3 -3
- package/dist/components/Modal.js +2 -2
- package/dist/components/Pagination.js +6 -1
- package/dist/components/Progress.js +8 -1
- package/dist/components/Rows.d.ts +1 -1
- package/dist/components/Rows.js +1 -1
- package/dist/components/Select.d.ts +3 -1
- package/dist/components/Select.js +28 -6
- package/dist/components/Shell.d.ts +35 -0
- package/dist/components/Shell.js +36 -0
- package/dist/components/SizeGrid.js +14 -2
- package/dist/components/Stat.js +11 -2
- package/dist/components/Table.js +7 -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.js +1 -1
- package/dist/components/Tooltip.js +16 -2
- package/dist/components/Tour.js +1 -1
- package/dist/components/fieldWiring.d.ts +44 -0
- package/dist/components/fieldWiring.js +30 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +8 -0
- package/dist/styles/index.css +1110 -355
- package/dist/tf.css +1110 -355
- package/dist/themes/choice.d.ts +71 -0
- package/dist/themes/choice.js +81 -0
- package/dist/valet.css +1110 -355
- package/package.json +3 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The frame a signed-in surface stands in: a header, and the page under it.
|
|
4
|
+
*
|
|
5
|
+
* Three apps have one, all called `shell.tsx`, all the same six elements. The
|
|
6
|
+
* differences were a wider variant in manage and where each put the theme
|
|
7
|
+
* control, which is a prop and a slot, not three components.
|
|
8
|
+
*
|
|
9
|
+
* **It gates nothing, and that is worth stating because a frame looks like
|
|
10
|
+
* the place to.** A layout cannot reliably stop the page beneath it
|
|
11
|
+
* rendering, so every page decides for itself who may see it. Putting a check
|
|
12
|
+
* here would read as protection and provide none. All three app docblocks say
|
|
13
|
+
* this; it moves with the component.
|
|
14
|
+
*
|
|
15
|
+
* **A `<header>` and a `<main>`, so the landmarks exist.** Hand-built
|
|
16
|
+
* versions reached for `<div>` often enough that this is the second reason to
|
|
17
|
+
* have one: "skip to content" and a screen reader's landmark list both come
|
|
18
|
+
* from the elements, and neither is visible to the person who wrote the div.
|
|
19
|
+
*
|
|
20
|
+
* Server-renderable: no hooks, no handlers. `who` and `brand` are slots, so
|
|
21
|
+
* the client parts an app needs -- a user menu, the theme switch -- are the
|
|
22
|
+
* app's to pass and stay its own client boundaries.
|
|
23
|
+
*/
|
|
24
|
+
export default function Shell({ brand, who, nav, wide = false, children, className, }) {
|
|
25
|
+
/* Utilities: this component is layout and nothing else, which is where they
|
|
26
|
+
earn their place. The measure is the one thing that is not a utility --
|
|
27
|
+
it is a number two elements have to agree on, so it is a custom property
|
|
28
|
+
on the frame rather than a literal repeated in three class lists, which
|
|
29
|
+
is how manage ended up with `head-inner-wide` and `main-wide` as separate
|
|
30
|
+
classes and had to remember both. */
|
|
31
|
+
const measure = wide ? 'max-w-[var(--shell-measure-wide)]' : 'max-w-[var(--shell-measure)]';
|
|
32
|
+
const band = `w-full mx-auto px-4 ${measure}`;
|
|
33
|
+
return (_jsxs("div", { className: ['shell flex flex-col min-h-screen surface-bg text-text', className]
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join(' '), children: [_jsxs("header", { className: "flex-none border-b border-border surface-panel", children: [_jsxs("div", { className: `${band} flex items-center justify-between gap-4 py-3 min-w-0`, children: [brand, who && _jsx("div", { className: "flex items-center gap-3 min-w-0", children: who })] }), nav && _jsx("div", { className: `${band} pb-2`, children: nav })] }), _jsx("main", { className: `${band} flex-1 py-6 min-w-0`, children: children })] }));
|
|
36
|
+
}
|
|
@@ -41,8 +41,20 @@ export default function SizeGrid({ value, max = { cols: 3, rows: 3 }, disabled =
|
|
|
41
41
|
for (let col = 1; col <= extent.cols; col++) {
|
|
42
42
|
const on = col <= shown.cols && row <= shown.rows;
|
|
43
43
|
const chosen = col <= value.cols && row <= value.rows;
|
|
44
|
-
cells.push(_jsx("button", { type: "button",
|
|
44
|
+
cells.push(_jsx("button", { type: "button",
|
|
45
|
+
/* `size-cell` stays as the hook for the two states: `on` is a fill
|
|
46
|
+
from `color-mix`, which is not a utility. The rest -- a painted
|
|
47
|
+
cell with no contents, so it says it is a block rather than
|
|
48
|
+
taking a control's arrangement -- is here. */
|
|
49
|
+
className: [
|
|
50
|
+
'size-cell block w-[18px] h-[14px] p-0 rounded-sm border border-border surface-panel-2 cursor-pointer',
|
|
51
|
+
'disabled:cursor-default disabled:opacity-50',
|
|
52
|
+
on ? 'on' : '',
|
|
53
|
+
chosen && !hover ? 'border-accent' : '',
|
|
54
|
+
]
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(' '), 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}`));
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
|
-
return (_jsxs("div", { className: "
|
|
59
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "grid gap-1", 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: "opacity-[0.55]", children: [' ', "\u00B7 max ", max.cols, "\u00D7", max.rows] }))] })] }));
|
|
48
60
|
}
|
package/dist/components/Stat.js
CHANGED
|
@@ -26,6 +26,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
26
26
|
* A bordered tile inside a bordered row is two boxes saying one thing.
|
|
27
27
|
*/
|
|
28
28
|
export default function Stat({ label, value, note, tone, look = 'tile', className, }) {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/* Utilities, so the tile's shape reads where it is drawn. `bare` states
|
|
30
|
+
the compact shape rather than adding a class that undoes the tile's --
|
|
31
|
+
which is the mistake the old element reset made across this package.
|
|
32
|
+
`stat` and `stat-value` stay as hooks: the tone is an attribute on the
|
|
33
|
+
tile colouring a child, which is a relationship, and a utility is a
|
|
34
|
+
property on one element. */
|
|
35
|
+
const tile = look === 'bare'
|
|
36
|
+
? 'grid gap-0 leading-[1.25]'
|
|
37
|
+
: 'grid gap-1 p-4 border border-border rounded-md surface-panel';
|
|
38
|
+
const classes = ['stat', tile, '[&>p]:m-0', className].filter(Boolean).join(' ');
|
|
39
|
+
return (_jsxs("div", { className: classes, "data-tone": tone, children: [_jsx("p", { className: "text-xs text-muted", children: label }), _jsx("p", { className: `stat-value font-strong tabular-nums ${look === 'bare' ? 'text-sm' : 'text-lg'}`, children: value }), note && _jsx("p", { className: "text-xs text-muted", children: note })] }));
|
|
31
40
|
}
|
package/dist/components/Table.js
CHANGED
|
@@ -3,7 +3,13 @@ export default function Table({ caption, columns, rows, keyOf, empty, className,
|
|
|
3
3
|
if (!rows.length) {
|
|
4
4
|
return empty ? _jsx("div", { className: "set-hint rows-empty", children: empty }) : null;
|
|
5
5
|
}
|
|
6
|
-
return (
|
|
6
|
+
return (
|
|
7
|
+
/* The one thing about a table that is a property of one element. Its
|
|
8
|
+
cells, headings and last-row rule are structural -- `th:first-child`,
|
|
9
|
+
`tbody tr:last-child td` -- and a structural pseudo-class is the right
|
|
10
|
+
tool for them: expressing the same thing with utilities means this
|
|
11
|
+
component doing index arithmetic to work out what CSS already knows. */
|
|
12
|
+
_jsx("div", { className: ['overflow-x-auto', className].filter(Boolean).join(' '), children: _jsxs("table", { className: "data-table", children: [_jsx("caption", { className: "sr-only", children: caption }), _jsx("thead", { children: _jsx("tr", { children: columns.map((c) => (_jsx("th", { scope: "col", style: { width: c.width, textAlign: c.align === 'end' ? 'right' : undefined }, children: _jsx("span", { className: c.quiet ? 'sr-only' : undefined, children: c.header }) }, c.header))) }) }), _jsx("tbody", { children: rows.map((item) => (_jsx("tr", { children: columns.map((c, i) => {
|
|
7
13
|
/* The first cell is the row's name, so it is a `<th scope="row">`
|
|
8
14
|
-- that is what lets a reader ask "which row am I in" and get
|
|
9
15
|
an answer instead of a cell index. */
|
|
@@ -8,15 +8,25 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
* this component keeps it by rendering the element.
|
|
9
9
|
*
|
|
10
10
|
* Same contract as `Input`: a plain `<textarea>` so every element rule in the
|
|
11
|
-
* stylesheet keeps working, and its `id`, `aria-describedby` and
|
|
12
|
-
* come from `Field`
|
|
13
|
-
*
|
|
11
|
+
* stylesheet keeps working, and its `id`, `aria-describedby` and
|
|
12
|
+
* `aria-invalid` come from the `Field` above it -- handed in by a render
|
|
13
|
+
* prop, or read from context when the caller passed plain children. On its
|
|
14
|
+
* own it needs an `aria-label`, for the same reason `Input` does.
|
|
14
15
|
*/
|
|
15
16
|
import { forwardRef } from 'react';
|
|
17
|
+
import { useFieldWiring } from './fieldWiring.js';
|
|
16
18
|
const Textarea = forwardRef(function Textarea({ size = 'md', mono, className, ...rest }, ref) {
|
|
17
19
|
const classes = [size === 'md' ? '' : `size-${size}`, mono ? 'mono' : '', className ?? '']
|
|
18
20
|
.filter(Boolean)
|
|
19
21
|
.join(' ');
|
|
20
|
-
|
|
22
|
+
/* Explicit props win; this is the fallback. After `rest` in the spread,
|
|
23
|
+
because a key present with an undefined value still overwrites. */
|
|
24
|
+
const field = useFieldWiring();
|
|
25
|
+
const wired = {
|
|
26
|
+
id: rest.id ?? field?.id,
|
|
27
|
+
'aria-describedby': rest['aria-describedby'] ?? field?.['aria-describedby'],
|
|
28
|
+
'aria-invalid': rest['aria-invalid'] ?? field?.['aria-invalid'],
|
|
29
|
+
};
|
|
30
|
+
return _jsx("textarea", { ref: ref, className: classes || undefined, ...rest, ...wired });
|
|
21
31
|
});
|
|
22
32
|
export default Textarea;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Product } from '../products/index.js';
|
|
2
|
+
import type { BrandName } from './brandMarks.js';
|
|
3
|
+
export default function ThemeSwitch({ product, only, label, labelHidden, size, storageKey, onChange, className, }: {
|
|
4
|
+
/** Whose themes to offer. A registered name, or a `Product` from
|
|
5
|
+
* `defineProduct` for an app with its own palette. */
|
|
6
|
+
product: BrandName | Product;
|
|
7
|
+
/** A subset of the product's themes, in the order to show them. */
|
|
8
|
+
only?: readonly string[];
|
|
9
|
+
label?: string;
|
|
10
|
+
/** The label becomes the accessible name and is not drawn, for a header
|
|
11
|
+
* where the control's meaning is obvious from its contents. */
|
|
12
|
+
labelHidden?: boolean;
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
storageKey?: string;
|
|
15
|
+
/** For an app that has something else to update. The theme is already
|
|
16
|
+
* applied by the time this runs. */
|
|
17
|
+
onChange?: (theme: string) => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client';
|
|
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. */
|
|
4
|
+
/**
|
|
5
|
+
* The control that changes the theme.
|
|
6
|
+
*
|
|
7
|
+
* All three apps have one and the first forty lines are the same in each: the
|
|
8
|
+
* guarded `localStorage` read, the guarded write, the assignment to
|
|
9
|
+
* `document.documentElement.dataset.theme`. What differed was the wrapper and
|
|
10
|
+
* the label -- one says "Appearance", one says "Theme" -- which is drift, not
|
|
11
|
+
* design.
|
|
12
|
+
*
|
|
13
|
+
* **The choice lives in the browser, not the account.** It is a preference
|
|
14
|
+
* about this screen rather than a fact about the person: the same account on
|
|
15
|
+
* a laptop in a bright room and a phone at night wants two answers. That is
|
|
16
|
+
* also why it needs no server round trip and no session.
|
|
17
|
+
*
|
|
18
|
+
* **The default is not stored.** A browser that never chose follows whatever
|
|
19
|
+
* the default becomes, so changing a product's default reaches everyone who
|
|
20
|
+
* never expressed an opinion, rather than only new visitors.
|
|
21
|
+
*
|
|
22
|
+
* **It writes `data-theme` and does not call `applyTheme`.** The palettes are
|
|
23
|
+
* already on the page as CSS from `productCss`; the attribute is the switch.
|
|
24
|
+
* `applyTheme` writes custom properties one by one and is for the case with
|
|
25
|
+
* no stylesheet to lean on.
|
|
26
|
+
*
|
|
27
|
+
* **`aria-labelledby`, or an `aria-label`, but never both and never neither.**
|
|
28
|
+
* The apps passed `aria-label="Appearance"` beside a visible "Appearance"
|
|
29
|
+
* span, which is the same string written twice -- exactly the drift `Field`
|
|
30
|
+
* exists to stop. Given `label`, this renders it and points the control at
|
|
31
|
+
* it; given `labelHidden`, the string becomes the accessible name and is not
|
|
32
|
+
* drawn.
|
|
33
|
+
*/
|
|
34
|
+
import { useEffect, useId, useState } from 'react';
|
|
35
|
+
import { THEME_STORAGE_KEY, storedTheme, themeChoices } from '../themes/choice.js';
|
|
36
|
+
import Select from './Select.js';
|
|
37
|
+
export default function ThemeSwitch({ product, only, label = 'Appearance', labelHidden = false, size = 'sm', storageKey = THEME_STORAGE_KEY, onChange, className, }) {
|
|
38
|
+
const choices = themeChoices(product, only);
|
|
39
|
+
const labelId = useId();
|
|
40
|
+
/* The server cannot know what this browser stored, so the first render has
|
|
41
|
+
to match what the server sent -- the attribute the blocking script wrote
|
|
42
|
+
is on `<html>` already, and reading `localStorage` during render would
|
|
43
|
+
be a hydration mismatch. The effect catches the control up. */
|
|
44
|
+
const [choice, setChoice] = useState(() => choices[0]?.id ?? '');
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setChoice(storedTheme(product, { only, storageKey }));
|
|
47
|
+
// The product object is rebuilt per render when passed inline; its
|
|
48
|
+
// identity is not the dependency, what it resolves to is.
|
|
49
|
+
}, [product, only, storageKey]);
|
|
50
|
+
const choose = (next) => {
|
|
51
|
+
if (!choices.some((c) => c.id === next))
|
|
52
|
+
return;
|
|
53
|
+
setChoice(next);
|
|
54
|
+
try {
|
|
55
|
+
localStorage.setItem(storageKey, next);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* A browser blocking site data still gets the theme, just not the
|
|
59
|
+
memory of it. Failing the change over the storage would be worse. */
|
|
60
|
+
}
|
|
61
|
+
document.documentElement.dataset.theme = next;
|
|
62
|
+
onChange?.(next);
|
|
63
|
+
};
|
|
64
|
+
return (_jsxs("div", { className: ['theme-switch', className].filter(Boolean).join(' '), children: [_jsx("span", { className: "theme-switch-label", id: labelId, hidden: labelHidden, children: label }), _jsx(Select, { "aria-labelledby": labelHidden ? undefined : labelId, "aria-label": labelHidden ? label : undefined, size: size, value: choice, onChange: (e) => choose(e.target.value), children: choices.map((c) => (_jsx("option", { value: c.id, children: c.label }, c.id))) })] }));
|
|
65
|
+
}
|
package/dist/components/Toast.js
CHANGED
|
@@ -115,5 +115,5 @@ export function ToastHost({ children }) {
|
|
|
115
115
|
React error. */
|
|
116
116
|
if (outer)
|
|
117
117
|
return _jsx(_Fragment, { children: children });
|
|
118
|
-
return (_jsxs(Ctx.Provider, { value: push, children: [children, _jsx("div", { className: "sr-only", "aria-live": "polite", "aria-atomic": "true", children: polite }), _jsx("div", { className: "sr-only", "aria-live": "assertive", "aria-atomic": "true", children: assertive }), _jsx(ToastRegion, { queue: queue, className: "toasts", children: ({ toast }) => (_jsxs(AriaToast, { toast: toast, className: `toast toast-${toast.content.tone}`, children: [_jsx(Icon, { name: MARK[toast.content.tone], size: 16, className: "toast-mark" }), _jsx(ToastContent, { className: "grow", children: _jsx(Text, { slot: "title", children: toast.content.text }) }), _jsx(Button, { slot: "close", className: "
|
|
118
|
+
return (_jsxs(Ctx.Provider, { value: push, children: [children, _jsx("div", { className: "sr-only", "aria-live": "polite", "aria-atomic": "true", children: polite }), _jsx("div", { className: "sr-only", "aria-live": "assertive", "aria-atomic": "true", children: assertive }), _jsx(ToastRegion, { queue: queue, className: "toasts", children: ({ toast }) => (_jsxs(AriaToast, { toast: toast, className: `toast toast-${toast.content.tone}`, children: [_jsx(Icon, { name: MARK[toast.content.tone], size: 16, className: "toast-mark" }), _jsx(ToastContent, { className: "ctl-grow", children: _jsx(Text, { slot: "title", children: toast.content.text }) }), _jsx(Button, { slot: "close", className: "toast-close", "aria-label": "Dismiss", children: _jsx(Icon, { name: "close", size: 14 }) })] })) })] }));
|
|
119
119
|
}
|
|
@@ -58,12 +58,26 @@ export default function Tooltip({ label, align = 'right', mark, className, child
|
|
|
58
58
|
is also why `overflow: hidden` on any ancestor no longer clips it -- the
|
|
59
59
|
Permissions pane's horizontal scrollbar, found by hiding one class at a
|
|
60
60
|
time, was this box sitting in the scrollable overflow. */
|
|
61
|
-
return (_jsxs(TooltipTrigger, { delay: 0, closeDelay: 150, children: [_jsx(Focusable, { children: _jsx("span", {
|
|
61
|
+
return (_jsxs(TooltipTrigger, { delay: 0, closeDelay: 150, children: [_jsx(Focusable, { children: _jsx("span", {
|
|
62
|
+
/* `explain` stays as the hook: the mark brightens on hover and on
|
|
63
|
+
focus of this element, which is a parent state reaching a child.
|
|
64
|
+
The rest is this element's own. `text-transform` and
|
|
65
|
+
`letter-spacing` are reset because one of these sits inside a
|
|
66
|
+
heading, and headings here are uppercase and tracked out. */
|
|
67
|
+
className: [
|
|
68
|
+
'explain',
|
|
69
|
+
`explain-${align}`,
|
|
70
|
+
'relative flex-none inline-flex items-center ml-2 cursor-help',
|
|
71
|
+
'normal-case tracking-normal font-normal',
|
|
72
|
+
className,
|
|
73
|
+
]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join(' '),
|
|
62
76
|
/* A role, because a name on a role-less span is prohibited -- axe's
|
|
63
77
|
`aria-prohibited-attr`, found the first time this was scanned. `img`
|
|
64
78
|
rather than `button`: the mark is a glyph that reveals help, not a
|
|
65
79
|
control that does something, and `button` would promise an action
|
|
66
80
|
and -- inside the `<label>` rows these sit in -- invite a click that
|
|
67
81
|
the label forwards to the setting behind it. */
|
|
68
|
-
role: "img", "aria-label": mark ? label : `About ${label}`, children: mark ?? (_jsx("span", { className: "explain-mark", "aria-hidden": "true", children: "?" })) }) }), _jsx(AriaTooltip, { className: "explain-tip", placement: align === 'left' ? 'bottom end' : 'bottom start', offset: 8, crossOffset: align === 'left' ? 6 : -6, children: children })] }));
|
|
82
|
+
role: "img", "aria-label": mark ? label : `About ${label}`, children: mark ?? (_jsx("span", { className: "explain-mark w-[18px] h-[18px] rounded-full border border-border text-muted text-xs leading-[16px] text-center", "aria-hidden": "true", children: "?" })) }) }), _jsx(AriaTooltip, { className: "explain-tip", placement: align === 'left' ? 'bottom end' : 'bottom start', offset: 8, crossOffset: align === 'left' ? 6 : -6, children: children })] }));
|
|
69
83
|
}
|
package/dist/components/Tour.js
CHANGED
|
@@ -111,5 +111,5 @@ export default function Tour({ stops, onDone, storageKey, }) {
|
|
|
111
111
|
height: box.height + 12,
|
|
112
112
|
} }), _jsxs("div", { ref: card, className: "tour-card", style: style,
|
|
113
113
|
// biome-ignore lint/a11y/useSemanticElements: a <dialog> brings the top layer, ::backdrop and showModal() focus semantics; this is an anchored coach-mark card that positions itself and manages its own focus, so the role is the faithful choice.
|
|
114
|
-
role: "dialog", "aria-modal": "true", "aria-labelledby": "tour-title", tabIndex: -1, children: [_jsx("strong", { id: "tour-title", className: "tour-title", children: stop.title }), _jsx("p", { className: "tour-body", children: stop.body }), _jsxs("div", { className: "tour-actions", children: [_jsxs("span", { className: "tour-count mono", children: [at + 1, " of ", live.length] }), _jsx("span", { className: "grow" }), _jsx(Button, { kind: "ghost", size: "sm", onPress: finish, children: "Skip" }), _jsx(Button, { kind: "primary", size: "sm", onPress: next, children: at + 1 < live.length ? 'Next' : 'Done' })] })] })] }), document.body);
|
|
114
|
+
role: "dialog", "aria-modal": "true", "aria-labelledby": "tour-title", tabIndex: -1, children: [_jsx("strong", { id: "tour-title", className: "tour-title", children: stop.title }), _jsx("p", { className: "tour-body", children: stop.body }), _jsxs("div", { className: "tour-actions", children: [_jsxs("span", { className: "tour-count mono", children: [at + 1, " of ", live.length] }), _jsx("span", { className: "ctl-grow" }), _jsx(Button, { kind: "ghost", size: "sm", onPress: finish, children: "Skip" }), _jsx(Button, { kind: "primary", size: "sm", onPress: next, children: at + 1 < live.length ? 'Next' : 'Done' })] })] })] }), document.body);
|
|
115
115
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `Field` tells the control inside it.
|
|
3
|
+
*
|
|
4
|
+
* Its own module for the reason `iconNames.ts` and `tourMarker.ts` are:
|
|
5
|
+
* `fastRefresh.test.ts` holds every component module to exporting its
|
|
6
|
+
* component and nothing else, because a module exporting a value beside a
|
|
7
|
+
* component loses its Fast Refresh boundary and editing it re-runs every
|
|
8
|
+
* importer instead of swapping the component in place.
|
|
9
|
+
*
|
|
10
|
+
* The context exists so `Field`'s children can be elements rather than a
|
|
11
|
+
* function. A function cannot cross the server boundary, so the render prop
|
|
12
|
+
* made every page with a form a client component whether or not it needed to
|
|
13
|
+
* be -- which is the line all three consuming apps have in their
|
|
14
|
+
* `design.ts`.
|
|
15
|
+
*/
|
|
16
|
+
export interface FieldWiring {
|
|
17
|
+
id: string;
|
|
18
|
+
'aria-describedby': string | undefined;
|
|
19
|
+
'aria-invalid': boolean | undefined;
|
|
20
|
+
/** The label element's own id, for a control a `<label for>` cannot name.
|
|
21
|
+
*
|
|
22
|
+
* `htmlFor` is enough for an `<input>`, which is what almost every caller
|
|
23
|
+
* wraps. It is not enough for `Select`, or for anything else built on a
|
|
24
|
+
* `<button>`: a button takes its accessible name from its *contents*, and
|
|
25
|
+
* a `<label for>` pointing at one is ignored by the name computation. A
|
|
26
|
+
* caller that put a `Select` in a `Field` got a label on screen and a
|
|
27
|
+
* control announcing only its current value — and the workaround was an
|
|
28
|
+
* `aria-label` repeating the label string, which is two literals and two
|
|
29
|
+
* chances to drift apart. That drift is exactly the bug `Field` exists to
|
|
30
|
+
* prevent, so: pass this as `aria-labelledby` and there is one string. */
|
|
31
|
+
labelId: string;
|
|
32
|
+
}
|
|
33
|
+
/** `null` outside a `Field`, so a control can tell "no field around me" from
|
|
34
|
+
* "a field that wired me with nothing". */
|
|
35
|
+
export declare const FieldWiringContext: import("react").Context<FieldWiring | null>;
|
|
36
|
+
/**
|
|
37
|
+
* What the `Field` above this control wired, if there is one.
|
|
38
|
+
*
|
|
39
|
+
* For the package's own controls, which apply it when the caller has not
|
|
40
|
+
* named an `id` themselves. A consumer building its own control can use it
|
|
41
|
+
* for the same reason: it is how a control gets a label without the caller
|
|
42
|
+
* threading four attributes by hand.
|
|
43
|
+
*/
|
|
44
|
+
export declare function useFieldWiring(): FieldWiring | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `Field` tells the control inside it.
|
|
3
|
+
*
|
|
4
|
+
* Its own module for the reason `iconNames.ts` and `tourMarker.ts` are:
|
|
5
|
+
* `fastRefresh.test.ts` holds every component module to exporting its
|
|
6
|
+
* component and nothing else, because a module exporting a value beside a
|
|
7
|
+
* component loses its Fast Refresh boundary and editing it re-runs every
|
|
8
|
+
* importer instead of swapping the component in place.
|
|
9
|
+
*
|
|
10
|
+
* The context exists so `Field`'s children can be elements rather than a
|
|
11
|
+
* function. A function cannot cross the server boundary, so the render prop
|
|
12
|
+
* made every page with a form a client component whether or not it needed to
|
|
13
|
+
* be -- which is the line all three consuming apps have in their
|
|
14
|
+
* `design.ts`.
|
|
15
|
+
*/
|
|
16
|
+
import { createContext, useContext } from 'react';
|
|
17
|
+
/** `null` outside a `Field`, so a control can tell "no field around me" from
|
|
18
|
+
* "a field that wired me with nothing". */
|
|
19
|
+
export const FieldWiringContext = createContext(null);
|
|
20
|
+
/**
|
|
21
|
+
* What the `Field` above this control wired, if there is one.
|
|
22
|
+
*
|
|
23
|
+
* For the package's own controls, which apply it when the caller has not
|
|
24
|
+
* named an `id` themselves. A consumer building its own control can use it
|
|
25
|
+
* for the same reason: it is how a control gets a label without the caller
|
|
26
|
+
* threading four attributes by hand.
|
|
27
|
+
*/
|
|
28
|
+
export function useFieldWiring() {
|
|
29
|
+
return useContext(FieldWiringContext);
|
|
30
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -36,7 +36,10 @@ export { DangerAction, default as DangerZone } from './components/DangerZone.js'
|
|
|
36
36
|
export { default as Dialog } from './components/Dialog.js';
|
|
37
37
|
export { default as Empty } from './components/Empty.js';
|
|
38
38
|
export { default as Field } from './components/Field.js';
|
|
39
|
-
|
|
39
|
+
/** The wiring a `Field` hands its control, and the hook that reads it —
|
|
40
|
+
* its own module, so `Field.tsx` keeps its Fast Refresh boundary. */
|
|
41
|
+
export { useFieldWiring } from './components/fieldWiring.js';
|
|
42
|
+
export type { FieldWiring } from './components/fieldWiring.js';
|
|
40
43
|
export { default as Icon } from './components/Icon.js';
|
|
41
44
|
/** A person in one line -- initials, name, address -- and the two pure
|
|
42
45
|
* functions behind the disc, exported so a caller colouring something else
|
|
@@ -72,6 +75,8 @@ export { default as ScrollArea } from './components/ScrollArea.js';
|
|
|
72
75
|
export type { Props as ScrollAreaProps } from './components/ScrollArea.js';
|
|
73
76
|
export { default as Select } from './components/Select.js';
|
|
74
77
|
export { default as SizeGrid } from './components/SizeGrid.js';
|
|
78
|
+
/** The frame a signed-in surface stands in. Gates nothing, on purpose. */
|
|
79
|
+
export { default as Shell } from './components/Shell.js';
|
|
75
80
|
export { default as Skeleton } from './components/Skeleton.js';
|
|
76
81
|
/** One figure, with what it counts over it. Three apps drew this before it
|
|
77
82
|
* was here. */
|
|
@@ -86,6 +91,10 @@ export type { Column } from './components/Table.js';
|
|
|
86
91
|
export { default as Tabs } from './components/Tabs.js';
|
|
87
92
|
export type { Tab, TabGroup } from './components/Tabs.js';
|
|
88
93
|
export { default as Textarea } from './components/Textarea.js';
|
|
94
|
+
/** The theme control, and the blocking script that beats it to the paint. */
|
|
95
|
+
export { default as ThemeSwitch } from './components/ThemeSwitch.js';
|
|
96
|
+
export { THEME_STORAGE_KEY, themeChoiceScript, themeChoices, storedTheme } from './themes/choice.js';
|
|
97
|
+
export type { ThemeChoice } from './themes/choice.js';
|
|
89
98
|
export type { Props as TextareaProps } from './components/Textarea.js';
|
|
90
99
|
/** `ToastHost` nests: one inside another renders through, so a package can
|
|
91
100
|
* wrap itself and still leave an application with a single region. */
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,9 @@ export { DangerAction, default as DangerZone } from './components/DangerZone.js'
|
|
|
33
33
|
export { default as Dialog } from './components/Dialog.js';
|
|
34
34
|
export { default as Empty } from './components/Empty.js';
|
|
35
35
|
export { default as Field } from './components/Field.js';
|
|
36
|
+
/** The wiring a `Field` hands its control, and the hook that reads it —
|
|
37
|
+
* its own module, so `Field.tsx` keeps its Fast Refresh boundary. */
|
|
38
|
+
export { useFieldWiring } from './components/fieldWiring.js';
|
|
36
39
|
export { default as Icon } from './components/Icon.js';
|
|
37
40
|
/** A person in one line -- initials, name, address -- and the two pure
|
|
38
41
|
* functions behind the disc, exported so a caller colouring something else
|
|
@@ -64,6 +67,8 @@ export { Row, Rows } from './components/Rows.js';
|
|
|
64
67
|
export { default as ScrollArea } from './components/ScrollArea.js';
|
|
65
68
|
export { default as Select } from './components/Select.js';
|
|
66
69
|
export { default as SizeGrid } from './components/SizeGrid.js';
|
|
70
|
+
/** The frame a signed-in surface stands in. Gates nothing, on purpose. */
|
|
71
|
+
export { default as Shell } from './components/Shell.js';
|
|
67
72
|
export { default as Skeleton } from './components/Skeleton.js';
|
|
68
73
|
/** One figure, with what it counts over it. Three apps drew this before it
|
|
69
74
|
* was here. */
|
|
@@ -75,6 +80,9 @@ export { default as SplitPane } from './components/SplitPane.js';
|
|
|
75
80
|
export { default as Table } from './components/Table.js';
|
|
76
81
|
export { default as Tabs } from './components/Tabs.js';
|
|
77
82
|
export { default as Textarea } from './components/Textarea.js';
|
|
83
|
+
/** The theme control, and the blocking script that beats it to the paint. */
|
|
84
|
+
export { default as ThemeSwitch } from './components/ThemeSwitch.js';
|
|
85
|
+
export { THEME_STORAGE_KEY, themeChoiceScript, themeChoices, storedTheme } from './themes/choice.js';
|
|
78
86
|
/** `ToastHost` nests: one inside another renders through, so a package can
|
|
79
87
|
* wrap itself and still leave an application with a single region. */
|
|
80
88
|
export { ToastHost, useHasToastHost, useToast } from './components/Toast.js';
|