@hackcove/ui-react 0.9.2 → 0.10.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/MIGRATING.md +21 -0
- package/README.md +24 -9
- package/dist/Avatar.d.ts +12 -5
- package/dist/Avatar.js +17 -12
- package/dist/AvatarStack.js +2 -1
- package/dist/Button.d.ts +15 -1
- package/dist/Button.js +17 -6
- package/dist/Card.d.ts +8 -1
- package/dist/Card.js +15 -6
- package/dist/CardParts.d.ts +2 -1
- package/dist/CardParts.js +6 -2
- package/dist/Checkbox.d.ts +1 -1
- package/dist/Checkbox.js +1 -13
- package/dist/ColourSchemeToggle.d.ts +4 -2
- package/dist/ColourSchemeToggle.js +23 -10
- package/dist/Dialog.d.ts +2 -1
- package/dist/Dialog.js +44 -3
- package/dist/Disclosure.d.ts +10 -1
- package/dist/Disclosure.js +13 -3
- package/dist/Field.d.ts +9 -1
- package/dist/Field.js +5 -3
- package/dist/IconButton.d.ts +2 -0
- package/dist/IconButton.js +2 -2
- package/dist/Image.d.ts +14 -4
- package/dist/Image.js +22 -6
- package/dist/InlineEdit.d.ts +22 -0
- package/dist/InlineEdit.js +60 -0
- package/dist/KeyValueList.d.ts +9 -2
- package/dist/KeyValueList.js +8 -3
- package/dist/Lightbox.d.ts +2 -0
- package/dist/Lightbox.js +2 -1
- package/dist/ListRow.d.ts +11 -0
- package/dist/ListRow.js +28 -6
- package/dist/MediaTile.d.ts +11 -3
- package/dist/MediaTile.js +10 -7
- package/dist/PageLayouts.d.ts +9 -3
- package/dist/PageLayouts.js +9 -6
- package/dist/SideColumn.d.ts +26 -0
- package/dist/SideColumn.js +22 -0
- package/dist/SidebarLayout.d.ts +5 -2
- package/dist/SidebarLayout.js +7 -3
- package/dist/Stack.d.ts +10 -2
- package/dist/Stack.js +5 -3
- package/dist/StatusPages.d.ts +13 -3
- package/dist/StatusPages.js +8 -5
- package/dist/Switch.d.ts +42 -0
- package/dist/Switch.js +56 -0
- package/dist/TableEditCell.d.ts +9 -7
- package/dist/TableEditCell.js +12 -53
- package/dist/foldedColumn.d.ts +18 -0
- package/dist/foldedColumn.js +45 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/labelWords.d.ts +12 -0
- package/dist/labelWords.js +14 -0
- package/dist/scrollStop.d.ts +27 -0
- package/dist/scrollStop.js +68 -0
- package/package.json +1 -1
package/dist/ListRow.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useId, } from "react";
|
|
3
3
|
import { Chevron } from "./Chevron.js";
|
|
4
4
|
import { cx, focusRing, liftInteractive } from "./cx.js";
|
|
@@ -10,6 +10,15 @@ const ROW = `${ROW_LOOK} ${INSET_RING}`;
|
|
|
10
10
|
const LINK_ROW = `relative ${ROW_LOOK} ${liftInteractive}`;
|
|
11
11
|
const OVERLAY = `link-stretch absolute inset-0 ${INSET_RING}`;
|
|
12
12
|
const STATIC = "group/row flex w-full items-center gap-3 text-sm text-slate-900";
|
|
13
|
+
/**
|
|
14
|
+
* A static row with `detail`: the row's own line (the label, `trailing`, `actions`) over the detail,
|
|
15
|
+
* `leading` in a column of its own beside both, so the detail starts where the label does.
|
|
16
|
+
*/
|
|
17
|
+
const STATIC_DETAIL = "group/row grid w-full items-center gap-3 text-sm text-slate-900";
|
|
18
|
+
const DETAIL_COLUMNS = {
|
|
19
|
+
withLeading: "grid-cols-[auto_minmax(0,1fr)]",
|
|
20
|
+
alone: "grid-cols-[minmax(0,1fr)]",
|
|
21
|
+
};
|
|
13
22
|
const FILL = "hover:bg-slate-50";
|
|
14
23
|
const SELECTED = "bg-violet-50 hover:bg-violet-50";
|
|
15
24
|
/**
|
|
@@ -32,9 +41,13 @@ function fillOf(props) {
|
|
|
32
41
|
return ACTIVE;
|
|
33
42
|
return props.static && !props.revealOnHover ? undefined : FILL;
|
|
34
43
|
}
|
|
44
|
+
/** `detail={open && <Panel />}` is a common way to write it, so `false` is as empty as `null`. */
|
|
45
|
+
function hasDetail(detail) {
|
|
46
|
+
return detail != null && detail !== false;
|
|
47
|
+
}
|
|
35
48
|
function rowBase(props) {
|
|
36
49
|
if (props.static)
|
|
37
|
-
return STATIC;
|
|
50
|
+
return hasDetail(props.detail) ? STATIC_DETAIL : STATIC;
|
|
38
51
|
return props.href !== undefined || props.render !== undefined ? LINK_ROW : ROW;
|
|
39
52
|
}
|
|
40
53
|
function look(props) {
|
|
@@ -42,11 +55,17 @@ function look(props) {
|
|
|
42
55
|
return cx(rowBase(props), PAD[size], divided && "border-t border-slate-200/70", fillOf(props), focused && FOCUSED);
|
|
43
56
|
}
|
|
44
57
|
function StaticRow(props) {
|
|
45
|
-
const { leading: _l, secondary: _sec, leadingWidth: _lw, trailing: _t, focused: _f, active = false, selected, size: _s, divided: _d, static: _st, actions, revealOnHover = false, expanded: _e, href: _h, render: _r, start, label, end, ...rest } = props;
|
|
46
|
-
|
|
58
|
+
const { leading: _l, secondary: _sec, leadingWidth: _lw, trailing: _t, focused: _f, active = false, selected, size: _s, divided: _d, static: _st, actions, revealOnHover = false, detail, expanded: _e, href: _h, render: _r, start, label, end, ...rest } = props;
|
|
59
|
+
const buttons = actions != null && (_jsx("span", { className: cx("flex shrink-0 items-center gap-2", revealOnHover && !active && REVEAL), children: actions }));
|
|
60
|
+
const withDetail = hasDetail(detail);
|
|
61
|
+
return (_jsx("div", { ...rest, ...(selected ? { "aria-current": "true" } : {}), className: cx(look(props), withDetail && DETAIL_COLUMNS[start ? "withLeading" : "alone"]), children: withDetail ? (_jsxs(DetailLines, { start: start, detail: detail, children: [label, end, buttons] })) : (_jsxs(_Fragment, { children: [start, label, end, buttons] })) }));
|
|
62
|
+
}
|
|
63
|
+
/** The cells of a static row with `detail`: `start` beside the row's line (its children), over the detail in the label's column. */
|
|
64
|
+
function DetailLines({ start, detail, children, }) {
|
|
65
|
+
return (_jsxs(_Fragment, { children: [start, _jsx("div", { className: "flex min-w-0 items-center gap-3", children: children }), _jsx("div", { className: cx("min-w-0", Boolean(start) && "col-start-2"), children: detail })] }));
|
|
47
66
|
}
|
|
48
67
|
function LinkRow(props) {
|
|
49
|
-
const { leading: _l, secondary: _sec, leadingWidth: _lw, trailing: _t, focused: _f, active: _ac, selected, expanded: _e, size: _s, divided: _d, static: _st, actions: _a, revealOnHover: _rh, start, label, end, onPointerEnter, onPointerLeave, ...rest } = props;
|
|
68
|
+
const { leading: _l, secondary: _sec, leadingWidth: _lw, trailing: _t, focused: _f, active: _ac, selected, expanded: _e, size: _s, divided: _d, static: _st, actions: _a, revealOnHover: _rh, detail: _dt, start, label, end, onPointerEnter, onPointerLeave, ...rest } = props;
|
|
50
69
|
const rowId = useId();
|
|
51
70
|
return (_jsxs("div", { id: rowId, onPointerEnter: onPointerEnter, onPointerLeave: onPointerLeave, className: look(props), children: [renderLink({
|
|
52
71
|
...(rest["aria-label"] === undefined ? { "aria-labelledby": rowId } : {}),
|
|
@@ -57,7 +76,7 @@ function LinkRow(props) {
|
|
|
57
76
|
}), start, label, end] }));
|
|
58
77
|
}
|
|
59
78
|
function ButtonRow(props) {
|
|
60
|
-
const { leading: _l, secondary: _sec, leadingWidth: _lw, trailing: _t, focused: _f, active: _ac, selected, expanded, size: _s, divided: _d, static: _st, actions: _a, revealOnHover: _rh, href: _h, render: _r, start, label, end, ...rest } = props;
|
|
79
|
+
const { leading: _l, secondary: _sec, leadingWidth: _lw, trailing: _t, focused: _f, active: _ac, selected, expanded, size: _s, divided: _d, static: _st, actions: _a, revealOnHover: _rh, detail: _dt, href: _h, render: _r, start, label, end, ...rest } = props;
|
|
61
80
|
return (_jsxs("button", { type: "button", ...rest, ...(selected ? { "aria-current": "true" } : {}), ...(expanded === undefined ? {} : { "aria-expanded": expanded }), className: look(props), children: [expanded !== undefined && _jsx(Chevron, { open: expanded }), start, label, end] }));
|
|
62
81
|
}
|
|
63
82
|
/**
|
|
@@ -75,6 +94,9 @@ function ButtonRow(props) {
|
|
|
75
94
|
* `<div>`, so it can hold block content (a name over a path). `static` makes the
|
|
76
95
|
* row a plain `<div>` for one that holds its own buttons (a button cannot sit inside a button or a
|
|
77
96
|
* link): `actions` follow `trailing`, and `revealOnHover` shows them only on hover or focus.
|
|
97
|
+
* A `static` row also takes `detail`, content under the row that starts where the label does (a
|
|
98
|
+
* conversation that opens under its chat); `divided` draws the line above the whole row, detail
|
|
99
|
+
* included, so a list of them is `divided={index > 0}` on each.
|
|
78
100
|
*/
|
|
79
101
|
export function ListRow(props) {
|
|
80
102
|
const { leading, leadingWidth, trailing, secondary, children } = props;
|
package/dist/MediaTile.d.ts
CHANGED
|
@@ -11,7 +11,8 @@ export type MediaTileProps = {
|
|
|
11
11
|
/** Muted text under the picture, e.g. when. */
|
|
12
12
|
caption?: ReactNode;
|
|
13
13
|
/**
|
|
14
|
-
* The shape of the thumbnail, as `Image`'s `aspect
|
|
14
|
+
* The shape of the thumbnail, as `Image`'s `aspect`; `natural` keeps the picture's own
|
|
15
|
+
* proportions (a chat image) and crops nothing.
|
|
15
16
|
* @default 3:4
|
|
16
17
|
*/
|
|
17
18
|
aspect?: ImageAspect | undefined;
|
|
@@ -27,11 +28,18 @@ export type MediaTileProps = {
|
|
|
27
28
|
* non-interactive block.
|
|
28
29
|
*/
|
|
29
30
|
onClick?: (() => void) | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Adds a round remove button on the picture's top-right corner, named "Remove" and `alt`: a
|
|
33
|
+
* photo attached to a form, until it is sent. It is its own button, beside the tile's, so a tile
|
|
34
|
+
* with `onClick` still opens its `Lightbox`.
|
|
35
|
+
*/
|
|
36
|
+
onRemove?: (() => void) | undefined;
|
|
30
37
|
} & Passthrough;
|
|
31
38
|
/**
|
|
32
39
|
* A thumbnail tile: a picture in a fixed shape with an optional label on its bottom edge and a
|
|
33
40
|
* caption under it. With `onClick` the whole tile is one button, which is how a grid or a
|
|
34
41
|
* `ScrollStrip` of media opens each item in a `Lightbox`. The label sits on the same near-black
|
|
35
|
-
* media scrim as the Lightbox, in every theme. The thumbnail is cropped to fill its shape
|
|
42
|
+
* media scrim as the Lightbox, in every theme. The thumbnail is cropped to fill its shape, or with
|
|
43
|
+
* `aspect="natural"` shown whole. `onRemove` adds a remove button for a tile that can be taken back out.
|
|
36
44
|
*/
|
|
37
|
-
export declare function MediaTile({ src, alt, label, caption, aspect, width, video, onClick, ...rest }: MediaTileProps): ReactElement;
|
|
45
|
+
export declare function MediaTile({ src, alt, label, caption, aspect, width, video, onClick, onRemove, ...rest }: MediaTileProps): ReactElement;
|
package/dist/MediaTile.js
CHANGED
|
@@ -2,18 +2,21 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { cx, focusRing } from "./cx.js";
|
|
3
3
|
import { Icon } from "./Icon.js";
|
|
4
4
|
import { IMAGE_WIDTH, Image } from "./Image.js";
|
|
5
|
+
import { CLOSE } from "./Lightbox.js";
|
|
5
6
|
const PLAY = "M6.3 2.84A1.5 1.5 0 0 0 4 4.11v11.78a1.5 1.5 0 0 0 2.3 1.27l9.34-5.89a1.5 1.5 0 0 0 0-2.54L6.3 2.84Z";
|
|
6
7
|
/**
|
|
7
8
|
* A thumbnail tile: a picture in a fixed shape with an optional label on its bottom edge and a
|
|
8
9
|
* caption under it. With `onClick` the whole tile is one button, which is how a grid or a
|
|
9
10
|
* `ScrollStrip` of media opens each item in a `Lightbox`. The label sits on the same near-black
|
|
10
|
-
* media scrim as the Lightbox, in every theme. The thumbnail is cropped to fill its shape
|
|
11
|
+
* media scrim as the Lightbox, in every theme. The thumbnail is cropped to fill its shape, or with
|
|
12
|
+
* `aspect="natural"` shown whole. `onRemove` adds a remove button for a tile that can be taken back out.
|
|
11
13
|
*/
|
|
12
|
-
export function MediaTile({ src, alt, label, caption, aspect = "3:4", width = "md", video = false, onClick, ...rest }) {
|
|
14
|
+
export function MediaTile({ src, alt, label, caption, aspect = "3:4", width = "md", video = false, onClick, onRemove, ...rest }) {
|
|
13
15
|
const body = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "relative block overflow-hidden rounded-lg", children: [_jsx(Image, { src: src, alt: alt, aspect: aspect }), video && (_jsx("span", { "aria-hidden": "true", className: "absolute inset-0 grid place-items-center", children: _jsx("span", { className: "grid size-10 place-items-center rounded-full bg-black/90 text-white", children: _jsx(Icon, { size: "lg", children: _jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { d: PLAY }) }) }) }) })), label && (_jsx("span", { className: "absolute inset-x-0 bottom-0 truncate bg-black/90 px-2 py-1 text-xs font-medium text-white", children: label }))] }), caption && _jsx("span", { className: "mt-1 block truncate text-xs text-slate-600", children: caption })] }));
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// With a remove button the tile fills a wrapper that carries the width and the button's anchor.
|
|
17
|
+
const box = cx("block min-w-0 text-left", onRemove ? "w-full" : IMAGE_WIDTH[width]);
|
|
18
|
+
const tile = onClick ? (_jsx("button", { ...rest, type: "button", onClick: onClick, className: cx(box, "group rounded-lg", focusRing), children: body })) : (_jsx("div", { ...rest, className: box, children: body }));
|
|
19
|
+
if (!onRemove)
|
|
20
|
+
return tile;
|
|
21
|
+
return (_jsxs("div", { className: cx("relative min-w-0", IMAGE_WIDTH[width]), children: [tile, _jsx("button", { type: "button", "aria-label": `Remove ${alt}`, title: "Remove", onClick: onRemove, className: "absolute top-1 right-1 inline-flex size-8 cursor-pointer items-center justify-center rounded-full bg-black/90 text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white", children: _jsx(Icon, { children: _jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { d: CLOSE }) }) }) })] }));
|
|
19
22
|
}
|
package/dist/PageLayouts.d.ts
CHANGED
|
@@ -18,9 +18,11 @@ export declare function ListLayout({ filters, empty, isEmpty, children, breadcru
|
|
|
18
18
|
* scrolls: it sticks under AppShell's header and, if it is taller than the room left, scrolls
|
|
19
19
|
* itself so its end stays reachable.
|
|
20
20
|
*/
|
|
21
|
-
export declare function DetailLayout({ aside, stickyAside, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }: PageHeaderProps & {
|
|
21
|
+
export declare function DetailLayout({ aside, asideLabel, stickyAside, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }: PageHeaderProps & {
|
|
22
22
|
/** The side column: one third from `lg`, below `children` on a smaller screen. */
|
|
23
23
|
aside: ReactNode;
|
|
24
|
+
/** Names the aside's landmark (`aria-label`), so a page with another complementary landmark can tell them apart. It does not fold the aside. Left out, the aside is unnamed. */
|
|
25
|
+
asideLabel?: string | undefined;
|
|
24
26
|
/**
|
|
25
27
|
* From `lg`, keeps the aside in view under the header as the page scrolls; an aside taller than
|
|
26
28
|
* the screen scrolls inside itself. Off by default, since an aside of ordinary cards may be
|
|
@@ -117,11 +119,13 @@ export declare function CardGrid({ columns, minWidth, align, as: Tag, children,
|
|
|
117
119
|
* a page of ordinary content beside a full-height `panel`. A `bar` is a `StickyBar` above both: the
|
|
118
120
|
* full width, on the surface with a bottom line, sticking under AppShell's header as the page scrolls.
|
|
119
121
|
*/
|
|
120
|
-
export declare function FullBleedLayout({ bar, panel, padded, children, ...rest }: {
|
|
122
|
+
export declare function FullBleedLayout({ bar, panel, panelLabel, padded, children, ...rest }: {
|
|
121
123
|
/** A full-width row above `children` and `panel` that sticks under the header, e.g. a viewer's title and actions. `padded` does not pad it. */
|
|
122
124
|
bar?: ReactNode;
|
|
123
125
|
/** A side column on the right (`lg:w-72`) on a tinted surface; below `lg` it stacks under `children`. */
|
|
124
126
|
panel?: ReactNode;
|
|
127
|
+
/** Names the panel's `<aside>` landmark (`aria-label`). Left out, the panel is unnamed. */
|
|
128
|
+
panelLabel?: string | undefined;
|
|
125
129
|
/**
|
|
126
130
|
* Pads `children` as the Container pads every other page. Off, they run to the viewport's edges.
|
|
127
131
|
* A notice in the column is a plain `Callout` in a `Stack`, never a `Callout banner`.
|
|
@@ -130,7 +134,9 @@ export declare function FullBleedLayout({ bar, panel, padded, children, ...rest
|
|
|
130
134
|
children: ReactNode;
|
|
131
135
|
} & Passthrough): ReactElement;
|
|
132
136
|
/** A narrow card (`max-w-md`) centred in the Container at a fixed offset from the top, for sign-in, landing, not-found and error pages. The one layout that does not share the left edge. */
|
|
133
|
-
export declare function CenteredLayout({ title, subtitle, children, ...rest }: {
|
|
137
|
+
export declare function CenteredLayout({ mark, title, subtitle, children, ...rest }: {
|
|
138
|
+
/** Drawn first in the card, above the title, e.g. a large status numeral. Not a heading. */
|
|
139
|
+
mark?: ReactNode;
|
|
134
140
|
/** The card's `h1`. */
|
|
135
141
|
title: ReactNode;
|
|
136
142
|
/** A muted line under the title. */
|
package/dist/PageLayouts.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Card } from "./Card.js";
|
|
|
4
4
|
import { cx } from "./cx.js";
|
|
5
5
|
import { PageHeader, PageHeaderContent } from "./PageHeader.js";
|
|
6
6
|
import { STACK_WIDTH, Stack } from "./Stack.js";
|
|
7
|
+
import { scrollStopProps, useScrollStop } from "./scrollStop.js";
|
|
7
8
|
import { STICKY_ASIDE_LG, STICKY_SCROLL_LG } from "./stickyColumn.js";
|
|
8
9
|
import { StickyBar } from "./StickyBar.js";
|
|
9
10
|
// Compositions of the layout foundation. None sets a page width or outer padding of its own; the
|
|
@@ -18,8 +19,10 @@ export function ListLayout({ filters, empty, isEmpty = false, children, breadcru
|
|
|
18
19
|
* scrolls: it sticks under AppShell's header and, if it is taller than the room left, scrolls
|
|
19
20
|
* itself so its end stays reachable.
|
|
20
21
|
*/
|
|
21
|
-
export function DetailLayout({ aside, stickyAside = false, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
|
|
22
|
-
|
|
22
|
+
export function DetailLayout({ aside, asideLabel, stickyAside = false, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
|
|
23
|
+
const { attach, stop } = useScrollStop();
|
|
24
|
+
const { props, ring } = scrollStopProps(stickyAside && stop, { label: asideLabel });
|
|
25
|
+
return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, actions: actions, tabs: tabs }), _jsxs("div", { ...rest, className: "grid gap-6 lg:grid-cols-3", children: [_jsx("div", { className: "min-w-0 lg:col-span-2", children: _jsx(Stack, { children: children }) }), _jsx("aside", { className: cx("min-w-0", stickyAside && STICKY_ASIDE_LG), "aria-label": asideLabel, children: _jsx("div", { ref: stickyAside ? attach : undefined, ...props, className: stickyAside ? cx(STICKY_SCROLL_LG, ring) : undefined, children: _jsx(Stack, { children: aside }) }) })] })] }));
|
|
23
26
|
}
|
|
24
27
|
/** A form page: header (no header `actions`), then a `max-w-3xl` column that starts at the Container's left edge (it is not re-centred), with an `actions` row at the end. Other props go on the `<form>`. */
|
|
25
28
|
export function FormLayout({ actions, children, breadcrumbs, title, subtitle, tabs, ...form }) {
|
|
@@ -80,14 +83,14 @@ export function CardGrid({ columns = 3, minWidth = "md", align = "stretch", as:
|
|
|
80
83
|
* a page of ordinary content beside a full-height `panel`. A `bar` is a `StickyBar` above both: the
|
|
81
84
|
* full width, on the surface with a bottom line, sticking under AppShell's header as the page scrolls.
|
|
82
85
|
*/
|
|
83
|
-
export function FullBleedLayout({ bar, panel, padded = false, children, ...rest }) {
|
|
84
|
-
const row = (_jsxs(_Fragment, { children: [_jsx("div", { className: cx("min-w-0 flex-1", padded && "px-4 py-8 sm:px-6"), children: children }), panel && (_jsx("aside", { className: "w-full shrink-0 border-t border-slate-200 bg-slate-50 p-4 lg:w-72 lg:border-t-0 lg:border-l", children: panel }))] }));
|
|
86
|
+
export function FullBleedLayout({ bar, panel, panelLabel, padded = false, children, ...rest }) {
|
|
87
|
+
const row = (_jsxs(_Fragment, { children: [_jsx("div", { className: cx("min-w-0 flex-1", padded && "px-4 py-8 sm:px-6"), children: children }), panel && (_jsx("aside", { className: "w-full shrink-0 border-t border-slate-200 bg-slate-50 p-4 lg:w-72 lg:border-t-0 lg:border-l", "aria-label": panelLabel, children: panel }))] }));
|
|
85
88
|
if (!bar) {
|
|
86
89
|
return (_jsx("div", { ...rest, className: "flex min-h-0 flex-1 flex-col lg:flex-row", children: row }));
|
|
87
90
|
}
|
|
88
91
|
return (_jsxs("div", { ...rest, className: "flex min-h-0 flex-1 flex-col", children: [_jsx(StickyBar, { children: bar }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col lg:flex-row", children: row })] }));
|
|
89
92
|
}
|
|
90
93
|
/** A narrow card (`max-w-md`) centred in the Container at a fixed offset from the top, for sign-in, landing, not-found and error pages. The one layout that does not share the left edge. */
|
|
91
|
-
export function CenteredLayout({ title, subtitle, children, ...rest }) {
|
|
92
|
-
return (_jsx("div", { ...rest, className: "mx-auto w-full max-w-md pt-16", children: _jsx(Card, { as: "div", children: _jsxs(Stack, { children: [_jsx(PageHeaderContent, { title: title, subtitle: subtitle }), children] }) }) }));
|
|
94
|
+
export function CenteredLayout({ mark, title, subtitle, children, ...rest }) {
|
|
95
|
+
return (_jsx("div", { ...rest, className: "mx-auto w-full max-w-md pt-16", children: _jsx(Card, { as: "div", children: _jsxs(Stack, { children: [mark, _jsx(PageHeaderContent, { title: title, subtitle: subtitle }), children] }) }) }));
|
|
93
96
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { Passthrough } from "./props.js";
|
|
3
|
+
export type SideColumnWidth = "sm" | "md";
|
|
4
|
+
/**
|
|
5
|
+
* A main area beside a side column of a fixed width, in the Container the page already has: from
|
|
6
|
+
* `lg` the `aside` is `asideWidth` wide on the right and `children` take the room left; below `lg`
|
|
7
|
+
* the aside stacks under `children`. It sets no page width, padding or header of its own, so it
|
|
8
|
+
* sits inside a layout, a `Card` or a panel. A side column that is a share of the page and sits
|
|
9
|
+
* under a page header is `DetailLayout`'s aside; a rail of links on the left is `SidebarLayout`.
|
|
10
|
+
*
|
|
11
|
+
* `asideLabel` is for an aside that should not take the phone's screen: below `lg` it folds,
|
|
12
|
+
* closed, behind a native disclosure of that name, and from `lg` it is the same column, always
|
|
13
|
+
* shown. The aside stays mounted while folded, so a conversation or a draft inside it is kept.
|
|
14
|
+
*/
|
|
15
|
+
export declare function SideColumn({ aside, asideWidth, asideLabel, children, ...rest }: {
|
|
16
|
+
/** The side column: `asideWidth` wide on the right from `lg`, under `children` below it. */
|
|
17
|
+
aside: ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* The aside's width from `lg`: `sm` 18rem or `md` 24rem.
|
|
20
|
+
* @default md
|
|
21
|
+
*/
|
|
22
|
+
asideWidth?: SideColumnWidth | undefined;
|
|
23
|
+
/** Folds the aside behind a disclosure with this name below `lg`, and names the `<aside>` landmark. Left out, the aside is always shown and unnamed. */
|
|
24
|
+
asideLabel?: string | undefined;
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
} & Passthrough): ReactElement;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { FoldedColumn } from "./foldedColumn.js";
|
|
4
|
+
/** The grid from `lg`: the main area takes the room left, the aside is exactly this wide. */
|
|
5
|
+
const COLUMNS = {
|
|
6
|
+
sm: "lg:grid-cols-[minmax(0,1fr)_18rem]",
|
|
7
|
+
md: "lg:grid-cols-[minmax(0,1fr)_24rem]",
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A main area beside a side column of a fixed width, in the Container the page already has: from
|
|
11
|
+
* `lg` the `aside` is `asideWidth` wide on the right and `children` take the room left; below `lg`
|
|
12
|
+
* the aside stacks under `children`. It sets no page width, padding or header of its own, so it
|
|
13
|
+
* sits inside a layout, a `Card` or a panel. A side column that is a share of the page and sits
|
|
14
|
+
* under a page header is `DetailLayout`'s aside; a rail of links on the left is `SidebarLayout`.
|
|
15
|
+
*
|
|
16
|
+
* `asideLabel` is for an aside that should not take the phone's screen: below `lg` it folds,
|
|
17
|
+
* closed, behind a native disclosure of that name, and from `lg` it is the same column, always
|
|
18
|
+
* shown. The aside stays mounted while folded, so a conversation or a draft inside it is kept.
|
|
19
|
+
*/
|
|
20
|
+
export function SideColumn({ aside, asideWidth = "md", asideLabel, children, ...rest }) {
|
|
21
|
+
return (_jsxs("div", { ...rest, className: cx("grid grid-cols-1 gap-6", COLUMNS[asideWidth]), children: [_jsx("div", { className: "min-w-0", children: children }), _jsx("aside", { className: "min-w-0", "aria-label": asideLabel, children: asideLabel === undefined ? (aside) : (_jsx(FoldedColumn, { from: "lg", label: asideLabel, children: aside })) })] }));
|
|
22
|
+
}
|
package/dist/SidebarLayout.d.ts
CHANGED
|
@@ -9,13 +9,16 @@ import type { Passthrough } from "./props.js";
|
|
|
9
9
|
* a rail taller than the screen scrolls itself, so its last link is reachable. Below `md` it stacks
|
|
10
10
|
* above the canvas and nothing sticks. A long rail sets `railLabel`:
|
|
11
11
|
* below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
|
|
12
|
-
* phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
|
|
12
|
+
* phone, and from `md` it is the same rail; `railLabel` also names the rail's `<aside>` landmark. Upgrading from 0.2: the rail no longer has a fill, a
|
|
13
13
|
* border or a fixed height, and it no longer sits beside the canvas on a phone.
|
|
14
14
|
*/
|
|
15
15
|
export declare function SidebarLayout({ rail, railLabel, children, ...rest }: {
|
|
16
16
|
/** The links beside the canvas, usually a `Tabs variant="pill" orientation="vertical"`. */
|
|
17
17
|
rail: ReactNode;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Folds the rail behind a disclosure with this name below `md` (a long rail), and names the rail's
|
|
20
|
+
* `<aside>` landmark. Left out, the rail is always open and the aside is unnamed.
|
|
21
|
+
*/
|
|
19
22
|
railLabel?: string | undefined;
|
|
20
23
|
children: ReactNode;
|
|
21
24
|
} & Passthrough): ReactElement;
|
package/dist/SidebarLayout.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { cx
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { FoldedColumn } from "./foldedColumn.js";
|
|
4
|
+
import { scrollStopProps, useScrollStop } from "./scrollStop.js";
|
|
3
5
|
import { STICKY_ASIDE_MD, STICKY_SCROLL_MD } from "./stickyColumn.js";
|
|
4
6
|
/**
|
|
5
7
|
* A `rail` of links beside the `children` canvas, in the Container the shell already gives every
|
|
@@ -10,9 +12,11 @@ import { STICKY_ASIDE_MD, STICKY_SCROLL_MD } from "./stickyColumn.js";
|
|
|
10
12
|
* a rail taller than the screen scrolls itself, so its last link is reachable. Below `md` it stacks
|
|
11
13
|
* above the canvas and nothing sticks. A long rail sets `railLabel`:
|
|
12
14
|
* below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
|
|
13
|
-
* phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
|
|
15
|
+
* phone, and from `md` it is the same rail; `railLabel` also names the rail's `<aside>` landmark. Upgrading from 0.2: the rail no longer has a fill, a
|
|
14
16
|
* border or a fixed height, and it no longer sits beside the canvas on a phone.
|
|
15
17
|
*/
|
|
16
18
|
export function SidebarLayout({ rail, railLabel, children, ...rest }) {
|
|
17
|
-
|
|
19
|
+
const { attach, stop } = useScrollStop();
|
|
20
|
+
const { props, ring } = scrollStopProps(stop, { label: railLabel });
|
|
21
|
+
return (_jsxs("div", { ...rest, className: "flex flex-col gap-8 md:flex-row md:gap-10", children: [_jsx("aside", { className: cx("w-full shrink-0 md:w-56", STICKY_ASIDE_MD), "aria-label": railLabel, children: _jsx("div", { ref: attach, ...props, className: cx(STICKY_SCROLL_MD, ring), children: railLabel === undefined ? (rail) : (_jsx(FoldedColumn, { from: "md", label: railLabel, children: rail })) }) }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
|
|
18
22
|
}
|
package/dist/Stack.d.ts
CHANGED
|
@@ -62,13 +62,21 @@ type StackProps = StackBase & (({
|
|
|
62
62
|
} & Unstyled<Omit<ComponentPropsWithoutRef<"ol">, "children">>));
|
|
63
63
|
/**
|
|
64
64
|
* A `<section>` Stack with an optional `SectionHeading` title (which names it) and `actions` beside
|
|
65
|
-
* it. Its blocks are `gap` apart, `md` by default as in a `Stack`.
|
|
65
|
+
* it. Its blocks are `gap` apart, `md` by default as in a `Stack`. `rule` draws a hairline under the
|
|
66
|
+
* title row, `actions` included, as `SectionHeading`'s `rule` does for a bare heading. A line
|
|
67
|
+
* between two sections is a `Divider` in the `Stack` that holds them.
|
|
66
68
|
*/
|
|
67
|
-
export declare function Section({ title, actions, gap, width, children, ...rest }: {
|
|
69
|
+
export declare function Section({ title, actions, rule, gap, width, children, ...rest }: {
|
|
68
70
|
/** A `SectionHeading` at the top, which names the section. */
|
|
69
71
|
title?: ReactNode;
|
|
70
72
|
/** Buttons or links at the end of the title row. */
|
|
71
73
|
actions?: ReactNode;
|
|
74
|
+
/**
|
|
75
|
+
* A hairline under the title row across the section's width, `actions` included. Needs a `title`
|
|
76
|
+
* or `actions`.
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
79
|
+
rule?: boolean | undefined;
|
|
72
80
|
/**
|
|
73
81
|
* The space between blocks: `xs` gap-2, `sm` gap-4, `md` gap-6, `lg` gap-8, `xl` gap-12 (gap-16 from `sm`).
|
|
74
82
|
* @default md
|
package/dist/Stack.js
CHANGED
|
@@ -49,9 +49,11 @@ function Box({ tag: Tag, ...props }) {
|
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* A `<section>` Stack with an optional `SectionHeading` title (which names it) and `actions` beside
|
|
52
|
-
* it. Its blocks are `gap` apart, `md` by default as in a `Stack`.
|
|
52
|
+
* it. Its blocks are `gap` apart, `md` by default as in a `Stack`. `rule` draws a hairline under the
|
|
53
|
+
* title row, `actions` included, as `SectionHeading`'s `rule` does for a bare heading. A line
|
|
54
|
+
* between two sections is a `Divider` in the `Stack` that holds them.
|
|
53
55
|
*/
|
|
54
|
-
export function Section({ title, actions, gap = "md", width = "default", children, ...rest }) {
|
|
56
|
+
export function Section({ title, actions, rule = false, gap = "md", width = "default", children, ...rest }) {
|
|
55
57
|
const titleId = useId();
|
|
56
|
-
return (_jsxs(Stack, { as: "section", ...rest, gap: gap, width: width, "aria-labelledby": title ? titleId : rest["aria-labelledby"], children: [(title || actions) && (_jsxs("div", { className: "flex items-center justify-between gap-4", children: [title && _jsx(SectionHeading, { id: titleId, children: title }), actions && _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] })), children] }));
|
|
58
|
+
return (_jsxs(Stack, { as: "section", ...rest, gap: gap, width: width, "aria-labelledby": title ? titleId : rest["aria-labelledby"], children: [(title || actions) && (_jsxs("div", { className: cx("flex items-center justify-between gap-4", rule && "border-b border-slate-200/70 pb-2"), children: [title && _jsx(SectionHeading, { id: titleId, children: title }), actions && _jsx("div", { className: "ml-auto flex items-center gap-2", children: actions })] })), children] }));
|
|
57
59
|
}
|
package/dist/StatusPages.d.ts
CHANGED
|
@@ -6,15 +6,25 @@ type StatusPageProps = {
|
|
|
6
6
|
title?: ReactNode;
|
|
7
7
|
/** Replaces the default body copy. */
|
|
8
8
|
children?: ReactNode;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* The primary action; the home link turns outline beside it. This is the one way to a button
|
|
11
|
+
* worded differently: pass `action` and leave out `homeHref`, so there is no "Back to home" link.
|
|
12
|
+
* There is no label prop.
|
|
13
|
+
*/
|
|
10
14
|
action?: ReactNode;
|
|
11
15
|
/** Adds a "Back to home" button linking here. With neither this nor `renderHome` there is no home link. */
|
|
12
16
|
homeHref?: string;
|
|
13
17
|
/** Draws the home link with the app's own router `<Link>`, as on `Button`. */
|
|
14
18
|
renderHome?: LinkRender;
|
|
15
19
|
} & Passthrough;
|
|
16
|
-
/**
|
|
17
|
-
|
|
20
|
+
/**
|
|
21
|
+
* 404: the page is not there. `numeral` draws a large "404" above the title, for a site whose 404
|
|
22
|
+
* is a designed page; the digits are fixed. Off, the page is the title and copy alone.
|
|
23
|
+
*/
|
|
24
|
+
export declare function NotFound({ numeral, ...props }: StatusPageProps & {
|
|
25
|
+
/** Draws a large "404" above the title. Off by default. The digits are fixed: there is no text prop. */
|
|
26
|
+
numeral?: boolean | undefined;
|
|
27
|
+
}): ReactElement;
|
|
18
28
|
/**
|
|
19
29
|
* 500 or any failure. `detail` is the error text, shown in a `CodeBlock` under the copy; pass it
|
|
20
30
|
* only where a reader may see it (a request id, a message), never a stack trace. `onRetry` adds a
|
package/dist/StatusPages.js
CHANGED
|
@@ -11,17 +11,20 @@ function homeLink(variant, href, render) {
|
|
|
11
11
|
return null;
|
|
12
12
|
return (_jsx(Button, { href: href, variant: variant, children: "Back to home" }));
|
|
13
13
|
}
|
|
14
|
-
function StatusPage({ copy, title, children, action, homeHref, renderHome, extra, ...rest }) {
|
|
14
|
+
function StatusPage({ copy, title, children, action, homeHref, renderHome, extra, mark, ...rest }) {
|
|
15
15
|
const home = homeLink(action === undefined ? "primary" : "outline", homeHref, renderHome);
|
|
16
|
-
return (_jsxs(CenteredLayout, { ...rest, title: title ?? copy.title, subtitle: children ?? copy.body, children: [extra, (action || home) && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [action, home] }))] }));
|
|
16
|
+
return (_jsxs(CenteredLayout, { ...rest, mark: mark, title: title ?? copy.title, subtitle: children ?? copy.body, children: [extra, (action || home) && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [action, home] }))] }));
|
|
17
17
|
}
|
|
18
18
|
const retry = (onRetry) => onRetry && _jsx(Button, { onClick: onRetry, children: "Try again" });
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* 404: the page is not there. `numeral` draws a large "404" above the title, for a site whose 404
|
|
21
|
+
* is a designed page; the digits are fixed. Off, the page is the title and copy alone.
|
|
22
|
+
*/
|
|
23
|
+
export function NotFound({ numeral = false, ...props }) {
|
|
21
24
|
return (_jsx(StatusPage, { copy: {
|
|
22
25
|
title: "Page not found",
|
|
23
26
|
body: "The page you are looking for does not exist or has moved.",
|
|
24
|
-
}, ...props }));
|
|
27
|
+
}, mark: numeral ? (_jsx("p", { className: "text-6xl font-semibold tracking-tight text-violet-600", children: "404" })) : undefined, ...props }));
|
|
25
28
|
}
|
|
26
29
|
/**
|
|
27
30
|
* 500 or any failure. `detail` is the error text, shown in a `CodeBlock` under the copy; pass it
|
package/dist/Switch.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { Unstyled } from "./props.js";
|
|
3
|
+
import type { Size } from "./size.js";
|
|
4
|
+
type Naming = {
|
|
5
|
+
/** The text beside the switch; the whole label is a click target. */
|
|
6
|
+
label: NonNullable<ReactNode>;
|
|
7
|
+
"aria-label"?: string | undefined;
|
|
8
|
+
} | {
|
|
9
|
+
label?: undefined;
|
|
10
|
+
/** A bare switch (a table cell) has no words to name it: this is its name. */
|
|
11
|
+
"aria-label": string;
|
|
12
|
+
};
|
|
13
|
+
type SwitchProps = Naming & {
|
|
14
|
+
/** Whether it is on. The caller owns it: there is no uncontrolled mode. */
|
|
15
|
+
checked: boolean;
|
|
16
|
+
/** Called with the next value when the person flips it. Not called while `busy`. */
|
|
17
|
+
onChange: (checked: boolean) => void;
|
|
18
|
+
/** A muted line under the label that says more. It is the switch's `aria-describedby`. */
|
|
19
|
+
description?: ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* The label's text size and the switch with it.
|
|
22
|
+
* @default md
|
|
23
|
+
*/
|
|
24
|
+
size?: Size | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The change is being saved: `aria-busy`, and a click or a key does nothing. It keeps its colour
|
|
27
|
+
* and its focus, which a native `disabled` would lose when the save starts.
|
|
28
|
+
*/
|
|
29
|
+
busy?: boolean | undefined;
|
|
30
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"button">, "type" | "role" | "children" | "onChange" | "aria-checked" | "aria-busy" | "aria-disabled" | "aria-label">>;
|
|
31
|
+
/**
|
|
32
|
+
* An on/off switch for a setting that takes effect at once, usually by saving it to a server: a
|
|
33
|
+
* native `<button role="switch" aria-checked>`. It is controlled: the caller owns `checked` and
|
|
34
|
+
* flips it in `onChange`. A value sent with a form is a `Checkbox`, not a Switch. Give it a `label`
|
|
35
|
+
* (the whole label is the click target), or in a table cell leave the label out and name it with
|
|
36
|
+
* `aria-label`. `description` is a muted line under the label, and the switch's
|
|
37
|
+
* `aria-describedby`. `size` is `sm`, `md` (the default) or `lg`. `busy` is for the save in
|
|
38
|
+
* flight: clicks and keys do nothing, and the switch keeps its colour and its focus. `disabled` is
|
|
39
|
+
* the native one, washed out.
|
|
40
|
+
*/
|
|
41
|
+
export declare function Switch({ label, description, size, busy, "aria-describedby": describedBy, ...rest }: SwitchProps): ReactElement;
|
|
42
|
+
export {};
|
package/dist/Switch.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
import { useDescription, Words } from "./labelWords.js";
|
|
4
|
+
/**
|
|
5
|
+
* The label's gap and text, the track, its thumb and the thumb's slide, by `size`. The track is a
|
|
6
|
+
* 2px border plus 2px of padding, so the thumb is the track's height less 8px and slides the
|
|
7
|
+
* track's width less 8px less its own width.
|
|
8
|
+
*/
|
|
9
|
+
const SIZE = {
|
|
10
|
+
sm: { label: "gap-2 text-xs", track: "h-5 w-9", thumb: "h-3 w-3", slide: "translate-x-4" },
|
|
11
|
+
md: { label: "gap-2.5 text-sm", track: "h-6 w-11", thumb: "h-4 w-4", slide: "translate-x-5" },
|
|
12
|
+
lg: { label: "gap-3 text-base", track: "h-7 w-13", thumb: "h-5 w-5", slide: "translate-x-6" },
|
|
13
|
+
};
|
|
14
|
+
// Off is an outline of the text colour with a thumb of the same colour; on is the accent filled,
|
|
15
|
+
// with a thumb in its pale text colour. `border-current` and `bg-current` are no palette colour.
|
|
16
|
+
const OFF = "border-current text-slate-500";
|
|
17
|
+
const ON = "border-violet-600 bg-violet-600 text-violet-50";
|
|
18
|
+
/** `not-allowed` for a disabled switch, `progress` for a busy one, and the pointer otherwise. */
|
|
19
|
+
function cursorOf(disabled, busy) {
|
|
20
|
+
if (disabled)
|
|
21
|
+
return "cursor-not-allowed";
|
|
22
|
+
return busy ? "cursor-progress" : "cursor-pointer";
|
|
23
|
+
}
|
|
24
|
+
/** A disabled switch washes out as a Button does; a busy one is only a little dimmer. */
|
|
25
|
+
function washOf(disabled, busy) {
|
|
26
|
+
return cx(cursorOf(disabled, busy), disabled && "opacity-50", busy && "opacity-85");
|
|
27
|
+
}
|
|
28
|
+
/** The switch itself: the track, with the thumb in it. */
|
|
29
|
+
function Track({ checked, onChange, size, busy, disabled, onClick, className, ...rest }) {
|
|
30
|
+
const { track, thumb, slide } = SIZE[size];
|
|
31
|
+
return (_jsx("button", { type: "button", role: "switch", "aria-checked": checked, "aria-busy": busy || undefined, "aria-disabled": busy || undefined, disabled: disabled, className: cx("inline-flex shrink-0 items-center rounded-full border-2 p-0.5", track, checked ? ON : OFF, className, focusRing), onClick: (event) => {
|
|
32
|
+
onClick?.(event);
|
|
33
|
+
if (event.defaultPrevented || busy)
|
|
34
|
+
return;
|
|
35
|
+
onChange(!checked);
|
|
36
|
+
}, ...rest, children: _jsx("span", { "aria-hidden": "true", className: cx("block rounded-full bg-current transition-transform motion-reduce:transition-none", thumb, checked ? slide : "translate-x-0") }) }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* An on/off switch for a setting that takes effect at once, usually by saving it to a server: a
|
|
40
|
+
* native `<button role="switch" aria-checked>`. It is controlled: the caller owns `checked` and
|
|
41
|
+
* flips it in `onChange`. A value sent with a form is a `Checkbox`, not a Switch. Give it a `label`
|
|
42
|
+
* (the whole label is the click target), or in a table cell leave the label out and name it with
|
|
43
|
+
* `aria-label`. `description` is a muted line under the label, and the switch's
|
|
44
|
+
* `aria-describedby`. `size` is `sm`, `md` (the default) or `lg`. `busy` is for the save in
|
|
45
|
+
* flight: clicks and keys do nothing, and the switch keeps its colour and its focus. `disabled` is
|
|
46
|
+
* the native one, washed out.
|
|
47
|
+
*/
|
|
48
|
+
export function Switch({ label, description, size = "md", busy = false, "aria-describedby": describedBy, ...rest }) {
|
|
49
|
+
const { descriptionId, describedIds } = useDescription(description, describedBy);
|
|
50
|
+
const wash = washOf(rest.disabled, busy);
|
|
51
|
+
const track = (_jsx(Track, { ...rest, size: size, busy: busy, className: label === undefined ? wash : cursorOf(rest.disabled, busy), ...(describedIds ? { "aria-describedby": describedIds } : {}) }));
|
|
52
|
+
if (label === undefined)
|
|
53
|
+
return track;
|
|
54
|
+
const start = descriptionId !== undefined;
|
|
55
|
+
return (_jsxs("label", { className: cx("inline-flex text-slate-700", start ? "items-start" : "items-center", SIZE[size].label, wash), children: [track, _jsx("span", { className: cx(start && "mt-0.5"), children: _jsx(Words, { label: label, description: description, id: descriptionId }) })] }));
|
|
56
|
+
}
|
package/dist/TableEditCell.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { ComponentProps, ReactElement, ReactNode } from "react";
|
|
2
2
|
import { TableCell } from "./Table.js";
|
|
3
3
|
/**
|
|
4
|
-
* A `TableCell` whose value opens an editor in place
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* closes it: both put the focus back on the value. A press outside it or
|
|
8
|
-
* focus where it went, so the editor saves its value on
|
|
9
|
-
* Put the control in the cell itself, not in a popover
|
|
10
|
-
* `TableCell`'s `align`, `tone`, `numeric`, `mono`,
|
|
4
|
+
* A `TableCell` whose value opens an editor in place: a cell that holds one `InlineEdit` and
|
|
5
|
+
* nothing else. It shows `children` as a button with a dotted underline; a click, Enter or Space
|
|
6
|
+
* swaps it for `renderEditor(close)` and moves the focus into the editor. The editor calls `close`
|
|
7
|
+
* when it has saved, and Esc closes it: both put the focus back on the value. A press outside it or
|
|
8
|
+
* Tab out of it closes it too and leaves the focus where it went, so the editor saves its value on
|
|
9
|
+
* its own change or submit, never on a close. Put the control in the cell itself, not in a popover
|
|
10
|
+
* of its own, which counts as outside. Takes `TableCell`'s `align`, `tone`, `numeric`, `mono`,
|
|
11
|
+
* `indent` and `secondary`. For an edit trigger in a cell that holds other content too, put an
|
|
12
|
+
* `InlineEdit` in a `TableCell`.
|
|
11
13
|
*/
|
|
12
14
|
export declare function TableEditCell({ label, renderEditor, children, ...cell }: {
|
|
13
15
|
/** What the button does, for a screen reader, heard after the value: "Edit category". It also names the editor. */
|