@hackcove/ui-react 0.0.0-stage → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MIGRATING.md +116 -0
- package/README.md +400 -2
- package/dist/AppShell.d.ts +97 -0
- package/dist/AppShell.js +106 -0
- package/dist/Avatar.d.ts +49 -0
- package/dist/Avatar.js +65 -0
- package/dist/AvatarStack.d.ts +29 -0
- package/dist/AvatarStack.js +26 -0
- package/dist/Badge.d.ts +14 -0
- package/dist/Badge.js +11 -0
- package/dist/Breadcrumbs.d.ts +28 -0
- package/dist/Breadcrumbs.js +30 -0
- package/dist/BulletList.d.ts +4 -0
- package/dist/BulletList.js +5 -0
- package/dist/Button.d.ts +78 -0
- package/dist/Button.js +133 -0
- package/dist/ButtonGroup.d.ts +15 -0
- package/dist/ButtonGroup.js +10 -0
- package/dist/Callout.d.ts +24 -0
- package/dist/Callout.js +28 -0
- package/dist/Card.d.ts +95 -0
- package/dist/Card.js +134 -0
- package/dist/CardBadge.d.ts +7 -0
- package/dist/CardBadge.js +12 -0
- package/dist/CardParts.d.ts +25 -0
- package/dist/CardParts.js +25 -0
- package/dist/Checkbox.d.ts +29 -0
- package/dist/Checkbox.js +36 -0
- package/dist/Chevron.d.ts +5 -0
- package/dist/Chevron.js +6 -0
- package/dist/Cluster.d.ts +51 -0
- package/dist/Cluster.js +38 -0
- package/dist/Code.d.ts +4 -0
- package/dist/Code.js +5 -0
- package/dist/CodeBlock.d.ts +27 -0
- package/dist/CodeBlock.js +12 -0
- package/dist/CodeExample.d.ts +17 -0
- package/dist/CodeExample.js +13 -0
- package/dist/CodeText.d.ts +7 -0
- package/dist/CodeText.js +21 -0
- package/dist/ColourSchemeToggle.d.ts +34 -0
- package/dist/ColourSchemeToggle.js +50 -0
- package/dist/Container.d.ts +22 -0
- package/dist/Container.js +13 -0
- package/dist/CopyButton.d.ts +17 -0
- package/dist/CopyButton.js +13 -0
- package/dist/CopyLine.d.ts +9 -0
- package/dist/CopyLine.js +7 -0
- package/dist/DateCalendar.d.ts +10 -0
- package/dist/DateCalendar.js +45 -0
- package/dist/DateField.d.ts +87 -0
- package/dist/DateField.js +100 -0
- package/dist/DateFieldCalendar.d.ts +6 -0
- package/dist/DateFieldCalendar.js +35 -0
- package/dist/Dialog.d.ts +26 -0
- package/dist/Dialog.js +18 -0
- package/dist/Disclosure.d.ts +15 -0
- package/dist/Disclosure.js +12 -0
- package/dist/Divider.d.ts +4 -0
- package/dist/Divider.js +5 -0
- package/dist/Drawer.d.ts +29 -0
- package/dist/Drawer.js +22 -0
- package/dist/EmptyState.d.ts +18 -0
- package/dist/EmptyState.js +14 -0
- package/dist/Field.d.ts +21 -0
- package/dist/Field.js +19 -0
- package/dist/FieldDialog.d.ts +28 -0
- package/dist/FieldDialog.js +34 -0
- package/dist/FileDrop.d.ts +17 -0
- package/dist/FileDrop.js +12 -0
- package/dist/FilterGroup.d.ts +7 -0
- package/dist/FilterGroup.js +6 -0
- package/dist/FormRow.d.ts +42 -0
- package/dist/FormRow.js +33 -0
- package/dist/Icon.d.ts +20 -0
- package/dist/Icon.js +13 -0
- package/dist/IconButton.d.ts +57 -0
- package/dist/IconButton.js +51 -0
- package/dist/Input.d.ts +22 -0
- package/dist/Input.js +22 -0
- package/dist/InputShell.d.ts +16 -0
- package/dist/InputShell.js +26 -0
- package/dist/Kbd.d.ts +6 -0
- package/dist/Kbd.js +5 -0
- package/dist/KeyValueList.d.ts +23 -0
- package/dist/KeyValueList.js +19 -0
- package/dist/Lightbox.d.ts +28 -0
- package/dist/Lightbox.js +44 -0
- package/dist/ListRow.d.ts +109 -0
- package/dist/ListRow.js +92 -0
- package/dist/Markdown.d.ts +9 -0
- package/dist/Markdown.js +43 -0
- package/dist/MetaList.d.ts +23 -0
- package/dist/MetaList.js +14 -0
- package/dist/OutlineRegion.d.ts +43 -0
- package/dist/OutlineRegion.js +27 -0
- package/dist/PageFade.d.ts +26 -0
- package/dist/PageFade.js +39 -0
- package/dist/PageHeader.d.ts +24 -0
- package/dist/PageHeader.js +14 -0
- package/dist/PageLayouts.d.ts +130 -0
- package/dist/PageLayouts.js +86 -0
- package/dist/Pill.d.ts +53 -0
- package/dist/Pill.js +34 -0
- package/dist/ProgressBar.d.ts +32 -0
- package/dist/ProgressBar.js +32 -0
- package/dist/RadioGroup.d.ts +48 -0
- package/dist/RadioGroup.js +39 -0
- package/dist/SearchInput.d.ts +8 -0
- package/dist/SearchInput.js +40 -0
- package/dist/SegmentedControl.d.ts +20 -0
- package/dist/SegmentedControl.js +31 -0
- package/dist/Select.d.ts +5 -0
- package/dist/Select.js +9 -0
- package/dist/SplitBar.d.ts +27 -0
- package/dist/SplitBar.js +15 -0
- package/dist/Stack.d.ts +70 -0
- package/dist/Stack.js +47 -0
- package/dist/StackedBar.d.ts +56 -0
- package/dist/StackedBar.js +35 -0
- package/dist/StatTile.d.ts +21 -0
- package/dist/StatTile.js +22 -0
- package/dist/StatusPages.d.ts +38 -0
- package/dist/StatusPages.js +57 -0
- package/dist/Steps.d.ts +17 -0
- package/dist/Steps.js +14 -0
- package/dist/StickyBar.d.ts +12 -0
- package/dist/StickyBar.js +10 -0
- package/dist/Table.d.ts +142 -0
- package/dist/Table.js +138 -0
- package/dist/Tabs.d.ts +40 -0
- package/dist/Tabs.js +49 -0
- package/dist/TextLink.d.ts +51 -0
- package/dist/TextLink.js +37 -0
- package/dist/Textarea.d.ts +5 -0
- package/dist/Textarea.js +7 -0
- package/dist/Timeline.d.ts +27 -0
- package/dist/Timeline.js +29 -0
- package/dist/Video.d.ts +13 -0
- package/dist/Video.js +9 -0
- package/dist/colourScheme.d.ts +21 -0
- package/dist/colourScheme.js +53 -0
- package/dist/control.d.ts +31 -0
- package/dist/control.js +38 -0
- package/dist/cx.d.ts +10 -0
- package/dist/cx.js +12 -0
- package/dist/highlight.d.ts +16 -0
- package/dist/highlight.js +292 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +66 -0
- package/dist/isoDate.d.ts +5 -0
- package/dist/isoDate.js +16 -0
- package/dist/lift.harness.d.ts +2 -0
- package/dist/lift.harness.js +3 -0
- package/dist/link.d.ts +50 -0
- package/dist/link.js +25 -0
- package/dist/modal.d.ts +11 -0
- package/dist/modal.js +41 -0
- package/dist/props.d.ts +14 -0
- package/dist/props.js +1 -0
- package/dist/shownDate.d.ts +45 -0
- package/dist/shownDate.js +88 -0
- package/dist/size.d.ts +6 -0
- package/dist/size.js +1 -0
- package/dist/switcher.d.ts +88 -0
- package/dist/switcher.js +71 -0
- package/dist/typography.d.ts +123 -0
- package/dist/typography.js +120 -0
- package/dist/ui.d.ts +10 -0
- package/dist/ui.js +27 -0
- package/package.json +91 -4
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useId, } from "react";
|
|
3
|
+
import { FieldError, Label, Radio as AriaRadio, RadioGroup as AriaRadioGroup, } from "react-aria-components";
|
|
4
|
+
import { cx } from "./cx.js";
|
|
5
|
+
/** A radio's circle, its dot, its drop onto the label's first line and the label's text, by the group's `size`. */
|
|
6
|
+
const SIZE = {
|
|
7
|
+
sm: { circle: "mt-px h-3.5 w-3.5", dot: "h-1.5 w-1.5", label: "text-xs", gap: "gap-2" },
|
|
8
|
+
md: { circle: "mt-0.5 h-4 w-4", dot: "h-2 w-2", label: "text-sm", gap: "gap-2.5" },
|
|
9
|
+
lg: { circle: "mt-0.5 h-5 w-5", dot: "h-2.5 w-2.5", label: "text-base", gap: "gap-3" },
|
|
10
|
+
};
|
|
11
|
+
/** The group's `size`, read by each `Radio` in it. */
|
|
12
|
+
const RadioSize = createContext("md");
|
|
13
|
+
/**
|
|
14
|
+
* A named group of radios, one chosen. The `label` names the group; an `error` is a red line under
|
|
15
|
+
* it with `role="alert"` and marks the group invalid. Give it `value` and `onChange`, or
|
|
16
|
+
* `defaultValue` and let it keep the choice. `name` posts the choice in a form.
|
|
17
|
+
* The arrow keys move between the radios and choose, and the group is one tab stop. `size` sizes
|
|
18
|
+
* every radio in the group, the same scale as a `Checkbox`.
|
|
19
|
+
*/
|
|
20
|
+
export function RadioGroup({ label, error, size = "md", children, ...rest }) {
|
|
21
|
+
return (_jsxs(AriaRadioGroup, { ...rest, ...(error ? { isInvalid: true } : {}), className: "flex flex-col gap-2", children: [_jsx(Label, { className: "text-sm font-medium text-slate-700", children: label }), _jsx("div", { className: "flex flex-col gap-2.5", children: _jsx(RadioSize.Provider, { value: size, children: children }) }), _jsx(FieldError, { className: "text-xs text-red-700", children: _jsx("span", { role: "alert", children: error }) })] }));
|
|
22
|
+
}
|
|
23
|
+
function ringTone(invalid, selected) {
|
|
24
|
+
if (invalid)
|
|
25
|
+
return "border-red-600";
|
|
26
|
+
return selected ? "border-violet-600" : "border-slate-300";
|
|
27
|
+
}
|
|
28
|
+
/** One option of a `RadioGroup`: a `label` and, under it, an optional `description`. Click the label or the circle. */
|
|
29
|
+
export function Radio({ label, description, value, isDisabled, "aria-describedby": describedBy, ...rest }) {
|
|
30
|
+
const descriptionId = useId();
|
|
31
|
+
const size = SIZE[useContext(RadioSize)];
|
|
32
|
+
return (_jsx(AriaRadio, { ...rest, value: value, ...(describedBy || description
|
|
33
|
+
? {
|
|
34
|
+
"aria-describedby": [describedBy, description ? descriptionId : undefined]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(" "),
|
|
37
|
+
}
|
|
38
|
+
: {}), ...(isDisabled ? { isDisabled } : {}), className: ({ isDisabled: off }) => cx("flex cursor-pointer items-start rounded-md has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-violet-600", size.gap, off && "cursor-default opacity-50"), children: ({ isSelected, isInvalid }) => (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: cx("flex shrink-0 items-center justify-center rounded-full border bg-white transition hover:border-violet-400", size.circle, ringTone(isInvalid, isSelected)), children: isSelected ? _jsx("span", { className: cx("rounded-full bg-violet-600", size.dot) }) : null }), _jsxs("span", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: cx("text-slate-700", size.label), children: label }), description ? (_jsx("span", { id: descriptionId, className: "text-xs text-slate-500", children: description })) : null] })] })) }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { type ControlAlign, type ControlProps } from "./control.js";
|
|
3
|
+
import type { Unstyled } from "./props.js";
|
|
4
|
+
/** `Input` for `type="search"`: native search look reset, and a "Clear search" button at the trailing edge while the field has a value. */
|
|
5
|
+
export declare function SearchInput({ icon, size, width, align, onChange, ...rest }: {
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
align?: ControlAlign | undefined;
|
|
8
|
+
} & ControlProps & Unstyled<Omit<ComponentPropsWithoutRef<"input">, "size" | "width">>): ReactElement;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState, } from "react";
|
|
3
|
+
import { CONTROL_ALIGN, controlClass, } from "./control.js";
|
|
4
|
+
import { cx, focusRing } from "./cx.js";
|
|
5
|
+
import { InputShell } from "./InputShell.js";
|
|
6
|
+
/** The browser's own search look is reset: no `searchfield` appearance, no cancel button or decoration. The clear button below replaces them. */
|
|
7
|
+
const SEARCH_RESET = "appearance-none [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden";
|
|
8
|
+
/** A search field's padding: the leading side as a plain field or with an icon, the trailing side clear of the clear button. */
|
|
9
|
+
const SEARCH_PAD = {
|
|
10
|
+
sm: { plain: "pl-2 pr-7", icon: "pl-7 pr-7" },
|
|
11
|
+
md: { plain: "pl-3 pr-9", icon: "pl-9 pr-9" },
|
|
12
|
+
lg: { plain: "pl-4 pr-11", icon: "pl-11 pr-11" },
|
|
13
|
+
};
|
|
14
|
+
/** The clear button's place and box, and its icon, by the field's size. */
|
|
15
|
+
const CLEAR_SIZE = {
|
|
16
|
+
sm: { box: "right-1 h-5 w-5", icon: "h-3 w-3" },
|
|
17
|
+
md: { box: "right-2 h-6 w-6", icon: "h-3.5 w-3.5" },
|
|
18
|
+
lg: { box: "right-2.5 h-7 w-7", icon: "h-4 w-4" },
|
|
19
|
+
};
|
|
20
|
+
const CLEAR_BUTTON = `absolute top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded text-slate-500 hover:bg-slate-100 ${focusRing}`;
|
|
21
|
+
/** Set `value` the way a keystroke does (past React's value tracker), so `onChange` fires for a controlled and an uncontrolled field alike. */
|
|
22
|
+
function clearValue(input) {
|
|
23
|
+
const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
|
|
24
|
+
descriptor?.set?.call(input, "");
|
|
25
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
26
|
+
input.focus();
|
|
27
|
+
}
|
|
28
|
+
function ClearButton({ size, onClear }) {
|
|
29
|
+
return (_jsx("button", { type: "button", "aria-label": "Clear search", title: "Clear search", onClick: onClear, className: cx(CLEAR_BUTTON, CLEAR_SIZE[size].box), children: _jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", className: CLEAR_SIZE[size].icon, children: _jsx("path", { d: "M5.28 4.22a.75.75 0 0 0-1.06 1.06L8.94 10l-4.72 4.72a.75.75 0 1 0 1.06 1.06L10 11.06l4.72 4.72a.75.75 0 1 0 1.06-1.06L11.06 10l4.72-4.72a.75.75 0 0 0-1.06-1.06L10 8.94 5.28 4.22Z" }) }) }));
|
|
30
|
+
}
|
|
31
|
+
/** `Input` for `type="search"`: native search look reset, and a "Clear search" button at the trailing edge while the field has a value. */
|
|
32
|
+
export function SearchInput({ icon, size = "md", width = "full", align = "start", onChange, ...rest }) {
|
|
33
|
+
const inputRef = useRef(null);
|
|
34
|
+
const [typed, setTyped] = useState(() => String(rest.value ?? rest.defaultValue ?? "") !== "");
|
|
35
|
+
const filled = rest.value !== undefined ? String(rest.value) !== "" : typed;
|
|
36
|
+
return (_jsxs(InputShell, { icon: icon, size: size, width: width, children: [_jsx("input", { ...rest, ref: inputRef, onChange: (event) => {
|
|
37
|
+
setTyped(event.currentTarget.value !== "");
|
|
38
|
+
onChange?.(event);
|
|
39
|
+
}, className: cx(controlClass(size, width === "auto" ? "auto" : "full"), SEARCH_PAD[size][icon === undefined ? "plain" : "icon"], SEARCH_RESET, CONTROL_ALIGN[align]) }), filled && !rest.disabled && !rest.readOnly && (_jsx(ClearButton, { size: size, onClear: () => inputRef.current && clearValue(inputRef.current) }))] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import type { Size } from "./size.js";
|
|
3
|
+
import { type SwitchItem, type SwitchProps } from "./switcher.js";
|
|
4
|
+
/** One option of a SegmentedControl. See `SwitchItem`. */
|
|
5
|
+
export type Segment = SwitchItem;
|
|
6
|
+
/**
|
|
7
|
+
* A joined pill group where one option is filled: a mode or a range chosen in place. `items` are
|
|
8
|
+
* `{ value, label }` buttons (`value` and `onChange` are the caller's state; the arrow keys, Home
|
|
9
|
+
* and End move the choice) or links (`href`, or `render` to draw the app's own router `<Link>`;
|
|
10
|
+
* `value` marks the current one, which gets `aria-current`). An item may carry a `count`, and a
|
|
11
|
+
* button one `disabled` (with a `title` for the reason); `disabled` on the group disables every
|
|
12
|
+
* option. `label` names the group. `size` matches the height of a `Button` of the same size.
|
|
13
|
+
*/
|
|
14
|
+
export declare function SegmentedControl({ size, ...props }: SwitchProps & {
|
|
15
|
+
/**
|
|
16
|
+
* The control's height and text, on the scale of a `Button`.
|
|
17
|
+
* @default md
|
|
18
|
+
*/
|
|
19
|
+
size?: Size;
|
|
20
|
+
}): ReactElement;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { Switcher } from "./switcher.js";
|
|
4
|
+
// The focus ring sits inside the option, since the group's rounded edge would clip an outer one.
|
|
5
|
+
const BASE = "border-l border-slate-300 transition first:rounded-l-full first:border-l-0 last:rounded-r-full focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-violet-600";
|
|
6
|
+
/** An option's padding and text, by `size`: with the group's border, the heights of a `Button`. */
|
|
7
|
+
const SIZE = {
|
|
8
|
+
sm: "px-2.5 py-1 text-xs",
|
|
9
|
+
md: "px-3 py-2 text-sm",
|
|
10
|
+
lg: "px-4 py-2.5 text-base",
|
|
11
|
+
};
|
|
12
|
+
const ON = "bg-violet-600 font-medium text-violet-50";
|
|
13
|
+
const OFF = "bg-white text-slate-600";
|
|
14
|
+
const OFF_LIVE = "hover:bg-violet-50 hover:text-violet-700";
|
|
15
|
+
/**
|
|
16
|
+
* A joined pill group where one option is filled: a mode or a range chosen in place. `items` are
|
|
17
|
+
* `{ value, label }` buttons (`value` and `onChange` are the caller's state; the arrow keys, Home
|
|
18
|
+
* and End move the choice) or links (`href`, or `render` to draw the app's own router `<Link>`;
|
|
19
|
+
* `value` marks the current one, which gets `aria-current`). An item may carry a `count`, and a
|
|
20
|
+
* button one `disabled` (with a `title` for the reason); `disabled` on the group disables every
|
|
21
|
+
* option. `label` names the group. `size` matches the height of a `Button` of the same size.
|
|
22
|
+
*/
|
|
23
|
+
export function SegmentedControl({ size = "md", ...props }) {
|
|
24
|
+
return (_jsx(Switcher, { ...props, parts: {
|
|
25
|
+
group: "radiogroup",
|
|
26
|
+
item: "radio",
|
|
27
|
+
vertical: true,
|
|
28
|
+
root: "inline-flex rounded-full border border-slate-300",
|
|
29
|
+
option: (active, disabled) => cx(BASE, SIZE[size], active ? ON : OFF, !active && !disabled && OFF_LIVE),
|
|
30
|
+
} }));
|
|
31
|
+
}
|
package/dist/Select.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
2
|
+
import { type ControlProps } from "./control.js";
|
|
3
|
+
import type { Unstyled } from "./props.js";
|
|
4
|
+
/** A native `<select>` styled like `Input`, keeping the browser's own arrow. Takes `size` and `width` too. */
|
|
5
|
+
export declare function Select({ size, width, children, ...rest }: ControlProps & Unstyled<Omit<ComponentPropsWithoutRef<"select">, "size" | "width">>): ReactElement;
|
package/dist/Select.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CONTROL_PAD, controlClass } from "./control.js";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
/** The height of a `Select` by `size`, the `Input`'s 26px, 38px and 46px: a native `<select>` sizes its text from the font and ignores the line height, so padding alone falls short at `md` and `lg`. */
|
|
5
|
+
const SELECT_HEIGHT = { sm: "h-6.5", md: "h-9.5", lg: "h-11.5" };
|
|
6
|
+
/** A native `<select>` styled like `Input`, keeping the browser's own arrow. Takes `size` and `width` too. */
|
|
7
|
+
export function Select({ size = "md", width = "full", children, ...rest }) {
|
|
8
|
+
return (_jsx("select", { ...rest, className: cx(controlClass(size, width), SELECT_HEIGHT[size], CONTROL_PAD[size].x), children: children }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { type BarSize, type BarTone } from "./ProgressBar.js";
|
|
3
|
+
import type { Passthrough } from "./props.js";
|
|
4
|
+
/**
|
|
5
|
+
* A bar split between two parties, `a` on the left and `b` on the right, each sized by its share of
|
|
6
|
+
* `a + b`. `label` names the split for a screen reader, which hears both percentages; give `names`
|
|
7
|
+
* (left, right) so it hears whose is whose. Put the parties' names under the bar yourself.
|
|
8
|
+
*/
|
|
9
|
+
export declare function SplitBar({ a, b, label, names, toneA, toneB, size, ...rest }: {
|
|
10
|
+
/** The left party's amount; its share is `a` over `a + b`. */
|
|
11
|
+
a: number;
|
|
12
|
+
/** The right party's amount; its share is `b` over `a + b`. */
|
|
13
|
+
b: number;
|
|
14
|
+
/** What the split is, for a screen reader, which hears it before both percentages. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** The left and right parties, spoken before their percentages. */
|
|
17
|
+
names?: readonly [string, string];
|
|
18
|
+
/** Defaults to "negative". */
|
|
19
|
+
toneA?: BarTone;
|
|
20
|
+
/** Defaults to "positive". */
|
|
21
|
+
toneB?: BarTone;
|
|
22
|
+
/**
|
|
23
|
+
* The bar's height: `sm` 6px, `md` 8px, `lg` 12px.
|
|
24
|
+
* @default md
|
|
25
|
+
*/
|
|
26
|
+
size?: BarSize;
|
|
27
|
+
} & Passthrough): ReactElement;
|
package/dist/SplitBar.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { BAR_FILL, BAR_TRACK, announcedPercent, sharePercent, } from "./ProgressBar.js";
|
|
4
|
+
/**
|
|
5
|
+
* A bar split between two parties, `a` on the left and `b` on the right, each sized by its share of
|
|
6
|
+
* `a + b`. `label` names the split for a screen reader, which hears both percentages; give `names`
|
|
7
|
+
* (left, right) so it hears whose is whose. Put the parties' names under the bar yourself.
|
|
8
|
+
*/
|
|
9
|
+
export function SplitBar({ a, b, label, names, toneA = "negative", toneB = "positive", size = "md", ...rest }) {
|
|
10
|
+
const shareA = sharePercent(a, a + b);
|
|
11
|
+
const shareB = a + b > 0 ? 100 - shareA : 0;
|
|
12
|
+
const percentA = announcedPercent(a, a + b);
|
|
13
|
+
const percentB = a + b > 0 ? 100 - percentA : 0;
|
|
14
|
+
return (_jsxs("div", { ...rest, role: "img", "aria-label": `${label}: ${names ? `${names[0]} ${percentA}%, ${names[1]} ${percentB}%` : `${percentA}% / ${percentB}%`}`, className: cx("flex overflow-hidden rounded-full bg-slate-100", BAR_TRACK[size]), children: [_jsx("div", { className: BAR_FILL[toneA], style: { width: `${shareA}%` } }), _jsx("div", { className: BAR_FILL[toneB], style: { width: `${shareB}%` } })] }));
|
|
15
|
+
}
|
package/dist/Stack.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { Passthrough, Unstyled } from "./props.js";
|
|
3
|
+
export type StackGap = "xs" | "sm" | "md" | "lg" | "xl";
|
|
4
|
+
export declare const STACK_GAP: Record<StackGap, string>;
|
|
5
|
+
export type StackWidth = "narrow" | "default" | "wide" | "full";
|
|
6
|
+
/** `default` adds nothing: the Stack is as wide as its parent. */
|
|
7
|
+
export declare const STACK_WIDTH: Record<StackWidth, string | undefined>;
|
|
8
|
+
/**
|
|
9
|
+
* A vertical column of blocks a fixed `gap` apart: `xs` between lines of text (a heading and the
|
|
10
|
+
* line under it), `sm` inside a group, `md` (default) between blocks, `lg` between groups, `xl` between a page's top-level blocks
|
|
11
|
+
* (`gap-12`, `sm:gap-16`: the one responsive page rhythm).
|
|
12
|
+
*
|
|
13
|
+
* `width` caps the column: `narrow` is a reading measure (`max-w-3xl`), `wide` is `max-w-5xl`, `full`
|
|
14
|
+
* is the parent's whole width and `default` sets nothing. A capped Stack starts at its parent's left
|
|
15
|
+
* edge (it is never re-centred), so its first line lines up with every other block on the page.
|
|
16
|
+
*
|
|
17
|
+
* `as` is a closed set: `div` (default), `section`, `form`, `ul` or `ol`. A `form` takes the form
|
|
18
|
+
* props (`onSubmit`, `action`, `method`, `noValidate`, ...). A `ul` or `ol` is drawn with no
|
|
19
|
+
* markers and its children are `<li>`s, as in `BulletList` and `Timeline`: the Stack does not wrap
|
|
20
|
+
* them, so a child is one list item and `Text as="li"` or a bare `<li>` both work.
|
|
21
|
+
*/
|
|
22
|
+
export declare function Stack({ gap, width, as, children, ...rest }: StackProps): ReactElement;
|
|
23
|
+
type StackBase = {
|
|
24
|
+
/**
|
|
25
|
+
* The space between blocks: `xs` gap-2, `sm` gap-4, `md` gap-6, `lg` gap-8, `xl` gap-12 (gap-16 from `sm`).
|
|
26
|
+
* @default md
|
|
27
|
+
*/
|
|
28
|
+
gap?: StackGap | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Caps the column: `narrow` max-w-3xl (a reading measure), `wide` max-w-5xl, `full` the parent's whole width, `default` sets nothing.
|
|
31
|
+
* @default default
|
|
32
|
+
*/
|
|
33
|
+
width?: StackWidth | undefined;
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
};
|
|
36
|
+
type StackProps = StackBase & (({
|
|
37
|
+
/** The element: `div`, `section`, `form` (takes the form's props), `ul` or `ol` (no markers; each child is an `<li>`). */
|
|
38
|
+
as?: "div" | "section";
|
|
39
|
+
} & Passthrough) | ({
|
|
40
|
+
/** The element: `div`, `section`, `form` (takes the form's props), `ul` or `ol` (no markers; each child is an `<li>`). */
|
|
41
|
+
as: "form";
|
|
42
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"form">, "children">>) | ({
|
|
43
|
+
/** The element: `div`, `section`, `form` (takes the form's props), `ul` or `ol` (no markers; each child is an `<li>`). */
|
|
44
|
+
as: "ul";
|
|
45
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"ul">, "children">>) | ({
|
|
46
|
+
/** The element: `div`, `section`, `form` (takes the form's props), `ul` or `ol` (no markers; each child is an `<li>`). */
|
|
47
|
+
as: "ol";
|
|
48
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"ol">, "children">>));
|
|
49
|
+
/**
|
|
50
|
+
* A `<section>` Stack with an optional `SectionHeading` title (which names it) and `actions` beside
|
|
51
|
+
* it. Its blocks are `gap` apart, `md` by default as in a `Stack`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function Section({ title, actions, gap, width, children, ...rest }: {
|
|
54
|
+
/** A `SectionHeading` at the top, which names the section. */
|
|
55
|
+
title?: ReactNode;
|
|
56
|
+
/** Buttons or links at the end of the title row. */
|
|
57
|
+
actions?: ReactNode;
|
|
58
|
+
/**
|
|
59
|
+
* The space between blocks: `xs` gap-2, `sm` gap-4, `md` gap-6, `lg` gap-8, `xl` gap-12 (gap-16 from `sm`).
|
|
60
|
+
* @default md
|
|
61
|
+
*/
|
|
62
|
+
gap?: StackGap | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Caps the column: `narrow` max-w-3xl (a reading measure), `wide` max-w-5xl, `full` the parent's whole width, `default` sets nothing.
|
|
65
|
+
* @default default
|
|
66
|
+
*/
|
|
67
|
+
width?: StackWidth | undefined;
|
|
68
|
+
children?: ReactNode;
|
|
69
|
+
} & Passthrough): ReactElement;
|
|
70
|
+
export {};
|
package/dist/Stack.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId, } from "react";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
import { SectionHeading } from "./typography.js";
|
|
5
|
+
export const STACK_GAP = {
|
|
6
|
+
xs: "gap-2",
|
|
7
|
+
sm: "gap-4",
|
|
8
|
+
md: "gap-6",
|
|
9
|
+
lg: "gap-8",
|
|
10
|
+
xl: "gap-12 sm:gap-16",
|
|
11
|
+
};
|
|
12
|
+
/** `default` adds nothing: the Stack is as wide as its parent. */
|
|
13
|
+
export const STACK_WIDTH = {
|
|
14
|
+
narrow: "max-w-3xl",
|
|
15
|
+
default: undefined,
|
|
16
|
+
wide: "max-w-5xl",
|
|
17
|
+
full: "w-full",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A vertical column of blocks a fixed `gap` apart: `xs` between lines of text (a heading and the
|
|
21
|
+
* line under it), `sm` inside a group, `md` (default) between blocks, `lg` between groups, `xl` between a page's top-level blocks
|
|
22
|
+
* (`gap-12`, `sm:gap-16`: the one responsive page rhythm).
|
|
23
|
+
*
|
|
24
|
+
* `width` caps the column: `narrow` is a reading measure (`max-w-3xl`), `wide` is `max-w-5xl`, `full`
|
|
25
|
+
* is the parent's whole width and `default` sets nothing. A capped Stack starts at its parent's left
|
|
26
|
+
* edge (it is never re-centred), so its first line lines up with every other block on the page.
|
|
27
|
+
*
|
|
28
|
+
* `as` is a closed set: `div` (default), `section`, `form`, `ul` or `ol`. A `form` takes the form
|
|
29
|
+
* props (`onSubmit`, `action`, `method`, `noValidate`, ...). A `ul` or `ol` is drawn with no
|
|
30
|
+
* markers and its children are `<li>`s, as in `BulletList` and `Timeline`: the Stack does not wrap
|
|
31
|
+
* them, so a child is one list item and `Text as="li"` or a bare `<li>` both work.
|
|
32
|
+
*/
|
|
33
|
+
export function Stack({ gap = "md", width = "default", as = "div", children, ...rest }) {
|
|
34
|
+
return (_jsx(Box, { tag: as, className: cx("flex flex-col", (as === "ul" || as === "ol") && "list-none", STACK_GAP[gap], STACK_WIDTH[width], "*:data-page-header:mb-0"), ...rest, children: children }));
|
|
35
|
+
}
|
|
36
|
+
/** The element `as` picked, drawn with props already narrowed by `StackProps`. */
|
|
37
|
+
function Box({ tag: Tag, ...props }) {
|
|
38
|
+
return _jsx(Tag, { ...props });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A `<section>` Stack with an optional `SectionHeading` title (which names it) and `actions` beside
|
|
42
|
+
* it. Its blocks are `gap` apart, `md` by default as in a `Stack`.
|
|
43
|
+
*/
|
|
44
|
+
export function Section({ title, actions, gap = "md", width = "default", children, ...rest }) {
|
|
45
|
+
const titleId = useId();
|
|
46
|
+
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] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { type BarSize } from "./ProgressBar.js";
|
|
3
|
+
import type { Passthrough } from "./props.js";
|
|
4
|
+
import { type TextTone } from "./typography.js";
|
|
5
|
+
/**
|
|
6
|
+
* The colour of a `StackedBar` segment or a `Legend` swatch: the five `TextTone` meanings plus two
|
|
7
|
+
* that only a chart needs, `neutral` (a grey that sits apart from `muted`, for "unmeasured" next to
|
|
8
|
+
* "same") and `violet` (the chart's own purple, apart from `info`). Text, links and icons keep the
|
|
9
|
+
* five `TextTone` names; a chart with more than five parts is why these two live here.
|
|
10
|
+
*/
|
|
11
|
+
export type VizTone = TextTone | "neutral" | "violet";
|
|
12
|
+
/** One part of a `StackedBar`: its `label`, its `value` (its share is `value` over the sum) and its colour. */
|
|
13
|
+
export type StackedSegment = {
|
|
14
|
+
label: string;
|
|
15
|
+
value: number;
|
|
16
|
+
tone: VizTone;
|
|
17
|
+
};
|
|
18
|
+
/** One entry of a `Legend`: the `label` the colour stands for, its swatch, and an optional `detail` (a count, a share) after it. */
|
|
19
|
+
export type LegendItem = {
|
|
20
|
+
label: string;
|
|
21
|
+
tone: VizTone;
|
|
22
|
+
/**
|
|
23
|
+
* `solid` (the default) is a filled dot; `dashed` is a dashed ring in the tone's colour with no
|
|
24
|
+
* fill, the key for an outline drawn over the chart (a snapshot overlay).
|
|
25
|
+
*/
|
|
26
|
+
swatch?: "solid" | "dashed" | undefined;
|
|
27
|
+
detail?: string | number | undefined;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A key to the colours of a chart or a row stripe: a swatch in the `VizTone` colour (a filled
|
|
31
|
+
* dot, or a dashed ring with `swatch="dashed"`), its `label` and an optional `detail` after it. Colour alone does not say it, so the label is always drawn.
|
|
32
|
+
* `StackedBar legend` draws one under the bar from its segments.
|
|
33
|
+
*/
|
|
34
|
+
export declare function Legend({ items, ...rest }: {
|
|
35
|
+
/** One entry per colour: its `label`, its `tone`, an optional `swatch` and `detail` (a count, a share). */
|
|
36
|
+
items: readonly LegendItem[];
|
|
37
|
+
} & Passthrough): ReactElement;
|
|
38
|
+
/**
|
|
39
|
+
* A bar of any number of parts, each sized by its share of the sum, in the order given. A part with
|
|
40
|
+
* no value is zero wide. `label` names the bar for a screen reader, which hears every part's name
|
|
41
|
+
* and whole percent (rounded, so they may not add up to exactly 100). `legend` draws a `Legend` under the bar, each part's
|
|
42
|
+
* label with its value, so the colours are named in words.
|
|
43
|
+
*/
|
|
44
|
+
export declare function StackedBar({ segments, label, legend, size, ...rest }: {
|
|
45
|
+
/** The parts in order, each a `label`, a `value` (its share is the value over the sum) and a `tone`. */
|
|
46
|
+
segments: readonly StackedSegment[];
|
|
47
|
+
/** What the bar shows, for a screen reader, which hears it before every part's name and percent. */
|
|
48
|
+
label: string;
|
|
49
|
+
/** Draw the key under the bar. Defaults to false. */
|
|
50
|
+
legend?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The bar's height: `sm` 6px, `md` 8px, `lg` 12px.
|
|
53
|
+
* @default md
|
|
54
|
+
*/
|
|
55
|
+
size?: BarSize;
|
|
56
|
+
} & Passthrough): ReactElement;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { announcedPercent, BAR_TRACK, sharePercent } from "./ProgressBar.js";
|
|
4
|
+
import { TEXT_TONE } from "./typography.js";
|
|
5
|
+
/** Tone → text utility, painted as a fill. The five `TextTone` colours are shared; the two extra ones are remapped by every theme. */
|
|
6
|
+
const VIZ_TONE = {
|
|
7
|
+
...TEXT_TONE,
|
|
8
|
+
neutral: "text-neutral-500",
|
|
9
|
+
violet: "text-purple-700",
|
|
10
|
+
};
|
|
11
|
+
/** The colour of a segment: the tone's text colour, painted as a fill, so no new fill utility. */
|
|
12
|
+
const FILL = (tone) => cx("bg-current", VIZ_TONE[tone]);
|
|
13
|
+
/** A swatch: a filled dot, or a dashed ring in the same colour. */
|
|
14
|
+
const SWATCH = (item) => item.swatch === "dashed"
|
|
15
|
+
? cx("border-2 border-dashed border-current", VIZ_TONE[item.tone])
|
|
16
|
+
: FILL(item.tone);
|
|
17
|
+
/**
|
|
18
|
+
* A key to the colours of a chart or a row stripe: a swatch in the `VizTone` colour (a filled
|
|
19
|
+
* dot, or a dashed ring with `swatch="dashed"`), its `label` and an optional `detail` after it. Colour alone does not say it, so the label is always drawn.
|
|
20
|
+
* `StackedBar legend` draws one under the bar from its segments.
|
|
21
|
+
*/
|
|
22
|
+
export function Legend({ items, ...rest }) {
|
|
23
|
+
return (_jsx("ul", { ...rest, className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-600", children: items.map((item) => (_jsxs("li", { className: "flex items-center gap-1.5", children: [_jsx("span", { "aria-hidden": "true", className: cx("size-2.5 shrink-0 rounded-full", SWATCH(item)) }), _jsx("span", { children: item.label }), item.detail !== undefined && (_jsx("span", { className: "tabular-nums text-slate-500", children: item.detail }))] }, item.label))) }));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A bar of any number of parts, each sized by its share of the sum, in the order given. A part with
|
|
27
|
+
* no value is zero wide. `label` names the bar for a screen reader, which hears every part's name
|
|
28
|
+
* and whole percent (rounded, so they may not add up to exactly 100). `legend` draws a `Legend` under the bar, each part's
|
|
29
|
+
* label with its value, so the colours are named in words.
|
|
30
|
+
*/
|
|
31
|
+
export function StackedBar({ segments, label, legend = false, size = "md", ...rest }) {
|
|
32
|
+
const total = segments.reduce((sum, s) => sum + Math.max(0, s.value), 0);
|
|
33
|
+
const spoken = segments.map((s) => `${s.label} ${announcedPercent(s.value, total)}%`).join(", ");
|
|
34
|
+
return (_jsxs("div", { ...rest, className: "flex flex-col gap-2", children: [_jsx("div", { role: "img", "aria-label": `${label}: ${spoken}`, className: cx("flex overflow-hidden rounded-full bg-slate-100", BAR_TRACK[size]), children: segments.map((s) => (_jsx("div", { className: FILL(s.tone), style: { width: `${sharePercent(s.value, total)}%` } }, s.label))) }), legend && (_jsx(Legend, { items: segments.map((s) => ({ label: s.label, tone: s.tone, detail: s.value })) }))] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { Unstyled } from "./props.js";
|
|
3
|
+
/** The colour of a tile's value (and of its accent bar): plain ink, green or red. */
|
|
4
|
+
export type StatTileTone = "neutral" | "positive" | "negative";
|
|
5
|
+
/**
|
|
6
|
+
* One headline figure: an uppercase caption over a large tabular-nums value, with an optional
|
|
7
|
+
* hint line beneath. `tone` colours the value; `accent` adds a bar across the top edge in the same
|
|
8
|
+
* tone. Pass the value already formatted.
|
|
9
|
+
*/
|
|
10
|
+
export declare function StatTile({ label, value, hint, tone, accent, ...rest }: {
|
|
11
|
+
/** The small uppercase caption over the value. */
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
/** The figure, already formatted, large and in tabular figures. */
|
|
14
|
+
value: ReactNode;
|
|
15
|
+
/** A muted line under the value. */
|
|
16
|
+
hint?: ReactNode;
|
|
17
|
+
/** Defaults to "neutral". */
|
|
18
|
+
tone?: StatTileTone;
|
|
19
|
+
/** A bar across the top edge in the tone's colour. */
|
|
20
|
+
accent?: boolean;
|
|
21
|
+
} & Unstyled<Omit<HTMLAttributes<HTMLDivElement>, "children">>): ReactElement;
|
package/dist/StatTile.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { BAR_FILL } from "./ProgressBar.js";
|
|
4
|
+
import { Overline } from "./typography.js";
|
|
5
|
+
const VALUE = {
|
|
6
|
+
neutral: "text-slate-900",
|
|
7
|
+
positive: "text-emerald-700",
|
|
8
|
+
negative: "text-red-700",
|
|
9
|
+
};
|
|
10
|
+
const BAR = {
|
|
11
|
+
neutral: BAR_FILL.accent,
|
|
12
|
+
positive: BAR_FILL.positive,
|
|
13
|
+
negative: BAR_FILL.negative,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* One headline figure: an uppercase caption over a large tabular-nums value, with an optional
|
|
17
|
+
* hint line beneath. `tone` colours the value; `accent` adds a bar across the top edge in the same
|
|
18
|
+
* tone. Pass the value already formatted.
|
|
19
|
+
*/
|
|
20
|
+
export function StatTile({ label, value, hint, tone = "neutral", accent = false, ...rest }) {
|
|
21
|
+
return (_jsxs("div", { ...rest, className: "relative overflow-hidden rounded-xl border border-slate-200/70 bg-white p-5 shadow-sm", children: [accent && (_jsx("span", { "aria-hidden": "true", className: cx("absolute inset-x-0 top-0 h-1", BAR[tone]) })), _jsx(Overline, { as: "p", children: label }), _jsx("p", { className: cx("mt-2 text-3xl font-bold tabular-nums", VALUE[tone]), children: value }), hint != null && _jsx("p", { className: "mt-1 text-xs text-slate-500", children: hint })] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { type LinkRender } from "./link.js";
|
|
3
|
+
import type { Passthrough } from "./props.js";
|
|
4
|
+
type StatusPageProps = {
|
|
5
|
+
/** Replaces the default heading. */
|
|
6
|
+
title?: ReactNode;
|
|
7
|
+
/** Replaces the default body copy. */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
/** The primary action; the home link turns outline beside it. */
|
|
10
|
+
action?: ReactNode;
|
|
11
|
+
/** Adds a "Back to home" button linking here. With neither this nor `renderHome` there is no home link. */
|
|
12
|
+
homeHref?: string;
|
|
13
|
+
/** Draws the home link with the app's own router `<Link>`, as on `Button`. */
|
|
14
|
+
renderHome?: LinkRender;
|
|
15
|
+
} & Passthrough;
|
|
16
|
+
/** 404: the page is not there. */
|
|
17
|
+
export declare function NotFound(props: StatusPageProps): ReactElement;
|
|
18
|
+
/**
|
|
19
|
+
* 500 or any failure. `detail` is the error text, shown in a `CodeBlock` under the copy; pass it
|
|
20
|
+
* only where a reader may see it (a request id, a message), never a stack trace. `onRetry` adds a
|
|
21
|
+
* "Try again" button, unless `action` replaces it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ErrorPage({ detail, onRetry, action, ...props }: StatusPageProps & {
|
|
24
|
+
/** The error text, in a `CodeBlock` under the copy: a request id or a message a reader may see, never a stack trace. */
|
|
25
|
+
detail?: string;
|
|
26
|
+
/** Adds a "Try again" button that calls it, unless `action` replaces the button. */
|
|
27
|
+
onRetry?: () => void;
|
|
28
|
+
}): ReactElement;
|
|
29
|
+
/** The device has no connection. `onRetry` adds a "Try again" button, unless `action` replaces it. */
|
|
30
|
+
export declare function Offline({ onRetry, action, ...props }: StatusPageProps & {
|
|
31
|
+
/** Adds a "Try again" button that calls it, unless `action` replaces the button. */
|
|
32
|
+
onRetry?: () => void;
|
|
33
|
+
}): ReactElement;
|
|
34
|
+
/** The site is down on purpose and will be back. */
|
|
35
|
+
export declare function Maintenance(props: StatusPageProps): ReactElement;
|
|
36
|
+
/** 403: signed in, or not, but not allowed here. */
|
|
37
|
+
export declare function Forbidden(props: StatusPageProps): ReactElement;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "./Button.js";
|
|
3
|
+
import { CodeBlock } from "./CodeBlock.js";
|
|
4
|
+
import {} from "./link.js";
|
|
5
|
+
import { CenteredLayout } from "./PageLayouts.js";
|
|
6
|
+
function homeLink(variant, href, render) {
|
|
7
|
+
if (render) {
|
|
8
|
+
return (_jsx(Button, { render: render, variant: variant, children: "Back to home" }));
|
|
9
|
+
}
|
|
10
|
+
if (href === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
return (_jsx(Button, { href: href, variant: variant, children: "Back to home" }));
|
|
13
|
+
}
|
|
14
|
+
function StatusPage({ copy, title, children, action, homeHref, renderHome, extra, ...rest }) {
|
|
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] }))] }));
|
|
17
|
+
}
|
|
18
|
+
const retry = (onRetry) => onRetry && _jsx(Button, { onClick: onRetry, children: "Try again" });
|
|
19
|
+
/** 404: the page is not there. */
|
|
20
|
+
export function NotFound(props) {
|
|
21
|
+
return (_jsx(StatusPage, { copy: {
|
|
22
|
+
title: "Page not found",
|
|
23
|
+
body: "The page you are looking for does not exist or has moved.",
|
|
24
|
+
}, ...props }));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 500 or any failure. `detail` is the error text, shown in a `CodeBlock` under the copy; pass it
|
|
28
|
+
* only where a reader may see it (a request id, a message), never a stack trace. `onRetry` adds a
|
|
29
|
+
* "Try again" button, unless `action` replaces it.
|
|
30
|
+
*/
|
|
31
|
+
export function ErrorPage({ detail, onRetry, action, ...props }) {
|
|
32
|
+
return (_jsx(StatusPage, { copy: {
|
|
33
|
+
title: "Something went wrong",
|
|
34
|
+
body: "An error stopped this page from loading. You can try again.",
|
|
35
|
+
}, action: action ?? retry(onRetry), extra: detail && _jsx(CodeBlock, { label: "Error", children: detail }), ...props }));
|
|
36
|
+
}
|
|
37
|
+
/** The device has no connection. `onRetry` adds a "Try again" button, unless `action` replaces it. */
|
|
38
|
+
export function Offline({ onRetry, action, ...props }) {
|
|
39
|
+
return (_jsx(StatusPage, { copy: {
|
|
40
|
+
title: "You are offline",
|
|
41
|
+
body: "Check your connection and try again.",
|
|
42
|
+
}, action: action ?? retry(onRetry), ...props }));
|
|
43
|
+
}
|
|
44
|
+
/** The site is down on purpose and will be back. */
|
|
45
|
+
export function Maintenance(props) {
|
|
46
|
+
return (_jsx(StatusPage, { copy: {
|
|
47
|
+
title: "Down for maintenance",
|
|
48
|
+
body: "We are making improvements and will be back shortly.",
|
|
49
|
+
}, ...props }));
|
|
50
|
+
}
|
|
51
|
+
/** 403: signed in, or not, but not allowed here. */
|
|
52
|
+
export function Forbidden(props) {
|
|
53
|
+
return (_jsx(StatusPage, { copy: {
|
|
54
|
+
title: "Access denied",
|
|
55
|
+
body: "You do not have permission to view this page.",
|
|
56
|
+
}, ...props }));
|
|
57
|
+
}
|
package/dist/Steps.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { Passthrough } from "./props.js";
|
|
3
|
+
/**
|
|
4
|
+
* A numbered list of things to do, in order: a `title` and, under it, a line of `children`. The
|
|
5
|
+
* numbers are counted by CSS, so `Step`s can sit in a fragment or behind a condition and still run
|
|
6
|
+
* 1, 2, 3. For the way into a product ("install, log in, run"), not for progress: nothing here is
|
|
7
|
+
* done or current. A wizard's progress indicator is a different part (HC-108, `Stepper`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function Steps({ children, ...rest }: {
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
} & Passthrough): ReactElement;
|
|
12
|
+
/** One step of `Steps`: a `title` in medium weight and the `children` as the detail under it. */
|
|
13
|
+
export declare function Step({ title, children, ...rest }: {
|
|
14
|
+
/** What to do, in medium weight; `children` is the detail under it. */
|
|
15
|
+
title: ReactNode;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
} & Passthrough): ReactElement;
|
package/dist/Steps.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A numbered list of things to do, in order: a `title` and, under it, a line of `children`. The
|
|
4
|
+
* numbers are counted by CSS, so `Step`s can sit in a fragment or behind a condition and still run
|
|
5
|
+
* 1, 2, 3. For the way into a product ("install, log in, run"), not for progress: nothing here is
|
|
6
|
+
* done or current. A wizard's progress indicator is a different part (HC-108, `Stepper`).
|
|
7
|
+
*/
|
|
8
|
+
export function Steps({ children, ...rest }) {
|
|
9
|
+
return (_jsx("ol", { role: "list", ...rest, className: "flex list-none flex-col gap-3 text-left [counter-reset:step]", children: children }));
|
|
10
|
+
}
|
|
11
|
+
/** One step of `Steps`: a `title` in medium weight and the `children` as the detail under it. */
|
|
12
|
+
export function Step({ title, children, ...rest }) {
|
|
13
|
+
return (_jsxs("li", { ...rest, className: "grid grid-cols-[1.625rem_1fr] gap-3 [counter-increment:step]", children: [_jsx("span", { "aria-hidden": "true", className: "flex size-[1.625rem] items-center justify-center rounded-full bg-violet-50 text-xs font-semibold tabular-nums text-violet-600 before:content-[counter(step)]" }), _jsxs("div", { className: "min-w-0 pt-0.5", children: [_jsx("div", { className: "text-sm/5 font-medium text-slate-900", children: title }), children != null && _jsx("div", { className: "mt-1 text-sm/5 text-slate-600", children: children })] })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { Passthrough } from "./props.js";
|
|
3
|
+
/**
|
|
4
|
+
* A full-width row that sticks under AppShell's header as the page scrolls: a filter or actions
|
|
5
|
+
* bar, or a viewer's title and its actions. It sits on the surface with a bottom line and pads its
|
|
6
|
+
* content as the Container pads a page. Put a `Cluster` in it for the row's layout. It spans its
|
|
7
|
+
* parent, so it belongs at the top of an `AppShell bleed` page; `FullBleedLayout`'s `bar` is one.
|
|
8
|
+
*/
|
|
9
|
+
export declare function StickyBar({ children, ...rest }: Passthrough & {
|
|
10
|
+
/** The bar's content, usually one `Cluster` (`justify="between"` for a title and its actions). */
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}): ReactElement;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A full-width row that sticks under AppShell's header as the page scrolls: a filter or actions
|
|
4
|
+
* bar, or a viewer's title and its actions. It sits on the surface with a bottom line and pads its
|
|
5
|
+
* content as the Container pads a page. Put a `Cluster` in it for the row's layout. It spans its
|
|
6
|
+
* parent, so it belongs at the top of an `AppShell bleed` page; `FullBleedLayout`'s `bar` is one.
|
|
7
|
+
*/
|
|
8
|
+
export function StickyBar({ children, ...rest }) {
|
|
9
|
+
return (_jsx("div", { ...rest, className: "sticky top-[var(--app-header,0px)] z-5 border-b border-slate-200 bg-white px-4 py-2 sm:px-6", children: children }));
|
|
10
|
+
}
|