@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.
Files changed (45) hide show
  1. package/dist/breadcrumb.css +2 -1
  2. package/dist/breadcrumb.layered.css +2 -1
  3. package/dist/cjs/components/breadcrumb/breadcrumb.js +5 -2
  4. package/dist/cjs/components/breadcrumb/breadcrumb.recipe.js +5 -2
  5. package/dist/cjs/components/datatable/datatable.js +10 -3
  6. package/dist/cjs/components/datatable/datatable.recipe.js +2 -0
  7. package/dist/cjs/components/datepickerfield/datepickerfield.js +5 -1
  8. package/dist/cjs/components/dialog/dialog.js +2 -1
  9. package/dist/cjs/components/drawer/drawer.js +2 -1
  10. package/dist/cjs/components/multiselect/multiselect.js +5 -1
  11. package/dist/cjs/components/quantitypicker/quantitypicker.js +4 -1
  12. package/dist/cjs/components/select/select.js +7 -1
  13. package/dist/cjs/components/statcard/statcard.js +3 -2
  14. package/dist/cjs/components/statcard/statcard.recipe.js +10 -0
  15. package/dist/cjs/components/toast/toast.js +3 -1
  16. package/dist/datatable.css +2 -0
  17. package/dist/datatable.layered.css +2 -0
  18. package/dist/esm/components/breadcrumb/breadcrumb.js +5 -2
  19. package/dist/esm/components/breadcrumb/breadcrumb.recipe.js +5 -2
  20. package/dist/esm/components/datatable/datatable.js +10 -3
  21. package/dist/esm/components/datatable/datatable.recipe.js +2 -0
  22. package/dist/esm/components/datepickerfield/datepickerfield.js +5 -1
  23. package/dist/esm/components/dialog/dialog.js +3 -2
  24. package/dist/esm/components/drawer/drawer.js +3 -2
  25. package/dist/esm/components/multiselect/multiselect.js +6 -2
  26. package/dist/esm/components/quantitypicker/quantitypicker.js +5 -2
  27. package/dist/esm/components/select/select.js +8 -2
  28. package/dist/esm/components/statcard/statcard.js +3 -2
  29. package/dist/esm/components/statcard/statcard.recipe.js +10 -0
  30. package/dist/esm/components/toast/toast.js +4 -2
  31. package/dist/statcard.css +8 -0
  32. package/dist/statcard.layered.css +8 -0
  33. package/dist/styles.css +12 -1
  34. package/dist/styles.layered.css +12 -1
  35. package/dist/types/components/breadcrumb/breadcrumb.d.ts +1 -1
  36. package/dist/types/components/datatable/datatable.d.ts +10 -0
  37. package/dist/types/components/datepickerfield/datepickerfield.d.ts +1 -1
  38. package/dist/types/components/dialog/dialog.d.ts +1 -1
  39. package/dist/types/components/drawer/drawer.d.ts +1 -1
  40. package/dist/types/components/multiselect/multiselect.d.ts +1 -1
  41. package/dist/types/components/quantitypicker/quantitypicker.d.ts +1 -1
  42. package/dist/types/components/select/select.d.ts +1 -1
  43. package/dist/types/components/statcard/statcard.d.ts +5 -1
  44. package/dist/types/components/toast/toast.d.ts +1 -1
  45. package/package.json +1 -1
@@ -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; color: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
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; color: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
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
- const sep = separator ?? ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { "data-rtl-flip": "", size: 14, strokeWidth: 1.75, "aria-hidden": "true" }));
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 / Google").
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; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 28%, transparent); }
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)("span", { className: "crisp-datatable-entity-name", children: e.name })] }));
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 = "Set date…", variant = "default", format = defaultFormat, clearable = true, clearLabel = "Clear date", presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
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 = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
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 = "Close", children, className, "aria-describedby": describedBy, ...rest }) {
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 = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
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 = "Decrease quantity", incrementLabel = "Increase quantity", ...props }) {
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 = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", onSearchChange, emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
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 = "Dismiss", regionLabel = "Notifications", }) {
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());
@@ -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
- const sep = separator ?? (_jsx(ChevronRight, { "data-rtl-flip": "", size: 14, strokeWidth: 1.75, "aria-hidden": "true" }));
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 / Google").
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; color: color-mix(in srgb, ${c.fgPrimary} 28%, transparent); }
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("span", { className: "crisp-datatable-entity-name", children: e.name })] }));
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 = "Set date…", variant = "default", format = defaultFormat, clearable = true, clearLabel = "Clear date", presets, calendarProps, "aria-label": ariaLabel, className, ...rest }) {
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 = "Close", children, className, style, "aria-describedby": describedBy, ...rest }) {
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 = "Close", children, className, "aria-describedby": describedBy, ...rest }) {
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 = "Select…", searchable = true, searchPlaceholder = "Search…", emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, ...rest }) {
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 = "Decrease quantity", incrementLabel = "Increase quantity", ...props }) {
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 = "Select…", variant = "default", width = "full", searchable, searchPlaceholder = "Search…", onSearchChange, emptyLabel = "No results", disabled, invalid, className, "aria-label": ariaLabel, id, ...rest }) {
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 = "Dismiss", regionLabel = "Notifications", }) {
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; color: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
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)); }
@@ -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; color: color-mix(in srgb, var(--crisp-fg-primary) 28%, transparent); }
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 chevron). */
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"). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.53.0",
3
+ "version": "0.53.2",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",