@hackcove/ui-react 0.0.0-stage → 0.8.1
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 +12 -0
- package/dist/Textarea.js +8 -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 +12 -0
- package/dist/cx.js +14 -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,57 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
import { type LinkRender } from "./link.js";
|
|
3
|
+
import type { Unstyled } from "./props.js";
|
|
4
|
+
import type { Size } from "./size.js";
|
|
5
|
+
export type IconButtonVariant = "outline" | "ghost";
|
|
6
|
+
export type IconButtonTone = "default" | "danger";
|
|
7
|
+
type IconButtonProps = {
|
|
8
|
+
/** The button's accessible name and its tooltip. */
|
|
9
|
+
label: string;
|
|
10
|
+
/**
|
|
11
|
+
* `outline` has a border; `ghost` has none, until the hover fill.
|
|
12
|
+
* @default outline
|
|
13
|
+
*/
|
|
14
|
+
variant?: IconButtonVariant | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* `danger` is red, for delete and clear.
|
|
17
|
+
* @default default
|
|
18
|
+
*/
|
|
19
|
+
tone?: IconButtonTone | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The button's height, matching a `Button` or `Input` of the same size.
|
|
22
|
+
* @default md
|
|
23
|
+
*/
|
|
24
|
+
size?: Size | undefined;
|
|
25
|
+
/** A red count on the top-right corner, for unread alerts. Not shown at 0; `label` carries it for a screen reader. */
|
|
26
|
+
badge?: number | undefined;
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
};
|
|
29
|
+
type IconButtonAsButton = IconButtonProps & Unstyled<Omit<ComponentPropsWithoutRef<"button">, "children">> & {
|
|
30
|
+
href?: undefined;
|
|
31
|
+
render?: undefined;
|
|
32
|
+
external?: undefined;
|
|
33
|
+
};
|
|
34
|
+
type IconButtonAsLink = IconButtonProps & {
|
|
35
|
+
/** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
|
|
36
|
+
external?: boolean | undefined;
|
|
37
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"a">, "href" | "children">> & ({
|
|
38
|
+
/** Makes the button a link to this URL. */
|
|
39
|
+
href: string;
|
|
40
|
+
/** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/settings" {...p} />`. */
|
|
41
|
+
render?: LinkRender | undefined;
|
|
42
|
+
} | {
|
|
43
|
+
/** Makes the button a link to this URL. */
|
|
44
|
+
href?: string | undefined;
|
|
45
|
+
/** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/settings" {...p} />`. */
|
|
46
|
+
render: LinkRender;
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* A square button holding one icon. `label` is its accessible name and tooltip. `variant="ghost"`
|
|
50
|
+
* drops the border for a quiet action in a row or a toolbar; `size` matches the height of a
|
|
51
|
+
* `Button` of the same size (`sm` 26px, `md` 38px, `lg` 46px); `tone="danger"` is red, for delete
|
|
52
|
+
* and clear; `badge` pins a red count to the corner. Defaults to `type="button"`. `href` makes it
|
|
53
|
+
* a link instead (a plain `<a>`, or with `render` the app's own router `<Link>`:
|
|
54
|
+
* `render={(p) => <Link to="/settings" {...p} />}`; `external` opens a new tab), with the same look.
|
|
55
|
+
*/
|
|
56
|
+
export declare function IconButton(props: IconButtonAsButton | IconButtonAsLink): ReactElement;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Badge } from "./Badge.js";
|
|
3
|
+
import { cx, focusRing } from "./cx.js";
|
|
4
|
+
import { renderLink } from "./link.js";
|
|
5
|
+
const BASE = `relative inline-flex items-center justify-center rounded-lg border transition disabled:pointer-events-none disabled:opacity-50 ${focusRing}`;
|
|
6
|
+
/** The same heights as a `Button` of the size: 26px, 38px and 46px. */
|
|
7
|
+
const SIZE = { sm: "size-6.5", md: "size-9.5", lg: "size-11.5" };
|
|
8
|
+
const LOOK = {
|
|
9
|
+
default: {
|
|
10
|
+
outline: "border-slate-200 text-slate-500 hover:bg-slate-50",
|
|
11
|
+
ghost: "border-transparent text-slate-500 hover:bg-slate-100",
|
|
12
|
+
},
|
|
13
|
+
danger: {
|
|
14
|
+
outline: "border-red-200 text-red-700 hover:bg-red-50",
|
|
15
|
+
ghost: "border-transparent text-red-700 hover:bg-red-50",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
/** The classes for a variant, tone and size, each at its default when left out. */
|
|
19
|
+
function look({ variant = "outline", tone = "default", size = "md", }) {
|
|
20
|
+
return cx(BASE, SIZE[size], LOOK[tone][variant]);
|
|
21
|
+
}
|
|
22
|
+
function isLink(props) {
|
|
23
|
+
return props.href !== undefined || props.render !== undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A square button holding one icon. `label` is its accessible name and tooltip. `variant="ghost"`
|
|
27
|
+
* drops the border for a quiet action in a row or a toolbar; `size` matches the height of a
|
|
28
|
+
* `Button` of the same size (`sm` 26px, `md` 38px, `lg` 46px); `tone="danger"` is red, for delete
|
|
29
|
+
* and clear; `badge` pins a red count to the corner. Defaults to `type="button"`. `href` makes it
|
|
30
|
+
* a link instead (a plain `<a>`, or with `render` the app's own router `<Link>`:
|
|
31
|
+
* `render={(p) => <Link to="/settings" {...p} />}`; `external` opens a new tab), with the same look.
|
|
32
|
+
*/
|
|
33
|
+
export function IconButton(props) {
|
|
34
|
+
if (isLink(props)) {
|
|
35
|
+
const { label, variant, tone, size, badge, children, ...link } = props;
|
|
36
|
+
return renderLink({
|
|
37
|
+
...link,
|
|
38
|
+
external: link.external ?? false,
|
|
39
|
+
"aria-label": named(label, badge),
|
|
40
|
+
title: named(label, badge),
|
|
41
|
+
className: look({ variant, tone, size }),
|
|
42
|
+
children: (_jsxs(_Fragment, { children: [children, badge ? _jsx(Badge, { count: badge, corner: true, "aria-hidden": "true" }) : null] })),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const { label, variant, tone, size, badge, children, ...rest } = props;
|
|
46
|
+
return (_jsxs("button", { type: "button", ...rest, "aria-label": named(label, badge), title: named(label, badge), className: look({ variant, tone, size }), children: [children, badge ? _jsx(Badge, { count: badge, corner: true, "aria-hidden": "true" }) : null] }));
|
|
47
|
+
}
|
|
48
|
+
/** The accessible name with its count: `Alerts, 3`. */
|
|
49
|
+
function named(label, badge) {
|
|
50
|
+
return badge ? `${label}, ${badge}` : label;
|
|
51
|
+
}
|
package/dist/Input.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
import { type ControlAlign, type ControlProps } from "./control.js";
|
|
3
|
+
import type { Unstyled } from "./props.js";
|
|
4
|
+
/**
|
|
5
|
+
* A text input. `icon` puts a leading icon inside the field (e.g. a search glass). `prefix` puts
|
|
6
|
+
* short text inside the border before the value, such as a currency symbol; it is for a plain
|
|
7
|
+
* field, not with an `icon` or `type="search"`. `align="end"` right-aligns the value, for a numeric
|
|
8
|
+
* field. `type="search"` drops the browser's own search look and shows a "Clear search" button at the trailing edge while the field has a value. */
|
|
9
|
+
export declare function Input({ icon, prefix, size, width, align, ...rest }: ({
|
|
10
|
+
/** A leading icon inside the field (a search glass). Not with `prefix`. */
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
/** Short text inside the border before the value (a currency symbol). For a plain field: not with `icon` or `type="search"`. */
|
|
13
|
+
prefix?: undefined;
|
|
14
|
+
} | {
|
|
15
|
+
/** A leading icon inside the field (a search glass). Not with `prefix`. */
|
|
16
|
+
icon?: undefined;
|
|
17
|
+
/** Short text inside the border before the value (a currency symbol). For a plain field: not with `icon` or `type="search"`. */
|
|
18
|
+
prefix: string;
|
|
19
|
+
}) & ControlProps & {
|
|
20
|
+
/** `end` right-aligns the value, for a numeric field. `start` (default) leaves it as typed. */
|
|
21
|
+
align?: ControlAlign | undefined;
|
|
22
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"input">, "size" | "width" | "prefix">>): ReactElement;
|
package/dist/Input.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CONTROL_ALIGN, CONTROL_PAD, controlClass, } from "./control.js";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
import { InputShell, PrefixShell } from "./InputShell.js";
|
|
5
|
+
import { SearchInput } from "./SearchInput.js";
|
|
6
|
+
/**
|
|
7
|
+
* A text input. `icon` puts a leading icon inside the field (e.g. a search glass). `prefix` puts
|
|
8
|
+
* short text inside the border before the value, such as a currency symbol; it is for a plain
|
|
9
|
+
* field, not with an `icon` or `type="search"`. `align="end"` right-aligns the value, for a numeric
|
|
10
|
+
* field. `type="search"` drops the browser's own search look and shows a "Clear search" button at the trailing edge while the field has a value. */
|
|
11
|
+
export function Input({ icon, prefix, size = "md", width = "full", align = "start", ...rest }) {
|
|
12
|
+
if (rest.type === "search") {
|
|
13
|
+
return _jsx(SearchInput, { ...rest, icon: icon, size: size, width: width, align: align });
|
|
14
|
+
}
|
|
15
|
+
if (prefix !== undefined) {
|
|
16
|
+
return (_jsx(PrefixShell, { prefix: prefix, size: size, width: width, disabled: rest.disabled, children: _jsx("input", { ...rest, className: cx("min-w-0 flex-1 bg-transparent px-1 outline-none placeholder:text-slate-400", CONTROL_ALIGN[align]) }) }));
|
|
17
|
+
}
|
|
18
|
+
if (icon === undefined) {
|
|
19
|
+
return (_jsx("input", { ...rest, className: cx(controlClass(size, width), CONTROL_PAD[size].x, CONTROL_ALIGN[align]) }));
|
|
20
|
+
}
|
|
21
|
+
return (_jsx(InputShell, { icon: icon, size: size, width: width, children: _jsx("input", { ...rest, className: cx(controlClass(size, width === "auto" ? "auto" : "full"), CONTROL_PAD[size].icon, CONTROL_ALIGN[align]) }) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { type ControlProps } from "./control.js";
|
|
3
|
+
/** The relative wrapper of an `Input` that holds more than the field: a leading `icon` and, for search, the clear button. Not exported from the package. */
|
|
4
|
+
export declare function InputShell({ icon, size, width, children, }: {
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
} & ControlProps): ReactElement;
|
|
8
|
+
/**
|
|
9
|
+
* The field box of an `Input` with a `prefix`: the control look and the focus ring sit on the
|
|
10
|
+
* wrapper, around the prefix text and the borderless input. Not exported from the package.
|
|
11
|
+
*/
|
|
12
|
+
export declare function PrefixShell({ prefix, size, width, disabled, children, }: {
|
|
13
|
+
prefix: string;
|
|
14
|
+
disabled: boolean | undefined;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
} & ControlProps): ReactElement;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CONTROL_WIDTH, controlClass } from "./control.js";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
/** Where a leading icon sits, and its size, by the control's size. */
|
|
5
|
+
const ICON = {
|
|
6
|
+
sm: "left-2 [&>svg]:h-3.5 [&>svg]:w-3.5",
|
|
7
|
+
md: "left-3 [&>svg]:h-4 [&>svg]:w-4",
|
|
8
|
+
lg: "left-4 [&>svg]:h-5 [&>svg]:w-5",
|
|
9
|
+
};
|
|
10
|
+
/** The prefix text's inset and size, by the control's size. */
|
|
11
|
+
const PREFIX = {
|
|
12
|
+
sm: "pl-2 text-xs",
|
|
13
|
+
md: "pl-3 text-sm",
|
|
14
|
+
lg: "pl-4 text-base",
|
|
15
|
+
};
|
|
16
|
+
/** The relative wrapper of an `Input` that holds more than the field: a leading `icon` and, for search, the clear button. Not exported from the package. */
|
|
17
|
+
export function InputShell({ icon, size = "md", width = "full", children, }) {
|
|
18
|
+
return (_jsxs("div", { className: cx("relative", width === "auto" && "inline-block", width !== "full" && CONTROL_WIDTH[width]), children: [icon !== undefined && (_jsx("span", { "aria-hidden": "true", className: cx("pointer-events-none absolute top-1/2 -translate-y-1/2 text-slate-400", ICON[size]), children: icon })), children] }));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The field box of an `Input` with a `prefix`: the control look and the focus ring sit on the
|
|
22
|
+
* wrapper, around the prefix text and the borderless input. Not exported from the package.
|
|
23
|
+
*/
|
|
24
|
+
export function PrefixShell({ prefix, size = "md", width = "full", disabled, children, }) {
|
|
25
|
+
return (_jsxs("div", { className: cx(controlClass(size, width), "flex items-center has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-violet-600", width === "auto" && "inline-flex", disabled && "opacity-50"), children: [_jsx("span", { className: cx("shrink-0 select-none text-slate-500", PREFIX[size]), children: prefix }), children] }));
|
|
26
|
+
}
|
package/dist/Kbd.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { Unstyled } from "./props.js";
|
|
3
|
+
/** A keyboard key. Its border takes the surrounding text colour, so it reads on a filled button and on plain text. */
|
|
4
|
+
export declare function Kbd({ children, ...rest }: {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
} & Unstyled<HTMLAttributes<HTMLElement>>): ReactElement;
|
package/dist/Kbd.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** A keyboard key. Its border takes the surrounding text colour, so it reads on a filled button and on plain text. */
|
|
3
|
+
export function Kbd({ children, ...rest }) {
|
|
4
|
+
return (_jsx("kbd", { ...rest, className: "rounded border border-current/30 px-1 font-mono text-xs leading-4", children: children }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { Unstyled } from "./props.js";
|
|
3
|
+
type KeyValueAlign = "end" | "start";
|
|
4
|
+
/**
|
|
5
|
+
* A definition list of label/value rows. By default each label is on the left and its value
|
|
6
|
+
* right-aligned. `align="start"` puts every value left-aligned in one column beside the labels,
|
|
7
|
+
* which are sized to the widest.
|
|
8
|
+
*/
|
|
9
|
+
export declare function KeyValueList({ align, children, ...rest }: {
|
|
10
|
+
/**
|
|
11
|
+
* `end` right-aligns each value; `start` puts every value left-aligned in one column beside the labels, sized to the widest.
|
|
12
|
+
* @default end
|
|
13
|
+
*/
|
|
14
|
+
align?: KeyValueAlign;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
} & Unstyled<HTMLAttributes<HTMLDListElement>>): ReactElement;
|
|
17
|
+
/** One row of a KeyValueList: a label on the left, a tabular value on the right (or beside it, in an `align="start"` list). */
|
|
18
|
+
export declare function KeyValue({ label, children, ...rest }: {
|
|
19
|
+
/** The term, on the left. */
|
|
20
|
+
label: ReactNode;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
} & Unstyled<HTMLAttributes<HTMLDivElement>>): ReactElement;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, } from "react";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
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", children, ...rest }) {
|
|
11
|
+
return (_jsx("dl", { ...rest, 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"), children: _jsx(AlignContext.Provider, { value: align, children: children }) }));
|
|
14
|
+
}
|
|
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). */
|
|
16
|
+
export function KeyValue({ label, children, ...rest }) {
|
|
17
|
+
const start = useContext(AlignContext) === "start";
|
|
18
|
+
return (_jsxs("div", { ...rest, className: start ? "col-span-2 grid grid-cols-subgrid" : "flex justify-between gap-3", children: [_jsx("dt", { className: "text-slate-600", children: label }), _jsx("dd", { className: start ? "tabular-nums" : "text-right tabular-nums", children: children })] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { Passthrough } from "./props.js";
|
|
3
|
+
export type LightboxProps = {
|
|
4
|
+
/** The viewer's accessible name, e.g. "Photo from Anna". */
|
|
5
|
+
label: string;
|
|
6
|
+
/** The media: one `<img>` or `<video>`, scaled down to fit the screen. */
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
/** A line on the scrim above the media, e.g. who posted it. */
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
/** A muted line under the title, e.g. when. */
|
|
11
|
+
meta?: ReactNode;
|
|
12
|
+
/** Muted text on the scrim under the media. */
|
|
13
|
+
caption?: ReactNode;
|
|
14
|
+
/** Called when the viewer closes: Esc, a click on the scrim or the close button. */
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
/** Shows a previous button and binds ArrowLeft. Leave it out at the first item. */
|
|
17
|
+
onPrevious?: (() => void) | undefined;
|
|
18
|
+
/** Shows a next button and binds ArrowRight. Leave it out at the last item. */
|
|
19
|
+
onNext?: (() => void) | undefined;
|
|
20
|
+
} & Passthrough;
|
|
21
|
+
/**
|
|
22
|
+
* A full-screen photo or video viewer on the browser's own `<dialog>`: it is opened with
|
|
23
|
+
* `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 around the media and its text closes it
|
|
25
|
+
* too. The scrim stays near-black with light text in every theme. `onPrevious` and `onNext` add
|
|
26
|
+
* step buttons and the arrow keys.
|
|
27
|
+
*/
|
|
28
|
+
export declare function Lightbox({ label, children, title, meta, caption, onClose, onPrevious, onNext, ...rest }: LightboxProps): ReactElement;
|
package/dist/Lightbox.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, } from "react";
|
|
3
|
+
import { Icon } from "./Icon.js";
|
|
4
|
+
import { useModal } from "./modal.js";
|
|
5
|
+
/**
|
|
6
|
+
* The full-screen viewer, painted with its own scrim: `bg-black/90` and the light text on it are the
|
|
7
|
+
* palette's media-scrim role, which every theme keeps near-black, light themes included.
|
|
8
|
+
*/
|
|
9
|
+
const VIEWER = "fixed inset-0 m-0 size-full max-h-none max-w-none flex-col border-0 bg-black/90 p-0 text-white opacity-0 transition-[opacity,overlay,display] transition-discrete duration-200 open:flex open:opacity-100 starting:open:opacity-0 motion-reduce:transition-none";
|
|
10
|
+
/** A round control on the scrim, with a light focus ring: the accent is too dark on near-black in a light theme. */
|
|
11
|
+
const CONTROL = "inline-flex size-11 shrink-0 items-center justify-center rounded-full text-white transition hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white";
|
|
12
|
+
const CLOSE = "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";
|
|
13
|
+
const PREVIOUS = "M12.78 4.22a.75.75 0 0 1 0 1.06L8.06 10l4.72 4.72a.75.75 0 1 1-1.06 1.06l-5.25-5.25a.75.75 0 0 1 0-1.06l5.25-5.25a.75.75 0 0 1 1.06 0Z";
|
|
14
|
+
const NEXT = "M7.22 4.22a.75.75 0 0 1 1.06 0l5.25 5.25a.75.75 0 0 1 0 1.06l-5.25 5.25a.75.75 0 1 1-1.06-1.06L11.94 10 7.22 5.28a.75.75 0 0 1 0-1.06Z";
|
|
15
|
+
/** A round icon button on the scrim, named by `label`, which is also its tooltip. */
|
|
16
|
+
function ScrimButton({ label, path, onClick, }) {
|
|
17
|
+
return (_jsx("button", { type: "button", "aria-label": label, title: label, onClick: onClick, className: CONTROL, children: _jsx(Icon, { children: _jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { d: path }) }) }) }));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A full-screen photo or video viewer on the browser's own `<dialog>`: it is opened with
|
|
21
|
+
* `showModal()` on mount, so render it only while it is open. The page behind goes inert, Esc
|
|
22
|
+
* closes it and focus stays inside; a click on the scrim around the media and its text closes it
|
|
23
|
+
* too. The scrim stays near-black with light text in every theme. `onPrevious` and `onNext` add
|
|
24
|
+
* step buttons and the arrow keys.
|
|
25
|
+
*/
|
|
26
|
+
export function Lightbox({ label, children, title, meta, caption, onClose, onPrevious, onNext, ...rest }) {
|
|
27
|
+
const dialog = useRef(null);
|
|
28
|
+
const closed = useModal(dialog, onClose);
|
|
29
|
+
const close = () => dialog.current?.close();
|
|
30
|
+
// The scrim is the dialog and the layout boxes in it, marked `data-scrim`; the media, the text and
|
|
31
|
+
// the buttons are not.
|
|
32
|
+
const clicked = (event) => {
|
|
33
|
+
if (event.target instanceof HTMLElement && event.target.hasAttribute("data-scrim"))
|
|
34
|
+
close();
|
|
35
|
+
};
|
|
36
|
+
const stepped = (event) => {
|
|
37
|
+
const step = { ArrowLeft: onPrevious, ArrowRight: onNext }[event.key];
|
|
38
|
+
if (!step)
|
|
39
|
+
return;
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
step();
|
|
42
|
+
};
|
|
43
|
+
return (_jsxs("dialog", { ...rest, ref: dialog, "aria-label": label, "data-scrim": "", onClose: closed, onClick: clicked, onKeyDown: stepped, className: VIEWER, children: [_jsxs("header", { "data-scrim": "", className: "flex shrink-0 items-start justify-between gap-3 p-3", children: [_jsxs("div", { className: "min-w-0 px-2 py-1.5", children: [title && _jsx("div", { className: "text-sm font-semibold", children: title }), meta && _jsx("div", { className: "text-xs text-white/70", children: meta })] }), _jsx(ScrimButton, { label: "Close", path: CLOSE, onClick: close })] }), _jsxs("div", { "data-scrim": "", className: "flex min-h-0 flex-1 items-center gap-2 px-3", children: [onPrevious && _jsx(ScrimButton, { label: "Previous", path: PREVIOUS, onClick: onPrevious }), _jsx("div", { "data-scrim": "", className: "flex size-full min-w-0 items-center justify-center *:max-h-full *:max-w-full *:object-contain", children: children }), onNext && _jsx(ScrimButton, { label: "Next", path: NEXT, onClick: onNext })] }), _jsx("footer", { "data-scrim": "", className: "flex shrink-0 justify-center px-5 py-4", children: caption && _jsx("p", { className: "max-w-prose text-center text-sm text-white/70", children: caption }) })] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type PointerEventHandler, type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import { type LinkRender } from "./link.js";
|
|
3
|
+
import type { Unstyled } from "./props.js";
|
|
4
|
+
import type { Size } from "./size.js";
|
|
5
|
+
type Shared = {
|
|
6
|
+
/**
|
|
7
|
+
* `sm` is dense padding, `lg` one step roomier than `md`.
|
|
8
|
+
* @default md
|
|
9
|
+
*/
|
|
10
|
+
size?: Size | undefined;
|
|
11
|
+
/** A hairline above the row: set it on every row after the first. */
|
|
12
|
+
divided?: boolean | undefined;
|
|
13
|
+
/** Content before the label: an Avatar, a thumbnail, an icon. */
|
|
14
|
+
leading?: ReactNode | undefined;
|
|
15
|
+
/** Content at the far end of the row: a value, a count, a Pill. */
|
|
16
|
+
trailing?: ReactNode | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Marks the row in view, the one whose detail is open: a violet fill and `aria-current="true"`.
|
|
19
|
+
* Set it on one row of a list.
|
|
20
|
+
*/
|
|
21
|
+
selected?: boolean | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Marks the row the keyboard cursor is on, for a list moved with the arrow keys or `j`/`k` while
|
|
24
|
+
* focus stays on the list: the focus ring, drawn inside the row. It says nothing to a screen
|
|
25
|
+
* reader (move real focus for that) and combines with `selected`.
|
|
26
|
+
*/
|
|
27
|
+
focused?: boolean | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Marks the row a list follows with the pointer and the keyboard alike (set it from
|
|
30
|
+
* `onPointerEnter` and `onFocus`): a fill one step deeper than hover, so it shows on a white card
|
|
31
|
+
* and on a tinted panel alike. On a static row it shows the fill without `revealOnHover`, and
|
|
32
|
+
* shows `actions` that `revealOnHover` hides. `selected` wins.
|
|
33
|
+
*/
|
|
34
|
+
active?: boolean | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* A fixed width for `leading`, for a thumbnail (an `<img>` fills it), so the labels of rows with
|
|
37
|
+
* and without a picture still line up. Without it `leading` is as wide as its content.
|
|
38
|
+
*/
|
|
39
|
+
leadingWidth?: "thumbnail" | undefined;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
};
|
|
42
|
+
type NotStatic = {
|
|
43
|
+
static?: undefined;
|
|
44
|
+
actions?: undefined;
|
|
45
|
+
revealOnHover?: undefined;
|
|
46
|
+
};
|
|
47
|
+
type ListRowButtonProps = Shared & NotStatic & {
|
|
48
|
+
/**
|
|
49
|
+
* Makes the row a collapsible header: sets `aria-expanded` and shows a chevron that turns down
|
|
50
|
+
* when open. The caller owns the state and the section; pass `aria-controls` to name it.
|
|
51
|
+
*/
|
|
52
|
+
expanded?: boolean | undefined;
|
|
53
|
+
href?: undefined;
|
|
54
|
+
render?: undefined;
|
|
55
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"button">, "children">>;
|
|
56
|
+
type ListRowLinkProps = Shared & NotStatic & {
|
|
57
|
+
/** Opens in a new tab: adds `target="_blank"` and `rel="noreferrer"`. */
|
|
58
|
+
external?: boolean | undefined;
|
|
59
|
+
expanded?: undefined;
|
|
60
|
+
/** On the row, not the link, so a link or button in the row does not count as leaving it. */
|
|
61
|
+
onPointerEnter?: PointerEventHandler<HTMLDivElement> | undefined;
|
|
62
|
+
onPointerLeave?: PointerEventHandler<HTMLDivElement> | undefined;
|
|
63
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"a">, "href" | "children" | "onPointerEnter" | "onPointerLeave">> & ({
|
|
64
|
+
/** Makes the row a link to this URL. */
|
|
65
|
+
href: string;
|
|
66
|
+
/** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/quotes/1" {...p} />`. */
|
|
67
|
+
render?: LinkRender | undefined;
|
|
68
|
+
} | {
|
|
69
|
+
/** Makes the row a link to this URL. */
|
|
70
|
+
href?: string | undefined;
|
|
71
|
+
/** Draws the link with the app's own router `<Link>`: `(p) => <Link to="/quotes/1" {...p} />`. */
|
|
72
|
+
render: LinkRender;
|
|
73
|
+
});
|
|
74
|
+
type ListRowStaticProps = Shared & {
|
|
75
|
+
/**
|
|
76
|
+
* A row that is neither a button nor a link, for one that holds its own controls: a `<div>` with
|
|
77
|
+
* no click target and no hover fill (unless `revealOnHover`), taking `actions` as well.
|
|
78
|
+
*/
|
|
79
|
+
static: true;
|
|
80
|
+
/** Buttons for this row, after `trailing`: `Button size="sm"`, `IconButton`. */
|
|
81
|
+
actions?: ReactNode | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Hides `actions` until the row is hovered or something in it has focus, so a long list is not a
|
|
84
|
+
* wall of buttons. A touch screen has no hover, so they are always shown there.
|
|
85
|
+
*/
|
|
86
|
+
revealOnHover?: boolean | undefined;
|
|
87
|
+
expanded?: undefined;
|
|
88
|
+
href?: undefined;
|
|
89
|
+
render?: undefined;
|
|
90
|
+
} & Unstyled<Omit<ComponentPropsWithoutRef<"div">, "children">>;
|
|
91
|
+
export type ListRowProps = ListRowButtonProps | ListRowLinkProps | ListRowStaticProps;
|
|
92
|
+
/**
|
|
93
|
+
* A full-width row that is a `<button>` (or, with `href` or `render`, a link) with a hover fill, a
|
|
94
|
+
* left-aligned label and `trailing` content at the far end. A linked row is a `<div>` with the link
|
|
95
|
+
* stretched over it, so the whole row opens the link; a `TextLink`, `Button` or `Checkbox` in the
|
|
96
|
+
* row sits above the link and is clickable on its own. With `expanded` it is the header of a
|
|
97
|
+
* collapsible section: `aria-expanded` and a chevron. Rows sit in a `Card flush`; `divided` draws
|
|
98
|
+
* the line between them, on each row after the first.
|
|
99
|
+
*
|
|
100
|
+
* `leading` is content before the label (`leadingWidth="thumbnail"` gives it a fixed picture width),
|
|
101
|
+
* `selected` marks the row in view, `focused` the one the keyboard cursor is on and `active` fills
|
|
102
|
+
* the row a list follows (`onPointerEnter`, `onPointerLeave` and `onFocus` reach
|
|
103
|
+
* every form; on a linked row the pointer handlers go on the row itself). The label is a
|
|
104
|
+
* `<div>`, so it can hold block content (a name over a path). `static` makes the
|
|
105
|
+
* row a plain `<div>` for one that holds its own buttons (a button cannot sit inside a button or a
|
|
106
|
+
* link): `actions` follow `trailing`, and `revealOnHover` shows them only on hover or focus.
|
|
107
|
+
*/
|
|
108
|
+
export declare function ListRow(props: ListRowProps): ReactElement;
|
|
109
|
+
export {};
|
package/dist/ListRow.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId, } from "react";
|
|
3
|
+
import { Chevron } from "./Chevron.js";
|
|
4
|
+
import { cx, focusRing, liftInteractive } from "./cx.js";
|
|
5
|
+
import { renderLink } from "./link.js";
|
|
6
|
+
const ROW_LOOK = "flex w-full items-center gap-3 text-left text-sm text-slate-900 disabled:pointer-events-none disabled:opacity-50";
|
|
7
|
+
const INSET_RING = `${focusRing} focus-visible:-outline-offset-2`;
|
|
8
|
+
const ROW = `${ROW_LOOK} ${INSET_RING}`;
|
|
9
|
+
/** A linked row is a `relative` box; its link is an empty overlay, so the row's own links and buttons are not nested in it. */
|
|
10
|
+
const LINK_ROW = `relative ${ROW_LOOK} ${liftInteractive}`;
|
|
11
|
+
const OVERLAY = `link-stretch absolute inset-0 ${INSET_RING}`;
|
|
12
|
+
const STATIC = "group/row flex w-full items-center gap-3 text-sm text-slate-900";
|
|
13
|
+
const FILL = "hover:bg-slate-50";
|
|
14
|
+
const SELECTED = "bg-violet-50 hover:bg-violet-50";
|
|
15
|
+
/**
|
|
16
|
+
* The row a pointer or keyboard cursor is on: the inset fill, one step deeper than hover, so it
|
|
17
|
+
* still shows on a `bg-slate-50` surface (a `FullBleedLayout` panel, a toned card).
|
|
18
|
+
*/
|
|
19
|
+
const ACTIVE = "bg-slate-100";
|
|
20
|
+
/** The keyboard cursor: the focus ring, drawn inside the row, on a row that does not hold DOM focus. */
|
|
21
|
+
const FOCUSED = "outline-2 -outline-offset-2 outline-violet-600";
|
|
22
|
+
/** A thumbnail's width. A direct-child image fills it. */
|
|
23
|
+
const THUMBNAIL = "block w-32 [&>img]:block [&>img]:h-auto [&>img]:w-full";
|
|
24
|
+
/** Hidden until the row is hovered or has focus inside it; always shown where any pointer is coarse (a touch screen, hybrids included). */
|
|
25
|
+
const REVEAL = "opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100 any-pointer-coarse:opacity-100";
|
|
26
|
+
const PAD = { sm: "px-3 py-2", md: "px-4 py-3", lg: "px-5 py-4" };
|
|
27
|
+
/** The row's background: the selected fill, else the hover fill unless a plain static row has none. */
|
|
28
|
+
function fillOf(props) {
|
|
29
|
+
if (props.selected)
|
|
30
|
+
return SELECTED;
|
|
31
|
+
if (props.active)
|
|
32
|
+
return ACTIVE;
|
|
33
|
+
return props.static && !props.revealOnHover ? undefined : FILL;
|
|
34
|
+
}
|
|
35
|
+
function rowBase(props) {
|
|
36
|
+
if (props.static)
|
|
37
|
+
return STATIC;
|
|
38
|
+
return props.href !== undefined || props.render !== undefined ? LINK_ROW : ROW;
|
|
39
|
+
}
|
|
40
|
+
function look(props) {
|
|
41
|
+
const { size = "md", divided = false, focused = false } = props;
|
|
42
|
+
return cx(rowBase(props), PAD[size], divided && "border-t border-slate-200/70", fillOf(props), focused && FOCUSED);
|
|
43
|
+
}
|
|
44
|
+
function StaticRow(props) {
|
|
45
|
+
const { leading: _l, leadingWidth: _lw, trailing: _t, focused: _f, active = false, selected, size: _s, divided: _d, static: _st, actions, revealOnHover = false, expanded: _e, href: _h, render: _r, start, label, end, ...rest } = props;
|
|
46
|
+
return (_jsxs("div", { ...rest, ...(selected ? { "aria-current": "true" } : {}), className: look(props), children: [start, label, end, actions != null && (_jsx("span", { className: cx("flex shrink-0 items-center gap-2", revealOnHover && !active && REVEAL), children: actions }))] }));
|
|
47
|
+
}
|
|
48
|
+
function LinkRow(props) {
|
|
49
|
+
const { leading: _l, leadingWidth: _lw, trailing: _t, focused: _f, active: _ac, selected, expanded: _e, size: _s, divided: _d, static: _st, actions: _a, revealOnHover: _rh, start, label, end, onPointerEnter, onPointerLeave, ...rest } = props;
|
|
50
|
+
const rowId = useId();
|
|
51
|
+
return (_jsxs("div", { id: rowId, onPointerEnter: onPointerEnter, onPointerLeave: onPointerLeave, className: look(props), children: [renderLink({
|
|
52
|
+
...(rest["aria-label"] === undefined ? { "aria-labelledby": rowId } : {}),
|
|
53
|
+
...rest,
|
|
54
|
+
...(selected ? { "aria-current": "true" } : {}),
|
|
55
|
+
className: OVERLAY,
|
|
56
|
+
children: null,
|
|
57
|
+
}), start, label, end] }));
|
|
58
|
+
}
|
|
59
|
+
function ButtonRow(props) {
|
|
60
|
+
const { leading: _l, leadingWidth: _lw, trailing: _t, focused: _f, active: _ac, selected, expanded, size: _s, divided: _d, static: _st, actions: _a, revealOnHover: _rh, href: _h, render: _r, start, label, end, ...rest } = props;
|
|
61
|
+
return (_jsxs("button", { type: "button", ...rest, ...(selected ? { "aria-current": "true" } : {}), ...(expanded === undefined ? {} : { "aria-expanded": expanded }), className: look(props), children: [expanded !== undefined && _jsx(Chevron, { open: expanded }), start, label, end] }));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A full-width row that is a `<button>` (or, with `href` or `render`, a link) with a hover fill, a
|
|
65
|
+
* left-aligned label and `trailing` content at the far end. A linked row is a `<div>` with the link
|
|
66
|
+
* stretched over it, so the whole row opens the link; a `TextLink`, `Button` or `Checkbox` in the
|
|
67
|
+
* row sits above the link and is clickable on its own. With `expanded` it is the header of a
|
|
68
|
+
* collapsible section: `aria-expanded` and a chevron. Rows sit in a `Card flush`; `divided` draws
|
|
69
|
+
* the line between them, on each row after the first.
|
|
70
|
+
*
|
|
71
|
+
* `leading` is content before the label (`leadingWidth="thumbnail"` gives it a fixed picture width),
|
|
72
|
+
* `selected` marks the row in view, `focused` the one the keyboard cursor is on and `active` fills
|
|
73
|
+
* the row a list follows (`onPointerEnter`, `onPointerLeave` and `onFocus` reach
|
|
74
|
+
* every form; on a linked row the pointer handlers go on the row itself). The label is a
|
|
75
|
+
* `<div>`, so it can hold block content (a name over a path). `static` makes the
|
|
76
|
+
* row a plain `<div>` for one that holds its own buttons (a button cannot sit inside a button or a
|
|
77
|
+
* link): `actions` follow `trailing`, and `revealOnHover` shows them only on hover or focus.
|
|
78
|
+
*/
|
|
79
|
+
export function ListRow(props) {
|
|
80
|
+
const { leading, leadingWidth, trailing, children } = props;
|
|
81
|
+
const pieces = {
|
|
82
|
+
start: leading != null && (_jsx("span", { className: cx("shrink-0", leadingWidth === "thumbnail" && THUMBNAIL), children: leading })),
|
|
83
|
+
label: _jsx("div", { className: "min-w-0 flex-1", children: children }),
|
|
84
|
+
end: trailing != null && (_jsx("span", { className: "shrink-0 tabular-nums text-slate-500", children: trailing })),
|
|
85
|
+
};
|
|
86
|
+
if (props.static)
|
|
87
|
+
return _jsx(StaticRow, { ...props, ...pieces });
|
|
88
|
+
if (props.href !== undefined || props.render !== undefined) {
|
|
89
|
+
return _jsx(LinkRow, { ...props, ...pieces });
|
|
90
|
+
}
|
|
91
|
+
return _jsx(ButtonRow, { ...props, ...pieces });
|
|
92
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Passthrough } from "./props.js";
|
|
2
|
+
/**
|
|
3
|
+
* A small, safe Markdown renderer for short notes: `## ` and `### ` headings, `- ` or `* ` bullets,
|
|
4
|
+
* `**bold**` and paragraphs. Text is rendered as React nodes, never as HTML.
|
|
5
|
+
*/
|
|
6
|
+
export declare function Markdown({ md, ...rest }: {
|
|
7
|
+
/** The Markdown source. */
|
|
8
|
+
md: string;
|
|
9
|
+
} & Passthrough): import("react").JSX.Element;
|
package/dist/Markdown.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// A minimal, SAFE markdown renderer (headings / bullets / bold). No
|
|
3
|
+
// dangerouslySetInnerHTML — text is rendered as React nodes, so it can't inject HTML.
|
|
4
|
+
function inline(text) {
|
|
5
|
+
return text.split(/(\*\*[^*]+\*\*)/g).map((p, i) => p.startsWith("**") && p.endsWith("**") ? (_jsx("strong", { className: "font-semibold text-slate-900", children: p.slice(2, -2) }, i)) : (_jsx("span", { children: p }, i)));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A small, safe Markdown renderer for short notes: `## ` and `### ` headings, `- ` or `* ` bullets,
|
|
9
|
+
* `**bold**` and paragraphs. Text is rendered as React nodes, never as HTML.
|
|
10
|
+
*/
|
|
11
|
+
export function Markdown({ md, ...rest }) {
|
|
12
|
+
const out = [];
|
|
13
|
+
let bullets = [];
|
|
14
|
+
const flush = () => {
|
|
15
|
+
if (bullets.length) {
|
|
16
|
+
out.push(_jsx("ul", { className: "ml-4 list-disc space-y-1", children: bullets }, `u${out.length}`));
|
|
17
|
+
bullets = [];
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
md.split("\n").forEach((ln, i) => {
|
|
21
|
+
const t = ln.trim();
|
|
22
|
+
if (t.startsWith("## ")) {
|
|
23
|
+
flush();
|
|
24
|
+
out.push(_jsx("h2", { className: "mt-4 text-xs font-semibold uppercase tracking-wide text-slate-400 first:mt-0", children: t.slice(3) }, i));
|
|
25
|
+
}
|
|
26
|
+
else if (t.startsWith("### ")) {
|
|
27
|
+
flush();
|
|
28
|
+
out.push(_jsx("h3", { className: "mt-3 text-sm font-semibold text-slate-700", children: t.slice(4) }, i));
|
|
29
|
+
}
|
|
30
|
+
else if (t.startsWith("- ") || t.startsWith("* ")) {
|
|
31
|
+
bullets.push(_jsx("li", { className: "text-sm text-slate-700", children: inline(t.slice(2)) }, i));
|
|
32
|
+
}
|
|
33
|
+
else if (t === "") {
|
|
34
|
+
flush();
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
flush();
|
|
38
|
+
out.push(_jsx("p", { className: "text-sm text-slate-700", children: inline(t) }, i));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
flush();
|
|
42
|
+
return (_jsx("div", { ...rest, className: "space-y-1", children: out }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { Passthrough } from "./props.js";
|
|
3
|
+
/** The mark between MetaList items: `·`, `•`, or `none` for a gap alone. */
|
|
4
|
+
export type MetaListSeparator = "·" | "•" | "none";
|
|
5
|
+
/**
|
|
6
|
+
* A wrapping row of facts or links in muted small text, joined by a `·`: branch · SHA · date, or a
|
|
7
|
+
* footer's Privacy · Terms · Email. An item is text, a `Code` or a `TextLink`. The separator is
|
|
8
|
+
* `aria-hidden` and leads each item but the first, held to its item so a wrap never leaves one at a
|
|
9
|
+
* line's end; `separator` swaps it for a `•` or none. `nowrap` keeps each item on one line, for
|
|
10
|
+
* links. `mono` sets the whole row in the monospace face, for ids and paths.
|
|
11
|
+
*/
|
|
12
|
+
export declare function MetaList({ mono, separator, nowrap, children, ...rest }: {
|
|
13
|
+
/** The whole row in the monospace face, for ids and paths. */
|
|
14
|
+
mono?: boolean | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The mark between items: `·`, `•`, or `none` for a gap alone.
|
|
17
|
+
* @default ·
|
|
18
|
+
*/
|
|
19
|
+
separator?: MetaListSeparator | undefined;
|
|
20
|
+
/** Each item stays on one line; the row still wraps between items. For a row of links. */
|
|
21
|
+
nowrap?: boolean | undefined;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
} & Passthrough): ReactElement;
|
package/dist/MetaList.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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 facts or links in muted small text, joined by a `·`: branch · SHA · date, or a
|
|
7
|
+
* footer's Privacy · Terms · Email. An item is text, a `Code` or a `TextLink`. The separator is
|
|
8
|
+
* `aria-hidden` and leads each item but the first, held to its item so a wrap never leaves one at a
|
|
9
|
+
* line's end; `separator` swaps it for a `•` or none. `nowrap` keeps each item on one line, for
|
|
10
|
+
* links. `mono` sets the whole row in the monospace face, for ids and paths.
|
|
11
|
+
*/
|
|
12
|
+
export function MetaList({ mono = false, separator = "·", nowrap = false, children, ...rest }) {
|
|
13
|
+
return (_jsx("ul", { ...rest, className: cx("flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-slate-500", mono && "font-mono"), children: Children.toArray(children).map((child, index) => (_jsxs("li", { className: cx("inline-flex items-center gap-x-3", nowrap && "whitespace-nowrap"), children: [index > 0 && separator !== "none" && _jsx(Caption, { "aria-hidden": "true", children: separator }), child] }, index))) }));
|
|
14
|
+
}
|