@hackcove/ui-react 0.9.0 → 0.9.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/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
@@ -26,6 +26,7 @@ export { Callout, type CalloutTone } from "./Callout.js";
26
26
  export { ListRow, type ListRowProps } from "./ListRow.js";
27
27
  export { OutlineRegion, RegionFrame, type RegionRect } from "./OutlineRegion.js";
28
28
  export { type CellTone, type RowTone, Table, TableBody, TableCell, TableFoot, TableGroupRow, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
29
+ export { TableEditCell } from "./TableEditCell.js";
29
30
  export { EmptyState } from "./EmptyState.js";
30
31
  export { Step, Steps } from "./Steps.js";
31
32
  export { Timeline, TimelineItem } from "./Timeline.js";
@@ -64,6 +65,9 @@ export { KeyValue, KeyValueList } from "./KeyValueList.js";
64
65
  export { Select } from "./Select.js";
65
66
  export { Textarea } from "./Textarea.js";
66
67
  export { Video } from "./Video.js";
68
+ export { Image, type ImageAspect, type ImageProps, type ImageWidth } from "./Image.js";
69
+ export { MediaTile, type MediaTileProps } from "./MediaTile.js";
70
+ export { ScrollStrip, type ScrollStripProps } from "./ScrollStrip.js";
67
71
  export { TextLink, type TextLinkSize } from "./TextLink.js";
68
72
  export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, type SubsectionHeadingSize, Text, type TextTone, type TypographyColor, type TypographyProps, } from "./typography.js";
69
73
  export { DateField, type DateFieldProps } from "./DateField.js";
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ export { Callout } from "./Callout.js";
26
26
  export { ListRow } from "./ListRow.js";
27
27
  export { OutlineRegion, RegionFrame } from "./OutlineRegion.js";
28
28
  export { Table, TableBody, TableCell, TableFoot, TableGroupRow, TableHead, TableHeaderCell, TableRow, } from "./Table.js";
29
+ export { TableEditCell } from "./TableEditCell.js";
29
30
  export { EmptyState } from "./EmptyState.js";
30
31
  export { Step, Steps } from "./Steps.js";
31
32
  export { Timeline, TimelineItem } from "./Timeline.js";
@@ -60,6 +61,9 @@ export { KeyValue, KeyValueList } from "./KeyValueList.js";
60
61
  export { Select } from "./Select.js";
61
62
  export { Textarea } from "./Textarea.js";
62
63
  export { Video } from "./Video.js";
64
+ export { Image } from "./Image.js";
65
+ export { MediaTile } from "./MediaTile.js";
66
+ export { ScrollStrip } from "./ScrollStrip.js";
63
67
  export { TextLink } from "./TextLink.js";
64
68
  export { Caption, Display, Lead, Overline, PageHeading, SectionHeading, SubsectionHeading, Text, } from "./typography.js";
65
69
  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";
@@ -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 {};
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, } from "react";
3
3
  import { cx } from "./cx.js";
4
4
  /** Colour token → slate/violet utility, which a theme remaps. */
@@ -17,6 +17,11 @@ export const TEXT_TONE = {
17
17
  warn: "text-amber-700",
18
18
  danger: "text-red-700",
19
19
  };
20
+ const ALIGN = {
21
+ start: "text-start",
22
+ center: "text-center",
23
+ end: "text-end",
24
+ };
20
25
  /**
21
26
  * Build a semantic component that pins `variantClass` (size + weight + tracking)
22
27
  * and a default colour. The scale/weight for each role therefore lives in exactly
@@ -42,9 +47,19 @@ function inlineRole(displayName, defaultAs, variantClass, defaultColor) {
42
47
  * The one place a pinned role renders. The few roles with an opt-in prop of their own are
43
48
  * written by hand, not by `typography`, and call this too.
44
49
  */
