@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.
Files changed (50) hide show
  1. package/README.md +18 -8
  2. package/dist/Avatar.d.ts +8 -4
  3. package/dist/Avatar.js +11 -3
  4. package/dist/AvatarStack.js +1 -0
  5. package/dist/Callout.d.ts +8 -2
  6. package/dist/Callout.js +3 -3
  7. package/dist/Card.d.ts +2 -0
  8. package/dist/Card.js +14 -8
  9. package/dist/CardParts.d.ts +6 -0
  10. package/dist/CardParts.js +6 -0
  11. package/dist/Cluster.d.ts +9 -4
  12. package/dist/Cluster.js +12 -5
  13. package/dist/Dialog.d.ts +4 -2
  14. package/dist/Dialog.js +9 -3
  15. package/dist/EmptyState.d.ts +7 -1
  16. package/dist/EmptyState.js +3 -2
  17. package/dist/Field.d.ts +8 -1
  18. package/dist/Field.js +4 -2
  19. package/dist/Image.d.ts +46 -0
  20. package/dist/Image.js +42 -0
  21. package/dist/KeyValueList.d.ts +4 -2
  22. package/dist/KeyValueList.js +3 -3
  23. package/dist/ListRow.d.ts +3 -1
  24. package/dist/ListRow.js +6 -6
  25. package/dist/MediaTile.d.ts +37 -0
  26. package/dist/MediaTile.js +19 -0
  27. package/dist/PageLayouts.d.ts +24 -22
  28. package/dist/PageLayouts.js +25 -21
  29. package/dist/Pill.d.ts +24 -6
  30. package/dist/Pill.js +72 -16
  31. package/dist/ScrollStrip.d.ts +21 -0
  32. package/dist/ScrollStrip.js +12 -0
  33. package/dist/SidebarLayout.d.ts +21 -0
  34. package/dist/SidebarLayout.js +18 -0
  35. package/dist/Stack.d.ts +15 -1
  36. package/dist/Stack.js +12 -2
  37. package/dist/StatTile.d.ts +2 -2
  38. package/dist/StatTile.js +2 -0
  39. package/dist/Table.d.ts +29 -12
  40. package/dist/Table.js +25 -11
  41. package/dist/TableEditCell.d.ts +19 -0
  42. package/dist/TableEditCell.js +58 -0
  43. package/dist/index.d.ts +6 -1
  44. package/dist/index.js +6 -1
  45. package/dist/size.d.ts +1 -1
  46. package/dist/stickyColumn.d.ts +17 -0
  47. package/dist/stickyColumn.js +17 -0
  48. package/dist/typography.d.ts +34 -10
  49. package/dist/typography.js +43 -13
  50. package/package.json +2 -2
