@hackcove/ui-react 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -8
- package/dist/Avatar.d.ts +8 -4
- package/dist/Avatar.js +11 -3
- package/dist/AvatarStack.js +1 -0
- package/dist/Callout.d.ts +8 -2
- package/dist/Callout.js +3 -3
- package/dist/Card.d.ts +2 -0
- package/dist/Card.js +14 -8
- package/dist/CardParts.d.ts +6 -0
- package/dist/CardParts.js +6 -0
- package/dist/Cluster.d.ts +9 -4
- package/dist/Cluster.js +12 -5
- package/dist/Dialog.d.ts +4 -2
- package/dist/Dialog.js +9 -3
- package/dist/EmptyState.d.ts +7 -1
- package/dist/EmptyState.js +3 -2
- package/dist/Field.d.ts +8 -1
- package/dist/Field.js +4 -2
- package/dist/Image.d.ts +46 -0
- package/dist/Image.js +42 -0
- package/dist/KeyValueList.d.ts +4 -2
- package/dist/KeyValueList.js +3 -3
- package/dist/ListRow.d.ts +3 -1
- package/dist/ListRow.js +6 -6
- package/dist/MediaTile.d.ts +37 -0
- package/dist/MediaTile.js +19 -0
- package/dist/PageLayouts.d.ts +24 -22
- package/dist/PageLayouts.js +25 -21
- package/dist/Pill.d.ts +24 -6
- package/dist/Pill.js +72 -16
- package/dist/ScrollStrip.d.ts +21 -0
- package/dist/ScrollStrip.js +12 -0
- package/dist/SidebarLayout.d.ts +21 -0
- package/dist/SidebarLayout.js +18 -0
- package/dist/Stack.d.ts +15 -1
- package/dist/Stack.js +12 -2
- package/dist/StatTile.d.ts +2 -2
- package/dist/StatTile.js +2 -0
- package/dist/Table.d.ts +29 -12
- package/dist/Table.js +25 -11
- package/dist/TableEditCell.d.ts +19 -0
- package/dist/TableEditCell.js +58 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/size.d.ts +1 -1
- package/dist/stickyColumn.d.ts +17 -0
- package/dist/stickyColumn.js +17 -0
- package/dist/typography.d.ts +34 -10
- package/dist/typography.js +43 -13
- package/package.json +2 -2
package/dist/Image.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import type { Passthrough } from "./props.js";
|
|
3
|
+
import type { Size } from "./size.js";
|
|
4
|
+
/** The shapes an image is cropped or letterboxed to. */
|
|
5
|
+
export type ImageAspect = "square" | "4:3" | "3:2" | "16:9" | "3:4" | "9:16";
|
|
6
|
+
/** How wide an image is: `full` fills its parent; the others are 96, 160 or 256px, capped at the parent's width. */
|
|
7
|
+
export type ImageWidth = Size | "full";
|
|
8
|
+
/**
|
|
9
|
+
* `full` is the parent's width; a size is a real width that a narrower parent still caps. It is
|
|
10
|
+
* a width and not a `max-width`, so it holds inside a shrink-to-fit parent, where a placeholder
|
|
11
|
+
* has no intrinsic size of its own to give it one.
|
|
12
|
+
*/
|
|
13
|
+
export declare const IMAGE_WIDTH: Record<ImageWidth, string>;
|
|
14
|
+
export type ImageProps = {
|
|
15
|
+
/**
|
|
16
|
+
* The picture, a local path or a data URL. It is never loaded for you: this is the app's own
|
|
17
|
+
* image. Left out, or if it fails to load, the picture is replaced by a placeholder.
|
|
18
|
+
*/
|
|
19
|
+
src?: string | undefined;
|
|
20
|
+
/** What the picture shows. An empty string marks it as decoration, hidden from a screen reader. */
|
|
21
|
+
alt: string;
|
|
22
|
+
/**
|
|
23
|
+
* The shape it is cropped to: `square`, `4:3`, `3:2`, `16:9`, or the portrait `3:4` and `9:16`.
|
|
24
|
+
* @default 4:3
|
|
25
|
+
*/
|
|
26
|
+
aspect?: ImageAspect | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* `cover` fills the shape and crops what overflows; `contain` shows the whole picture, with the
|
|
29
|
+
* rest of the shape left empty (a QR code, a logo).
|
|
30
|
+
* @default cover
|
|
31
|
+
*/
|
|
32
|
+
fit?: "cover" | "contain" | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* `sm` 96px, `md` 160px and `lg` 256px set the width, capped at the parent's; `full` fills the parent.
|
|
35
|
+
* @default full
|
|
36
|
+
*/
|
|
37
|
+
width?: ImageWidth | undefined;
|
|
38
|
+
} & Passthrough;
|
|
39
|
+
/**
|
|
40
|
+
* A picture in a fixed shape: `aspect` reserves the space before it loads, so the page does not
|
|
41
|
+
* jump, and `fit` crops it to the shape or shows all of it. The corners are rounded and the frame
|
|
42
|
+
* has a hairline. If the picture is missing or fails to load, a same-sized placeholder with a
|
|
43
|
+
* picture mark stands in, still named by `alt`. A user's photo is an `Avatar`; a clickable
|
|
44
|
+
* thumbnail with a label is a `MediaTile`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function Image({ src, alt, aspect, fit, width, ...rest }: ImageProps): ReactElement;
|
package/dist/Image.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { cx } from "./cx.js";
|
|
4
|
+
import { Icon } from "./Icon.js";
|
|
5
|
+
const ASPECT = {
|
|
6
|
+
square: "aspect-square",
|
|
7
|
+
"4:3": "aspect-[4/3]",
|
|
8
|
+
"3:2": "aspect-[3/2]",
|
|
9
|
+
"16:9": "aspect-video",
|
|
10
|
+
"3:4": "aspect-[3/4]",
|
|
11
|
+
"9:16": "aspect-[9/16]",
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* `full` is the parent's width; a size is a real width that a narrower parent still caps. It is
|
|
15
|
+
* a width and not a `max-width`, so it holds inside a shrink-to-fit parent, where a placeholder
|
|
16
|
+
* has no intrinsic size of its own to give it one.
|
|
17
|
+
*/
|
|
18
|
+
export const IMAGE_WIDTH = {
|
|
19
|
+
sm: "w-24 max-w-full",
|
|
20
|
+
md: "w-40 max-w-full",
|
|
21
|
+
lg: "w-64 max-w-full",
|
|
22
|
+
full: "w-full",
|
|
23
|
+
};
|
|
24
|
+
/** Every image sits on the inset surface with a hairline, so a letterbox or a pale picture has an edge. */
|
|
25
|
+
const FRAME = "block rounded-lg border border-slate-200 bg-slate-100";
|
|
26
|
+
const PICTURE = "M1 5.25A2.25 2.25 0 0 1 3.25 3h13.5A2.25 2.25 0 0 1 19 5.25v9.5A2.25 2.25 0 0 1 16.75 17H3.25A2.25 2.25 0 0 1 1 14.75v-9.5Zm1.5 5.81v3.69c0 .414.336.75.75.75h13.5a.75.75 0 0 0 .75-.75v-2.69l-2.22-2.22a.75.75 0 0 0-1.06 0l-1.91 1.91.47.47a.75.75 0 1 1-1.06 1.06L6.53 8.09a.75.75 0 0 0-1.06 0l-2.97 2.97ZM12 7a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z";
|
|
27
|
+
/**
|
|
28
|
+
* A picture in a fixed shape: `aspect` reserves the space before it loads, so the page does not
|
|
29
|
+
* jump, and `fit` crops it to the shape or shows all of it. The corners are rounded and the frame
|
|
30
|
+
* has a hairline. If the picture is missing or fails to load, a same-sized placeholder with a
|
|
31
|
+
* picture mark stands in, still named by `alt`. A user's photo is an `Avatar`; a clickable
|
|
32
|
+
* thumbnail with a label is a `MediaTile`.
|
|
33
|
+
*/
|
|
34
|
+
export function Image({ src, alt, aspect = "4:3", fit = "cover", width = "full", ...rest }) {
|
|
35
|
+
// The `src` that failed to load: a new `src` gets its own try, so a broken one never sticks.
|
|
36
|
+
const [failedSrc, setFailedSrc] = useState();
|
|
37
|
+
const box = cx(FRAME, ASPECT[aspect], IMAGE_WIDTH[width]);
|
|
38
|
+
if (src && src !== failedSrc) {
|
|
39
|
+
return (_jsx("img", { ...rest, src: src, alt: alt, className: cx(box, fit === "cover" ? "object-cover" : "object-contain"), onError: () => setFailedSrc(src) }));
|
|
40
|
+
}
|
|
41
|
+
return (_jsx("span", { ...rest, ...(alt ? { role: "img", "aria-label": alt } : { "aria-hidden": true }), className: cx(box, "grid place-items-center text-slate-400"), children: _jsx(Icon, { size: "lg", children: _jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { d: PICTURE }) }) }) }));
|
|
42
|
+
}
|
package/dist/KeyValueList.d.ts
CHANGED
|
@@ -14,10 +14,12 @@ export declare function KeyValueList({ align, children, ...rest }: {
|
|
|
14
14
|
align?: KeyValueAlign;
|
|
15
15
|
children: ReactNode;
|
|
16
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 }: {
|
|
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). `total` makes it the sum row: a rule above, strong text. */
|
|
18
|
+
export declare function KeyValue({ label, total, children, ...rest }: {
|
|
19
19
|
/** The term, on the left. */
|
|
20
20
|
label: ReactNode;
|
|
21
|
+
/** The sum the rows above add up to: a firm rule above it and the label and value in strong ink. */
|
|
22
|
+
total?: boolean | undefined;
|
|
21
23
|
children: ReactNode;
|
|
22
24
|
} & Unstyled<HTMLAttributes<HTMLDivElement>>): ReactElement;
|
|
23
25
|
export {};
|
package/dist/KeyValueList.js
CHANGED
|
@@ -12,8 +12,8 @@ export function KeyValueList({ align = "end", children, ...rest }) {
|
|
|
12
12
|
? "grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1.5"
|
|
13
13
|
: "flex flex-col gap-1.5", "text-sm"), children: _jsx(AlignContext.Provider, { value: align, children: children }) }));
|
|
14
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 }) {
|
|
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). `total` makes it the sum row: a rule above, strong text. */
|
|
16
|
+
export function KeyValue({ label, total = false, children, ...rest }) {
|
|
17
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
|
|
18
|
+
return (_jsxs("div", { ...rest, className: cx(start ? "col-span-2 grid grid-cols-subgrid" : "flex justify-between gap-3", total && "border-t border-slate-300 pt-1.5 font-semibold"), children: [_jsx("dt", { className: total ? "text-slate-900" : "text-slate-600", children: label }), _jsx("dd", { className: cx(!start && "text-right", "tabular-nums"), children: children })] }));
|
|
19
19
|
}
|
package/dist/ListRow.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ type Shared = {
|
|
|
12
12
|
divided?: boolean | undefined;
|
|
13
13
|
/** Content before the label: an Avatar, a thumbnail, an icon. */
|
|
14
14
|
leading?: ReactNode | undefined;
|
|
15
|
+
/** A second, muted line under the label (a date, a note), like `TableCell`'s `secondary`. */
|
|
16
|
+
secondary?: ReactNode | undefined;
|
|
15
17
|
/** Content at the far end of the row: a value, a count, a Pill. */
|
|
16
18
|
trailing?: ReactNode | undefined;
|
|
17
19
|
/**
|
|
@@ -97,7 +99,7 @@ export type ListRowProps = ListRowButtonProps | ListRowLinkProps | ListRowStatic
|
|
|
97
99
|
* collapsible section: `aria-expanded` and a chevron. Rows sit in a `Card flush`; `divided` draws
|
|
98
100
|
* the line between them, on each row after the first.
|
|
99
101
|
*
|
|
100
|
-
* `leading` is content before the label (`leadingWidth="thumbnail"` gives it a fixed picture width),
|
|
102
|
+
* `secondary` is a second, muted line under the label. `leading` is content before the label (`leadingWidth="thumbnail"` gives it a fixed picture width),
|
|
101
103
|
* `selected` marks the row in view, `focused` the one the keyboard cursor is on and `active` fills
|
|
102
104
|
* the row a list follows (`onPointerEnter`, `onPointerLeave` and `onFocus` reach
|
|
103
105
|
* every form; on a linked row the pointer handlers go on the row itself). The label is a
|
package/dist/ListRow.js
CHANGED
|
@@ -42,11 +42,11 @@ function look(props) {
|
|
|
42
42
|
return cx(rowBase(props), PAD[size], divided && "border-t border-slate-200/70", fillOf(props), focused && FOCUSED);
|
|
43
43
|
}
|
|
44
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;
|
|
45
|
+
const { leading: _l, secondary: _sec, leadingWidth: _lw, trailing: _t, focused: _f, active = false, selected, size: _s, divided: _d, static: _st, actions, revealOnHover = false, expanded: _e, href: _h, render: _r, start, label, end, ...rest } = props;
|
|
46
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
47
|
}
|
|
48
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;
|
|
49
|
+
const { leading: _l, secondary: _sec, leadingWidth: _lw, trailing: _t, focused: _f, active: _ac, selected, expanded: _e, size: _s, divided: _d, static: _st, actions: _a, revealOnHover: _rh, start, label, end, onPointerEnter, onPointerLeave, ...rest } = props;
|
|
50
50
|
const rowId = useId();
|
|
51
51
|
return (_jsxs("div", { id: rowId, onPointerEnter: onPointerEnter, onPointerLeave: onPointerLeave, className: look(props), children: [renderLink({
|
|
52
52
|
...(rest["aria-label"] === undefined ? { "aria-labelledby": rowId } : {}),
|
|
@@ -57,7 +57,7 @@ function LinkRow(props) {
|
|
|
57
57
|
}), start, label, end] }));
|
|
58
58
|
}
|
|
59
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;
|
|
60
|
+
const { leading: _l, secondary: _sec, leadingWidth: _lw, trailing: _t, focused: _f, active: _ac, selected, expanded, size: _s, divided: _d, static: _st, actions: _a, revealOnHover: _rh, href: _h, render: _r, start, label, end, ...rest } = props;
|
|
61
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
62
|
}
|
|
63
63
|
/**
|
|
@@ -68,7 +68,7 @@ function ButtonRow(props) {
|
|
|
68
68
|
* collapsible section: `aria-expanded` and a chevron. Rows sit in a `Card flush`; `divided` draws
|
|
69
69
|
* the line between them, on each row after the first.
|
|
70
70
|
*
|
|
71
|
-
* `leading` is content before the label (`leadingWidth="thumbnail"` gives it a fixed picture width),
|
|
71
|
+
* `secondary` is a second, muted line under the label. `leading` is content before the label (`leadingWidth="thumbnail"` gives it a fixed picture width),
|
|
72
72
|
* `selected` marks the row in view, `focused` the one the keyboard cursor is on and `active` fills
|
|
73
73
|
* the row a list follows (`onPointerEnter`, `onPointerLeave` and `onFocus` reach
|
|
74
74
|
* every form; on a linked row the pointer handlers go on the row itself). The label is a
|
|
@@ -77,10 +77,10 @@ function ButtonRow(props) {
|
|
|
77
77
|
* link): `actions` follow `trailing`, and `revealOnHover` shows them only on hover or focus.
|
|
78
78
|
*/
|
|
79
79
|
export function ListRow(props) {
|
|
80
|
-
const { leading, leadingWidth, trailing, children } = props;
|
|
80
|
+
const { leading, leadingWidth, trailing, secondary, children } = props;
|
|
81
81
|
const pieces = {
|
|
82
82
|
start: leading != null && (_jsx("span", { className: cx("shrink-0", leadingWidth === "thumbnail" && THUMBNAIL), children: leading })),
|
|
83
|
-
label:
|
|
83
|
+
label: (_jsxs("div", { className: "min-w-0 flex-1", children: [children, secondary != null && secondary !== false && (_jsx("div", { className: "text-xs font-normal text-slate-500", children: secondary }))] })),
|
|
84
84
|
end: trailing != null && (_jsx("span", { className: "shrink-0 tabular-nums text-slate-500", children: trailing })),
|
|
85
85
|
};
|
|
86
86
|
if (props.static)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { type ImageAspect, type ImageWidth } from "./Image.js";
|
|
3
|
+
import type { Passthrough } from "./props.js";
|
|
4
|
+
export type MediaTileProps = {
|
|
5
|
+
/** The thumbnail, a local path or a data URL. Left out, or if it fails to load, a placeholder shows. */
|
|
6
|
+
src?: string | undefined;
|
|
7
|
+
/** What the thumbnail shows; it is the tile's name, with `label`. */
|
|
8
|
+
alt: string;
|
|
9
|
+
/** A short line on the bottom edge of the picture, on the media scrim: who posted it, or "Earlier". */
|
|
10
|
+
label?: ReactNode;
|
|
11
|
+
/** Muted text under the picture, e.g. when. */
|
|
12
|
+
caption?: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* The shape of the thumbnail, as `Image`'s `aspect`.
|
|
15
|
+
* @default 3:4
|
|
16
|
+
*/
|
|
17
|
+
aspect?: ImageAspect | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* `sm` 96px, `md` 160px or `lg` 256px wide; `full` fills the parent (a tile in a grid).
|
|
20
|
+
* @default md
|
|
21
|
+
*/
|
|
22
|
+
width?: ImageWidth | undefined;
|
|
23
|
+
/** Draws a play mark on the thumbnail: the tile opens a video, not a photo. Say so in `alt` too. */
|
|
24
|
+
video?: boolean | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Makes the whole tile one button; open a `Lightbox` from it. Without it the tile is a plain,
|
|
27
|
+
* non-interactive block.
|
|
28
|
+
*/
|
|
29
|
+
onClick?: (() => void) | undefined;
|
|
30
|
+
} & Passthrough;
|
|
31
|
+
/**
|
|
32
|
+
* A thumbnail tile: a picture in a fixed shape with an optional label on its bottom edge and a
|
|
33
|
+
* caption under it. With `onClick` the whole tile is one button, which is how a grid or a
|
|
34
|
+
* `ScrollStrip` of media opens each item in a `Lightbox`. The label sits on the same near-black
|
|
35
|
+
* media scrim as the Lightbox, in every theme. The thumbnail is cropped to fill its shape.
|
|
36
|
+
*/
|
|
37
|
+
export declare function MediaTile({ src, alt, label, caption, aspect, width, video, onClick, ...rest }: MediaTileProps): ReactElement;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing } from "./cx.js";
|
|
3
|
+
import { Icon } from "./Icon.js";
|
|
4
|
+
import { IMAGE_WIDTH, Image } from "./Image.js";
|
|
5
|
+
const PLAY = "M6.3 2.84A1.5 1.5 0 0 0 4 4.11v11.78a1.5 1.5 0 0 0 2.3 1.27l9.34-5.89a1.5 1.5 0 0 0 0-2.54L6.3 2.84Z";
|
|
6
|
+
/**
|
|
7
|
+
* A thumbnail tile: a picture in a fixed shape with an optional label on its bottom edge and a
|
|
8
|
+
* caption under it. With `onClick` the whole tile is one button, which is how a grid or a
|
|
9
|
+
* `ScrollStrip` of media opens each item in a `Lightbox`. The label sits on the same near-black
|
|
10
|
+
* media scrim as the Lightbox, in every theme. The thumbnail is cropped to fill its shape.
|
|
11
|
+
*/
|
|
12
|
+
export function MediaTile({ src, alt, label, caption, aspect = "3:4", width = "md", video = false, onClick, ...rest }) {
|
|
13
|
+
const body = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "relative block overflow-hidden rounded-lg", children: [_jsx(Image, { src: src, alt: alt, aspect: aspect }), video && (_jsx("span", { "aria-hidden": "true", className: "absolute inset-0 grid place-items-center", children: _jsx("span", { className: "grid size-10 place-items-center rounded-full bg-black/90 text-white", children: _jsx(Icon, { size: "lg", children: _jsx("svg", { viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { d: PLAY }) }) }) }) })), label && (_jsx("span", { className: "absolute inset-x-0 bottom-0 truncate bg-black/90 px-2 py-1 text-xs font-medium text-white", children: label }))] }), caption && _jsx("span", { className: "mt-1 block truncate text-xs text-slate-600", children: caption })] }));
|
|
14
|
+
const box = cx("block min-w-0 text-left", IMAGE_WIDTH[width]);
|
|
15
|
+
if (!onClick) {
|
|
16
|
+
return (_jsx("div", { ...rest, className: box, children: body }));
|
|
17
|
+
}
|
|
18
|
+
return (_jsx("button", { ...rest, type: "button", onClick: onClick, className: cx(box, "group rounded-lg", focusRing), children: body }));
|
|
19
|
+
}
|
package/dist/PageLayouts.d.ts
CHANGED
|
@@ -12,10 +12,22 @@ export declare function ListLayout({ filters, empty, isEmpty, children, breadcru
|
|
|
12
12
|
isEmpty?: boolean;
|
|
13
13
|
children?: ReactNode;
|
|
14
14
|
} & Passthrough): ReactElement;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* A detail page: header, then `children` (two thirds from `lg`) beside `aside` (one third), which
|
|
17
|
+
* stacks below on a smaller screen. `stickyAside` keeps the aside in view from `lg` as a long page
|
|
18
|
+
* scrolls: it sticks under AppShell's header and, if it is taller than the room left, scrolls
|
|
19
|
+
* itself so its end stays reachable.
|
|
20
|
+
*/
|
|
21
|
+
export declare function DetailLayout({ aside, stickyAside, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }: PageHeaderProps & {
|
|
17
22
|
/** The side column: one third from `lg`, below `children` on a smaller screen. */
|
|
18
23
|
aside: ReactNode;
|
|
24
|
+
/**
|
|
25
|
+
* From `lg`, keeps the aside in view under the header as the page scrolls; an aside taller than
|
|
26
|
+
* the screen scrolls inside itself. Off by default, since an aside of ordinary cards may be
|
|
27
|
+
* taller than the screen. Below `lg` the aside stacks as ever.
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
stickyAside?: boolean | undefined;
|
|
19
31
|
children: ReactNode;
|
|
20
32
|
} & Passthrough): ReactElement;
|
|
21
33
|
/** A form page: header (no header `actions`), then a `max-w-3xl` column that starts at the Container's left edge (it is not re-centred), with an `actions` row at the end. Other props go on the `<form>`. */
|
|
@@ -51,6 +63,11 @@ declare const COLUMNS: {
|
|
|
51
63
|
readonly 3: "lg:grid-cols-3";
|
|
52
64
|
readonly 4: "lg:grid-cols-4";
|
|
53
65
|
};
|
|
66
|
+
/** Two unequal columns, one on a phone, the ratio from `md`; `minmax(0,…)` lets a long value shrink instead of widening its column. */
|
|
67
|
+
declare const RATIOS: {
|
|
68
|
+
readonly "2:1": "md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]";
|
|
69
|
+
readonly "1:2": "md:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]";
|
|
70
|
+
};
|
|
54
71
|
/** `columns="auto"` fits as many cards per row as the width allows, none narrower than this. */
|
|
55
72
|
declare const AUTO_MIN_WIDTH: {
|
|
56
73
|
readonly sm: "grid-cols-[repeat(auto-fill,minmax(12rem,1fr))]";
|
|
@@ -58,10 +75,11 @@ declare const AUTO_MIN_WIDTH: {
|
|
|
58
75
|
};
|
|
59
76
|
type CardGridProps = ({
|
|
60
77
|
/**
|
|
61
|
-
* Cards per row from `lg` (one on a phone, two from `sm`),
|
|
78
|
+
* Cards per row from `lg` (one on a phone, two from `sm`), `auto` for as many as fit, or a ratio:
|
|
79
|
+
* `2:1` or `1:2` is two columns, the first or the second twice as wide, from `md` (one on a phone).
|
|
62
80
|
* @default 3
|
|
63
81
|
*/
|
|
64
|
-
columns?: keyof typeof COLUMNS | undefined;
|
|
82
|
+
columns?: keyof typeof COLUMNS | keyof typeof RATIOS | undefined;
|
|
65
83
|
/** With `columns="auto"`, the narrowest a card gets: `md` 17rem or `sm` 12rem. */
|
|
66
84
|
minWidth?: undefined;
|
|
67
85
|
} | {
|
|
@@ -84,29 +102,13 @@ type CardGridProps = ({
|
|
|
84
102
|
} & Passthrough;
|
|
85
103
|
/**
|
|
86
104
|
* A responsive grid of tiles: one column on a phone, two from `sm`, `columns` from `lg`.
|
|
87
|
-
* `columns="
|
|
105
|
+
* `columns="2:1"` or `"1:2"` is instead two unequal columns from `md`, one on a phone: a main card beside a
|
|
106
|
+
* narrow one (a record's detail and its side panel). `columns="auto"` instead fits as many per row as the width allows, each at least `minWidth` wide:
|
|
88
107
|
* `md` (17rem, the default) or `sm` (12rem). `align="start"` keeps each card its own height
|
|
89
108
|
* instead of stretching it to the tallest in its row. `as="ul"` makes it a list of cards for a
|
|
90
109
|
* screen reader: the grid is a `<ul>` and each child is wrapped in an `<li>`, still stretched.
|
|
91
110
|
*/
|
|
92
111
|
export declare function CardGrid({ columns, minWidth, align, as: Tag, children, ...rest }: CardGridProps): ReactElement;
|
|
93
|
-
/**
|
|
94
|
-
* A `rail` of links beside the `children` canvas, in the Container the shell already gives every
|
|
95
|
-
* page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
|
|
96
|
-
* ends at the header's right edge. The rail has no background and no border, so it is a list of
|
|
97
|
-
* links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
|
|
98
|
-
* the canvas filling the rest; below `md` it stacks above the canvas. A long rail sets `railLabel`:
|
|
99
|
-
* below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
|
|
100
|
-
* phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
|
|
101
|
-
* border or a fixed height, and it no longer sits beside the canvas on a phone.
|
|
102
|
-
*/
|
|
103
|
-
export declare function SidebarLayout({ rail, railLabel, children, ...rest }: {
|
|
104
|
-
/** The links beside the canvas, usually a `Tabs variant="pill" orientation="vertical"`. */
|
|
105
|
-
rail: ReactNode;
|
|
106
|
-
/** Folds the rail behind a disclosure with this name below `md` (a long rail). Left out, the rail is always open. */
|
|
107
|
-
railLabel?: string | undefined;
|
|
108
|
-
children: ReactNode;
|
|
109
|
-
} & Passthrough): ReactElement;
|
|
110
112
|
/**
|
|
111
113
|
* Edge-to-edge `children` with an optional right `panel` (`lg:w-72`) that stacks below on a smaller
|
|
112
114
|
* screen. Render it in `<AppShell bleed>`. It deliberately breaks the shared left edge: a viewer
|
package/dist/PageLayouts.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children, isValidElement, } from "react";
|
|
3
3
|
import { Card } from "./Card.js";
|
|
4
|
-
import { cx
|
|
4
|
+
import { cx } from "./cx.js";
|
|
5
5
|
import { PageHeader, PageHeaderContent } from "./PageHeader.js";
|
|
6
6
|
import { STACK_WIDTH, Stack } from "./Stack.js";
|
|
7
|
+
import { STICKY_ASIDE_LG, STICKY_SCROLL_LG } from "./stickyColumn.js";
|
|
7
8
|
import { StickyBar } from "./StickyBar.js";
|
|
8
9
|
// Compositions of the layout foundation. None sets a page width or outer padding of its own; the
|
|
9
10
|
// ones that narrow their content do it inside the Container they are rendered in.
|
|
@@ -11,9 +12,14 @@ import { StickyBar } from "./StickyBar.js";
|
|
|
11
12
|
export function ListLayout({ filters, empty, isEmpty = false, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
|
|
12
13
|
return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, actions: actions, tabs: tabs }), _jsxs(Stack, { ...rest, children: [filters, isEmpty ? empty : children] })] }));
|
|
13
14
|
}
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* A detail page: header, then `children` (two thirds from `lg`) beside `aside` (one third), which
|
|
17
|
+
* stacks below on a smaller screen. `stickyAside` keeps the aside in view from `lg` as a long page
|
|
18
|
+
* scrolls: it sticks under AppShell's header and, if it is taller than the room left, scrolls
|
|
19
|
+
* itself so its end stays reachable.
|
|
20
|
+
*/
|
|
21
|
+
export function DetailLayout({ aside, stickyAside = false, children, breadcrumbs, title, subtitle, actions, tabs, ...rest }) {
|
|
22
|
+
return (_jsxs(_Fragment, { children: [_jsx(PageHeader, { breadcrumbs: breadcrumbs, title: title, subtitle: subtitle, actions: actions, tabs: tabs }), _jsxs("div", { ...rest, className: "grid gap-6 lg:grid-cols-3", children: [_jsx("div", { className: "min-w-0 lg:col-span-2", children: _jsx(Stack, { children: children }) }), _jsx("aside", { className: cx("min-w-0", stickyAside && STICKY_ASIDE_LG), children: _jsx("div", { className: stickyAside ? STICKY_SCROLL_LG : undefined, children: _jsx(Stack, { children: aside }) }) })] })] }));
|
|
17
23
|
}
|
|
18
24
|
/** A form page: header (no header `actions`), then a `max-w-3xl` column that starts at the Container's left edge (it is not re-centred), with an `actions` row at the end. Other props go on the `<form>`. */
|
|
19
25
|
export function FormLayout({ actions, children, breadcrumbs, title, subtitle, tabs, ...form }) {
|
|
@@ -36,38 +42,36 @@ const COLUMNS = {
|
|
|
36
42
|
3: "lg:grid-cols-3",
|
|
37
43
|
4: "lg:grid-cols-4",
|
|
38
44
|
};
|
|
45
|
+
/** Two unequal columns, one on a phone, the ratio from `md`; `minmax(0,…)` lets a long value shrink instead of widening its column. */
|
|
46
|
+
const RATIOS = {
|
|
47
|
+
"2:1": "md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]",
|
|
48
|
+
"1:2": "md:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]",
|
|
49
|
+
};
|
|
39
50
|
/** `columns="auto"` fits as many cards per row as the width allows, none narrower than this. */
|
|
40
51
|
const AUTO_MIN_WIDTH = {
|
|
41
52
|
sm: "grid-cols-[repeat(auto-fill,minmax(12rem,1fr))]",
|
|
42
53
|
md: "grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]",
|
|
43
54
|
};
|
|
55
|
+
function gridColumns(columns, minWidth) {
|
|
56
|
+
if (columns === "auto")
|
|
57
|
+
return AUTO_MIN_WIDTH[minWidth];
|
|
58
|
+
if (typeof columns === "string")
|
|
59
|
+
return cx("grid-cols-1", RATIOS[columns]);
|
|
60
|
+
return cx("grid-cols-1 sm:grid-cols-2", COLUMNS[columns]);
|
|
61
|
+
}
|
|
44
62
|
/**
|
|
45
63
|
* A responsive grid of tiles: one column on a phone, two from `sm`, `columns` from `lg`.
|
|
46
|
-
* `columns="
|
|
64
|
+
* `columns="2:1"` or `"1:2"` is instead two unequal columns from `md`, one on a phone: a main card beside a
|
|
65
|
+
* narrow one (a record's detail and its side panel). `columns="auto"` instead fits as many per row as the width allows, each at least `minWidth` wide:
|
|
47
66
|
* `md` (17rem, the default) or `sm` (12rem). `align="start"` keeps each card its own height
|
|
48
67
|
* instead of stretching it to the tallest in its row. `as="ul"` makes it a list of cards for a
|
|
49
68
|
* screen reader: the grid is a `<ul>` and each child is wrapped in an `<li>`, still stretched.
|
|
50
69
|
*/
|
|
51
70
|
export function CardGrid({ columns = 3, minWidth = "md", align = "stretch", as: Tag = "div", children, ...rest }) {
|
|
52
|
-
return (_jsx(Tag, { ...rest, ...(Tag === "ul" ? { role: "list" } : {}), className: cx("grid gap-4", columns === "
|
|
53
|
-
? AUTO_MIN_WIDTH[minWidth]
|
|
54
|
-
: cx("grid-cols-1 sm:grid-cols-2", COLUMNS[columns]), align === "start" && "items-start"), children: Tag === "ul"
|
|
71
|
+
return (_jsx(Tag, { ...rest, ...(Tag === "ul" ? { role: "list" } : {}), className: cx("grid gap-4", gridColumns(columns, minWidth), align === "start" && "items-start"), children: Tag === "ul"
|
|
55
72
|
? Children.toArray(children).map((child, index) => (_jsx("li", { className: "grid", children: child }, isValidElement(child) ? child.key : index)))
|
|
56
73
|
: children }));
|
|
57
74
|
}
|
|
58
|
-
/**
|
|
59
|
-
* A `rail` of links beside the `children` canvas, in the Container the shell already gives every
|
|
60
|
-
* page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
|
|
61
|
-
* ends at the header's right edge. The rail has no background and no border, so it is a list of
|
|
62
|
-
* links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
|
|
63
|
-
* the canvas filling the rest; below `md` it stacks above the canvas. A long rail sets `railLabel`:
|
|
64
|
-
* below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
|
|
65
|
-
* phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
|
|
66
|
-
* border or a fixed height, and it no longer sits beside the canvas on a phone.
|
|
67
|
-
*/
|
|
68
|
-
export function SidebarLayout({ rail, railLabel, children, ...rest }) {
|
|
69
|
-
return (_jsxs("div", { ...rest, className: "flex flex-col gap-8 md:flex-row md:gap-10", children: [_jsx("aside", { className: "w-full shrink-0 md:w-56", children: railLabel === undefined ? (rail) : (_jsxs("details", { className: "group md:[&::details-content]:[content-visibility:visible]", children: [_jsxs("summary", { className: cx("flex cursor-pointer list-none items-center justify-between rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-900 select-none hover:bg-slate-50 md:hidden [&::-webkit-details-marker]:hidden", focusRing), children: [railLabel, _jsx("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className: "shrink-0 text-slate-500 group-open:rotate-180", children: _jsx("path", { d: "M5 8l5 5 5-5" }) })] }), _jsx("div", { className: "mt-2 md:mt-0", children: rail })] })) }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
|
|
70
|
-
}
|
|
71
75
|
/**
|
|
72
76
|
* Edge-to-edge `children` with an optional right `panel` (`lg:w-72`) that stacks below on a smaller
|
|
73
77
|
* screen. Render it in `<AppShell bleed>`. It deliberately breaks the shared left edge: a viewer
|
package/dist/Pill.d.ts
CHANGED
|
@@ -15,8 +15,23 @@ type PillBase = {
|
|
|
15
15
|
round?: boolean;
|
|
16
16
|
/** The tooltip on hover, for a pill whose text is short or cut off: the full status, a date. */
|
|
17
17
|
title?: string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The name half of a two-tone `label: value` pill: `children` is the value. The label keeps the
|
|
20
|
+
* tone's tint and the value sits on a plain surface beside it, in a hairline border. Keep the
|
|
21
|
+
* label to a word or two (`Weight`, `Sleep`).
|
|
22
|
+
*/
|
|
23
|
+
label?: string | undefined;
|
|
24
|
+
};
|
|
25
|
+
type Removable = {
|
|
26
|
+
onRemove?: undefined;
|
|
27
|
+
removeLabel?: undefined;
|
|
28
|
+
} | {
|
|
29
|
+
/** Adds a small x button after the text that calls this: for a chip the reader can drop (an attachment, a filter). It sits beside a link or button pill, never inside it. */
|
|
30
|
+
onRemove: () => void;
|
|
31
|
+
/** The x button's accessible name and tooltip: `Remove invoice.pdf`. Required with `onRemove`. */
|
|
32
|
+
removeLabel: string;
|
|
18
33
|
};
|
|
19
|
-
type PillProps = PillBase & Passthrough & ({
|
|
34
|
+
type PillProps = PillBase & Removable & Passthrough & ({
|
|
20
35
|
href?: undefined;
|
|
21
36
|
render?: undefined;
|
|
22
37
|
external?: undefined;
|
|
@@ -44,10 +59,13 @@ type PillProps = PillBase & Passthrough & ({
|
|
|
44
59
|
})));
|
|
45
60
|
/**
|
|
46
61
|
* A small tinted label; `dot` adds a leading dot in the pill's own text colour, `round` swaps the
|
|
47
|
-
* rounded-md corners for a full pill shape. `
|
|
48
|
-
* the
|
|
49
|
-
*
|
|
50
|
-
*
|
|
62
|
+
* rounded-md corners for a full pill shape. `label` makes it two-tone, `label: value`: the label
|
|
63
|
+
* keeps the tone's tint and the children, the value, sit on the surface beside it. `href` makes it
|
|
64
|
+
* a link (a plain `<a>`, or with `render` the app's own router `<Link>`:
|
|
65
|
+
* `render={(p) => <Link to="/flakes" {...p} />}`; `external` opens a new tab) and `onClick` makes it
|
|
66
|
+
* a button, each with a hover dim and the focus ring. `onRemove` adds an x button after the text
|
|
67
|
+
* (named by `removeLabel`), a separate button beside a link or button pill rather than nested in
|
|
68
|
+
* it. `title` is the native tooltip.
|
|
51
69
|
*/
|
|
52
|
-
export declare function Pill({ children, tone, dot, round, href, render, external, onClick, ...rest }: PillProps): ReactElement;
|
|
70
|
+
export declare function Pill({ children, tone, dot, round, label, href, render, external, onClick, onRemove, removeLabel, ...rest }: PillProps): ReactElement;
|
|
53
71
|
export {};
|
package/dist/Pill.js
CHANGED
|
@@ -10,25 +10,81 @@ const PILL_TONES = {
|
|
|
10
10
|
violet: "bg-violet-50 text-violet-700",
|
|
11
11
|
sky: "bg-sky-50 text-sky-700",
|
|
12
12
|
};
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const INTERACTIVE = cx("transition hover:opacity-80", focusRing);
|
|
14
|
+
/** The x on a removable pill: two strokes in the pill's text colour. */
|
|
15
|
+
const REMOVE_MARK = (_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 12 12", className: "size-3", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", children: _jsx("path", { d: "M3 3l6 6M9 3l-6 6" }) }));
|
|
16
|
+
/** The two-tone pill is a hairline-bordered box: the label half carries the tint, the value half sits on the surface. */
|
|
17
|
+
const SPLIT = "inline-flex overflow-hidden border border-slate-200 text-xs font-medium";
|
|
18
|
+
/** The box a one-piece pill draws, or the whole pill when it is a single element. */
|
|
19
|
+
function pillClass({ tone, round, dot, labelled }, interactive) {
|
|
20
|
+
return cx(round ? "rounded-full" : "rounded-md", labelled ? SPLIT : cx("px-1.5 py-0.5 text-xs font-medium", PILL_TONES[tone]), dot && !labelled && "inline-flex items-center gap-1", interactive && INTERACTIVE);
|
|
21
|
+
}
|
|
22
|
+
/** The shell round a pill with an x: it carries the tint and the corners, and the text and the x are its two children. */
|
|
23
|
+
function shellClass({ tone, round, labelled }) {
|
|
24
|
+
return cx(round ? "rounded-full" : "rounded-md", "inline-flex items-center text-xs font-medium", labelled ? "overflow-hidden border border-slate-200" : PILL_TONES[tone]);
|
|
25
|
+
}
|
|
26
|
+
/** The text half of a removable pill, which is a link, a button or a span beside the x. */
|
|
27
|
+
function mainClass({ dot, labelled }, interactive) {
|
|
28
|
+
return cx(labelled ? "inline-flex self-stretch" : "py-0.5 pl-1.5 pr-1", dot && !labelled && "inline-flex items-center gap-1", interactive && INTERACTIVE);
|
|
29
|
+
}
|
|
30
|
+
function removeClass({ labelled }) {
|
|
31
|
+
return cx("inline-flex cursor-pointer items-center", labelled ? "self-stretch bg-white pl-0.5 pr-1.5 text-slate-900" : "py-0.5 pl-0.5 pr-1.5", INTERACTIVE);
|
|
32
|
+
}
|
|
33
|
+
/** The inside of a pill: the dot and the text, or for a two-tone one the tinted label beside the value. */
|
|
34
|
+
function PillBody({ look: { tone, dot, removable }, label, children, }) {
|
|
35
|
+
const mark = dot && _jsx("span", { "aria-hidden": "true", className: "h-1.5 w-1.5 rounded-full bg-current" });
|
|
36
|
+
if (label === undefined) {
|
|
37
|
+
return (_jsxs(_Fragment, { children: [mark, children] }));
|
|
38
|
+
}
|
|
39
|
+
return (_jsxs(_Fragment, { children: [_jsxs("span", { className: cx("px-1.5 py-px", PILL_TONES[tone], dot && "inline-flex items-center gap-1"), children: [mark, label, _jsx("span", { className: "sr-only", children: ":" })] }), _jsx("span", { className: cx("bg-white py-px text-slate-900", removable ? "pl-1.5 pr-0.5" : "px-1.5"), children: children })] }));
|
|
40
|
+
}
|
|
41
|
+
/** The pill itself, or the text half of a removable one: a link, a button or a span. */
|
|
42
|
+
function renderMain({ isLink, interactive, look, rest, href, render, external, onClick, body, }) {
|
|
43
|
+
const className = look.removable ? mainClass(look, interactive) : pillClass(look, interactive);
|
|
44
|
+
// A removable pill's attributes go on its link or button; on a plain one, the shell is the pill.
|
|
45
|
+
const own = look.removable && !interactive ? {} : rest;
|
|
46
|
+
if (isLink)
|
|
47
|
+
return renderLink({ ...own, href, render, external, className, children: body });
|
|
48
|
+
if (onClick) {
|
|
49
|
+
return (_jsx("button", { type: "button", ...own, onClick: onClick, className: cx(className, "cursor-pointer"), children: body }));
|
|
50
|
+
}
|
|
51
|
+
return (_jsx("span", { ...own, className: className, children: body }));
|
|
52
|
+
}
|
|
53
|
+
/** The shell round a removable pill: the text half and the x button beside it. */
|
|
54
|
+
function RemoveShell({ look, remove: { onRemove, removeLabel }, attrs, interactive, children, }) {
|
|
55
|
+
return (_jsxs("span", { ...(interactive ? {} : attrs), className: shellClass(look), children: [children, _jsx("button", { type: "button", "aria-label": removeLabel, title: removeLabel, onClick: onRemove, className: removeClass(look), children: REMOVE_MARK })] }));
|
|
15
56
|
}
|
|
16
57
|
/**
|
|
17
58
|
* A small tinted label; `dot` adds a leading dot in the pill's own text colour, `round` swaps the
|
|
18
|
-
* rounded-md corners for a full pill shape. `
|
|
19
|
-
* the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
59
|
+
* rounded-md corners for a full pill shape. `label` makes it two-tone, `label: value`: the label
|
|
60
|
+
* keeps the tone's tint and the children, the value, sit on the surface beside it. `href` makes it
|
|
61
|
+
* a link (a plain `<a>`, or with `render` the app's own router `<Link>`:
|
|
62
|
+
* `render={(p) => <Link to="/flakes" {...p} />}`; `external` opens a new tab) and `onClick` makes it
|
|
63
|
+
* a button, each with a hover dim and the focus ring. `onRemove` adds an x button after the text
|
|
64
|
+
* (named by `removeLabel`), a separate button beside a link or button pill rather than nested in
|
|
65
|
+
* it. `title` is the native tooltip.
|
|
22
66
|
*/
|
|
23
|
-
export function Pill({ children, tone = "slate", dot = false, round = false, href, render, external = false, onClick, ...rest }) {
|
|
67
|
+
export function Pill({ children, tone = "slate", dot = false, round = false, label, href, render, external = false, onClick, onRemove, removeLabel, ...rest }) {
|
|
24
68
|
const isLink = href !== undefined || render !== undefined;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
69
|
+
const interactive = isLink || onClick !== undefined;
|
|
70
|
+
const look = {
|
|
71
|
+
tone,
|
|
72
|
+
round,
|
|
73
|
+
dot,
|
|
74
|
+
labelled: label !== undefined,
|
|
75
|
+
removable: onRemove !== undefined,
|
|
76
|
+
};
|
|
77
|
+
const body = (_jsx(PillBody, { look: look, label: label, children: children }));
|
|
78
|
+
const main = renderMain({
|
|
79
|
+
isLink,
|
|
80
|
+
interactive,
|
|
81
|
+
look,
|
|
82
|
+
rest,
|
|
83
|
+
href,
|
|
84
|
+
render,
|
|
85
|
+
external,
|
|
86
|
+
onClick,
|
|
87
|
+
body,
|
|
88
|
+
});
|
|
89
|
+
return onRemove === undefined ? (main) : (_jsx(RemoveShell, { look: look, remove: { onRemove, removeLabel }, attrs: rest, interactive: interactive, children: main }));
|
|
34
90
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { ClusterGap } from "./Cluster.js";
|
|
3
|
+
import type { Passthrough } from "./props.js";
|
|
4
|
+
export type ScrollStripProps = {
|
|
5
|
+
/** Names the strip for a screen reader, e.g. "Recent statuses". */
|
|
6
|
+
label: string;
|
|
7
|
+
/** The items, one after another: `MediaTile`s, `Avatar`s. They keep their own width. */
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* The space between items, as `Cluster`'s `gap`.
|
|
11
|
+
* @default sm
|
|
12
|
+
*/
|
|
13
|
+
gap?: ClusterGap | undefined;
|
|
14
|
+
} & Passthrough;
|
|
15
|
+
/**
|
|
16
|
+
* A single row of items that scrolls sideways when it runs past the width of its parent, with a
|
|
17
|
+
* thin themed scrollbar and the items snapping into place: a strip of `MediaTile`s or `Avatar`s.
|
|
18
|
+
* Items never shrink or wrap. The strip takes the keyboard focus, so the arrow keys scroll it
|
|
19
|
+
* without a pointer. A grid that wraps is a `CardGrid` or a `Cluster`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ScrollStrip({ label, children, gap, ...rest }: ScrollStripProps): ReactElement;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx, focusRing, themedScrollbar } from "./cx.js";
|
|
3
|
+
import { STACK_GAP } from "./Stack.js";
|
|
4
|
+
/**
|
|
5
|
+
* A single row of items that scrolls sideways when it runs past the width of its parent, with a
|
|
6
|
+
* thin themed scrollbar and the items snapping into place: a strip of `MediaTile`s or `Avatar`s.
|
|
7
|
+
* Items never shrink or wrap. The strip takes the keyboard focus, so the arrow keys scroll it
|
|
8
|
+
* without a pointer. A grid that wraps is a `CardGrid` or a `Cluster`.
|
|
9
|
+
*/
|
|
10
|
+
export function ScrollStrip({ label, children, gap = "sm", ...rest }) {
|
|
11
|
+
return (_jsx("div", { ...rest, role: "group", "aria-label": label, tabIndex: 0, className: cx("overflow-x-auto rounded-lg pb-2 [scroll-snap-type:x_proximity]", themedScrollbar, focusRing), children: _jsx("div", { className: cx("flex w-max min-w-full text-slate-900 *:shrink-0 *:snap-start", STACK_GAP[gap]), children: children }) }));
|
|
12
|
+
}
|