45
- function pinned(defaultAs, variantClass, defaultColor, { as, color, tone, numeric, mono, truncate, children, ...rest }) {
50
+ function pinned(defaultAs, variantClass, defaultColor, { as, color, tone, numeric, mono, truncate, align, children, ...rest }) {
46
51
  const Tag = as ?? defaultAs;
47
- return (_jsx(Tag, { ...rest, ...(truncate ? { title: rest.title ?? plainText(children) } : {}), className: cx(variantClass, tone ? TEXT_TONE[tone] : COLOR[color ?? defaultColor], numeric && "tabular-nums", mono && "font-mono", truncate && "inline-block min-w-0 truncate align-bottom"), children: children }));
52
+ return (_jsx(Tag, { ...rest, ...(truncate ? { title: rest.title ?? plainText(children) } : {}), className: cx(variantClass, tone ? TEXT_TONE[tone] : COLOR[color ?? defaultColor], numeric && "tabular-nums", mono && "font-mono", layoutClass(align, truncate)), children: children }));
53
+ }
54
+ /**
55
+ * The alignment, and the box it needs. An aligned run is block-level, so text-align has a box to set
56
+ * the text in. A truncated run clips: inline-block by default, a block when it is aligned too.
57
+ */
58
+ function layoutClass(align, truncate) {
59
+ const box = truncate
60
+ ? cx("min-w-0 truncate align-bottom", align ? "block" : "inline-block")
61
+ : align && "block";
62
+ return cx(align && ALIGN[align], box);
48
63
  }
49
64
  /** The text of `children` when it is all strings and numbers: what a truncated run shows on hover. */
50
65
  function plainText(children) {
@@ -71,11 +86,16 @@ export const PageHeading = typography("PageHeading", "h1", "text-3xl/9 font-bold
71
86
  /**
72
87
  * Top-level section header. `h2`, text-base/6 semibold, primary text. `rule` sets
73
88
  * a hairline under it (the `Divider` line colour) with the space either side of
74
- * the line built in.
89
+ * the line built in. `description` is a muted line under the heading, outside the `<h2>`, so the
90
+ * heading's name stays the title; the rule then sits under the description.
75
91
  */
76
- export function SectionHeading({ rule, ...props }) {
77
- const variant = cx(rule && "mb-4 border-b border-slate-200/70 pb-2", "text-base/6 font-semibold");
78
- return pinned("h2", variant, "default", props);
92
+ export function SectionHeading({ rule, description, ...props }) {
93
+ const hairline = "mb-4 border-b border-slate-200/70 pb-2";
94
+ const hasDescription = description != null && description !== false;
95
+ if (!hasDescription) {
96
+ return pinned("h2", cx(rule && hairline, "text-base/6 font-semibold"), "default", props);
97
+ }
98
+ return (_jsxs("div", { className: cx("flex flex-col gap-1", rule && hairline), children: [pinned("h2", "text-base/6 font-semibold", "default", props), _jsx(Text, { as: "p", color: "tertiary", children: description })] }));
79
99
  }
80
100
  const SUBSECTION_SIZE = {
81
101
  sm: "text-sm/5 font-semibold",
@@ -93,7 +113,7 @@ export function SubsectionHeading({ size = "md", ...props }) {
93
113
  // --- Inline roles (inline by default) --------------------------------------
94
114
  /**
95
115
  * Small all-caps label above a group. `span`, text-xs semibold uppercase, tertiary. `tone`,
96
- * `numeric`, `mono` and `truncate` are opt-in, as on `Text`.
116
+ * `numeric`, `mono`, `truncate` and `align` are opt-in, as on `Text`.
97
117
  */
98
118
  export const Overline = inlineRole("Overline", "span", "text-xs font-semibold uppercase tracking-wide", "tertiary");
99
119
  /**
@@ -103,10 +123,14 @@ export const Overline = inlineRole("Overline", "span", "text-xs font-semibold up
103
123
  * `preserveNewlines` keeps the line breaks in the text (a user-typed note); runs of spaces still collapse.
104
124
  * `tone` colours it by meaning (`positive` for "Saved.", `danger` for an error; it wins over `color`),
105
125
  * `numeric` sets tabular figures, `mono` the monospace face, and `truncate` cuts it to one line with an
106
- * ellipsis (the full text becomes its `title`).
126
+ * ellipsis (the full text becomes its `title`). `align` sets it at `start`, `center` or `end`.
127
+ * `italic` slants it, `breakWords` breaks a long unbroken string at the edge of its box, and `minWidth="amount"`
128
+ * fixes a width and right-aligns the figure in it (a column of amounts lines up); it does not take `align`.
107
129
  */
108
- export function Text({ strong, preserveNewlines, ...props }) {
109
- return pinned("span", textVariant(strong, preserveNewlines, true), strong ? "default" : "secondary", props);
130
+ export function Text({ strong, preserveNewlines, minWidth, italic, breakWords, ...props }) {
131
+ return pinned("span", cx(textVariant(strong, preserveNewlines, true), italic && "italic", breakWords && "break-words", minWidth === "amount" && "inline-block min-w-24 text-end"), strong ? "default" : "secondary",
132
+ // An amount is always in tabular figures, so it lines up; `numeric` already emits them.
133
+ { ...props, numeric: props.numeric || minWidth === "amount" });
110
134
  }
111
135
  /**
112
136
  * The supporting line under a `Dialog` title: `Text as="p"` without the measure, so
@@ -118,5 +142,11 @@ export function HeaderText({ children }) {
118
142
  function textVariant(strong, preserveNewlines, measure) {
119
143
  return cx(preserveNewlines && "whitespace-pre-line", measure && "max-w-[72ch]", "text-sm/5", strong ? "font-medium" : "font-normal");
120
144
  }
121
- /** Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. Takes `tone`, `numeric`, `mono` and `truncate` like `Text`. */
122
- export const Caption = inlineRole("Caption", "span", "text-xs/4 font-normal", "tertiary");
145
+ /**
146
+ * Fine print / metadata. `span` (inline; `as="p"` for a block), text-xs/4 normal, tertiary. Takes `tone`,
147
+ * `numeric`, `mono`, `truncate` and `align` like `Text`, and `italic` for a muted hint and `breakWords`
148
+ * for a long unbroken string such as a URL.
149
+ */
150
+ export function Caption({ italic, breakWords, ...props }) {
151
+ return pinned("span", cx("text-xs/4 font-normal", italic && "italic", breakWords && "break-words"), "tertiary", props);
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hackcove/ui-react",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Hack Cove React components: primitives, typography roles and a safe Markdown renderer, written against Tailwind v4 utilities. Pair with a @hackcove/theme-* package.",
5
5
  "keywords": [
6
6
  "components",