@crisp-ui-kit/crisp 0.53.0 → 0.53.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/breadcrumb.css +2 -1
- package/dist/breadcrumb.layered.css +2 -1
- package/dist/cjs/components/breadcrumb/breadcrumb.js +5 -2
- package/dist/cjs/components/breadcrumb/breadcrumb.recipe.js +5 -2
- package/dist/cjs/components/datatable/datatable.js +10 -3
- package/dist/cjs/components/datatable/datatable.recipe.js +2 -0
- package/dist/cjs/components/datepickerfield/datepickerfield.js +5 -1
- package/dist/cjs/components/dialog/dialog.js +2 -1
- package/dist/cjs/components/drawer/drawer.js +2 -1
- package/dist/cjs/components/multiselect/multiselect.js +5 -1
- package/dist/cjs/components/quantitypicker/quantitypicker.js +4 -1
- package/dist/cjs/components/select/select.js +7 -1
- package/dist/cjs/components/statcard/statcard.js +3 -2
- package/dist/cjs/components/statcard/statcard.recipe.js +10 -0
- package/dist/cjs/components/toast/toast.js +3 -1
- package/dist/datatable.css +2 -0
- package/dist/datatable.layered.css +2 -0
- package/dist/esm/components/breadcrumb/breadcrumb.js +5 -2
- package/dist/esm/components/breadcrumb/breadcrumb.recipe.js +5 -2
- package/dist/esm/components/datatable/datatable.js +10 -3
- package/dist/esm/components/datatable/datatable.recipe.js +2 -0
- package/dist/esm/components/datepickerfield/datepickerfield.js +5 -1
- package/dist/esm/components/dialog/dialog.js +3 -2
- package/dist/esm/components/drawer/drawer.js +3 -2
- package/dist/esm/components/multiselect/multiselect.js +6 -2
- package/dist/esm/components/quantitypicker/quantitypicker.js +5 -2
- package/dist/esm/components/select/select.js +8 -2
- package/dist/esm/components/statcard/statcard.js +3 -2
- package/dist/esm/components/statcard/statcard.recipe.js +10 -0
- package/dist/esm/components/toast/toast.js +4 -2
- package/dist/statcard.css +8 -0
- package/dist/statcard.layered.css +8 -0
- package/dist/styles.css +12 -1
- package/dist/styles.layered.css +12 -1
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +1 -1
- package/dist/types/components/datatable/datatable.d.ts +10 -0
- package/dist/types/components/datepickerfield/datepickerfield.d.ts +1 -1
- package/dist/types/components/dialog/dialog.d.ts +1 -1
- package/dist/types/components/drawer/drawer.d.ts +1 -1
- package/dist/types/components/multiselect/multiselect.d.ts +1 -1
- package/dist/types/components/quantitypicker/quantitypicker.d.ts +1 -1
- package/dist/types/components/select/select.d.ts +1 -1
- package/dist/types/components/statcard/statcard.d.ts +5 -1
- package/dist/types/components/toast/toast.d.ts +1 -1
- package/package.json +1 -1
package/dist/breadcrumb.css
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: var(--crisp-fg-primary); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); }
|
|
6
6
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
7
7
|
& .crisp-breadcrumb-item[aria-current] { color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
8
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
8
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
9
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: var(--crisp-radius-full); background: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
|
|
9
10
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
10
11
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: var(--crisp-fg-primary); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); }
|
|
7
7
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
8
8
|
& .crisp-breadcrumb-item[aria-current] { color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
9
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
9
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
10
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: var(--crisp-radius-full); background: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
|
|
10
11
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -35,7 +35,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.Breadcrumb = Breadcrumb;
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
|
-
const lucide_react_1 = require("lucide-react");
|
|
39
38
|
const React = __importStar(require("react"));
|
|
40
39
|
const breadcrumb_recipe_1 = require("./breadcrumb.recipe");
|
|
41
40
|
/**
|
|
@@ -43,7 +42,11 @@ const breadcrumb_recipe_1 = require("./breadcrumb.recipe");
|
|
|
43
42
|
* marked `aria-current="page"`; earlier items render as links or buttons.
|
|
44
43
|
*/
|
|
45
44
|
function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
|
|
46
|
-
|
|
45
|
+
// Live-measured (app.attio.com/<ws>/home, /companies,
|
|
46
|
+
// /company/<id>/overview): a plain rounded divider, not a chevron -- a
|
|
47
|
+
// symmetric shape needs no RTL mirroring, unlike the ChevronRight this
|
|
48
|
+
// used to default to.
|
|
49
|
+
const sep = separator ?? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-breadcrumb-sep-dot", "aria-hidden": "true" }));
|
|
47
50
|
return ((0, jsx_runtime_1.jsx)("nav", { "aria-label": ariaLabel, className: (0, breadcrumb_recipe_1.breadcrumbRecipe)({ className }), ...rest, children: items.map((item, i) => {
|
|
48
51
|
const last = i === items.length - 1;
|
|
49
52
|
const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [item.icon, item.label] }));
|
|
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.breadcrumbRecipe = void 0;
|
|
4
4
|
const core_1 = require("../../core");
|
|
5
5
|
const tokens_1 = require("../../tokens");
|
|
6
|
-
// A breadcrumb trail — record-page nav ("Companies
|
|
6
|
+
// A breadcrumb trail — record-page nav ("Companies · Google"), the segments
|
|
7
|
+
// joined by a small rounded divider (crisp#498), not the literal "/" this
|
|
8
|
+
// comment's own example used to imply.
|
|
7
9
|
const config = (0, core_1.defineRecipe)({
|
|
8
10
|
name: "breadcrumb",
|
|
9
11
|
base: `display: flex; align-items: center; gap: ${tokens_1.contract.space["0.5"]}; font-family: ${tokens_1.contract.font.sans}; font-size: ${tokens_1.contract.text.lg};
|
|
@@ -12,7 +14,8 @@ const config = (0, core_1.defineRecipe)({
|
|
|
12
14
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: ${tokens_1.contract.fgPrimary}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 5%, transparent); }
|
|
13
15
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
14
16
|
& .crisp-breadcrumb-item[aria-current] { color: ${tokens_1.contract.fgPrimary}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
15
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
17
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
18
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: ${tokens_1.contract.radius.full}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 28%, transparent); }
|
|
16
19
|
${core_1.rtlFlipCss}
|
|
17
20
|
`,
|
|
18
21
|
variants: {},
|
|
@@ -201,7 +201,7 @@ function formatCell(type, v, symbol, locale = "en-US") {
|
|
|
201
201
|
// silently drop the cell's data. Each branch decides for itself where a
|
|
202
202
|
// display string belongs -- as an entity's name, a status' label -- while the
|
|
203
203
|
// raw value keeps driving structure and tone.
|
|
204
|
-
function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
204
|
+
function renderTyped(col, value, avatarTone, row, display, locale = "en-US") {
|
|
205
205
|
// Attio's own empty cell is genuinely blank -- no placeholder character at
|
|
206
206
|
// all, for any column type (#412).
|
|
207
207
|
const empty = null;
|
|
@@ -213,7 +213,14 @@ function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
|
213
213
|
const e = display ? { ...raw, name: display } : raw;
|
|
214
214
|
if (!e.name)
|
|
215
215
|
return empty;
|
|
216
|
-
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-entity", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? ((0, jsx_runtime_1.jsx)("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), (0, jsx_runtime_1.jsx)("
|
|
216
|
+
return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-entity", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? ((0, jsx_runtime_1.jsx)("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), col.onEntityClick ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-entity-name crisp-datatable-entity-name--link", onClick: (ev) => {
|
|
217
|
+
// the cell's own click already just selects (see the
|
|
218
|
+
// TableCell onClick above) -- this must additionally stop
|
|
219
|
+
// the row's onRowClick from firing, since this name points
|
|
220
|
+
// at a DIFFERENT record than the row itself (#501).
|
|
221
|
+
ev.stopPropagation();
|
|
222
|
+
col.onEntityClick?.(row);
|
|
223
|
+
}, children: e.name })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-entity-name", children: e.name }))] }));
|
|
217
224
|
}
|
|
218
225
|
case "tags": {
|
|
219
226
|
// asking for a format on a tag column means "render it your way" -- show
|
|
@@ -950,7 +957,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
|
|
|
950
957
|
? () => openEditing(row.id, col.key)
|
|
951
958
|
: undefined, children: [col.render
|
|
952
959
|
? col.render(value, row)
|
|
953
|
-
: renderTyped(col, value, avatarTone, col.format?.(value, row), locale), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
960
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale), errorHere && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
954
961
|
})] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && ((0, jsx_runtime_1.jsxs)(table_1.TableFooter, { children: [hasSummary && ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { children: [showGrip && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-2) }), selectable && (0, jsx_runtime_1.jsx)(table_1.TableCell, { ...stickyCol(-1) }), columns.map((col, i) => ((0, jsx_runtime_1.jsx)(table_1.TableCell, { align: alignFor(col), ...dataStickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
|
|
955
962
|
}
|
|
956
963
|
DataTable.displayName = "DataTable";
|
|
@@ -124,6 +124,8 @@ const config = (0, core_1.defineRecipe)({
|
|
|
124
124
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
125
125
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; color: ${tokens_1.contract.fgPrimary}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
126
126
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
127
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
128
|
+
& button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
|
|
127
129
|
& .crisp-datatable-avatar { width: ${tokens_1.contract.size.icon}; height: ${tokens_1.contract.size.icon}; border-radius: ${tokens_1.contract.radius.sm}; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: ${tokens_1.contract.weight.semibold}; text-transform: uppercase; color: ${tokens_1.contract.fgOnBrand}; background: ${tokens_1.contract.bgBrandSolid}; }
|
|
128
130
|
& .crisp-datatable-avatar[data-tone="success"] { background: ${tokens_1.contract.bgSuccess}; } & .crisp-datatable-avatar[data-tone="warning"] { background: ${tokens_1.contract.bgWarning}; } & .crisp-datatable-avatar[data-tone="danger"] { background: ${tokens_1.contract.bgDanger}; } & .crisp-datatable-avatar[data-tone="purple"] { background: ${tokens_1.contract.bgPurple}; } & .crisp-datatable-avatar[data-tone="cyan"] { background: ${tokens_1.contract.bgCyan}; } & .crisp-datatable-avatar[data-tone="orange"] { background: ${tokens_1.contract.bgOrange}; }
|
|
129
131
|
& .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
|
|
@@ -37,6 +37,7 @@ exports.DatePickerField = DatePickerField;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
|
+
const core_1 = require("../../core");
|
|
40
41
|
const datepicker_1 = require("../datepicker");
|
|
41
42
|
const popover_1 = require("../popover");
|
|
42
43
|
const datepickerfield_recipe_1 = require("./datepickerfield.recipe");
|
|
@@ -57,7 +58,10 @@ const fromDate = (d) => ({
|
|
|
57
58
|
* popover. This is what `DatePicker` used to be; `DatePicker` is now
|
|
58
59
|
* seed-design's bare calendar, and this composes it.
|
|
59
60
|
*/
|
|
60
|
-
function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder
|
|
61
|
+
function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder: placeholderProp, variant = "default", format = defaultFormat, clearable = true, clearLabel: clearLabelProp, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
|
|
62
|
+
// crisp/#471: same resolution order as Select's own.
|
|
63
|
+
const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Set date…");
|
|
64
|
+
const clearLabel = (0, core_1.useCrispMessage)(clearLabelProp, "Clear date");
|
|
61
65
|
const [internal, setInternal] = React.useState(defaultValue ?? null);
|
|
62
66
|
const selected = value ?? internal;
|
|
63
67
|
const [open, setOpen] = React.useState(false);
|
|
@@ -60,7 +60,8 @@ function useDialogInstanceId() {
|
|
|
60
60
|
* Escape / outside-click dismissal) with the measured modal surface.
|
|
61
61
|
* Compose the body with `DialogBody` and actions with `DialogFooter`.
|
|
62
62
|
*/
|
|
63
|
-
function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel
|
|
63
|
+
function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
|
|
64
|
+
const closeLabel = (0, core_1.useCrispMessage)(closeLabelProp, "Close");
|
|
64
65
|
const id = useDialogInstanceId();
|
|
65
66
|
const service = (0, react_1.useMachine)(dialog.machine, {
|
|
66
67
|
id,
|
|
@@ -57,7 +57,8 @@ function useDrawerInstanceId() {
|
|
|
57
57
|
* Escape / outside-click dismissal) with a panel that slides in from the edge.
|
|
58
58
|
* Compose the body with `DrawerBody` and actions with `DrawerFooter`.
|
|
59
59
|
*/
|
|
60
|
-
function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel
|
|
60
|
+
function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, "aria-describedby": describedBy, ...rest }) {
|
|
61
|
+
const closeLabel = (0, core_1.useCrispMessage)(closeLabelProp, "Close");
|
|
61
62
|
const id = useDrawerInstanceId();
|
|
62
63
|
const service = (0, react_1.useMachine)(dialog.machine, {
|
|
63
64
|
id,
|
|
@@ -69,7 +69,11 @@ function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, n
|
|
|
69
69
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
70
70
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
71
71
|
// above this control named nothing (#247). Select already did this.
|
|
72
|
-
id, placeholder
|
|
72
|
+
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
|
|
73
|
+
// crisp/#471: same resolution order as Select's own.
|
|
74
|
+
const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Select…");
|
|
75
|
+
const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search…");
|
|
76
|
+
const emptyLabel = (0, core_1.useCrispMessage)(emptyLabelProp, "No results");
|
|
73
77
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
74
78
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
75
79
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -91,7 +91,10 @@ function IncrementIcon({ size }) {
|
|
|
91
91
|
* Slider documents. Here it also decides child ORDER, since the engine
|
|
92
92
|
* reverses its visible children rather than relying on CSS.
|
|
93
93
|
*/
|
|
94
|
-
function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel
|
|
94
|
+
function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel: decrementLabelProp, incrementLabel: incrementLabelProp, ...props }) {
|
|
95
|
+
// crisp/#471: same resolution order as Select's own.
|
|
96
|
+
const decrementLabel = (0, core_1.useCrispMessage)(decrementLabelProp, "Decrease quantity");
|
|
97
|
+
const incrementLabel = (0, core_1.useCrispMessage)(incrementLabelProp, "Increase quantity");
|
|
95
98
|
// A `<label for>` cannot name a div, and this root is a role="group" (#247).
|
|
96
99
|
const rootRef = React.useRef(null);
|
|
97
100
|
(0, core_1.useLabelAssociation)(rootRef);
|
|
@@ -52,7 +52,13 @@ function optionText(o) {
|
|
|
52
52
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
53
53
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
54
54
|
*/
|
|
55
|
-
function Select({ options, value, defaultValue, onValueChange, name, required, placeholder
|
|
55
|
+
function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
56
|
+
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
57
|
+
// CrispMessagesProvider's translation of the English default, or that
|
|
58
|
+
// default itself with no provider — unchanged from before this existed.
|
|
59
|
+
const placeholder = (0, core_1.useCrispMessage)(placeholderProp, "Select…");
|
|
60
|
+
const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search…");
|
|
61
|
+
const emptyLabel = (0, core_1.useCrispMessage)(emptyLabelProp, "No results");
|
|
56
62
|
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
57
63
|
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
58
64
|
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
@@ -8,8 +8,9 @@ const statcard_recipe_1 = require("./statcard.recipe");
|
|
|
8
8
|
* A single-metric KPI tile — measured from a reports number/insight card.
|
|
9
9
|
* Label, a large value, and an optional up/down delta pill with a caption.
|
|
10
10
|
*/
|
|
11
|
-
function StatCard({ label, icon, value, delta, deltaSuffix = "%", caption, card, className, ...rest }) {
|
|
11
|
+
function StatCard({ label, icon, value, delta, deltaSuffix = "%", caption, captionTone, card, className, ...rest }) {
|
|
12
12
|
const trend = delta == null ? null : delta > 0 ? "up" : delta < 0 ? "down" : "flat";
|
|
13
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, statcard_recipe_1.statcardRecipe)({ card, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-label", children: [icon, label] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-statcard-value", children: value }), (trend != null || caption != null) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-foot", children: [trend != null && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-delta", "data-trend": trend, children: [trend === "up" && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 12, strokeWidth: 2.5 }), trend === "down" && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 12, strokeWidth: 2.5 }), Math.abs(delta), deltaSuffix] })), caption
|
|
13
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, statcard_recipe_1.statcardRecipe)({ card, className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-label", children: [icon, label] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-statcard-value", children: value }), (trend != null || caption != null) && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-foot", children: [trend != null && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-statcard-delta", "data-trend": trend, children: [trend === "up" && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { size: 12, strokeWidth: 2.5 }), trend === "down" && (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { size: 12, strokeWidth: 2.5 }), Math.abs(delta), deltaSuffix] })), caption != null &&
|
|
14
|
+
(captionTone ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-statcard-caption", "data-tone": captionTone, children: caption })) : (caption))] }))] }));
|
|
14
15
|
}
|
|
15
16
|
StatCard.displayName = "StatCard";
|
|
@@ -14,6 +14,16 @@ const config = (0, core_1.defineRecipe)({
|
|
|
14
14
|
& .crisp-statcard-label svg { color: ${tokens_1.contract.fgMuted}; }
|
|
15
15
|
& .crisp-statcard-value { font-size: 28px; line-height: 32px; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.4px; color: ${tokens_1.contract.fgPrimary}; }
|
|
16
16
|
& .crisp-statcard-foot { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; }
|
|
17
|
+
/* caption's own status colour, opt-in via data-tone: not a new measured
|
|
18
|
+
value — the same fg*Soft tokens Badge's tone already uses (and Badge's
|
|
19
|
+
own doc comment records why: a dedicated soft-text token, not the
|
|
20
|
+
tone's accent colour, is the one that clears AA). No "ok" reading on
|
|
21
|
+
Badge itself; fgSuccessSoft is Badge's own "success" tone's text
|
|
22
|
+
colour, reused here under the name a caption's own good-news state
|
|
23
|
+
reads as. */
|
|
24
|
+
& .crisp-statcard-caption[data-tone="ok"] { color: ${tokens_1.contract.fgSuccessSoft}; }
|
|
25
|
+
& .crisp-statcard-caption[data-tone="warning"] { color: ${tokens_1.contract.fgWarningSoft}; }
|
|
26
|
+
& .crisp-statcard-caption[data-tone="critical"] { color: ${tokens_1.contract.fgDangerSoft}; }
|
|
17
27
|
& .crisp-statcard-delta { display: inline-flex; align-items: center; gap: 2px; height: 20px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; }
|
|
18
28
|
& .crisp-statcard-delta svg { flex-shrink: 0; }
|
|
19
29
|
& .crisp-statcard-delta[data-trend="up"] { background: color-mix(in srgb, ${tokens_1.contract.bgSuccess} 16%, transparent); color: color-mix(in srgb, ${tokens_1.contract.bgSuccess} 80%, ${tokens_1.contract.fgPrimary}); }
|
|
@@ -59,7 +59,9 @@ function ToastCard({ item, onClose, dismissLabel, }) {
|
|
|
59
59
|
* anywhere to push a notification. Toasts stack bottom-right and auto-dismiss
|
|
60
60
|
* after `duration` (default 4s; 0 keeps them until dismissed).
|
|
61
61
|
*/
|
|
62
|
-
function ToastProvider({ children, duration = 4000, dismissLabel
|
|
62
|
+
function ToastProvider({ children, duration = 4000, dismissLabel: dismissLabelProp, regionLabel: regionLabelProp, }) {
|
|
63
|
+
const dismissLabel = (0, core_1.useCrispMessage)(dismissLabelProp, "Dismiss");
|
|
64
|
+
const regionLabel = (0, core_1.useCrispMessage)(regionLabelProp, "Notifications");
|
|
63
65
|
const [items, setItems] = React.useState([]);
|
|
64
66
|
const idRef = React.useRef(0);
|
|
65
67
|
const timers = React.useRef(new Map());
|
package/dist/datatable.css
CHANGED
|
@@ -93,6 +93,8 @@
|
|
|
93
93
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
94
94
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
95
95
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
96
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
97
|
+
& button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
|
|
96
98
|
& .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
|
|
97
99
|
& .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
|
|
98
100
|
& .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
|
|
@@ -94,6 +94,8 @@
|
|
|
94
94
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
95
95
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
96
96
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
97
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
98
|
+
& button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
|
|
97
99
|
& .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
|
|
98
100
|
& .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
|
|
99
101
|
& .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ChevronRight } from "lucide-react";
|
|
3
2
|
import * as React from "react";
|
|
4
3
|
import { breadcrumbRecipe } from "./breadcrumb.recipe.js";
|
|
5
4
|
/**
|
|
@@ -7,7 +6,11 @@ import { breadcrumbRecipe } from "./breadcrumb.recipe.js";
|
|
|
7
6
|
* marked `aria-current="page"`; earlier items render as links or buttons.
|
|
8
7
|
*/
|
|
9
8
|
export function Breadcrumb({ items, separator, "aria-label": ariaLabel = "Breadcrumb", className, ...rest }) {
|
|
10
|
-
|
|
9
|
+
// Live-measured (app.attio.com/<ws>/home, /companies,
|
|
10
|
+
// /company/<id>/overview): a plain rounded divider, not a chevron -- a
|
|
11
|
+
// symmetric shape needs no RTL mirroring, unlike the ChevronRight this
|
|
12
|
+
// used to default to.
|
|
13
|
+
const sep = separator ?? (_jsx("span", { className: "crisp-breadcrumb-sep-dot", "aria-hidden": "true" }));
|
|
11
14
|
return (_jsx("nav", { "aria-label": ariaLabel, className: breadcrumbRecipe({ className }), ...rest, children: items.map((item, i) => {
|
|
12
15
|
const last = i === items.length - 1;
|
|
13
16
|
const content = (_jsxs(_Fragment, { children: [item.icon, item.label] }));
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineRecipe, resolveRecipe, rtlFlipCss } from "../../core/index.js";
|
|
2
2
|
import { contract as c } from "../../tokens/index.js";
|
|
3
|
-
// A breadcrumb trail — record-page nav ("Companies
|
|
3
|
+
// A breadcrumb trail — record-page nav ("Companies · Google"), the segments
|
|
4
|
+
// joined by a small rounded divider (crisp#498), not the literal "/" this
|
|
5
|
+
// comment's own example used to imply.
|
|
4
6
|
const config = defineRecipe({
|
|
5
7
|
name: "breadcrumb",
|
|
6
8
|
base: `display: flex; align-items: center; gap: ${c.space["0.5"]}; font-family: ${c.font.sans}; font-size: ${c.text.lg};
|
|
@@ -9,7 +11,8 @@ const config = defineRecipe({
|
|
|
9
11
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: ${c.fgPrimary}; background: color-mix(in srgb, ${c.fgPrimary} 5%, transparent); }
|
|
10
12
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
11
13
|
& .crisp-breadcrumb-item[aria-current] { color: ${c.fgPrimary}; font-weight: ${c.weight.medium}; }
|
|
12
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
14
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
15
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 28%, transparent); }
|
|
13
16
|
${rtlFlipCss}
|
|
14
17
|
`,
|
|
15
18
|
variants: {},
|
|
@@ -165,7 +165,7 @@ function formatCell(type, v, symbol, locale = "en-US") {
|
|
|
165
165
|
// silently drop the cell's data. Each branch decides for itself where a
|
|
166
166
|
// display string belongs -- as an entity's name, a status' label -- while the
|
|
167
167
|
// raw value keeps driving structure and tone.
|
|
168
|
-
function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
168
|
+
function renderTyped(col, value, avatarTone, row, display, locale = "en-US") {
|
|
169
169
|
// Attio's own empty cell is genuinely blank -- no placeholder character at
|
|
170
170
|
// all, for any column type (#412).
|
|
171
171
|
const empty = null;
|
|
@@ -177,7 +177,14 @@ function renderTyped(col, value, avatarTone, display, locale = "en-US") {
|
|
|
177
177
|
const e = display ? { ...raw, name: display } : raw;
|
|
178
178
|
if (!e.name)
|
|
179
179
|
return empty;
|
|
180
|
-
return (_jsxs("span", { className: "crisp-datatable-entity", children: [_jsx("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? (_jsx("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), _jsx("
|
|
180
|
+
return (_jsxs("span", { className: "crisp-datatable-entity", children: [_jsx("span", { className: "crisp-datatable-avatar", "data-tone": avatarTone === "hashed" ? toneFor(raw.name) : undefined, children: e.avatar ? (_jsx("img", { src: e.avatar, alt: "" })) : (initials(e.name).slice(0, 2)) }), col.onEntityClick ? (_jsx("button", { type: "button", className: "crisp-datatable-entity-name crisp-datatable-entity-name--link", onClick: (ev) => {
|
|
181
|
+
// the cell's own click already just selects (see the
|
|
182
|
+
// TableCell onClick above) -- this must additionally stop
|
|
183
|
+
// the row's onRowClick from firing, since this name points
|
|
184
|
+
// at a DIFFERENT record than the row itself (#501).
|
|
185
|
+
ev.stopPropagation();
|
|
186
|
+
col.onEntityClick?.(row);
|
|
187
|
+
}, children: e.name })) : (_jsx("span", { className: "crisp-datatable-entity-name", children: e.name }))] }));
|
|
181
188
|
}
|
|
182
189
|
case "tags": {
|
|
183
190
|
// asking for a format on a tag column means "render it your way" -- show
|
|
@@ -914,7 +921,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
|
|
|
914
921
|
? () => openEditing(row.id, col.key)
|
|
915
922
|
: undefined, children: [col.render
|
|
916
923
|
? col.render(value, row)
|
|
917
|
-
: renderTyped(col, value, avatarTone, col.format?.(value, row), locale), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
924
|
+
: renderTyped(col, value, avatarTone, row, col.format?.(value, row), locale), errorHere && (_jsx("span", { className: "crisp-datatable-edit-error", children: errorHere }))] }, col.key));
|
|
918
925
|
})] }, row.id)))] }, group ?? "__ungrouped")))) }), (hasSummary || onAddRow) && (_jsxs(TableFooter, { children: [hasSummary && (_jsxs(TableRow, { children: [showGrip && _jsx(TableCell, { ...stickyCol(-2) }), selectable && _jsx(TableCell, { ...stickyCol(-1) }), columns.map((col, i) => (_jsx(TableCell, { align: alignFor(col), ...dataStickyCol(i), children: col.summary ? col.summary(rows) : null }, col.key)))] })), onAddRow && (_jsx(TableRow, { children: _jsx(TableCell, { className: "crisp-datatable-addcell", colSpan: totalCols, children: _jsxs("button", { type: "button", className: "crisp-datatable-addrow", onClick: onAddRow, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), addRowLabel] }) }) }))] }))] })] }));
|
|
919
926
|
}
|
|
920
927
|
DataTable.displayName = "DataTable";
|
|
@@ -121,6 +121,8 @@ const config = defineRecipe({
|
|
|
121
121
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
122
122
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; color: ${c.fgPrimary}; font-weight: ${c.weight.medium}; }
|
|
123
123
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
124
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
125
|
+
& button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
|
|
124
126
|
& .crisp-datatable-avatar { width: ${c.size.icon}; height: ${c.size.icon}; border-radius: ${c.radius.sm}; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: ${c.weight.semibold}; text-transform: uppercase; color: ${c.fgOnBrand}; background: ${c.bgBrandSolid}; }
|
|
125
127
|
& .crisp-datatable-avatar[data-tone="success"] { background: ${c.bgSuccess}; } & .crisp-datatable-avatar[data-tone="warning"] { background: ${c.bgWarning}; } & .crisp-datatable-avatar[data-tone="danger"] { background: ${c.bgDanger}; } & .crisp-datatable-avatar[data-tone="purple"] { background: ${c.bgPurple}; } & .crisp-datatable-avatar[data-tone="cyan"] { background: ${c.bgCyan}; } & .crisp-datatable-avatar[data-tone="orange"] { background: ${c.bgOrange}; }
|
|
126
128
|
& .crisp-datatable-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { CalendarDays, X } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useCrispMessage } from "../../core/index.js";
|
|
4
5
|
import { DatePicker, } from "../datepicker/index.js";
|
|
5
6
|
import { Popover } from "../popover/index.js";
|
|
6
7
|
import { datepickerfieldRecipe } from "./datepickerfield.recipe.js";
|
|
@@ -21,7 +22,10 @@ const fromDate = (d) => ({
|
|
|
21
22
|
* popover. This is what `DatePicker` used to be; `DatePicker` is now
|
|
22
23
|
* seed-design's bare calendar, and this composes it.
|
|
23
24
|
*/
|
|
24
|
-
export function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder
|
|
25
|
+
export function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder: placeholderProp, variant = "default", format = defaultFormat, clearable = true, clearLabel: clearLabelProp, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
|
|
26
|
+
// crisp/#471: same resolution order as Select's own.
|
|
27
|
+
const placeholder = useCrispMessage(placeholderProp, "Set date…");
|
|
28
|
+
const clearLabel = useCrispMessage(clearLabelProp, "Clear date");
|
|
25
29
|
const [internal, setInternal] = React.useState(defaultValue ?? null);
|
|
26
30
|
const selected = value ?? internal;
|
|
27
31
|
const [open, setOpen] = React.useState(false);
|
|
@@ -3,7 +3,7 @@ import * as dialog from "@zag-js/dialog";
|
|
|
3
3
|
import { normalizeProps, Portal, useMachine } from "@zag-js/react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { cx, hasOpenFloatingLayer, withDescribedBy } from "../../core/index.js";
|
|
6
|
+
import { cx, hasOpenFloatingLayer, useCrispMessage, withDescribedBy, } from "../../core/index.js";
|
|
7
7
|
import { dialogRecipe } from "./dialog.recipe.js";
|
|
8
8
|
// A globally unique id per mounted instance (see command.tsx for the rationale —
|
|
9
9
|
// the machine defers its dismissal listeners a frame, so tree-relative useId
|
|
@@ -22,7 +22,8 @@ function useDialogInstanceId() {
|
|
|
22
22
|
* Escape / outside-click dismissal) with the measured modal surface.
|
|
23
23
|
* Compose the body with `DialogBody` and actions with `DialogFooter`.
|
|
24
24
|
*/
|
|
25
|
-
export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel
|
|
25
|
+
export function Dialog({ open, onOpenChange, title, icon, width = 640, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }) {
|
|
26
|
+
const closeLabel = useCrispMessage(closeLabelProp, "Close");
|
|
26
27
|
const id = useDialogInstanceId();
|
|
27
28
|
const service = useMachine(dialog.machine, {
|
|
28
29
|
id,
|
|
@@ -3,7 +3,7 @@ import * as dialog from "@zag-js/dialog";
|
|
|
3
3
|
import { normalizeProps, Portal, useMachine } from "@zag-js/react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { cx, hasOpenFloatingLayer, withDescribedBy } from "../../core/index.js";
|
|
6
|
+
import { cx, hasOpenFloatingLayer, useCrispMessage, withDescribedBy, } from "../../core/index.js";
|
|
7
7
|
import { drawerRecipe } from "./drawer.recipe.js";
|
|
8
8
|
let instanceCounter = 0;
|
|
9
9
|
function useDrawerInstanceId() {
|
|
@@ -19,7 +19,8 @@ function useDrawerInstanceId() {
|
|
|
19
19
|
* Escape / outside-click dismissal) with a panel that slides in from the edge.
|
|
20
20
|
* Compose the body with `DrawerBody` and actions with `DrawerFooter`.
|
|
21
21
|
*/
|
|
22
|
-
export function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel
|
|
22
|
+
export function Drawer({ open, onOpenChange, side = "right", title, icon, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, "aria-describedby": describedBy, ...rest }) {
|
|
23
|
+
const closeLabel = useCrispMessage(closeLabelProp, "Close");
|
|
23
24
|
const id = useDrawerInstanceId();
|
|
24
25
|
const service = useMachine(dialog.machine, {
|
|
25
26
|
id,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useFormReset, } from "../../core/index.js";
|
|
4
|
+
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, useFormReset, } from "../../core/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "../select/index.js";
|
|
6
6
|
import { multiselectRecipe } from "./multiselect.recipe.js";
|
|
7
7
|
const TONES = [
|
|
@@ -33,7 +33,11 @@ export function MultiSelect({ options, value: valueProp, defaultValue, onValueCh
|
|
|
33
33
|
// The id belongs on the trigger, not the wrapper: `<label for>` only names a
|
|
34
34
|
// LABELABLE element, so an id left on the root div meant a FormField label
|
|
35
35
|
// above this control named nothing (#247). Select already did this.
|
|
36
|
-
id, placeholder
|
|
36
|
+
id, placeholder: placeholderProp, searchable = true, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
|
|
37
|
+
// crisp/#471: same resolution order as Select's own.
|
|
38
|
+
const placeholder = useCrispMessage(placeholderProp, "Select…");
|
|
39
|
+
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search…");
|
|
40
|
+
const emptyLabel = useCrispMessage(emptyLabelProp, "No results");
|
|
37
41
|
// Virtual focus: DOM focus stays on the trigger (or the search field) and the
|
|
38
42
|
// active option is named by `aria-activedescendant`. `data-highlighted` is a
|
|
39
43
|
// styling hook only -- a screen reader cannot see it.
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { QuantityPickerDecrementButton, QuantityPickerHiddenInput, QuantityPickerIncrementButton, QuantityPickerRoot, useQuantityPickerContext, } from "@seed-design/react-quantity-picker";
|
|
4
4
|
import { Minus, Plus, Trash2 } from "lucide-react";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { useLabelAssociation } from "../../core/index.js";
|
|
6
|
+
import { useCrispMessage, useLabelAssociation } from "../../core/index.js";
|
|
7
7
|
import { Spinner } from "../spinner/index.js";
|
|
8
8
|
import { quantitypickerRecipe } from "./quantitypicker.recipe.js";
|
|
9
9
|
/** seed's icon box per size, read from their stylesheet. */
|
|
@@ -55,7 +55,10 @@ function IncrementIcon({ size }) {
|
|
|
55
55
|
* Slider documents. Here it also decides child ORDER, since the engine
|
|
56
56
|
* reverses its visible children rather than relying on CSS.
|
|
57
57
|
*/
|
|
58
|
-
export function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel
|
|
58
|
+
export function QuantityPicker({ size = "medium", fill = false, className, name, decrementLabel: decrementLabelProp, incrementLabel: incrementLabelProp, ...props }) {
|
|
59
|
+
// crisp/#471: same resolution order as Select's own.
|
|
60
|
+
const decrementLabel = useCrispMessage(decrementLabelProp, "Decrease quantity");
|
|
61
|
+
const incrementLabel = useCrispMessage(incrementLabelProp, "Increase quantity");
|
|
59
62
|
// A `<label for>` cannot name a div, and this root is a role="group" (#247).
|
|
60
63
|
const rootRef = React.useRef(null);
|
|
61
64
|
useLabelAssociation(rootRef);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Check, ChevronDown, Search } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useFormReset, } from "../../core/index.js";
|
|
4
|
+
import { FloatingPortal, formValueStyle, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, useFormReset, } from "../../core/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "./listbox-keyboard.js";
|
|
6
6
|
import { selectRecipe } from "./select.recipe.js";
|
|
7
7
|
function optionText(o) {
|
|
@@ -16,7 +16,13 @@ function optionText(o) {
|
|
|
16
16
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
17
17
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
18
18
|
*/
|
|
19
|
-
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder
|
|
19
|
+
export function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant = "default", width = "full", searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
|
|
20
|
+
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
21
|
+
// CrispMessagesProvider's translation of the English default, or that
|
|
22
|
+
// default itself with no provider — unchanged from before this existed.
|
|
23
|
+
const placeholder = useCrispMessage(placeholderProp, "Select…");
|
|
24
|
+
const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search…");
|
|
25
|
+
const emptyLabel = useCrispMessage(emptyLabelProp, "No results");
|
|
20
26
|
// Virtual focus: DOM focus stays on the trigger (or the search field), and the
|
|
21
27
|
// active option is named by `aria-activedescendant`. That is the only thing a
|
|
22
28
|
// screen reader has to go on -- `data-highlighted` is invisible to it -- so the
|
|
@@ -5,8 +5,9 @@ import { statcardRecipe } from "./statcard.recipe.js";
|
|
|
5
5
|
* A single-metric KPI tile — measured from a reports number/insight card.
|
|
6
6
|
* Label, a large value, and an optional up/down delta pill with a caption.
|
|
7
7
|
*/
|
|
8
|
-
export function StatCard({ label, icon, value, delta, deltaSuffix = "%", caption, card, className, ...rest }) {
|
|
8
|
+
export function StatCard({ label, icon, value, delta, deltaSuffix = "%", caption, captionTone, card, className, ...rest }) {
|
|
9
9
|
const trend = delta == null ? null : delta > 0 ? "up" : delta < 0 ? "down" : "flat";
|
|
10
|
-
return (_jsxs("div", { className: statcardRecipe({ card, className }), ...rest, children: [_jsxs("span", { className: "crisp-statcard-label", children: [icon, label] }), _jsx("span", { className: "crisp-statcard-value", children: value }), (trend != null || caption != null) && (_jsxs("span", { className: "crisp-statcard-foot", children: [trend != null && (_jsxs("span", { className: "crisp-statcard-delta", "data-trend": trend, children: [trend === "up" && _jsx(ArrowUp, { size: 12, strokeWidth: 2.5 }), trend === "down" && _jsx(ArrowDown, { size: 12, strokeWidth: 2.5 }), Math.abs(delta), deltaSuffix] })), caption
|
|
10
|
+
return (_jsxs("div", { className: statcardRecipe({ card, className }), ...rest, children: [_jsxs("span", { className: "crisp-statcard-label", children: [icon, label] }), _jsx("span", { className: "crisp-statcard-value", children: value }), (trend != null || caption != null) && (_jsxs("span", { className: "crisp-statcard-foot", children: [trend != null && (_jsxs("span", { className: "crisp-statcard-delta", "data-trend": trend, children: [trend === "up" && _jsx(ArrowUp, { size: 12, strokeWidth: 2.5 }), trend === "down" && _jsx(ArrowDown, { size: 12, strokeWidth: 2.5 }), Math.abs(delta), deltaSuffix] })), caption != null &&
|
|
11
|
+
(captionTone ? (_jsx("span", { className: "crisp-statcard-caption", "data-tone": captionTone, children: caption })) : (caption))] }))] }));
|
|
11
12
|
}
|
|
12
13
|
StatCard.displayName = "StatCard";
|
|
@@ -11,6 +11,16 @@ const config = defineRecipe({
|
|
|
11
11
|
& .crisp-statcard-label svg { color: ${c.fgMuted}; }
|
|
12
12
|
& .crisp-statcard-value { font-size: 28px; line-height: 32px; font-weight: ${c.weight.semibold}; letter-spacing: -0.4px; color: ${c.fgPrimary}; }
|
|
13
13
|
& .crisp-statcard-foot { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.sm}; color: ${c.fgMuted}; }
|
|
14
|
+
/* caption's own status colour, opt-in via data-tone: not a new measured
|
|
15
|
+
value — the same fg*Soft tokens Badge's tone already uses (and Badge's
|
|
16
|
+
own doc comment records why: a dedicated soft-text token, not the
|
|
17
|
+
tone's accent colour, is the one that clears AA). No "ok" reading on
|
|
18
|
+
Badge itself; fgSuccessSoft is Badge's own "success" tone's text
|
|
19
|
+
colour, reused here under the name a caption's own good-news state
|
|
20
|
+
reads as. */
|
|
21
|
+
& .crisp-statcard-caption[data-tone="ok"] { color: ${c.fgSuccessSoft}; }
|
|
22
|
+
& .crisp-statcard-caption[data-tone="warning"] { color: ${c.fgWarningSoft}; }
|
|
23
|
+
& .crisp-statcard-caption[data-tone="critical"] { color: ${c.fgDangerSoft}; }
|
|
14
24
|
& .crisp-statcard-delta { display: inline-flex; align-items: center; gap: 2px; height: 20px; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.sm}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; }
|
|
15
25
|
& .crisp-statcard-delta svg { flex-shrink: 0; }
|
|
16
26
|
& .crisp-statcard-delta[data-trend="up"] { background: color-mix(in srgb, ${c.bgSuccess} 16%, transparent); color: color-mix(in srgb, ${c.bgSuccess} 80%, ${c.fgPrimary}); }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { CircleAlert, CircleCheck, Info, TriangleAlert, X } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { useFocusAfterRemoval } from "../../core/index.js";
|
|
4
|
+
import { useCrispMessage, useFocusAfterRemoval } from "../../core/index.js";
|
|
5
5
|
import { toastRecipe } from "./toast.recipe.js";
|
|
6
6
|
const ToastContext = React.createContext(null);
|
|
7
7
|
const ICONS = {
|
|
@@ -22,7 +22,9 @@ function ToastCard({ item, onClose, dismissLabel, }) {
|
|
|
22
22
|
* anywhere to push a notification. Toasts stack bottom-right and auto-dismiss
|
|
23
23
|
* after `duration` (default 4s; 0 keeps them until dismissed).
|
|
24
24
|
*/
|
|
25
|
-
export function ToastProvider({ children, duration = 4000, dismissLabel
|
|
25
|
+
export function ToastProvider({ children, duration = 4000, dismissLabel: dismissLabelProp, regionLabel: regionLabelProp, }) {
|
|
26
|
+
const dismissLabel = useCrispMessage(dismissLabelProp, "Dismiss");
|
|
27
|
+
const regionLabel = useCrispMessage(regionLabelProp, "Notifications");
|
|
26
28
|
const [items, setItems] = React.useState([]);
|
|
27
29
|
const idRef = React.useRef(0);
|
|
28
30
|
const timers = React.useRef(new Map());
|
package/dist/statcard.css
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
& .crisp-statcard-label svg { color: var(--crisp-fg-muted); }
|
|
5
5
|
& .crisp-statcard-value { font-size: 28px; line-height: 32px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.4px; color: var(--crisp-fg-primary); }
|
|
6
6
|
& .crisp-statcard-foot { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
|
|
7
|
+
/* caption's own status colour, opt-in via data-tone: not a new measured
|
|
8
|
+
value — the same fg*Soft tokens Badge's tone already uses (and Badge's
|
|
9
|
+
own doc comment records why: a dedicated soft-text token, not the
|
|
10
|
+
tone's accent colour, is the one that clears AA). No "ok" reading on Badge itself; fgSuccessSoft is Badge's own "success" tone's text colour, reused here under the name a caption's own good-news state
|
|
11
|
+
reads as. */
|
|
12
|
+
& .crisp-statcard-caption[data-tone="ok"] { color: var(--crisp-fg-success-soft); }
|
|
13
|
+
& .crisp-statcard-caption[data-tone="warning"] { color: var(--crisp-fg-warning-soft); }
|
|
14
|
+
& .crisp-statcard-caption[data-tone="critical"] { color: var(--crisp-fg-danger-soft); }
|
|
7
15
|
& .crisp-statcard-delta { display: inline-flex; align-items: center; gap: 2px; height: 20px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); }
|
|
8
16
|
& .crisp-statcard-delta svg { flex-shrink: 0; }
|
|
9
17
|
& .crisp-statcard-delta[data-trend="up"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 80%, var(--crisp-fg-primary)); }
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
& .crisp-statcard-label svg { color: var(--crisp-fg-muted); }
|
|
6
6
|
& .crisp-statcard-value { font-size: 28px; line-height: 32px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.4px; color: var(--crisp-fg-primary); }
|
|
7
7
|
& .crisp-statcard-foot { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
|
|
8
|
+
/* caption's own status colour, opt-in via data-tone: not a new measured
|
|
9
|
+
value — the same fg*Soft tokens Badge's tone already uses (and Badge's
|
|
10
|
+
own doc comment records why: a dedicated soft-text token, not the
|
|
11
|
+
tone's accent colour, is the one that clears AA). No "ok" reading on Badge itself; fgSuccessSoft is Badge's own "success" tone's text colour, reused here under the name a caption's own good-news state
|
|
12
|
+
reads as. */
|
|
13
|
+
& .crisp-statcard-caption[data-tone="ok"] { color: var(--crisp-fg-success-soft); }
|
|
14
|
+
& .crisp-statcard-caption[data-tone="warning"] { color: var(--crisp-fg-warning-soft); }
|
|
15
|
+
& .crisp-statcard-caption[data-tone="critical"] { color: var(--crisp-fg-danger-soft); }
|
|
8
16
|
& .crisp-statcard-delta { display: inline-flex; align-items: center; gap: 2px; height: 20px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); }
|
|
9
17
|
& .crisp-statcard-delta svg { flex-shrink: 0; }
|
|
10
18
|
& .crisp-statcard-delta[data-trend="up"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 80%, var(--crisp-fg-primary)); }
|
package/dist/styles.css
CHANGED
|
@@ -286,7 +286,8 @@
|
|
|
286
286
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: var(--crisp-fg-primary); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); }
|
|
287
287
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
288
288
|
& .crisp-breadcrumb-item[aria-current] { color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
289
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
289
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
290
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: var(--crisp-radius-full); background: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
|
|
290
291
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
291
292
|
}
|
|
292
293
|
|
|
@@ -638,6 +639,8 @@
|
|
|
638
639
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
639
640
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
640
641
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
642
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
643
|
+
& button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
|
|
641
644
|
& .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
|
|
642
645
|
& .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
|
|
643
646
|
& .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
|
|
@@ -1541,6 +1544,14 @@
|
|
|
1541
1544
|
& .crisp-statcard-label svg { color: var(--crisp-fg-muted); }
|
|
1542
1545
|
& .crisp-statcard-value { font-size: 28px; line-height: 32px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.4px; color: var(--crisp-fg-primary); }
|
|
1543
1546
|
& .crisp-statcard-foot { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
|
|
1547
|
+
/* caption's own status colour, opt-in via data-tone: not a new measured
|
|
1548
|
+
value — the same fg*Soft tokens Badge's tone already uses (and Badge's
|
|
1549
|
+
own doc comment records why: a dedicated soft-text token, not the
|
|
1550
|
+
tone's accent colour, is the one that clears AA). No "ok" reading on Badge itself; fgSuccessSoft is Badge's own "success" tone's text colour, reused here under the name a caption's own good-news state
|
|
1551
|
+
reads as. */
|
|
1552
|
+
& .crisp-statcard-caption[data-tone="ok"] { color: var(--crisp-fg-success-soft); }
|
|
1553
|
+
& .crisp-statcard-caption[data-tone="warning"] { color: var(--crisp-fg-warning-soft); }
|
|
1554
|
+
& .crisp-statcard-caption[data-tone="critical"] { color: var(--crisp-fg-danger-soft); }
|
|
1544
1555
|
& .crisp-statcard-delta { display: inline-flex; align-items: center; gap: 2px; height: 20px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); }
|
|
1545
1556
|
& .crisp-statcard-delta svg { flex-shrink: 0; }
|
|
1546
1557
|
& .crisp-statcard-delta[data-trend="up"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 80%, var(--crisp-fg-primary)); }
|
package/dist/styles.layered.css
CHANGED
|
@@ -287,7 +287,8 @@
|
|
|
287
287
|
& a.crisp-breadcrumb-item:hover, & button.crisp-breadcrumb-item:hover { color: var(--crisp-fg-primary); background: color-mix(in srgb, var(--crisp-fg-primary) 5%, transparent); }
|
|
288
288
|
& button.crisp-breadcrumb-item { border: none; background: none; font: inherit; cursor: pointer; }
|
|
289
289
|
& .crisp-breadcrumb-item[aria-current] { color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
290
|
-
& .crisp-breadcrumb-sep { flex-shrink: 0;
|
|
290
|
+
& .crisp-breadcrumb-sep { flex-shrink: 0; display: inline-flex; }
|
|
291
|
+
& .crisp-breadcrumb-sep-dot { flex-shrink: 0; width: 3px; height: 10px; border-radius: var(--crisp-radius-full); background: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
|
|
291
292
|
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
292
293
|
}
|
|
293
294
|
|
|
@@ -639,6 +640,8 @@
|
|
|
639
640
|
/* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
|
|
640
641
|
& .crisp-datatable-entity { display: flex; align-items: center; gap: var(--crisp-space-1-5); min-width: 0; color: var(--crisp-fg-primary); font-weight: var(--crisp-weight-medium); }
|
|
641
642
|
& .crisp-datatable-entity-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
643
|
+
& .crisp-datatable-entity:hover .crisp-datatable-entity-name { text-decoration: underline; }
|
|
644
|
+
& button.crisp-datatable-entity-name--link { margin: 0; padding: 0; border: none; background: none; font: inherit; color: inherit; letter-spacing: inherit; text-align: start; cursor: pointer; max-width: 100%; }
|
|
642
645
|
& .crisp-datatable-avatar { width: var(--crisp-size-icon); height: var(--crisp-size-icon); border-radius: var(--crisp-radius-sm); flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; line-height: 1; font-weight: var(--crisp-weight-semibold); text-transform: uppercase; color: var(--crisp-fg-on-brand); background: var(--crisp-bg-brand-solid); }
|
|
643
646
|
& .crisp-datatable-avatar[data-tone="success"] { background: var(--crisp-bg-success); }
|
|
644
647
|
& .crisp-datatable-avatar[data-tone="warning"] { background: var(--crisp-bg-warning); }
|
|
@@ -1542,6 +1545,14 @@
|
|
|
1542
1545
|
& .crisp-statcard-label svg { color: var(--crisp-fg-muted); }
|
|
1543
1546
|
& .crisp-statcard-value { font-size: 28px; line-height: 32px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.4px; color: var(--crisp-fg-primary); }
|
|
1544
1547
|
& .crisp-statcard-foot { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
|
|
1548
|
+
/* caption's own status colour, opt-in via data-tone: not a new measured
|
|
1549
|
+
value — the same fg*Soft tokens Badge's tone already uses (and Badge's
|
|
1550
|
+
own doc comment records why: a dedicated soft-text token, not the
|
|
1551
|
+
tone's accent colour, is the one that clears AA). No "ok" reading on Badge itself; fgSuccessSoft is Badge's own "success" tone's text colour, reused here under the name a caption's own good-news state
|
|
1552
|
+
reads as. */
|
|
1553
|
+
& .crisp-statcard-caption[data-tone="ok"] { color: var(--crisp-fg-success-soft); }
|
|
1554
|
+
& .crisp-statcard-caption[data-tone="warning"] { color: var(--crisp-fg-warning-soft); }
|
|
1555
|
+
& .crisp-statcard-caption[data-tone="critical"] { color: var(--crisp-fg-danger-soft); }
|
|
1545
1556
|
& .crisp-statcard-delta { display: inline-flex; align-items: center; gap: 2px; height: 20px; padding: 0 var(--crisp-space-1); border-radius: var(--crisp-radius-sm); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); }
|
|
1546
1557
|
& .crisp-statcard-delta svg { flex-shrink: 0; }
|
|
1547
1558
|
& .crisp-statcard-delta[data-trend="up"] { background: color-mix(in srgb, var(--crisp-bg-success) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-success) 80%, var(--crisp-fg-primary)); }
|
|
@@ -7,7 +7,7 @@ export type BreadcrumbItem = {
|
|
|
7
7
|
};
|
|
8
8
|
export type BreadcrumbProps = React.ComponentPropsWithoutRef<"nav"> & {
|
|
9
9
|
items: BreadcrumbItem[];
|
|
10
|
-
/** Separator between items (default a
|
|
10
|
+
/** Separator between items (default a small rounded divider — see below). */
|
|
11
11
|
separator?: React.ReactNode;
|
|
12
12
|
"aria-label"?: string;
|
|
13
13
|
className?: string;
|
|
@@ -100,6 +100,16 @@ export type DataTableColumn<T> = {
|
|
|
100
100
|
managed?: boolean;
|
|
101
101
|
/** Hidden from the grid but still listed in the column manager. */
|
|
102
102
|
hidden?: boolean;
|
|
103
|
+
/** `type: "entity"` only (#501) — makes the cell's name its own click
|
|
104
|
+
* target, separate from the row's own `onRowClick`: the name renders as a
|
|
105
|
+
* `stopPropagation`-guarded button instead of a plain span, the same
|
|
106
|
+
* `crisp-datatable-entity-name--link` pattern RecordsTable's own
|
|
107
|
+
* `EntityCell.onOpenPage` already uses (`crisp-recordstable-entity-name
|
|
108
|
+
* --link`). For a cell whose entity points at a DIFFERENT record than the
|
|
109
|
+
* row itself (a reference/owner/approver column), not for navigating the
|
|
110
|
+
* row's own record — that's what `onRowClick` is for. Ignored on any
|
|
111
|
+
* other `type`. */
|
|
112
|
+
onEntityClick?: (row: T) => void;
|
|
103
113
|
};
|
|
104
114
|
export type SortState = {
|
|
105
115
|
key: string;
|
|
@@ -51,7 +51,7 @@ export type DatePickerFieldProps = Omit<React.ComponentPropsWithoutRef<"div">, "
|
|
|
51
51
|
* popover. This is what `DatePicker` used to be; `DatePicker` is now
|
|
52
52
|
* seed-design's bare calendar, and this composes it.
|
|
53
53
|
*/
|
|
54
|
-
export declare function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder, variant, format, clearable, clearLabel, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }: DatePickerFieldProps): React.JSX.Element;
|
|
54
|
+
export declare function DatePickerField({ value, defaultValue, onValueChange, onChange, placeholder: placeholderProp, variant, format, clearable, clearLabel: clearLabelProp, presets, calendarProps, "aria-label": ariaLabel, className, ...rest }: DatePickerFieldProps): React.JSX.Element;
|
|
55
55
|
export declare namespace DatePickerField {
|
|
56
56
|
var displayName: string;
|
|
57
57
|
}
|
|
@@ -29,7 +29,7 @@ export type DialogProps = Omit<React.ComponentPropsWithoutRef<"div">, "title" |
|
|
|
29
29
|
* Escape / outside-click dismissal) with the measured modal surface.
|
|
30
30
|
* Compose the body with `DialogBody` and actions with `DialogFooter`.
|
|
31
31
|
*/
|
|
32
|
-
export declare function Dialog({ open, onOpenChange, title, icon, width, "aria-label": ariaLabel, closeLabel, children, className, style, "aria-describedby": describedBy, ...rest }: DialogProps): React.JSX.Element | null;
|
|
32
|
+
export declare function Dialog({ open, onOpenChange, title, icon, width, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, style, "aria-describedby": describedBy, ...rest }: DialogProps): React.JSX.Element | null;
|
|
33
33
|
export declare namespace Dialog {
|
|
34
34
|
var displayName: string;
|
|
35
35
|
}
|
|
@@ -20,7 +20,7 @@ export type DrawerProps = Omit<React.ComponentPropsWithoutRef<"div">, "title" |
|
|
|
20
20
|
* Escape / outside-click dismissal) with a panel that slides in from the edge.
|
|
21
21
|
* Compose the body with `DrawerBody` and actions with `DrawerFooter`.
|
|
22
22
|
*/
|
|
23
|
-
export declare function Drawer({ open, onOpenChange, side, title, icon, "aria-label": ariaLabel, closeLabel, children, className, "aria-describedby": describedBy, ...rest }: DrawerProps): React.JSX.Element | null;
|
|
23
|
+
export declare function Drawer({ open, onOpenChange, side, title, icon, "aria-label": ariaLabel, closeLabel: closeLabelProp, children, className, "aria-describedby": describedBy, ...rest }: DrawerProps): React.JSX.Element | null;
|
|
24
24
|
export declare namespace Drawer {
|
|
25
25
|
var displayName: string;
|
|
26
26
|
}
|
|
@@ -56,7 +56,7 @@ export type MultiSelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "valu
|
|
|
56
56
|
* popover with a search field over rows of "[chip] … [checkbox]" that toggle on
|
|
57
57
|
* click (the menu stays open). Keyboard: ↑/↓ move, Enter toggles, Esc closes.
|
|
58
58
|
*/
|
|
59
|
-
export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, required, id, placeholder, searchable, searchPlaceholder, emptyLabel, disabled, invalid, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
|
|
59
|
+
export declare function MultiSelect({ options, value: valueProp, defaultValue, onValueChange, name, required, id, placeholder: placeholderProp, searchable, searchPlaceholder: searchPlaceholderProp, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, ...rest }: MultiSelectProps): React.JSX.Element;
|
|
60
60
|
export declare namespace MultiSelect {
|
|
61
61
|
var displayName: string;
|
|
62
62
|
}
|
|
@@ -37,7 +37,7 @@ export type QuantityPickerProps = QuantityPickerRootProps & {
|
|
|
37
37
|
* Slider documents. Here it also decides child ORDER, since the engine
|
|
38
38
|
* reverses its visible children rather than relying on CSS.
|
|
39
39
|
*/
|
|
40
|
-
export declare function QuantityPicker({ size, fill, className, name, decrementLabel, incrementLabel, ...props }: QuantityPickerProps): React.JSX.Element;
|
|
40
|
+
export declare function QuantityPicker({ size, fill, className, name, decrementLabel: decrementLabelProp, incrementLabel: incrementLabelProp, ...props }: QuantityPickerProps): React.JSX.Element;
|
|
41
41
|
export declare namespace QuantityPicker {
|
|
42
42
|
var displayName: string;
|
|
43
43
|
}
|
|
@@ -69,7 +69,7 @@ export type SelectProps = Omit<React.ComponentPropsWithoutRef<"div">, "value" |
|
|
|
69
69
|
* "No results" empty state). Full keyboard model — ↑/↓ move, Enter/Space pick,
|
|
70
70
|
* Esc close, Home/End jump, opens upward when tight. Controlled or uncontrolled.
|
|
71
71
|
*/
|
|
72
|
-
export declare function Select({ options, value, defaultValue, onValueChange, name, required, placeholder, variant, width, searchable, searchPlaceholder, onSearchChange, emptyLabel, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
|
|
72
|
+
export declare function Select({ options, value, defaultValue, onValueChange, name, required, placeholder: placeholderProp, variant, width, searchable, searchPlaceholder: searchPlaceholderProp, onSearchChange, emptyLabel: emptyLabelProp, disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }: SelectProps): React.JSX.Element;
|
|
73
73
|
export declare namespace Select {
|
|
74
74
|
var displayName: string;
|
|
75
75
|
}
|
|
@@ -12,6 +12,10 @@ export type StatCardProps = React.ComponentPropsWithoutRef<"div"> & {
|
|
|
12
12
|
deltaSuffix?: string;
|
|
13
13
|
/** Caption after the delta (e.g. "vs last month"). */
|
|
14
14
|
caption?: React.ReactNode;
|
|
15
|
+
/** Status colour for `caption` (e.g. "3 without a granter" in red) —
|
|
16
|
+
* independent of `delta`'s own up/down colouring, for a caption that
|
|
17
|
+
* reads a state rather than a trend. Unset keeps the caption muted. */
|
|
18
|
+
captionTone?: "ok" | "warning" | "critical";
|
|
15
19
|
/** Opt-in bordered card chrome (padding + border + radius), for use
|
|
16
20
|
* standalone instead of hand-wrapping in `<Card><CardBody>`. Default
|
|
17
21
|
* `false` — StatCard renders bare, as measured (pair with `Card` yourself
|
|
@@ -23,7 +27,7 @@ export type StatCardProps = React.ComponentPropsWithoutRef<"div"> & {
|
|
|
23
27
|
* A single-metric KPI tile — measured from a reports number/insight card.
|
|
24
28
|
* Label, a large value, and an optional up/down delta pill with a caption.
|
|
25
29
|
*/
|
|
26
|
-
export declare function StatCard({ label, icon, value, delta, deltaSuffix, caption, card, className, ...rest }: StatCardProps): React.JSX.Element;
|
|
30
|
+
export declare function StatCard({ label, icon, value, delta, deltaSuffix, caption, captionTone, card, className, ...rest }: StatCardProps): React.JSX.Element;
|
|
27
31
|
export declare namespace StatCard {
|
|
28
32
|
var displayName: string;
|
|
29
33
|
}
|
|
@@ -20,7 +20,7 @@ type ToastApi = {
|
|
|
20
20
|
* anywhere to push a notification. Toasts stack bottom-right and auto-dismiss
|
|
21
21
|
* after `duration` (default 4s; 0 keeps them until dismissed).
|
|
22
22
|
*/
|
|
23
|
-
export declare function ToastProvider({ children, duration, dismissLabel, regionLabel, }: {
|
|
23
|
+
export declare function ToastProvider({ children, duration, dismissLabel: dismissLabelProp, regionLabel: regionLabelProp, }: {
|
|
24
24
|
children: React.ReactNode;
|
|
25
25
|
duration?: number;
|
|
26
26
|
/** aria-label on each toast's dismiss button (default "Dismiss"). */
|