@hackcove/ui 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -13
- package/dist/AppShell.d.ts +19 -4
- package/dist/AppShell.js +45 -3
- package/dist/Avatar.d.ts +9 -4
- package/dist/Avatar.js +9 -3
- package/dist/Badge.d.ts +9 -0
- package/dist/Badge.js +10 -0
- package/dist/Breadcrumbs.d.ts +22 -3
- package/dist/Breadcrumbs.js +23 -2
- package/dist/BulletList.d.ts +3 -0
- package/dist/BulletList.js +6 -0
- package/dist/Button.d.ts +18 -8
- package/dist/Button.js +54 -22
- package/dist/ButtonGroup.d.ts +14 -0
- package/dist/ButtonGroup.js +10 -0
- package/dist/ButtonLink.d.ts +20 -0
- package/dist/ButtonLink.js +18 -0
- package/dist/Callout.d.ts +10 -2
- package/dist/Callout.js +8 -3
- package/dist/Checkbox.d.ts +10 -4
- package/dist/Checkbox.js +8 -3
- package/dist/Code.d.ts +3 -0
- package/dist/Code.js +6 -0
- package/dist/ColourSchemeToggle.d.ts +14 -3
- package/dist/ColourSchemeToggle.js +13 -9
- package/dist/Container.d.ts +3 -1
- package/dist/Container.js +2 -2
- package/dist/DateField.d.ts +15 -2
- package/dist/DateField.js +8 -4
- package/dist/DatePicker.d.ts +1 -1
- package/dist/DatePicker.js +3 -3
- package/dist/Divider.d.ts +3 -0
- package/dist/Divider.js +6 -0
- package/dist/Drawer.d.ts +29 -0
- package/dist/Drawer.js +26 -0
- package/dist/FileDrop.d.ts +10 -0
- package/dist/FileDrop.js +11 -0
- package/dist/InlineLinks.d.ts +11 -0
- package/dist/InlineLinks.js +11 -0
- package/dist/Input.d.ts +3 -2
- package/dist/Input.js +5 -4
- package/dist/KeyValueList.d.ts +11 -4
- package/dist/KeyValueList.js +14 -5
- package/dist/ListRow.d.ts +36 -0
- package/dist/ListRow.js +31 -0
- package/dist/Markdown.js +1 -1
- package/dist/NavTabs.d.ts +15 -4
- package/dist/NavTabs.js +13 -2
- package/dist/PageFade.d.ts +25 -0
- package/dist/PageFade.js +39 -0
- package/dist/PageLayouts.d.ts +22 -1
- package/dist/PageLayouts.js +20 -2
- package/dist/ProgressBar.d.ts +26 -0
- package/dist/ProgressBar.js +32 -0
- package/dist/SegmentedControl.d.ts +11 -0
- package/dist/SegmentedControl.js +22 -0
- package/dist/Select.d.ts +3 -2
- package/dist/Select.js +4 -4
- package/dist/SplitBar.d.ts +21 -0
- package/dist/SplitBar.js +15 -0
- package/dist/Stack.d.ts +18 -6
- package/dist/Stack.js +20 -6
- package/dist/StatTile.d.ts +16 -0
- package/dist/StatTile.js +21 -0
- package/dist/Table.d.ts +48 -0
- package/dist/Table.js +42 -0
- package/dist/Tabs.d.ts +13 -0
- package/dist/Tabs.js +23 -0
- package/dist/TextLink.d.ts +27 -0
- package/dist/TextLink.js +23 -0
- package/dist/Textarea.d.ts +3 -2
- package/dist/Textarea.js +4 -4
- package/dist/Timeline.d.ts +21 -0
- package/dist/Timeline.js +19 -0
- package/dist/Video.d.ts +10 -0
- package/dist/Video.js +10 -0
- package/dist/colourScheme.d.ts +11 -2
- package/dist/colourScheme.js +16 -3
- package/dist/control.d.ts +19 -2
- package/dist/control.js +25 -2
- package/dist/index.d.ts +29 -7
- package/dist/index.js +24 -3
- package/dist/link.d.ts +22 -0
- package/dist/link.js +11 -0
- package/dist/switcher.d.ts +66 -0
- package/dist/switcher.js +53 -0
- package/dist/typography.d.ts +32 -24
- package/dist/typography.js +39 -9
- package/dist/ui.d.ts +25 -5
- package/dist/ui.js +51 -7
- package/package.json +19 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement } from "react";
|
|
2
|
+
import { type ButtonLook } from "./Button.js";
|
|
3
|
+
import { type LinkRender } from "./link.js";
|
|
4
|
+
type ButtonLinkProps = ButtonLook & {
|
|
5
|
+
/** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
|
|
6
|
+
external?: boolean | undefined;
|
|
7
|
+
} & Omit<ComponentPropsWithoutRef<"a">, "href"> & ({
|
|
8
|
+
href: string;
|
|
9
|
+
render?: LinkRender | undefined;
|
|
10
|
+
} | {
|
|
11
|
+
href?: string | undefined;
|
|
12
|
+
render: LinkRender;
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* A link that looks like a Button. A plain `<a href>` by default; with `render` it draws the app's
|
|
16
|
+
* own router `<Link>` instead (`render={(p) => <Link to="/quotes" {...p} />}`), already carrying the
|
|
17
|
+
* button classes. `variant`, `size` and `tone` are the Button's.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ButtonLink({ variant, size, tone, href, render, external, className, children, ...rest }: ButtonLinkProps): ReactElement;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buttonClass } from "./Button.js";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
import { renderLink } from "./link.js";
|
|
4
|
+
/**
|
|
5
|
+
* A link that looks like a Button. A plain `<a href>` by default; with `render` it draws the app's
|
|
6
|
+
* own router `<Link>` instead (`render={(p) => <Link to="/quotes" {...p} />}`), already carrying the
|
|
7
|
+
* button classes. `variant`, `size` and `tone` are the Button's.
|
|
8
|
+
*/
|
|
9
|
+
export function ButtonLink({ variant = "primary", size = "md", tone = "default", href, render, external = false, className, children, ...rest }) {
|
|
10
|
+
return renderLink({
|
|
11
|
+
...rest,
|
|
12
|
+
href,
|
|
13
|
+
render,
|
|
14
|
+
external,
|
|
15
|
+
className: cx(buttonClass(variant, { size, tone }), className),
|
|
16
|
+
children,
|
|
17
|
+
});
|
|
18
|
+
}
|
package/dist/Callout.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
|
2
2
|
/** The colour family of a callout. */
|
|
3
3
|
export type CalloutTone = "neutral" | "info" | "positive" | "warn" | "danger";
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* A tinted notice box, with an optional bold title above its body. `actions` puts a button or link
|
|
6
|
+
* at the end of the row; `banner` makes it a flat strip (bottom border only, no radius) for the
|
|
7
|
+
* top of a `Card flush` (a padded card would inset it).
|
|
8
|
+
*/
|
|
9
|
+
export declare function Callout({ tone, title, actions, banner, children, className, ...rest }: {
|
|
6
10
|
/** Defaults to "info". */
|
|
7
11
|
tone?: CalloutTone;
|
|
8
12
|
title?: ReactNode;
|
|
13
|
+
/** A button or link at the end of the row, beside the title and body. */
|
|
14
|
+
actions?: ReactNode;
|
|
15
|
+
/** A flat strip: `border-b` only and no radius, to sit above a card body. */
|
|
16
|
+
banner?: boolean;
|
|
9
17
|
children: ReactNode;
|
|
10
18
|
} & Omit<HTMLAttributes<HTMLDivElement>, "title">): ReactElement;
|
package/dist/Callout.js
CHANGED
|
@@ -7,7 +7,12 @@ const TONES = {
|
|
|
7
7
|
warn: "border-amber-200 bg-amber-50 text-amber-800",
|
|
8
8
|
danger: "border-red-200 bg-red-50 text-red-800",
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A tinted notice box, with an optional bold title above its body. `actions` puts a button or link
|
|
12
|
+
* at the end of the row; `banner` makes it a flat strip (bottom border only, no radius) for the
|
|
13
|
+
* top of a `Card flush` (a padded card would inset it).
|
|
14
|
+
*/
|
|
15
|
+
export function Callout({ tone = "info", title, actions, banner = false, children, className, ...rest }) {
|
|
16
|
+
const content = title ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "font-semibold", children: title }), _jsx("div", { className: "mt-1", children: children })] })) : (children);
|
|
17
|
+
return (_jsx("div", { className: cx(banner ? "border-b px-5 py-3 text-sm" : "rounded-xl border p-4 text-sm", TONES[tone], className), ...rest, children: actions ? (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("div", { className: "min-w-0", children: content }), _jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })] })) : (content) }));
|
|
13
18
|
}
|
package/dist/Checkbox.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import type { ControlSize } from "./control.js";
|
|
3
|
+
/**
|
|
4
|
+
* A native checkbox with its label. `className` goes on the label, every other prop on the input.
|
|
5
|
+
* `size="sm"` is `text-xs` and a smaller box, for a dense row. Leave `label` out for a bare box in
|
|
6
|
+
* a table cell, and name it with `aria-label`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Checkbox({ label, size, className, ...rest }: {
|
|
9
|
+
label?: ReactNode;
|
|
10
|
+
size?: ControlSize | undefined;
|
|
11
|
+
} & Omit<ComponentPropsWithoutRef<"input">, "type" | "size" | "width">): ReactElement;
|
package/dist/Checkbox.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx, focusRing } from "./cx.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* A native checkbox with its label. `className` goes on the label, every other prop on the input.
|
|
5
|
+
* `size="sm"` is `text-xs` and a smaller box, for a dense row. Leave `label` out for a bare box in
|
|
6
|
+
* a table cell, and name it with `aria-label`.
|
|
7
|
+
*/
|
|
8
|
+
export function Checkbox({ label, size = "md", className, ...rest }) {
|
|
9
|
+
const small = size === "sm";
|
|
10
|
+
return (_jsxs("label", { className: cx("inline-flex cursor-pointer items-center text-slate-700", small ? "gap-1.5 text-xs" : "gap-2 text-sm", className), children: [_jsx("input", { type: "checkbox", className: cx(small ? "h-3.5 w-3.5" : "h-4 w-4", "accent-violet-600", focusRing), ...rest }), label] }));
|
|
6
11
|
}
|
package/dist/Code.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
/** Inline code: mono, on a subtle inset background. It takes the surrounding text colour and size (a touch smaller, for the mono face). */
|
|
3
|
+
export declare function Code({ className, ...rest }: HTMLAttributes<HTMLElement>): ReactElement;
|
package/dist/Code.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** Inline code: mono, on a subtle inset background. It takes the surrounding text colour and size (a touch smaller, for the mono face). */
|
|
4
|
+
export function Code({ className, ...rest }) {
|
|
5
|
+
return (_jsx("code", { className: cx("rounded bg-slate-100 px-1 py-0.5 font-mono text-[0.9em]", className), ...rest }));
|
|
6
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
|
-
import { type ColourScheme } from "./colourScheme.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
2
|
+
import { type ColourScheme, type ColourSchemeClasses } from "./colourScheme.js";
|
|
3
|
+
/**
|
|
4
|
+
* A sun/moon button that switches `<html>` between light and dark and remembers the choice. It shows the scheme you would switch to. Pair it with `colourSchemeScript` in `<head>`, unless you pass `schemes` or `initial`: the script knows only the `dark` class and the stored choice, so an app that sets the scheme itself leaves it out.
|
|
5
|
+
*
|
|
6
|
+
* An app whose light scheme has a class of its own passes `schemes` (`{ light: "paper", dark: "dark" }`);
|
|
7
|
+
* the toggle swaps those classes, and `schemeClass` gives the same one for the server to render on `<html>`.
|
|
8
|
+
* An app that knows the scheme on the server (from a cookie) passes it as `initial`, so the server
|
|
9
|
+
* markup, the hydrating client and the `<html>` class agree; persist a change from `onChange`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ColourSchemeToggle({ className, onChange, schemes, initial, }: {
|
|
5
12
|
className?: string;
|
|
6
13
|
onChange?: (scheme: ColourScheme) => void;
|
|
14
|
+
/** The class each scheme puts on `<html>`. Default `{ light: "", dark: "dark" }`. */
|
|
15
|
+
schemes?: ColourSchemeClasses | undefined;
|
|
16
|
+
/** The scheme the server rendered. What the server and the hydrating client show; the real state follows right after. Default `"light"`. */
|
|
17
|
+
initial?: ColourScheme | undefined;
|
|
7
18
|
}): ReactElement;
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useSyncExternalStore } from "react";
|
|
3
|
-
import { applyColourScheme, storeColourScheme } from "./colourScheme.js";
|
|
3
|
+
import { appliedColourScheme, applyColourScheme, storeColourScheme, } from "./colourScheme.js";
|
|
4
4
|
import { IconButton } from "./IconButton.js";
|
|
5
5
|
const sun = (_jsxs("svg", { viewBox: "0 0 24 24", className: "h-4 w-4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "4" }), _jsx("path", { d: "M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" })] }));
|
|
6
6
|
const moon = (_jsx("svg", { viewBox: "0 0 24 24", className: "h-4 w-4", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" }) }));
|
|
7
|
-
// The
|
|
7
|
+
// The scheme's class on <html> is the source of truth: the head script or the server set it before React ran.
|
|
8
8
|
function subscribe(onChange) {
|
|
9
9
|
const observer = new MutationObserver(onChange);
|
|
10
10
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
11
11
|
return () => observer.disconnect();
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
/**
|
|
14
|
+
* A sun/moon button that switches `<html>` between light and dark and remembers the choice. It shows the scheme you would switch to. Pair it with `colourSchemeScript` in `<head>`, unless you pass `schemes` or `initial`: the script knows only the `dark` class and the stored choice, so an app that sets the scheme itself leaves it out.
|
|
15
|
+
*
|
|
16
|
+
* An app whose light scheme has a class of its own passes `schemes` (`{ light: "paper", dark: "dark" }`);
|
|
17
|
+
* the toggle swaps those classes, and `schemeClass` gives the same one for the server to render on `<html>`.
|
|
18
|
+
* An app that knows the scheme on the server (from a cookie) passes it as `initial`, so the server
|
|
19
|
+
* markup, the hydrating client and the `<html>` class agree; persist a change from `onChange`.
|
|
20
|
+
*/
|
|
21
|
+
export function ColourSchemeToggle({ className, onChange, schemes = { light: "", dark: "dark" }, initial, }) {
|
|
22
|
+
const scheme = useSyncExternalStore(subscribe, () => appliedColourScheme(document.documentElement, schemes), () => initial ?? "light");
|
|
19
23
|
const next = scheme === "dark" ? "light" : "dark";
|
|
20
24
|
return (_jsx(IconButton, { label: `Switch to ${next} theme`, className: className, onClick: () => {
|
|
21
|
-
applyColourScheme(next);
|
|
25
|
+
applyColourScheme(next, document.documentElement, schemes);
|
|
22
26
|
storeColourScheme(next);
|
|
23
27
|
onChange?.(next);
|
|
24
28
|
}, children: next === "light" ? sun : moon }));
|
package/dist/Container.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type { ReactElement, ReactNode } from "react";
|
|
|
3
3
|
* The one content edge: every page's width and side padding. This file is the only place they are
|
|
4
4
|
* defined, so a header, a page and a footer inside a Container start at the same x.
|
|
5
5
|
*/
|
|
6
|
-
export declare function Container({ as: Tag, className, children, }: {
|
|
6
|
+
export declare function Container({ as: Tag, className, elementRef, children, }: {
|
|
7
7
|
as?: "div" | "main" | "header" | "footer" | "section" | "nav";
|
|
8
8
|
className?: string;
|
|
9
|
+
/** The rendered element, for a parent that measures or scrolls it. */
|
|
10
|
+
elementRef?: (element: HTMLElement | null) => void;
|
|
9
11
|
children?: ReactNode;
|
|
10
12
|
}): ReactElement;
|
package/dist/Container.js
CHANGED
|
@@ -4,6 +4,6 @@ import { cx } from "./cx.js";
|
|
|
4
4
|
* The one content edge: every page's width and side padding. This file is the only place they are
|
|
5
5
|
* defined, so a header, a page and a footer inside a Container start at the same x.
|
|
6
6
|
*/
|
|
7
|
-
export function Container({ as: Tag = "div", className, children, }) {
|
|
8
|
-
return _jsx(Tag, { className: cx("mx-auto w-full max-w-6xl px-4 sm:px-6", className), children: children });
|
|
7
|
+
export function Container({ as: Tag = "div", className, elementRef, children, }) {
|
|
8
|
+
return (_jsx(Tag, { ref: elementRef, className: cx("mx-auto w-full max-w-6xl px-4 sm:px-6", className), children: children }));
|
|
9
9
|
}
|
package/dist/DateField.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ export type DateFieldProps = {
|
|
|
14
14
|
description?: ReactNode;
|
|
15
15
|
/** Replaces the built-in error message (an impossible date, or one outside min/max). */
|
|
16
16
|
errorMessage?: ReactNode;
|
|
17
|
+
/** An error from outside the field (a server's, say). Shown in place of the description, in red with `role="alert"`, and marks the field invalid. Like `Field`'s `error`. */
|
|
18
|
+
error?: ReactNode;
|
|
19
|
+
/** Marks the field invalid with no message of its own; pair it with `errorMessage`. */
|
|
20
|
+
isInvalid?: boolean;
|
|
17
21
|
className?: string;
|
|
18
22
|
} & Pick<AriaDateFieldProps<CalendarDate>, "value" | "defaultValue" | "onChange" | "minValue" | "maxValue" | "isDisabled" | "isRequired" | "name" | "aria-label">;
|
|
19
23
|
/** What the segments show, as the day segment reports it to the field. */
|
|
@@ -35,15 +39,24 @@ export declare function frameClass({ isInvalid, isFocusWithin, isDisabled, }: {
|
|
|
35
39
|
isFocusWithin: boolean;
|
|
36
40
|
isDisabled: boolean;
|
|
37
41
|
}): string;
|
|
42
|
+
/** The props that force a field invalid: an impossible date, `isInvalid`, or an `error` from outside. Spread onto the React Aria field after the rest. */
|
|
43
|
+
export declare function forcedInvalid({ impossible, isInvalid, error, }: {
|
|
44
|
+
impossible: string | null;
|
|
45
|
+
isInvalid: boolean | undefined;
|
|
46
|
+
error: ReactNode;
|
|
47
|
+
}): {
|
|
48
|
+
isInvalid?: true;
|
|
49
|
+
};
|
|
38
50
|
/** The label, the description or the error line: the shared outside of both components. */
|
|
39
|
-
export declare function Notes({ description, errorMessage, impossible, isInvalid, }: {
|
|
51
|
+
export declare function Notes({ description, errorMessage, error, impossible, isInvalid, }: {
|
|
40
52
|
description: ReactNode;
|
|
41
53
|
errorMessage: ReactNode;
|
|
54
|
+
error: ReactNode;
|
|
42
55
|
impossible: string | null;
|
|
43
56
|
isInvalid: boolean;
|
|
44
57
|
}): ReactElement;
|
|
45
58
|
export declare const labelClass = "text-sm font-medium text-slate-700";
|
|
46
59
|
export declare const iconButtonClass = "inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-slate-500 transition hover:bg-slate-100 disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600";
|
|
47
60
|
/** A segmented DD / MM / YYYY date input. Values are `CalendarDate`s; `name` posts it as `YYYY-MM-DD`. */
|
|
48
|
-
export declare function DateField({ label, description, errorMessage, className, value, defaultValue, onChange, name, ...rest }: DateFieldProps): ReactElement;
|
|
61
|
+
export declare function DateField({ label, description, errorMessage, error, isInvalid: invalidProp, className, value, defaultValue, onChange, name, ...rest }: DateFieldProps): ReactElement;
|
|
49
62
|
export {};
|
package/dist/DateField.js
CHANGED
|
@@ -57,14 +57,18 @@ export function frameClass({ isInvalid, isFocusWithin, isDisabled, }) {
|
|
|
57
57
|
tone = "border-violet-600 bg-white text-slate-900";
|
|
58
58
|
return cx("flex w-full items-center rounded-lg border py-2 pl-2.5 text-sm transition", tone, isDisabled && "opacity-50");
|
|
59
59
|
}
|
|
60
|
+
/** The props that force a field invalid: an impossible date, `isInvalid`, or an `error` from outside. Spread onto the React Aria field after the rest. */
|
|
61
|
+
export function forcedInvalid({ impossible, isInvalid, error, }) {
|
|
62
|
+
return impossible || isInvalid || error ? { isInvalid: true } : {};
|
|
63
|
+
}
|
|
60
64
|
/** The label, the description or the error line: the shared outside of both components. */
|
|
61
|
-
export function Notes({ description, errorMessage, impossible, isInvalid, }) {
|
|
62
|
-
return (_jsxs(_Fragment, { children: [!isInvalid && description ? (_jsx(Text, { slot: "description", className: "text-xs text-slate-500", children: description })) : null, _jsx(FieldError, { className: "text-xs text-red-700", children: ({ validationErrors }) => errorMessage ?? impossible ?? validationErrors.join(" ") })] }));
|
|
65
|
+
export function Notes({ description, errorMessage, error, impossible, isInvalid, }) {
|
|
66
|
+
return (_jsxs(_Fragment, { children: [!isInvalid && description ? (_jsx(Text, { slot: "description", className: "text-xs text-slate-500", children: description })) : null, _jsx(FieldError, { className: "text-xs text-red-700", children: ({ validationErrors }) => error ? (_jsx("span", { role: "alert", children: error })) : ((errorMessage ?? impossible ?? validationErrors.join(" "))) })] }));
|
|
63
67
|
}
|
|
64
68
|
export const labelClass = "text-sm font-medium text-slate-700";
|
|
65
69
|
export const iconButtonClass = `inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-slate-500 transition hover:bg-slate-100 disabled:opacity-50 ${focusRing}`;
|
|
66
70
|
/** A segmented DD / MM / YYYY date input. Values are `CalendarDate`s; `name` posts it as `YYYY-MM-DD`. */
|
|
67
|
-
export function DateField({ label, description, errorMessage, className, value, defaultValue, onChange, name, ...rest }) {
|
|
71
|
+
export function DateField({ label, description, errorMessage, error, isInvalid: invalidProp, className, value, defaultValue, onChange, name, ...rest }) {
|
|
68
72
|
const { inner, setInner, shown, impossible, onRead, validate, leaving, onBlurCapture, onLeft } = useShownDate({
|
|
69
73
|
value,
|
|
70
74
|
defaultValue,
|
|
@@ -73,5 +77,5 @@ export function DateField({ label, description, errorMessage, className, value,
|
|
|
73
77
|
maxValue: rest.maxValue,
|
|
74
78
|
isRequired: rest.isRequired,
|
|
75
79
|
});
|
|
76
|
-
return (_jsx(I18nProvider, { locale: LOCALE, children: _jsx(AriaDateField, { ...rest, value: inner, onChange: setInner, validate: validate, ...(impossible
|
|
80
|
+
return (_jsx(I18nProvider, { locale: LOCALE, children: _jsx(AriaDateField, { ...rest, value: inner, onChange: setInner, validate: validate, ...forcedInvalid({ impossible, isInvalid: invalidProp, error }), isReadOnly: leaving, onBlur: onLeft, granularity: "day", shouldForceLeadingZeros: true, className: cx("flex flex-col gap-1.5", className), children: ({ isInvalid }) => (_jsxs(_Fragment, { children: [label ? _jsx(Label, { className: labelClass, children: label }) : null, _jsx("div", { className: "contents", onBlurCapture: onBlurCapture, children: _jsx(DateInput, { className: (state) => cx(frameClass(state), "pr-2.5"), children: (segment) => (_jsx(FieldSegment, { segment: segment, ...(segment.type === "day" ? { onRead } : {}) })) }) }), _jsx(ShownInput, { name: name, shown: shown, isDisabled: rest.isDisabled }), _jsx(Notes, { description: description, errorMessage: errorMessage, error: error, impossible: impossible, isInvalid: isInvalid })] })) }) }));
|
|
77
81
|
}
|
package/dist/DatePicker.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export type DatePickerProps = DateFieldProps & {
|
|
|
5
5
|
defaultOpen?: boolean;
|
|
6
6
|
};
|
|
7
7
|
/** A `DateField` with a calendar button that opens a month view in a popover. */
|
|
8
|
-
export declare function DatePicker({ label, description, errorMessage, className, value, defaultValue, onChange, name, ...rest }: DatePickerProps): ReactElement;
|
|
8
|
+
export declare function DatePicker({ label, description, errorMessage, error, isInvalid: invalidProp, className, value, defaultValue, onChange, name, ...rest }: DatePickerProps): ReactElement;
|
package/dist/DatePicker.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useContext } from "react";
|
|
|
3
3
|
import { Button, DatePicker as AriaDatePicker, DateFieldContext, DateInput, Group, I18nProvider, Label, } from "react-aria-components";
|
|
4
4
|
import { cx } from "./cx.js";
|
|
5
5
|
import { CalendarPopover, calendarIcon } from "./DateCalendar.js";
|
|
6
|
-
import { FieldSegment, LOCALE, Notes, frameClass, iconButtonClass, labelClass, } from "./DateField.js";
|
|
6
|
+
import { FieldSegment, LOCALE, forcedInvalid, Notes, frameClass, iconButtonClass, labelClass, } from "./DateField.js";
|
|
7
7
|
import { ShownInput, useShownDate } from "./shownDate.js";
|
|
8
8
|
/**
|
|
9
9
|
* The picker's input, read-only while `leaving`. Only the field: a read-only picker would also
|
|
@@ -14,7 +14,7 @@ function PickerInput({ leaving, onLeft, children, }) {
|
|
|
14
14
|
return (_jsx(DateFieldContext.Provider, { value: { ...field, isReadOnly: leaving, onBlur: onLeft }, children: children }));
|
|
15
15
|
}
|
|
16
16
|
/** A `DateField` with a calendar button that opens a month view in a popover. */
|
|
17
|
-
export function DatePicker({ label, description, errorMessage, className, value, defaultValue, onChange, name, ...rest }) {
|
|
17
|
+
export function DatePicker({ label, description, errorMessage, error, isInvalid: invalidProp, className, value, defaultValue, onChange, name, ...rest }) {
|
|
18
18
|
const { inner, setInner, shown, impossible, onRead, validate, leaving, onBlurCapture, onLeft } = useShownDate({
|
|
19
19
|
value,
|
|
20
20
|
defaultValue,
|
|
@@ -23,5 +23,5 @@ export function DatePicker({ label, description, errorMessage, className, value,
|
|
|
23
23
|
maxValue: rest.maxValue,
|
|
24
24
|
isRequired: rest.isRequired,
|
|
25
25
|
});
|
|
26
|
-
return (_jsx(I18nProvider, { locale: LOCALE, children: _jsx(AriaDatePicker, { ...rest, value: inner, onChange: setInner, validate: validate, ...(impossible
|
|
26
|
+
return (_jsx(I18nProvider, { locale: LOCALE, children: _jsx(AriaDatePicker, { ...rest, value: inner, onChange: setInner, validate: validate, ...forcedInvalid({ impossible, isInvalid: invalidProp, error }), granularity: "day", shouldForceLeadingZeros: true, className: cx("flex flex-col gap-1.5", className), children: ({ isInvalid }) => (_jsxs(_Fragment, { children: [label ? _jsx(Label, { className: labelClass, children: label }) : null, _jsxs(Group, { className: (state) => cx(frameClass(state), "gap-1 pr-1"), children: [_jsx("div", { className: "contents", onBlurCapture: onBlurCapture, children: _jsx(PickerInput, { leaving: leaving, onLeft: onLeft, children: _jsx(DateInput, { className: "flex flex-1", children: (segment) => (_jsx(FieldSegment, { segment: segment, ...(segment.type === "day" ? { onRead } : {}) })) }) }) }), _jsx(Button, { className: iconButtonClass, children: calendarIcon })] }), _jsx(ShownInput, { name: name, shown: shown, isDisabled: rest.isDisabled }), _jsx(Notes, { description: description, errorMessage: errorMessage, error: error, impossible: impossible, isInvalid: isInvalid }), _jsx(CalendarPopover, {})] })) }) }));
|
|
27
27
|
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement } from "react";
|
|
2
|
+
/** A hairline rule in the theme's line colour: between a heading and its content, or above a footer. Spacing is the caller's, on `className`. */
|
|
3
|
+
export declare function Divider({ className, ...rest }: Omit<HTMLAttributes<HTMLHRElement>, "children">): ReactElement;
|
package/dist/Divider.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/** A hairline rule in the theme's line colour: between a heading and its content, or above a footer. Spacing is the caller's, on `className`. */
|
|
4
|
+
export function Divider({ className, ...rest }) {
|
|
5
|
+
return _jsx("hr", { className: cx("border-t border-slate-200/70", className), ...rest });
|
|
6
|
+
}
|
package/dist/Drawer.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
declare const SIZE: {
|
|
3
|
+
readonly sm: "w-80";
|
|
4
|
+
readonly md: "w-110";
|
|
5
|
+
readonly lg: "w-160";
|
|
6
|
+
};
|
|
7
|
+
export type DrawerProps = {
|
|
8
|
+
/** The drawer's accessible name, shown as its heading. */
|
|
9
|
+
title: ReactNode;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
/** Actions pinned under the scrolling body, right-aligned. */
|
|
12
|
+
footer?: ReactNode;
|
|
13
|
+
/** `md` (the default) holds a form or a record; `sm` is narrower and `lg` wider. */
|
|
14
|
+
size?: keyof typeof SIZE;
|
|
15
|
+
/** Called when the drawer closes: Esc, a click on the scrim or the close button. */
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
/** The accessible name of the close button. */
|
|
18
|
+
closeLabel?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A modal slide-over anchored to the right edge, on the browser's own `<dialog>`: it is opened
|
|
23
|
+
* with `showModal()` on mount, so render it only while it is open. The page behind goes inert, Esc
|
|
24
|
+
* closes it and focus stays inside; a click on the scrim (the `<dialog>` element itself, not its
|
|
25
|
+
* contents) closes it too. The header holds the title and a close button; the body scrolls between
|
|
26
|
+
* it and an optional pinned `footer`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function Drawer({ title, children, footer, size, onClose, closeLabel, className, }: DrawerProps): ReactElement;
|
|
29
|
+
export {};
|
package/dist/Drawer.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useId, useRef } from "react";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
import { IconButton } from "./IconButton.js";
|
|
5
|
+
import { ModalHeading } from "./typography.js";
|
|
6
|
+
const PANEL = "fixed inset-y-0 right-0 left-auto m-0 h-dvh max-h-none max-w-full flex-col overflow-hidden border-l border-slate-200 bg-white p-0 text-slate-900 shadow-2xl backdrop:bg-slate-900/35 open:flex transition-transform duration-200 starting:open:translate-x-full motion-reduce:transition-none";
|
|
7
|
+
const SIZE = { sm: "w-80", md: "w-110", lg: "w-160" };
|
|
8
|
+
/**
|
|
9
|
+
* A modal slide-over anchored to the right edge, on the browser's own `<dialog>`: it is opened
|
|
10
|
+
* with `showModal()` on mount, so render it only while it is open. The page behind goes inert, Esc
|
|
11
|
+
* closes it and focus stays inside; a click on the scrim (the `<dialog>` element itself, not its
|
|
12
|
+
* contents) closes it too. The header holds the title and a close button; the body scrolls between
|
|
13
|
+
* it and an optional pinned `footer`.
|
|
14
|
+
*/
|
|
15
|
+
export function Drawer({ title, children, footer, size = "md", onClose, closeLabel = "Close", className, }) {
|
|
16
|
+
const dialog = useRef(null);
|
|
17
|
+
const titleId = useId();
|
|
18
|
+
// Closing on unmount (not just on Esc or the scrim) hands focus back to the opener when the
|
|
19
|
+
// parent unmounts the drawer, and keeps StrictMode's second mount from reopening an open dialog.
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
const el = dialog.current;
|
|
22
|
+
el?.showModal();
|
|
23
|
+
return () => el?.close();
|
|
24
|
+
}, []);
|
|
25
|
+
return (_jsxs("dialog", { ref: dialog, "aria-labelledby": titleId, onClose: onClose, onClick: (event) => event.target === dialog.current && dialog.current?.close(), className: cx(PANEL, SIZE[size], className), children: [_jsxs("header", { className: "flex shrink-0 items-center justify-between gap-3 border-b border-slate-200 px-5.5 py-3.5", children: [_jsx(ModalHeading, { id: titleId, children: title }), _jsx(IconButton, { label: closeLabel, onClick: () => dialog.current?.close(), children: _jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", className: "h-4 w-4", 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" }) }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-5.5 py-4", children: children }), footer && (_jsx("footer", { className: "flex shrink-0 justify-end gap-2 border-t border-slate-200 px-5.5 py-3.5", children: footer }))] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A dashed drop zone around a file input. The input covers the whole zone, invisible, so a click
|
|
4
|
+
* opens the file dialog and a file dragged onto it is taken by the browser as it is by any file
|
|
5
|
+
* input. `children` is the prompt; show the chosen file's name there from `onChange`. `className`
|
|
6
|
+
* goes on the label (which also shows the keyboard focus ring, the input being invisible), every other prop (`accept`, `multiple`, `name`, `disabled`, `onChange`) on the input.
|
|
7
|
+
*/
|
|
8
|
+
export declare function FileDrop({ children, className, ...rest }: {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
} & Omit<ComponentPropsWithoutRef<"input">, "type" | "children" | "size" | "width">): ReactElement;
|
package/dist/FileDrop.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "./cx.js";
|
|
3
|
+
/**
|
|
4
|
+
* A dashed drop zone around a file input. The input covers the whole zone, invisible, so a click
|
|
5
|
+
* opens the file dialog and a file dragged onto it is taken by the browser as it is by any file
|
|
6
|
+
* input. `children` is the prompt; show the chosen file's name there from `onChange`. `className`
|
|
7
|
+
* goes on the label (which also shows the keyboard focus ring, the input being invisible), every other prop (`accept`, `multiple`, `name`, `disabled`, `onChange`) on the input.
|
|
8
|
+
*/
|
|
9
|
+
export function FileDrop({ children, className, ...rest }) {
|
|
10
|
+
return (_jsxs("label", { className: cx("relative flex items-center justify-center rounded-lg border border-dashed border-slate-300 px-4 py-6 text-center text-sm text-slate-500 transition hover:border-violet-400 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-violet-600", rest.disabled && "opacity-50", className), children: [_jsx("input", { type: "file", className: "absolute inset-0 h-full w-full cursor-pointer rounded-lg opacity-0 disabled:cursor-not-allowed", ...rest }), children] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
export type InlineLinksSeparator = "·" | "•" | "none";
|
|
3
|
+
/**
|
|
4
|
+
* A wrapping row of links, small text, joined by a separator. The separator is `aria-hidden` and
|
|
5
|
+
* leads each link but the first, held to its link so a wrap never leaves one at a line's end.
|
|
6
|
+
*/
|
|
7
|
+
export declare function InlineLinks({ separator, className, children, }: {
|
|
8
|
+
separator?: InlineLinksSeparator;
|
|
9
|
+
className?: string;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}): ReactElement;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children } from "react";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
import { Caption } from "./typography.js";
|
|
5
|
+
/**
|
|
6
|
+
* A wrapping row of links, small text, joined by a separator. The separator is `aria-hidden` and
|
|
7
|
+
* leads each link but the first, held to its link so a wrap never leaves one at a line's end.
|
|
8
|
+
*/
|
|
9
|
+
export function InlineLinks({ separator = "·", className, children, }) {
|
|
10
|
+
return (_jsx("div", { className: cx("flex flex-wrap items-center gap-x-3 gap-y-1 text-sm", className), children: Children.toArray(children).map((child, index) => (_jsxs("span", { className: "inline-flex items-center gap-x-3 whitespace-nowrap", children: [index > 0 && separator !== "none" && _jsx(Caption, { "aria-hidden": "true", children: separator }), child] }, index))) }));
|
|
11
|
+
}
|
package/dist/Input.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
import { type ControlProps } from "./control.js";
|
|
2
3
|
/** A text input. `icon` puts a leading icon inside the field (e.g. a search glass). */
|
|
3
|
-
export declare function Input({ icon, className, ...rest }: {
|
|
4
|
+
export declare function Input({ icon, size, width, className, ...rest }: {
|
|
4
5
|
icon?: ReactNode;
|
|
5
|
-
} & ComponentPropsWithoutRef<"input">): ReactElement;
|
|
6
|
+
} & ControlProps & Omit<ComponentPropsWithoutRef<"input">, "size" | "width">): ReactElement;
|
package/dist/Input.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { controlClass } from "./control.js";
|
|
2
|
+
import { CONTROL_PAD, CONTROL_WIDTH, controlClass } from "./control.js";
|
|
3
3
|
import { cx } from "./cx.js";
|
|
4
4
|
/** A text input. `icon` puts a leading icon inside the field (e.g. a search glass). */
|
|
5
|
-
export function Input({ icon, className, ...rest }) {
|
|
5
|
+
export function Input({ icon, size = "md", width = "full", className, ...rest }) {
|
|
6
6
|
if (icon === undefined) {
|
|
7
|
-
return _jsx("input", { ...rest, className: cx(controlClass,
|
|
7
|
+
return (_jsx("input", { ...rest, className: cx(controlClass(size, width), CONTROL_PAD[size].x, className) }));
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
const small = size === "sm";
|
|
10
|
+
return (_jsxs("div", { className: cx("relative", width === "auto" && "inline-block", width !== "full" && CONTROL_WIDTH[width]), children: [_jsx("span", { "aria-hidden": "true", className: cx("pointer-events-none absolute top-1/2 -translate-y-1/2 text-slate-400", small ? "left-2 [&>svg]:h-3.5 [&>svg]:w-3.5" : "left-3 [&>svg]:h-4 [&>svg]:w-4"), children: icon }), _jsx("input", { ...rest, className: cx(controlClass(size, width === "auto" ? "auto" : "full"), CONTROL_PAD[size].icon, className) })] }));
|
|
10
11
|
}
|
package/dist/KeyValueList.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { type HTMLAttributes, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
type KeyValueAlign = "end" | "start";
|
|
3
|
+
/**
|
|
4
|
+
* A definition list of label/value rows. By default each label is on the left and its value
|
|
5
|
+
* right-aligned. `align="start"` puts every value left-aligned in one column beside the labels,
|
|
6
|
+
* which are sized to the widest.
|
|
7
|
+
*/
|
|
8
|
+
export declare function KeyValueList({ align, className, children, ...rest }: {
|
|
9
|
+
align?: KeyValueAlign;
|
|
4
10
|
children: ReactNode;
|
|
5
11
|
} & HTMLAttributes<HTMLDListElement>): ReactElement;
|
|
6
|
-
/** One row of a KeyValueList: a label on the left, a
|
|
12
|
+
/** One row of a KeyValueList: a label on the left, a tabular value on the right (or beside it, in an `align="start"` list). */
|
|
7
13
|
export declare function KeyValue({ label, children, className, ...rest }: {
|
|
8
14
|
label: ReactNode;
|
|
9
15
|
children: ReactNode;
|
|
10
16
|
} & HTMLAttributes<HTMLDivElement>): ReactElement;
|
|
17
|
+
export {};
|
package/dist/KeyValueList.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, } from "react";
|
|
2
3
|
import { cx } from "./cx.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const AlignContext = createContext("end");
|
|
5
|
+
/**
|
|
6
|
+
* A definition list of label/value rows. By default each label is on the left and its value
|
|
7
|
+
* right-aligned. `align="start"` puts every value left-aligned in one column beside the labels,
|
|
8
|
+
* which are sized to the widest.
|
|
9
|
+
*/
|
|
10
|
+
export function KeyValueList({ align = "end", className, children, ...rest }) {
|
|
11
|
+
return (_jsx("dl", { className: cx(align === "start"
|
|
12
|
+
? "grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1.5"
|
|
13
|
+
: "flex flex-col gap-1.5", "text-sm", className), ...rest, children: _jsx(AlignContext.Provider, { value: align, children: children }) }));
|
|
6
14
|
}
|
|
7
|
-
/** One row of a KeyValueList: a label on the left, a
|
|
15
|
+
/** One row of a KeyValueList: a label on the left, a tabular value on the right (or beside it, in an `align="start"` list). */
|
|
8
16
|
export function KeyValue({ label, children, className, ...rest }) {
|
|
9
|
-
|
|
17
|
+
const start = useContext(AlignContext) === "start";
|
|
18
|
+
return (_jsxs("div", { className: cx(start ? "col-span-2 grid grid-cols-subgrid" : "flex justify-between gap-3", className), ...rest, children: [_jsx("dt", { className: "text-slate-600", children: label }), _jsx("dd", { className: start ? "tabular-nums" : "text-right tabular-nums", children: children })] }));
|
|
10
19
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
import { type LinkRender } from "./link.js";
|
|
3
|
+
type Shared = {
|
|
4
|
+
/** Content at the far end of the row: a value, a count, a Pill. */
|
|
5
|
+
trailing?: ReactNode | undefined;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
type ListRowButtonProps = Shared & {
|
|
9
|
+
/**
|
|
10
|
+
* Makes the row a collapsible header: sets `aria-expanded` and shows a chevron that turns down
|
|
11
|
+
* when open. The caller owns the state and the section; pass `aria-controls` to name it.
|
|
12
|
+
*/
|
|
13
|
+
expanded?: boolean | undefined;
|
|
14
|
+
href?: undefined;
|
|
15
|
+
render?: undefined;
|
|
16
|
+
} & Omit<ComponentPropsWithoutRef<"button">, "children">;
|
|
17
|
+
type ListRowLinkProps = Shared & {
|
|
18
|
+
/** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
|
|
19
|
+
external?: boolean | undefined;
|
|
20
|
+
expanded?: undefined;
|
|
21
|
+
} & Omit<ComponentPropsWithoutRef<"a">, "href" | "children"> & ({
|
|
22
|
+
href: string;
|
|
23
|
+
render?: LinkRender | undefined;
|
|
24
|
+
} | {
|
|
25
|
+
href?: string | undefined;
|
|
26
|
+
render: LinkRender;
|
|
27
|
+
});
|
|
28
|
+
export type ListRowProps = ListRowButtonProps | ListRowLinkProps;
|
|
29
|
+
/**
|
|
30
|
+
* A full-width row that is a `<button>` (or, with `href` or `render`, a link) with a hover fill, a
|
|
31
|
+
* left-aligned label and `trailing` content at the far end. With `expanded` it is the header of a
|
|
32
|
+
* collapsible section: `aria-expanded` and a chevron. Rows sit in a `Card flush`; lines between
|
|
33
|
+
* them are the caller's (`border-t border-slate-200/70` on each row after the first).
|
|
34
|
+
*/
|
|
35
|
+
export declare function ListRow(props: ListRowProps): ReactElement;
|
|
36
|
+
export {};
|
package/dist/ListRow.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
import { renderLink } from "./link.js";
|
|
4
|
+
const ROW = `flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-slate-900 hover:bg-slate-50 disabled:pointer-events-none disabled:opacity-50 ${focusRing} focus-visible:-outline-offset-2`;
|
|
5
|
+
function Chevron({ open }) {
|
|
6
|
+
return (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 20 20", fill: "currentColor", className: cx("h-4 w-4 shrink-0 text-slate-400", open && "rotate-90"), children: _jsx("path", { fillRule: "evenodd", d: "M7.2 4.8a.75.75 0 0 1 1.06 0l4.7 4.67a.75.75 0 0 1 0 1.06l-4.7 4.67a.75.75 0 1 1-1.06-1.06L11.37 10 7.2 5.86a.75.75 0 0 1 0-1.06Z", clipRule: "evenodd" }) }));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A full-width row that is a `<button>` (or, with `href` or `render`, a link) with a hover fill, a
|
|
10
|
+
* left-aligned label and `trailing` content at the far end. With `expanded` it is the header of a
|
|
11
|
+
* collapsible section: `aria-expanded` and a chevron. Rows sit in a `Card flush`; lines between
|
|
12
|
+
* them are the caller's (`border-t border-slate-200/70` on each row after the first).
|
|
13
|
+
*/
|
|
14
|
+
export function ListRow(props) {
|
|
15
|
+
const { trailing, children, className } = props;
|
|
16
|
+
const label = _jsx("span", { className: "min-w-0 flex-1", children: children });
|
|
17
|
+
const end = trailing != null && (_jsx("span", { className: "shrink-0 tabular-nums text-slate-500", children: trailing }));
|
|
18
|
+
if (props.href !== undefined || props.render !== undefined) {
|
|
19
|
+
const { trailing: _t, expanded: _e, href, render, external, className: _c, ...rest } = props;
|
|
20
|
+
return renderLink({
|
|
21
|
+
...rest,
|
|
22
|
+
href,
|
|
23
|
+
render,
|
|
24
|
+
external,
|
|
25
|
+
className: cx(ROW, className),
|
|
26
|
+
children: (_jsxs(_Fragment, { children: [label, end] })),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const { trailing: _t, expanded, href: _h, render: _r, className: _c, ...rest } = props;
|
|
30
|
+
return (_jsxs("button", { type: "button", ...rest, ...(expanded === undefined ? {} : { "aria-expanded": expanded }), className: cx(ROW, className), children: [expanded !== undefined && _jsx(Chevron, { open: expanded }), label, end] }));
|
|
31
|
+
}
|