@@ -0,0 +1,21 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Passthrough } from "./props.js";
3
+ /**
4
+ * A `rail` of links beside the `children` canvas, in the Container the shell already gives every
5
+ * page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
6
+ * ends at the header's right edge. The rail has no background and no border, so it is a list of
7
+ * links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
8
+ * the canvas filling the rest, and it stays in view under the shell's header as a long page scrolls;
9
+ * a rail taller than the screen scrolls itself, so its last link is reachable. Below `md` it stacks
10
+ * above the canvas and nothing sticks. A long rail sets `railLabel`:
11
+ * below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
12
+ * phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
13
+ * border or a fixed height, and it no longer sits beside the canvas on a phone.
14
+ */
15
+ export declare function SidebarLayout({ rail, railLabel, children, ...rest }: {
16
+ /** The links beside the canvas, usually a `Tabs variant="pill" orientation="vertical"`. */
17
+ rail: ReactNode;
18
+ /** Folds the rail behind a disclosure with this name below `md` (a long rail). Left out, the rail is always open. */
19
+ railLabel?: string | undefined;
20
+ children: ReactNode;
21
+ } & Passthrough): ReactElement;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx, focusRing } from "./cx.js";
3
+ import { STICKY_ASIDE_MD, STICKY_SCROLL_MD } from "./stickyColumn.js";
4
+ /**
5
+ * A `rail` of links beside the `children` canvas, in the Container the shell already gives every
6
+ * page (a plain `<AppShell>`, no `bleed`): the rail starts at the header logo's x and the canvas
7
+ * ends at the header's right edge. The rail has no background and no border, so it is a list of
8
+ * links on the page; a `Tabs variant="pill" orientation="vertical"` is its content. From `md` it is `w-56` with
9
+ * the canvas filling the rest, and it stays in view under the shell's header as a long page scrolls;
10
+ * a rail taller than the screen scrolls itself, so its last link is reachable. Below `md` it stacks
11
+ * above the canvas and nothing sticks. A long rail sets `railLabel`:
12
+ * below `md` it then folds behind a disclosure of that name, closed, so the page comes first on a
13
+ * phone, and from `md` it is the same rail. Upgrading from 0.2: the rail no longer has a fill, a
14
+ * border or a fixed height, and it no longer sits beside the canvas on a phone.
15
+ */
16
+ export function SidebarLayout({ rail, railLabel, children, ...rest }) {
17
+ return (_jsxs("div", { ...rest, className: "flex flex-col gap-8 md:flex-row md:gap-10", children: [_jsx("aside", { className: cx("w-full shrink-0 md:w-56", STICKY_ASIDE_MD), children: _jsx("div", { className: STICKY_SCROLL_MD, 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 })] }));
18
+ }
package/dist/Stack.d.ts CHANGED
@@ -2,6 +2,9 @@ import { type ComponentPropsWithoutRef, type ReactElement, type ReactNode } from
2
2
  import type { Passthrough, Unstyled } from "./props.js";
3
3
  export type StackGap = "xs" | "sm" | "md" | "lg" | "xl";
4
4
  export declare const STACK_GAP: Record<StackGap, string>;
5
+ export type StackRowFrom = "sm" | "md" | "lg";
6
+ /** The breakpoint where a `rowFrom` Stack turns from a column into a row. */
7
+ export declare const STACK_ROW_FROM: Record<StackRowFrom, string>;
5
8
  export type StackWidth = "narrow" | "default" | "wide" | "full";
6
9
  /** `default` adds nothing: the Stack is as wide as its parent. */
7
10
  export declare const STACK_WIDTH: Record<StackWidth, string | undefined>;
@@ -14,12 +17,16 @@ export declare const STACK_WIDTH: Record<StackWidth, string | undefined>;
14
17
  * is the parent's whole width and `default` sets nothing. A capped Stack starts at its parent's left
15
18
  * edge (it is never re-centred), so its first line lines up with every other block on the page.
16
19
  *
20
+ * `rowFrom` turns the column into a row from `sm`, `md` or `lg` (a column below it): the blocks sit
21
+ * side by side, `gap` apart, and a `ClusterGrow` around one of them takes the width left. Without it the
22
+ * Stack is always a column; a row that is a row at every width is a `Cluster`.
23
+ *
17
24
  * `as` is a closed set: `div` (default), `section`, `form`, `ul` or `ol`. A `form` takes the form
18
25
  * props (`onSubmit`, `action`, `method`, `noValidate`, ...). A `ul` or `ol` is drawn with no
19
26
  * markers and its children are `<li>`s, as in `BulletList` and `Timeline`: the Stack does not wrap
20
27
  * them, so a child is one list item and `Text as="li"` or a bare `<li>` both work.
21
28
  */
22
- export declare function Stack({ gap, width, as, children, ...rest }: StackProps): ReactElement;
29
+ export declare function Stack({ gap, width, rowFrom, as, children, ...rest }: StackProps): ReactElement;
23
30
  type StackBase = {
24
31
  /**
25
32
  * The space between blocks: `xs` gap-2, `sm` gap-4, `md` gap-6, `lg` gap-8, `xl` gap-12 (gap-16 from `sm`).
@@ -31,6 +38,13 @@ type StackBase = {
31
38
  * @default default
32
39
  */
33
40
  width?: StackWidth | undefined;
41
+ /**
42
+ * Turns the column into a row from this breakpoint (`sm`, `md` or `lg`), a column below it: a main
43
+ * area beside a side column on a wide screen, stacked on a phone. In the row the blocks sit side by side
44
+ * the same `gap` apart and are as tall as the tallest; wrap the one that takes the width left in a
45
+ * `ClusterGrow`. Left out, the Stack is always a column.
46
+ */
47
+ rowFrom?: StackRowFrom | undefined;
34
48
  children?: ReactNode;
35
49
  };
36
50
  type StackProps = StackBase & (({
package/dist/Stack.js CHANGED
@@ -9,6 +9,12 @@ export const STACK_GAP = {
9
9
  lg: "gap-8",
10
10
  xl: "gap-12 sm:gap-16",
11
11
  };
12
+ /** The breakpoint where a `rowFrom` Stack turns from a column into a row. */
13
+ export const STACK_ROW_FROM = {
14
+ sm: "sm:flex-row",
15
+ md: "md:flex-row",
16
+ lg: "lg:flex-row",
17
+ };
12
18
  /** `default` adds nothing: the Stack is as wide as its parent. */
13
19
  export const STACK_WIDTH = {
14
20
  narrow: "max-w-3xl",
@@ -25,13 +31,17 @@ export const STACK_WIDTH = {
25
31
  * is the parent's whole width and `default` sets nothing. A capped Stack starts at its parent's left
26
32
  * edge (it is never re-centred), so its first line lines up with every other block on the page.
27
33
  *
34
+ * `rowFrom` turns the column into a row from `sm`, `md` or `lg` (a column below it): the blocks sit
35
+ * side by side, `gap` apart, and a `ClusterGrow` around one of them takes the width left. Without it the
36
+ * Stack is always a column; a row that is a row at every width is a `Cluster`.
37
+ *
28
38
  * `as` is a closed set: `div` (default), `section`, `form`, `ul` or `ol`. A `form` takes the form
29
39
  * props (`onSubmit`, `action`, `method`, `noValidate`, ...). A `ul` or `ol` is drawn with no
30
40
  * markers and its children are `<li>`s, as in `BulletList` and `Timeline`: the Stack does not wrap
31
41
  * them, so a child is one list item and `Text as="li"` or a bare `<li>` both work.
32
42
  */
33
- export function Stack({ gap = "md", width = "default", as = "div", children, ...rest }) {
34
- return (_jsx(Box, { tag: as, className: cx("flex flex-col", (as === "ul" || as === "ol") && "list-none", STACK_GAP[gap], STACK_WIDTH[width], "*:data-page-header:mb-0"), ...rest, children: children }));
43
+ export function Stack({ gap = "md", width = "default", rowFrom, as = "div", children, ...rest }) {
44
+ return (_jsx(Box, { tag: as, className: cx("flex flex-col", (as === "ul" || as === "ol") && "list-none", rowFrom && STACK_ROW_FROM[rowFrom], STACK_GAP[gap], STACK_WIDTH[width], "*:data-page-header:mb-0"), ...rest, children: children }));
35
45
  }
36
46
  /** The element `as` picked, drawn with props already narrowed by `StackProps`. */
37
47
  function Box({ tag: Tag, ...props }) {
@@ -1,7 +1,7 @@
1
1
  import type { HTMLAttributes, ReactElement, ReactNode } from "react";
2
2
  import type { Unstyled } from "./props.js";
3
- /** The colour of a tile's value (and of its accent bar): plain ink, green or red. */
4
- export type StatTileTone = "neutral" | "positive" | "negative";
3
+ /** The colour of a tile's value (and of its accent bar): plain ink, green, amber or red. */
4
+ export type StatTileTone = "neutral" | "positive" | "warn" | "negative";
5
5
  /**
6
6
  * One headline figure: an uppercase caption over a large tabular-nums value, with an optional
7
7
  * hint line beneath. `tone` colours the value; `accent` adds a bar across the top edge in the same
package/dist/StatTile.js CHANGED
@@ -5,11 +5,13 @@ import { Overline } from "./typography.js";
5
5
  const VALUE = {
6
6
  neutral: "text-slate-900",
7
7
  positive: "text-emerald-700",
8
+ warn: "text-amber-700",
8
9
  negative: "text-red-700",
9
10
  };
10
11
  const BAR = {
11
12
  neutral: BAR_FILL.accent,
12
13
  positive: BAR_FILL.positive,
14
+ warn: BAR_FILL.warn,
13
15
  negative: BAR_FILL.negative,
14
16
  };
15
17
  /**
package/dist/Table.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
2
  import type { CalloutTone } from "./Callout.js";
3
3
  import { type TextTone } from "./typography.js";
4
- type Align = "start" | "end";
4
+ type Align = "start" | "center" | "end";
5
5
  /**
6
6
  * The text of a cell: its weight and colour. `default`, `strong` and `muted` are the plain ones; the
7
7
  * rest are the Callout's tones, for an amount that carries a meaning (a gain, an overspend). Colour
@@ -15,17 +15,28 @@ type TableVariant = "card" | "plain";
15
15
  * stripe takes the tone's text colour, which every theme already remaps, so no new palette entry.
16
16
  */
17
17
  export type RowTone = "neutral" | "info" | "positive" | "warn" | "danger" | "muted";
18
- /** The row's status mark: a `tone` (stripe and fill, or faded) or a `stripe` alone, never both. */
18
+ /** The row's mark: a `tone` (stripe and fill, or faded), a `stripe` alone or `emphasis`, only one of them. */
19
19
  type RowMark = {
20
- /** The row's status: a stripe on the leading edge plus the tone's soft fill, or `muted` to fade the row. Give `tone` or `stripe`, not both. */
20
+ /** The row's status: a stripe on the leading edge plus the tone's soft fill, or `muted` to fade the row. Give only one of `tone`, `stripe` and `emphasis`. */
21
21
  tone?: RowTone | undefined;
22
- /** A stripe on the leading edge in this tone, with no fill. Give `stripe` or `tone`, not both. */
22
+ /** A stripe on the leading edge in this tone, with no fill. Give only one of `tone`, `stripe` and `emphasis`. */
23
23
  stripe?: undefined;
24
+ /** A subtotal in the body: the look of a `TableFoot` row, with strong cells, a firmer line above and no hover. Give only one of `tone`, `stripe` and `emphasis`. */
25
+ emphasis?: false | undefined;
24
26
  } | {
25
- /** A stripe on the leading edge in this tone, with no fill. Give `stripe` or `tone`, not both. */
27
+ /** A stripe on the leading edge in this tone, with no fill. Give only one of `tone`, `stripe` and `emphasis`. */
26
28
  stripe?: TextTone | undefined;
27
- /** The row's status: a stripe on the leading edge plus the tone's soft fill, or `muted` to fade the row. Give `tone` or `stripe`, not both. */
29
+ /** The row's status: a stripe on the leading edge plus the tone's soft fill, or `muted` to fade the row. Give only one of `tone`, `stripe` and `emphasis`. */
28
30
  tone?: undefined;
31
+ /** A subtotal in the body: the look of a `TableFoot` row, with strong cells, a firmer line above and no hover. Give only one of `tone`, `stripe` and `emphasis`. */
32
+ emphasis?: false | undefined;
33
+ } | {
34
+ /** A subtotal in the body: the look of a `TableFoot` row, with strong cells, a firmer line above and no hover. Give only one of `tone`, `stripe` and `emphasis`. */
35
+ emphasis: boolean;
36
+ /** The row's status: a stripe on the leading edge plus the tone's soft fill, or `muted` to fade the row. Give only one of `tone`, `stripe` and `emphasis`. */
37
+ tone?: undefined;
38
+ /** A stripe on the leading edge in this tone, with no fill. Give only one of `tone`, `stripe` and `emphasis`. */
39
+ stripe?: undefined;
29
40
  };
30
41
  /** What an element passes through (`data-*`, `aria-*`, `id`, …): the layout and the children stay the component's. */
31
42
  type Rest<Tag extends "table" | "thead" | "tbody" | "tfoot" | "tr" | "th" | "td", Own extends string = never> = Omit<ComponentPropsWithoutRef<Tag>, "className" | "style" | "children" | Own>;
@@ -73,9 +84,10 @@ export declare function TableFoot({ children, ...rest }: {
73
84
  * linked row's link, not a hover fill, says it is clickable. `stripe` is the stripe alone, in the
74
85
  * `TextTone` colours (the first five names of a `StackedBar` segment's `VizTone`), with no fill: the row keeps its hover
75
86
  * and its own look. Give `tone` or `stripe`, not both. Body rows only; colour alone does not say it,
76
- * so name the status in a cell too (a `Pill`). A bold subtotal is not a tone: it is a `TableFoot` row.
87
+ * so name the status in a cell too (a `Pill`). A bold subtotal is not a tone: a total of the whole
88
+ * table is a `TableFoot` row, and a subtotal among the body's rows is `emphasis`, the same look.
77
89
  */
78
- export declare function TableRow({ tone, stripe, children, ...rest }: RowMark & {
90
+ export declare function TableRow({ tone, stripe, emphasis, children, ...rest }: RowMark & {
79
91
  children: ReactNode;
80
92
  } & Rest<"tr">): ReactElement;
81
93
  /**
@@ -99,7 +111,7 @@ export declare function TableGroupRow({ label, trailing, colSpan, ...rest }: {
99
111
  */
100
112
  export declare function TableHeaderCell({ align, children, srLabel, ...rest }: {
101
113
  /**
102
- * The cell's horizontal alignment; `end` for figures.
114
+ * The cell's horizontal alignment; `end` for figures, `center` over a centred column.
103
115
  * @default start
104
116
  */
105
117
  align?: Align | undefined;
@@ -111,6 +123,8 @@ export declare function TableHeaderCell({ align, children, srLabel, ...rest }: {
111
123
  /** The column's name for a screen reader, visually hidden, for a column with no visible title (a row's actions). */
112
124
  srLabel: string;
113
125
  }) & Rest<"th", "align" | "scope">): ReactElement;
126
+ /** A step of a hierarchy under a row: the cell's own `pl-4` plus 1rem a level. */
127
+ type Indent = 1 | 2 | 3;
114
128
  /**
115
129
  * A `<td>`. `tone` is the text weight and colour (see `CellTone`; in a `TableFoot` it defaults to
116
130
  * `strong`); `numeric` sets tabular figures, `mono` a small mono face. `secondary` is a second,
@@ -119,14 +133,17 @@ export declare function TableHeaderCell({ align, children, srLabel, ...rest }: {
119
133
  * cell (a figure, a code, a type) never breaks. `truncate` caps a long value's width, cuts it with
120
134
  * an ellipsis on one line and puts the full text in a `title`; the content is then plain text (strings and
121
135
  * numbers), since that is what the title shows. `colSpan` passes through: a cell that
122
- * spans the row is a full-width panel (an empty state, a note under a group).
136
+ * spans the row is a full-width panel (an empty state, a note under a group), and `align="center"`
137
+ * centres it. `indent` sets a start-aligned cell in by 1 to 3 levels, for a line under a parent row.
123
138
  */
124
- export declare function TableCell({ align, tone, numeric, mono, truncate, secondary, children, ...rest }: {
139
+ export declare function TableCell({ align, tone, numeric, mono, truncate, indent, secondary, children, ...rest }: {
125
140
  /**
126
- * The cell's horizontal alignment; `end` for figures.
141
+ * The cell's horizontal alignment; `end` for figures, `center` for a full-width note or empty state.
127
142
  * @default start
128
143
  */
129
144
  align?: Align | undefined;
145
+ /** Sets the cell in by this many levels (1 to 3), for a line under a parent row. Start-aligned cells only. */
146
+ indent?: Indent | undefined;
130
147
  /** The text weight and colour (see `CellTone`). In a `TableFoot` it defaults to `strong`. */
131
148
  tone?: CellTone | undefined;
132
149
  /** Tabular figures, and the cell never wraps. */
package/dist/Table.js CHANGED
@@ -2,7 +2,11 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Children, createContext, useContext } from "react";
3
3
  import { cx, liftInteractive } from "./cx.js";
4
4
  import { TEXT_TONE } from "./typography.js";
5
- const ALIGN = { start: "text-left", end: "text-right" };
5
+ const ALIGN = {
6
+ start: "text-left",
7
+ center: "text-center",
8
+ end: "text-right",
9
+ };
6
10
  const TONE = {
7
11
  default: "text-slate-700",
8
12
  strong: "font-medium text-slate-900",
@@ -62,12 +66,14 @@ export function TableFoot({ children, ...rest }) {
62
66
  * linked row's link, not a hover fill, says it is clickable. `stripe` is the stripe alone, in the
63
67
  * `TextTone` colours (the first five names of a `StackedBar` segment's `VizTone`), with no fill: the row keeps its hover
64
68
  * and its own look. Give `tone` or `stripe`, not both. Body rows only; colour alone does not say it,
65
- * so name the status in a cell too (a `Pill`). A bold subtotal is not a tone: it is a `TableFoot` row.
69
+ * so name the status in a cell too (a `Pill`). A bold subtotal is not a tone: a total of the whole
70
+ * table is a `TableFoot` row, and a subtotal among the body's rows is `emphasis`, the same look.
66
71
  */
67
- export function TableRow({ tone, stripe, children, ...rest }) {
68
- const section = useContext(SectionContext);
72
+ export function TableRow({ tone, stripe, emphasis = false, children, ...rest }) {
73
+ const outer = useContext(SectionContext);
69
74
  const plain = useContext(VariantContext) === "plain";
70
- return (_jsx("tr", { ...rest, className: rowClass(section, plain, tone, stripe), children: children }));
75
+ const section = emphasis && outer === "body" ? "foot" : outer;
76
+ return (_jsx(SectionContext.Provider, { value: section, children: _jsx("tr", { ...rest, className: rowClass(section, plain, tone, stripe), children: children }) }));
71
77
  }
72
78
  /** The classes of a row: a line above in the body and the foot, the hover fill and the tone's or stripe's look in the body only. */
73
79
  function rowClass(section, plain, tone, stripe) {
@@ -110,6 +116,13 @@ function figureClass(numeric, mono) {
110
116
  }
111
117
  /** The widest a `truncate` cell grows. */
112
118
  const TRUNCATE_WIDTH = "max-w-xs";
119
+ /** Where a `truncate` cell's capped box sits, so it follows the cell's alignment. */
120
+ const TRUNCATE_ALIGN = {
121
+ start: undefined,
122
+ center: "mx-auto",
123
+ end: "ml-auto",
124
+ };
125
+ const INDENT = { 1: "pl-8", 2: "pl-12", 3: "pl-16" };
113
126
  /**
114
127
  * A `<td>`. `tone` is the text weight and colour (see `CellTone`; in a `TableFoot` it defaults to
115
128
  * `strong`); `numeric` sets tabular figures, `mono` a small mono face. `secondary` is a second,
@@ -118,21 +131,22 @@ const TRUNCATE_WIDTH = "max-w-xs";
118
131
  * cell (a figure, a code, a type) never breaks. `truncate` caps a long value's width, cuts it with
119
132
  * an ellipsis on one line and puts the full text in a `title`; the content is then plain text (strings and
120
133
  * numbers), since that is what the title shows. `colSpan` passes through: a cell that
121
- * spans the row is a full-width panel (an empty state, a note under a group).
134
+ * spans the row is a full-width panel (an empty state, a note under a group), and `align="center"`
135
+ * centres it. `indent` sets a start-aligned cell in by 1 to 3 levels, for a line under a parent row.
122
136
  */
123
- export function TableCell({ align = "start", tone, numeric = false, mono = false, truncate = false, secondary, children, ...rest }) {
137
+ export function TableCell({ align = "start", tone, numeric = false, mono = false, truncate = false, indent, secondary, children, ...rest }) {
124
138
  const foot = useContext(SectionContext) === "foot";
125
- return (_jsx("td", { ...rest, className: cellClass({ align, tone, foot, numeric, mono }), children: _jsx(CellBody, { align: align, truncate: truncate, secondary: secondary, children: children }) }));
139
+ return (_jsx("td", { ...rest, className: cellClass({ align, indent, tone, foot, numeric, mono }), children: _jsx(CellBody, { align: align, truncate: truncate, secondary: secondary, children: children }) }));
126
140
  }
127
141
  /** The classes of a cell: the tone (`strong` by default in a foot, and a foot's cells are medium whatever the tone), then the wrapping rule. */
128
- function cellClass({ align, tone, foot, numeric, mono, }) {
142
+ function cellClass({ align, indent, tone, foot, numeric, mono, }) {
129
143
  const resolved = tone ?? (foot ? "strong" : "default");
130
- return cx("px-4 py-3", ALIGN[align], TONE[resolved], foot && resolved !== "strong" && "font-medium", figureClass(numeric, mono));
144
+ return cx("px-4 py-3", align === "start" && indent !== undefined && INDENT[indent], ALIGN[align], TONE[resolved], foot && resolved !== "strong" && "font-medium", figureClass(numeric, mono));
131
145
  }
132
146
  const isText = (node) => typeof node === "string" || typeof node === "number";
133
147
  /** What a cell holds: the content, cut to one line with its full text in a `title` when `truncate`, then the `secondary` line. */
134
148
  function CellBody({ align, truncate, secondary, children, }) {
135
149
  const parts = Children.toArray(children);
136
150
  const text = parts.every(isText) ? parts.join("") : undefined;
137
- return (_jsxs(_Fragment, { children: [truncate ? (_jsx("div", { className: cx(TRUNCATE_WIDTH, "truncate", align === "end" && "ml-auto"), title: text, children: children })) : (children), secondary != null && secondary !== false && (_jsx("div", { className: "text-xs font-normal text-slate-500", children: secondary }))] }));
151
+ return (_jsxs(_Fragment, { children: [truncate ? (_jsx("div", { className: cx(TRUNCATE_WIDTH, "truncate", TRUNCATE_ALIGN[align]), title: text, children: children })) : (children), secondary != null && secondary !== false && (_jsx("div", { className: "text-xs font-normal text-slate-500", children: secondary }))] }));
138
152
  }
@@ -0,0 +1,19 @@
1
+ import type { ComponentProps, ReactElement, ReactNode } from "react";
2
+ import { TableCell } from "./Table.js";
3
+ /**
4
+ * A `TableCell` whose value opens an editor in place. It shows `children` as a button with a dotted
5
+ * underline; a click, Enter or Space swaps it for `renderEditor(close)` and moves the focus into the
6
+ * editor. The cell keeps its own open state. The editor calls `close` when it has saved, and Esc
7
+ * closes it: both put the focus back on the value. A press outside it or Tab out of it closes it too and leaves the
8
+ * focus where it went, so the editor saves its value on its own change or submit, never on a close.
9
+ * Put the control in the cell itself, not in a popover of its own, which counts as outside. Takes
10
+ * `TableCell`'s `align`, `tone`, `numeric`, `mono`, `indent` and `secondary`.
11
+ */
12
+ export declare function TableEditCell({ label, renderEditor, children, ...cell }: {
13
+ /** What the button does, for a screen reader, heard after the value: "Edit category". It also names the editor. */
14
+ label: string;
15
+ /** The control that edits the value, drawn while open. `close` shuts it and returns the focus to the value. */
16
+ renderEditor: (close: () => void) => ReactNode;
17
+ /** The value, shown while closed. */
18
+ children: ReactNode;
19
+ } & Omit<ComponentProps<typeof TableCell>, "children" | "truncate">): ReactElement;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { cx, focusRing } from "./cx.js";
4
+ import { TableCell } from "./Table.js";
5
+ /** What takes the focus when the editor opens, unless the editor already took it (`autoFocus`). */
6
+ const FOCUSABLE = "input, select, textarea, button, a[href]";
7
+ /** Whether `target` is a node outside `box`. */
8
+ function isOutside(box, target) {
9
+ return target instanceof Node && box !== null && !box.contains(target);
10
+ }
11
+ /** Closes the editor when a press lands outside `box`, for as long as `open`. */
12
+ function useDismissOnOutsidePress(box, open, dismiss) {
13
+ useEffect(() => {
14
+ if (!open)
15
+ return undefined;
16
+ const onPress = (event) => {
17
+ if (isOutside(box.current, event.target))
18
+ dismiss();
19
+ };
20
+ document.addEventListener("pointerdown", onPress);
21
+ return () => document.removeEventListener("pointerdown", onPress);
22
+ }, [box, open, dismiss]);
23
+ }
24
+ /**
25
+ * A `TableCell` whose value opens an editor in place. It shows `children` as a button with a dotted
26
+ * underline; a click, Enter or Space swaps it for `renderEditor(close)` and moves the focus into the
27
+ * editor. The cell keeps its own open state. The editor calls `close` when it has saved, and Esc
28
+ * closes it: both put the focus back on the value. A press outside it or Tab out of it closes it too and leaves the
29
+ * focus where it went, so the editor saves its value on its own change or submit, never on a close.
30
+ * Put the control in the cell itself, not in a popover of its own, which counts as outside. Takes
31
+ * `TableCell`'s `align`, `tone`, `numeric`, `mono`, `indent` and `secondary`.
32
+ */
33
+ export function TableEditCell({ label, renderEditor, children, ...cell }) {
34
+ const [phase, setPhase] = useState("closed");
35
+ const open = phase === "open";
36
+ const box = useRef(null);
37
+ const trigger = useRef(null);
38
+ const dismiss = useCallback(() => setPhase("closed"), []);
39
+ const close = () => setPhase("returning");
40
+ useDismissOnOutsidePress(box, open, dismiss);
41
+ useEffect(() => {
42
+ if (phase === "returning")
43
+ trigger.current?.focus();
44
+ if (phase === "open" && !box.current?.contains(document.activeElement))
45
+ box.current?.querySelector(FOCUSABLE)?.focus();
46
+ }, [phase]);
47
+ const onKeyDown = (event) => {
48
+ if (event.key !== "Escape")
49
+ return;
50
+ event.preventDefault();
51
+ event.stopPropagation();
52
+ close();
53
+ };
54
+ return (_jsx(TableCell, { ...cell, children: open ? (_jsx("div", { ref: box, role: "group", "aria-label": label, className: "-mx-2 -my-1", onKeyDown: onKeyDown, onBlur: (event) => {
55
+ if (isOutside(event.currentTarget, event.relatedTarget))
56
+ dismiss();
57
+ }, children: renderEditor(close) })) : (_jsxs("button", { ref: trigger, type: "button", onClick: () => setPhase("open"), className: cx("-mx-2 -my-1 block w-[calc(100%+1rem)] cursor-pointer rounded-md px-2 py-1 [text-align:inherit] underline decoration-dotted underline-offset-4 transition hover:bg-slate-100", focusRing), children: [children, _jsxs("span", { className: "sr-only", children: [" ", label] })] })) }));
58
+ }
package/dist/index.d.ts CHANGED
@@ -10,7 +10,8 @@ export { Markdown } from "./Markdown.js";
10
10
  export { AppShell, AppShellBrand, type AppShellBreakpoint, AppShellHideBelow, appShellRootClassFor, } from "./AppShell.js";
11
11
  export { Container } from "./Container.js";
12
12
  export { PageHeader, type PageHeaderProps } from "./PageHeader.js";
13
- export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
13
+ export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, } from "./PageLayouts.js";
14
+ export { SidebarLayout } from "./SidebarLayout.js";
14
15
  export { StickyBar } from "./StickyBar.js";
15
16
  export { ErrorPage, Forbidden, Maintenance, NotFound, Offline } from "./StatusPages.js";
16
17
  export { Cluster, ClusterGrow, type ClusterAlign, type ClusterGap, type ClusterJustify, } from "./Cluster.js";
@@ -26,6 +27,7 @@ export { Callout, type CalloutTone } from "./Callout.js";
26
27
  export { ListRow, type ListRowProps } from "./ListRow.js";
27
28
  export { OutlineRegion, RegionFrame, type RegionRect } from "./OutlineRegion.js";
28
29
  export { type CellTone, type RowTone, Table, TableBody, TableCell, TableFoot, TableGroupRow, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
30
+ export { TableEditCell } from "./TableEditCell.js";
29
31
  export { EmptyState } from "./EmptyState.js";
30
32
  export { Step, Steps } from "./Steps.js";
31
33
  export { Timeline, TimelineItem } from "./Timeline.js";
@@ -64,6 +66,9 @@ export { KeyValue, KeyValueList } from "./KeyValueList.js";
64
66
  export { Select } from "./Select.js";
65
67
  export { Textarea } from "./Textarea.js";
66
68
  export { Video } from "./Video.js";
69
+ export { Image, type ImageAspect, type ImageProps, type ImageWidth } from "./Image.js";
70
+ export { MediaTile, type MediaTileProps } from "./MediaTile.js";
71
+ export { ScrollStrip, type ScrollStripProps } from "./ScrollStrip.js";
67
72
  export { TextLink, type TextLinkSize } from "./TextLink.js";
68
73
  export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, type SubsectionHeadingSize, Text, type TextTone, type TypographyColor, type TypographyProps, } from "./typography.js";
69
74
  export { DateField, type DateFieldProps } from "./DateField.js";
package/dist/index.js CHANGED
@@ -10,7 +10,8 @@ export { Markdown } from "./Markdown.js";
10
10
  export { AppShell, AppShellBrand, AppShellHideBelow, appShellRootClassFor, } from "./AppShell.js";
11
11
  export { Container } from "./Container.js";
12
12
  export { PageHeader } from "./PageHeader.js";
13
- export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, SidebarLayout, } from "./PageLayouts.js";
13
+ export { CardGrid, CenteredLayout, DetailLayout, FormLayout, FullBleedLayout, ListLayout, ProseLayout, } from "./PageLayouts.js";
14
+ export { SidebarLayout } from "./SidebarLayout.js";
14
15
  export { StickyBar } from "./StickyBar.js";
15
16
  export { ErrorPage, Forbidden, Maintenance, NotFound, Offline } from "./StatusPages.js";
16
17
  export { Cluster, ClusterGrow, } from "./Cluster.js";
@@ -26,6 +27,7 @@ export { Callout } from "./Callout.js";
26
27
  export { ListRow } from "./ListRow.js";
27
28
  export { OutlineRegion, RegionFrame } from "./OutlineRegion.js";
28
29
  export { Table, TableBody, TableCell, TableFoot, TableGroupRow, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
30
+ export { TableEditCell } from "./TableEditCell.js";
29
31
  export { EmptyState } from "./EmptyState.js";
30
32
  export { Step, Steps } from "./Steps.js";
31
33
  export { Timeline, TimelineItem } from "./Timeline.js";
@@ -60,6 +62,9 @@ export { KeyValue, KeyValueList } from "./KeyValueList.js";
60
62
  export { Select } from "./Select.js";
61
63
  export { Textarea } from "./Textarea.js";
62
64
  export { Video } from "./Video.js";
65
+ export { Image } from "./Image.js";
66
+ export { MediaTile } from "./MediaTile.js";
67
+ export { ScrollStrip } from "./ScrollStrip.js";
63
68
  export { TextLink } from "./TextLink.js";
64
69
  export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, Text, } from "./typography.js";
65
70
  export { DateField } from "./DateField.js";
package/dist/size.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The one size scale of the library: `sm`, `md` (the default everywhere) and `lg`. A component
3
- * extends it outward only where it has a real use (Avatar's `xl` and `2xl`, the gap scale's `xs`
3
+ * extends it outward only where it has a real use (Avatar's `xs`, `xl` and `2xl`, the gap scale's `xs`
4
4
  * and `xl`), and no scale skips `md`.
5
5
  */
6
6
  export type Size = "sm" | "md" | "lg";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A column that stays in view as the page scrolls (`SidebarLayout`'s rail from `md`, `DetailLayout`'s
3
+ * aside from `lg` when asked). The `<aside>` sticks directly under AppShell's header (`--app-header`,
4
+ * as `StickyBar` does; 0 outside a shell) and is as tall as its content. A sticky `top` also pushes a
5
+ * column that starts above it down, so it is exactly the header and no gap: a gap would move a rail
6
+ * rendered at the top of its box. Inside it the scroller is capped at the viewport less the header
7
+ * and 2rem (the room the Container's `py-8` leaves at the page's end, so the column is never pushed
8
+ * up under the header there), so a column taller than that scrolls itself and its end stays
9
+ * reachable. `overflow` clips, which would cut a keyboard focus ring (an outline 4px out) at the
10
+ * scroller's edges, so it is padded 4px (`p-1`) and pulled back out by the same (`-m-1`): the ring
11
+ * has room and the column stays on the page's edges. Each variant is written out whole, so Tailwind
12
+ * sees every class.
13
+ */
14
+ export declare const STICKY_ASIDE_MD = "md:sticky md:top-[var(--app-header,0px)] md:self-start";
15
+ export declare const STICKY_SCROLL_MD = "md:-m-1 md:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] md:overflow-y-auto md:p-1";
16
+ export declare const STICKY_ASIDE_LG = "lg:sticky lg:top-[var(--app-header,0px)] lg:self-start";
17
+ export declare const STICKY_SCROLL_LG = "lg:-m-1 lg:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] lg:overflow-y-auto lg:p-1";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A column that stays in view as the page scrolls (`SidebarLayout`'s rail from `md`, `DetailLayout`'s
3
+ * aside from `lg` when asked). The `<aside>` sticks directly under AppShell's header (`--app-header`,
4
+ * as `StickyBar` does; 0 outside a shell) and is as tall as its content. A sticky `top` also pushes a
5
+ * column that starts above it down, so it is exactly the header and no gap: a gap would move a rail
6
+ * rendered at the top of its box. Inside it the scroller is capped at the viewport less the header
7
+ * and 2rem (the room the Container's `py-8` leaves at the page's end, so the column is never pushed
8
+ * up under the header there), so a column taller than that scrolls itself and its end stays
9
+ * reachable. `overflow` clips, which would cut a keyboard focus ring (an outline 4px out) at the
10
+ * scroller's edges, so it is padded 4px (`p-1`) and pulled back out by the same (`-m-1`): the ring
11
+ * has room and the column stays on the page's edges. Each variant is written out whole, so Tailwind
12
+ * sees every class.
13
+ */
14
+ export const STICKY_ASIDE_MD = "md:sticky md:top-[var(--app-header,0px)] md:self-start";
15
+ export const STICKY_SCROLL_MD = "md:-m-1 md:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] md:overflow-y-auto md:p-1";
16
+ export const STICKY_ASIDE_LG = "lg:sticky lg:top-[var(--app-header,0px)] lg:self-start";
17
+ export const STICKY_SCROLL_LG = "lg:-m-1 lg:max-h-[calc(100dvh-var(--app-header,0px)-2rem)] lg:overflow-y-auto lg:p-1";
@@ -15,6 +15,8 @@ type OwnProps = {
15
15
  color?: TypographyColor;
16
16
  children?: ReactNode;
17
17
  };
18
+ /** Where a run of text sits across its block. */
19
+ export type TextAlign = "start" | "center" | "end";
18
20
  /** The opt-in props of the inline roles (`Text`, `Caption`, `Overline`). */
19
21
  type InlineOwnProps = {
20
22
  /** A meaning's colour (see `TextTone`). Wins over `color`. */
@@ -25,6 +27,18 @@ type InlineOwnProps = {
25
27
  mono?: boolean | undefined;
26
28
  /** One line, cut with an ellipsis where the container ends; the full text is the `title`. */
27
29
  truncate?: boolean | undefined;
30
+ /**
31
+ * Set the text at `start`, `center` or `end` of its block. The run becomes block-level, so the
32
+ * alignment has a box to act on (a table header cell, a right-hand column).
33
+ */
34
+ align?: TextAlign | undefined;
35
+ };
36
+ /** The opt-in props of `Text` and `Caption` only: the face of the run, not a label's. */
37
+ type RunOwnProps = {
38
+ /** Italic: a muted hint or a caption. */
39
+ italic?: boolean | undefined;
40
+ /** Break a long unbroken string (a URL, a token) where the container ends, instead of overflowing. */
41
+ breakWords?: boolean | undefined;
28
42
  };
29
43
  type Own = OwnProps & InlineOwnProps;
30
44
  /** Polymorphic props: own props + `as` + the intrinsic props of whichever element `as` selects. */
@@ -66,11 +80,14 @@ export declare const PageHeading: {
66
80
  /**
67
81
  * Top-level section header. `h2`, text-base/6 semibold, primary text. `rule` sets
68
82
  * a hairline under it (the `Divider` line colour) with the space either side of
69
- * the line built in.
83
+ * the line built in. `description` is a muted line under the heading, outside the `<h2>`, so the
84
+ * heading's name stays the title; the rule then sits under the description.
70
85
  */
71
- export declare function SectionHeading<E extends ElementType = "h2">({ rule, ...props }: TypographyProps<E> & {
86
+ export declare function SectionHeading<E extends ElementType = "h2">({ rule, description, ...props }: TypographyProps<E> & {
72
87
  /** A hairline under the heading, with the space either side of it built in. */
73
88
  rule?: boolean;
89
+ /** A muted line under the heading that says more. It is a paragraph beside the heading, not part of its name. */
90
+ description?: ReactNode;
74
91
  }): ReactElement;
75
92
  export type SubsectionHeadingSize = Size;
76
93
  /**
@@ -89,12 +106,14 @@ export declare function SubsectionHeading<E extends ElementType = "h3">({ size,
89
106
  }): ReactElement;
90
107
  /**
91
108
  * Small all-caps label above a group. `span`, text-xs semibold uppercase, tertiary. `tone`,
92
- * `numeric`, `mono` and `truncate` are opt-in, as on `Text`.
109
+ * `numeric`, `mono`, `truncate` and `align` are opt-in, as on `Text`.
93
110
  */
94
111
  export declare const Overline: {
95
112
  <E extends ElementType = "span">(props: InlineTypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
96
113
  displayName: string;
97
114
  };
115
+ /** A fixed width for a run of text: `amount` is a money figure, right-aligned, at a width that lines a column up. */
116
+ export type TextMinWidth = "amount";
98
117
  /**
99
118
  * Running text. `span` (inline; `as="p"` for a paragraph), text-sm/5 normal, secondary. A block of it
100
119
  * stops at a readable line length (`max-w-[72ch]`), wherever it sits. `strong` emphasises it:
@@ -102,13 +121,17 @@ export declare const Overline: {
102
121
  * `preserveNewlines` keeps the line breaks in the text (a user-typed note); runs of spaces still collapse.
103
122
  * `tone` colours it by meaning (`positive` for "Saved.", `danger` for an error; it wins over `color`),
104
123
  * `numeric` sets tabular figures, `mono` the monospace face, and `truncate` cuts it to one line with an
105
- * ellipsis (the full text becomes its `title`).
124
+ * ellipsis (the full text becomes its `title`). `align` sets it at `start`, `center` or `end`.
125
+ * `italic` slants it, `breakWords` breaks a long unbroken string at the edge of its box, and `minWidth="amount"`
126
+ * fixes a width and right-aligns the figure in it (a column of amounts lines up); it does not take `align`.
106
127
  */
107
- export declare function Text<E extends ElementType = "span">({ strong, preserveNewlines, ...props }: InlineTypographyProps<E> & {
128
+ export declare function Text<E extends ElementType = "span">({ strong, preserveNewlines, minWidth, italic, breakWords, ...props }: InlineTypographyProps<E> & RunOwnProps & {
108
129
  /** Emphasis: font-medium in the primary ink (`color` and `tone` still win). */
109
130
  strong?: boolean | undefined;
110
131
  /** Keep the line breaks in the text (a user-typed note); runs of spaces still collapse. */
111
132
  preserveNewlines?: boolean;
133
+ /** A fixed width: `amount` is a right-aligned money figure, min-w-24, in tabular figures. */
134
+ minWidth?: TextMinWidth | undefined;
112
135
  }): ReactElement;
113
136
  /**
114
137
  * The supporting line under a `Dialog` title: `Text as="p"` without the measure, so
@@ -117,9 +140,10 @@ export declare function Text<E extends ElementType = "span">({ strong, preserveN
117
140
  export declare function HeaderText({ children }: {
118
141
  children: ReactNode;
119
142
  }): ReactElement;
120
- /** Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. Takes `tone`, `numeric`, `mono` and `truncate` like `Text`. */
121
- export declare const Caption: {
122
- <E extends ElementType = "span">(props: InlineTypographyProps<E>): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
123
- displayName: string;
124
- };
143
+ /**
144
+ * Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. Takes `tone`,
145
+ * `numeric`, `mono`, `truncate` and `align` like `Text`, and `italic` for a muted hint and `breakWords`
146
+ * for a long unbroken string such as a URL.
147
+ */
148
+ export declare function Caption<E extends ElementType = "span">({ italic, breakWords, ...props }: InlineTypographyProps<E> & RunOwnProps): ReactElement;
125
149
  export {};