@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/README.md +10 -5
- 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 +10 -3
- package/dist/PageLayouts.js +15 -4
- 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/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 +4 -0
- package/dist/index.js +4 -0
- package/dist/size.d.ts +1 -1
- package/dist/typography.d.ts +34 -10
- package/dist/typography.js +43 -13
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 = {
|
|
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:
|
|
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
|
|
72
|
+
export function TableRow({ tone, stripe, emphasis = false, children, ...rest }) {
|
|
73
|
+
const outer = useContext(SectionContext);
|
|
69
74
|
const plain = useContext(VariantContext) === "plain";
|
|
70
|
-
|
|
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
|
|
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";
|
package/dist/typography.d.ts
CHANGED
|
@@ -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 `
|
|
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
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 {};
|
package/dist/typography.js
CHANGED
|
@@ -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",
|
|
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
|
|
78
|
-
|
|
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 `
|
|
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",
|
|
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
|
-
/**
|
|
122
|
-
|
|
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.
|
|
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",
|