@crisp-ui-kit/crisp 0.52.6 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/breadcrumb.css +2 -1
  2. package/dist/breadcrumb.layered.css +2 -1
  3. package/dist/button.css +2 -2
  4. package/dist/button.layered.css +2 -2
  5. package/dist/cjs/components/breadcrumb/breadcrumb.js +5 -2
  6. package/dist/cjs/components/breadcrumb/breadcrumb.recipe.js +5 -2
  7. package/dist/cjs/components/button/button.recipe.js +10 -2
  8. package/dist/cjs/components/datatable/datatable.js +55 -10
  9. package/dist/cjs/components/datatable/datatable.recipe.js +2 -0
  10. package/dist/cjs/components/datepickerfield/datepickerfield.js +5 -1
  11. package/dist/cjs/components/dialog/dialog.js +2 -1
  12. package/dist/cjs/components/drawer/drawer.js +2 -1
  13. package/dist/cjs/components/multiselect/multiselect.js +5 -1
  14. package/dist/cjs/components/quantitypicker/quantitypicker.js +4 -1
  15. package/dist/cjs/components/select/select.js +7 -1
  16. package/dist/cjs/components/statcard/statcard.js +3 -2
  17. package/dist/cjs/components/statcard/statcard.recipe.js +10 -0
  18. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +40 -7
  19. package/dist/cjs/components/toast/toast.js +3 -1
  20. package/dist/cjs/core/i18n.js +75 -0
  21. package/dist/cjs/core/index.js +5 -1
  22. package/dist/datatable.css +2 -0
  23. package/dist/datatable.layered.css +2 -0
  24. package/dist/esm/components/breadcrumb/breadcrumb.js +5 -2
  25. package/dist/esm/components/breadcrumb/breadcrumb.recipe.js +5 -2
  26. package/dist/esm/components/button/button.recipe.js +10 -2
  27. package/dist/esm/components/datatable/datatable.js +56 -11
  28. package/dist/esm/components/datatable/datatable.recipe.js +2 -0
  29. package/dist/esm/components/datepickerfield/datepickerfield.js +5 -1
  30. package/dist/esm/components/dialog/dialog.js +3 -2
  31. package/dist/esm/components/drawer/drawer.js +3 -2
  32. package/dist/esm/components/multiselect/multiselect.js +6 -2
  33. package/dist/esm/components/quantitypicker/quantitypicker.js +5 -2
  34. package/dist/esm/components/select/select.js +8 -2
  35. package/dist/esm/components/statcard/statcard.js +3 -2
  36. package/dist/esm/components/statcard/statcard.recipe.js +10 -0
  37. package/dist/esm/components/tabletoolbar/tabletoolbar.js +41 -8
  38. package/dist/esm/components/toast/toast.js +4 -2
  39. package/dist/esm/core/i18n.js +37 -0
  40. package/dist/esm/core/index.js +1 -0
  41. package/dist/statcard.css +8 -0
  42. package/dist/statcard.layered.css +8 -0
  43. package/dist/styles.css +14 -3
  44. package/dist/styles.layered.css +14 -3
  45. package/dist/types/components/breadcrumb/breadcrumb.d.ts +1 -1
  46. package/dist/types/components/button/button.recipe.d.ts +2 -2
  47. package/dist/types/components/datatable/datatable.d.ts +31 -1
  48. package/dist/types/components/datepickerfield/datepickerfield.d.ts +1 -1
  49. package/dist/types/components/dialog/dialog.d.ts +1 -1
  50. package/dist/types/components/drawer/drawer.d.ts +1 -1
  51. package/dist/types/components/multiselect/multiselect.d.ts +1 -1
  52. package/dist/types/components/quantitypicker/quantitypicker.d.ts +1 -1
  53. package/dist/types/components/select/select.d.ts +1 -1
  54. package/dist/types/components/statcard/statcard.d.ts +5 -1
  55. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +5 -1
  56. package/dist/types/components/toast/toast.d.ts +1 -1
  57. package/dist/types/core/i18n.d.ts +58 -0
  58. package/dist/types/core/index.d.ts +1 -0
  59. 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
  }
package/dist/button.css CHANGED
@@ -3,7 +3,7 @@
3
3
  &:disabled { opacity: 0.5; cursor: not-allowed; }
4
4
  }
5
5
  .crisp-button--intent_primary {
6
- background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08);
6
+ background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08);
7
7
  &:hover { background: var(--crisp-bg-brand-solid-pressed); }
8
8
  &:active { background: var(--crisp-bg-brand-solid-pressed); }
9
9
  }
@@ -12,7 +12,7 @@
12
12
  &:hover { background: var(--crisp-bg-raised); }
13
13
  }
14
14
  .crisp-button--intent_danger {
15
- background: var(--crisp-bg-danger-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(186,37,37,0.20), 0 3px 6px -2px rgba(186,37,37,0.12);
15
+ background: var(--crisp-bg-danger-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(186,37,37,0.20), 0 3px 6px -2px rgba(186,37,37,0.12);
16
16
  &:hover { filter: brightness(0.96); }
17
17
  &:active { filter: brightness(0.92); }
18
18
  }
@@ -4,7 +4,7 @@
4
4
  &:disabled { opacity: 0.5; cursor: not-allowed; }
5
5
  }
6
6
  .crisp-button--intent_primary {
7
- background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08);
7
+ background: var(--crisp-bg-brand-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08);
8
8
  &:hover { background: var(--crisp-bg-brand-solid-pressed); }
9
9
  &:active { background: var(--crisp-bg-brand-solid-pressed); }
10
10
  }
@@ -13,7 +13,7 @@
13
13
  &:hover { background: var(--crisp-bg-raised); }
14
14
  }
15
15
  .crisp-button--intent_danger {
16
- background: var(--crisp-bg-danger-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(186,37,37,0.20), 0 3px 6px -2px rgba(186,37,37,0.12);
16
+ background: var(--crisp-bg-danger-solid); color: var(--crisp-fg-on-brand); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(186,37,37,0.20), 0 3px 6px -2px rgba(186,37,37,0.12);
17
17
  &:hover { filter: brightness(0.96); }
18
18
  &:active { filter: brightness(0.92); }
19
19
  }
@@ -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: {},
@@ -28,7 +28,12 @@ const spec = (0, core_1.defineSpec)({
28
28
  decls: {
29
29
  background: tokens_1.contract.bgBrandSolid,
30
30
  color: tokens_1.contract.fgOnBrand,
31
- boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08)",
31
+ // crisp#494: the inset ring is a black tint, not white -- live-
32
+ // measured on Attio's own "New Company" button as
33
+ // rgba(0,0,0,0.06), not the rgba(255,255,255,0.07) both solid
34
+ // intents shared. The other two shadow layers already matched
35
+ // exactly.
36
+ boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08)",
32
37
  },
33
38
  parts: [
34
39
  { selector: "&:hover", decls: { background: tokens_1.contract.bgBrandSolidPressed } },
@@ -50,7 +55,10 @@ const spec = (0, core_1.defineSpec)({
50
55
  decls: {
51
56
  background: tokens_1.contract.bgDangerSolid,
52
57
  color: tokens_1.contract.fgOnBrand,
53
- boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.07), 0 2px 4px -2px rgba(186,37,37,0.20), 0 3px 6px -2px rgba(186,37,37,0.12)",
58
+ // Same corrected inset ring as primary (crisp#494) -- shared
59
+ // literal, same solid-fill button family; the coloured layers
60
+ // themselves weren't independently re-measured this round.
61
+ boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(186,37,37,0.20), 0 3px 6px -2px rgba(186,37,37,0.12)",
54
62
  },
55
63
  parts: [
56
64
  { selector: "&:hover", decls: { filter: "brightness(0.96)" } },
@@ -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
@@ -330,9 +337,47 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
330
337
  onCommit(v);
331
338
  } }) }));
332
339
  }
333
- function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel = "Select all rows", selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement = (count) => count === 0 ? "Selection cleared" : `${count} selected`, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder = "Search…", searchLabel = "Search table", onEdit, stickyFirst = true, stickyLast = false, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", empty = "No results.", showCount = true, countLabel = (shown, total) => shown === total
334
- ? `${total} ${total === 1 ? "row" : "rows"}`
335
- : `${shown} of ${total}`, toolbar, className, reorderRowLabel = "Reorder row", ...rest }) {
340
+ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds, onSelectionChange, selectAllLabel: selectAllLabelProp, selectRowLabel = (row) => `Select row ${row.id}`, selectionAnnouncement, columnMenu = false, onColumnsChange, groupBy, onRowsReorder, error, sort: sortProp, defaultSort, onSortChange, filterable = false, searchPlaceholder: searchPlaceholderProp, searchLabel: searchLabelProp, onEdit, stickyFirst = true, stickyLast = false, locale: localeProp, avatarTone = "brand", surface, interactive = false, onRowClick, onAddRow, addRowLabel: addRowLabelProp, empty: emptyProp, showCount = true, countLabel, toolbar, className, reorderRowLabel: reorderRowLabelProp, sortAscendingLabel: sortAscendingLabelProp, sortDescendingLabel: sortDescendingLabelProp, moveColumnLeftLabel: moveColumnLeftLabelProp, moveColumnRightLabel: moveColumnRightLabelProp, editColumnLabel: editColumnLabelProp, hideColumnLabel: hideColumnLabelProp, renameColumnLabel: renameColumnLabelProp, viewSettingsLabel: viewSettingsLabelProp, columnsGroupLabel: columnsGroupLabelProp, ...rest }) {
341
+ // crisp/#471: same resolution order as TableToolbar's own — an explicit
342
+ // prop wins, then the provider's translation of the built-in English
343
+ // default, then that default itself with no provider in the tree.
344
+ const crispMessages = (0, core_1.useCrispMessages)();
345
+ const selectAllLabel = (0, core_1.useCrispMessage)(selectAllLabelProp, "Select all rows");
346
+ const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search…");
347
+ const searchLabel = (0, core_1.useCrispMessage)(searchLabelProp, "Search table");
348
+ // Also `React.ReactNode`-typed, same reasoning as `empty` below.
349
+ const addRowLabel = addRowLabelProp ?? crispMessages["New record"] ?? "New record";
350
+ // `empty` is typed `React.ReactNode`, not `string` — a consumer can pass a
351
+ // whole custom empty-state element, not just replacement text, so it
352
+ // can't go through `useCrispMessage` (string-only). A ReactNode override
353
+ // passes straight through unchanged (it's already whatever the consumer
354
+ // wants shown); only the built-in string default translates.
355
+ const empty = emptyProp ?? crispMessages["No results."] ?? "No results.";
356
+ const reorderRowLabel = (0, core_1.useCrispMessage)(reorderRowLabelProp, "Reorder row");
357
+ const sortAscendingLabel = (0, core_1.useCrispMessage)(sortAscendingLabelProp, "Sort ascending");
358
+ const sortDescendingLabel = (0, core_1.useCrispMessage)(sortDescendingLabelProp, "Sort descending");
359
+ const moveColumnLeftLabel = (0, core_1.useCrispMessage)(moveColumnLeftLabelProp, "Move left");
360
+ const moveColumnRightLabel = (0, core_1.useCrispMessage)(moveColumnRightLabelProp, "Move right");
361
+ const editColumnLabel = (0, core_1.useCrispMessage)(editColumnLabelProp, "Edit column label");
362
+ const hideColumnLabel = (0, core_1.useCrispMessage)(hideColumnLabelProp, "Hide from view");
363
+ const renameColumnLabel = (0, core_1.useCrispMessage)(renameColumnLabelProp, "Rename column");
364
+ const viewSettingsLabel = (0, core_1.useCrispMessage)(viewSettingsLabelProp, "View settings");
365
+ const columnsGroupLabel = (0, core_1.useCrispMessage)(columnsGroupLabelProp, "Columns");
366
+ // countLabel/selectionAnnouncement stay formatter props, same escape hatch
367
+ // as TableToolbar's sortTriggerLabel — word order and pluralisation rules
368
+ // differ too much across languages for a template. Their BUILT-IN default
369
+ // (when the consumer supplies neither) still translates its own static
370
+ // fragments through the provider, same as TableToolbar's "Sorted by".
371
+ const resolvedCountLabel = countLabel ??
372
+ ((shown, total) => shown === total
373
+ ? `${total} ${total === 1
374
+ ? (crispMessages.row ?? "row")
375
+ : (crispMessages.rows ?? "rows")}`
376
+ : `${shown} ${crispMessages.of ?? "of"} ${total}`);
377
+ const resolvedSelectionAnnouncement = selectionAnnouncement ??
378
+ ((count) => count === 0
379
+ ? (crispMessages["Selection cleared"] ?? "Selection cleared")
380
+ : `${count} ${crispMessages.selected ?? "selected"}`);
336
381
  // Two different defaults on purpose. FORMATTING falls back to "en-US", which
337
382
  // is what the built-in date/number/currency renderers have always produced,
338
383
  // so no existing output moves. SORTING falls back to `undefined`, which is
@@ -426,7 +471,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
426
471
  const [selectionText, setSelectionText] = React.useState("");
427
472
  if (prevSelectedCount.current !== selected.size) {
428
473
  prevSelectedCount.current = selected.size;
429
- setSelectionText(selectionAnnouncement(selected.size));
474
+ setSelectionText(resolvedSelectionAnnouncement(selected.size));
430
475
  }
431
476
  const [editing, setEditing] = React.useState(null);
432
477
  // Set only while an async `onEdit` is in flight (#385) -- a sync `onEdit`
@@ -772,7 +817,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
772
817
  return dir === "asc" ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUp, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
773
818
  return ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
774
819
  }
775
- return ((0, jsx_runtime_1.jsxs)("div", { ref: gridRef, className: (0, datatable_recipe_1.datatableRecipe)({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-datatable-toolbar", children: [columnMenu && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-viewsettings", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75 }), "View settings"] }), children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-group-label", children: "Columns" }), allColumns.map((c) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { onSelect: () => c.hidden ? showColumn(c.key) : hideColumn(c.key), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 14, strokeWidth: 2, style: { opacity: c.hidden ? 0 : 1 } }), typeof c.header === "string" ? c.header : c.key] }, c.key)))] })), filterable && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchLabel })] })), showCount && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-count", children: countLabel(rows.length, data.length) })), toolbar] })), (0, jsx_runtime_1.jsxs)(table_1.Table, { interactive: interactive, surface: surface, style: { minWidth }, scrollProps: {
820
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: gridRef, className: (0, datatable_recipe_1.datatableRecipe)({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-datatable-toolbar", children: [columnMenu && ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-datatable-viewsettings", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75 }), viewSettingsLabel] }), children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-group-label", children: columnsGroupLabel }), allColumns.map((c) => ((0, jsx_runtime_1.jsxs)(menu_1.MenuItem, { onSelect: () => c.hidden ? showColumn(c.key) : hideColumn(c.key), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { size: 14, strokeWidth: 2, style: { opacity: c.hidden ? 0 : 1 } }), typeof c.header === "string" ? c.header : c.key] }, c.key)))] })), filterable && ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchLabel })] })), showCount && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-count", children: resolvedCountLabel(rows.length, data.length) })), toolbar] })), (0, jsx_runtime_1.jsxs)(table_1.Table, { interactive: interactive, surface: surface, style: { minWidth }, scrollProps: {
776
821
  ref: scrollRef,
777
822
  onScroll: (e) => {
778
823
  const el = e.currentTarget;
@@ -802,7 +847,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
802
847
  const inner = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-datatable-th-inner", children: [Icon ? (0, jsx_runtime_1.jsx)(Icon, { size: 16, strokeWidth: 1.75 }) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-th-label", children: col.header }), col.sortable ? (0, jsx_runtime_1.jsx)(ChevronFor, { colKey: col.key }) : null] }));
803
848
  return ((0, jsx_runtime_1.jsxs)(table_1.TableHead, { align: alignFor(col), "aria-sort": ariaSort, ...dataStickyCol(i), children: [renaming === col.key ? ((0, jsx_runtime_1.jsx)("input", {
804
849
  // biome-ignore lint/a11y/noAutofocus: the rename item opens this field
805
- autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": `Rename column`, onBlur: (e) => {
850
+ autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": renameColumnLabel, onBlur: (e) => {
806
851
  renameColumn(col.key, e.currentTarget.value);
807
852
  setRenaming(null);
808
853
  }, onKeyDown: (e) => {
@@ -814,7 +859,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
814
859
  setRenaming(null);
815
860
  } })) : columnMenu ? ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "start", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
816
861
  ? col.header
817
- : col.key} column options`, children: inner }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: "Sort ascending" }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: "Sort descending" }), col.managed === false ? null : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, -1), children: "Move left" }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, 1), children: "Move right" }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setRenaming(col.key), children: "Edit column label" }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => hideColumn(col.key), children: "Hide from view" })] }))] })) : col.sortable ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.resizable !== false && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
862
+ : col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.resizable !== false && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
818
863
  })] }) }), (0, jsx_runtime_1.jsx)(table_1.TableBody, { children: error ? ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { colSpan: totalCols, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-empty", children: error }) }) })) : rows.length === 0 ? ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsx)(table_1.TableCell, { colSpan: totalCols, children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-datatable-empty", children: empty }) }) })) : (groupedRows.map(({ group, rows: groupRows }) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [group !== null && ((0, jsx_runtime_1.jsx)(table_1.TableRow, { children: (0, jsx_runtime_1.jsxs)(table_1.TableCell, { colSpan: totalCols, className: "crisp-datatable-group", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-group-label", children: group }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-datatable-group-count", children: groupRows.length })] }) })), groupRows.map((row) => ((0, jsx_runtime_1.jsxs)(table_1.TableRow, { "data-selected": selected.has(row.id) ? "" : undefined, "data-drop-target": showGrip &&
819
864
  dragRowId &&
820
865
  dragRowId !== row.id &&
@@ -912,7 +957,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
912
957
  ? () => openEditing(row.id, col.key)
913
958
  : undefined, children: [col.render
914
959
  ? col.render(value, row)
915
- : 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));
916
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] }) }) }))] }))] })] }));
917
962
  }
918
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}); }
@@ -156,7 +156,33 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
156
156
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
157
157
  * Created by), which one shared list can't reproduce.
158
158
  */
159
- function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel = "Sort", reorderSortLabel = "Reorder sort", noSortsLabel = "No sorts applied to this view.", addSortLabel = "Add sort", learnSortingLabel = "Learn about sorting", selectConditionLabel = "Select condition", sortFieldLabel = "Sort field", sortDirectionLabel = "Sort direction", removeSortLabel = "Remove sort", filterFieldLabel = "Filter by field", filterFieldPlaceholder = "Filter by…", filterValuePlaceholder = "Value…", ascendingLabel = "Ascending", descendingLabel = "Descending", sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
159
+ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, ...rest }) {
160
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
161
+ // CrispMessagesProvider's translation of the English default below, or
162
+ // that default itself with no provider — unchanged from before this
163
+ // existed. One resolved constant per prop, same name every existing usage
164
+ // below already reads, so nothing past this block changes.
165
+ const sortLabel = (0, core_1.useCrispMessage)(sortLabelProp, "Sort");
166
+ const filterLabel = (0, core_1.useCrispMessage)(filterLabelProp, "Filter");
167
+ const reorderSortLabel = (0, core_1.useCrispMessage)(reorderSortLabelProp, "Reorder sort");
168
+ const noSortsLabel = (0, core_1.useCrispMessage)(noSortsLabelProp, "No sorts applied to this view.");
169
+ const addSortLabel = (0, core_1.useCrispMessage)(addSortLabelProp, "Add sort");
170
+ const learnSortingLabel = (0, core_1.useCrispMessage)(learnSortingLabelProp, "Learn about sorting");
171
+ const selectConditionLabel = (0, core_1.useCrispMessage)(selectConditionLabelProp, "Select condition");
172
+ const sortFieldLabel = (0, core_1.useCrispMessage)(sortFieldLabelProp, "Sort field");
173
+ const sortDirectionLabel = (0, core_1.useCrispMessage)(sortDirectionLabelProp, "Sort direction");
174
+ const removeSortLabel = (0, core_1.useCrispMessage)(removeSortLabelProp, "Remove sort");
175
+ const filterFieldLabel = (0, core_1.useCrispMessage)(filterFieldLabelProp, "Filter by field");
176
+ const filterFieldPlaceholder = (0, core_1.useCrispMessage)(filterFieldPlaceholderProp, "Filter by…");
177
+ const filterValuePlaceholder = (0, core_1.useCrispMessage)(filterValuePlaceholderProp, "Value…");
178
+ const ascendingLabel = (0, core_1.useCrispMessage)(ascendingLabelProp, "Ascending");
179
+ const descendingLabel = (0, core_1.useCrispMessage)(descendingLabelProp, "Descending");
180
+ // OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
181
+ // their own — a whole lookup table, not one string — so they translate
182
+ // straight from the provider's dictionary at the two spots they render
183
+ // (#628, #673 below), same fallback order as every resolved constant
184
+ // above.
185
+ const crispMessages = (0, core_1.useCrispMessages)();
160
186
  // Each side falls back to the shared `columns` independently — a caller
161
187
  // that only overrides one (Filter's own Record/Created by, say) still
162
188
  // gets `columns` on the other, unchanged from before either prop existed.
@@ -245,12 +271,18 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
245
271
  };
246
272
  const activeFilters = filters.length;
247
273
  // The default phrase only works in English word order, so a caller can
248
- // replace the whole thing rather than fill a template.
274
+ // replace the whole thing rather than fill a template — crisp/#471's
275
+ // `sortTriggerLabel` formatter is exactly that escape hatch. Short of a
276
+ // full formatter, the "Sorted by" fragment itself still translates (the
277
+ // sort trigger reading "Sort" in Mongolian and "Sorted by X" in English is
278
+ // worse than an imperfect word order); `crispMessages` rather than a
279
+ // second `useCrispMessage` call, since this only fires on the branch that
280
+ // needs it.
249
281
  const sortedLabel = sortTriggerLabel
250
282
  ? sortTriggerLabel(sorts, sortColumns)
251
283
  : sorts.length
252
- ? `Sorted by ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
253
- : "Sort";
284
+ ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
285
+ : sortLabel;
254
286
  return ((0, jsx_runtime_1.jsx)("div", { className: (0, tabletoolbar_recipe_1.tabletoolbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && ((0, jsx_runtime_1.jsxs)("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
255
287
  if (dragSort !== null)
256
288
  e.preventDefault();
@@ -276,8 +308,8 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
276
308
  ], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "filter" ? null : "filter"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
277
309
  ? filterTriggerLabel(activeFilters)
278
310
  : activeFilters
279
- ? `Filter · ${activeFilters}`
280
- : "Filter"] }), openMenu === "filter" && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
311
+ ? `${filterLabel} · ${activeFilters}`
312
+ : filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
281
313
  // biome-ignore lint/a11y/noAutofocus: focus the picker on open
282
314
  autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
283
315
  .filter((col) => col.label
@@ -288,7 +320,8 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
288
320
  const open = openMenu === `fcond:${cond.id}`;
289
321
  const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
290
322
  (!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
291
- return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2 }) })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} condition`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " condition"] }), opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
323
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fchip", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: crispMessages[OP_LABELS[cond.op]] ??
324
+ OP_LABELS[cond.op] }), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "aria-label": `${crispMessages.Remove ?? "Remove"} ${col?.label} ${crispMessages.filter ?? "filter"}`, onClick: () => removeFilter(cond.id), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2 }) })] }), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " ", crispMessages.condition ?? "condition"] }), opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
292
325
  // biome-ignore lint/a11y/noAutofocus: focus the value field
293
326
  autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })), cond.op === "isanyof" && col?.values && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
294
327
  // biome-ignore lint/a11y/noAutofocus: focus the search field
@@ -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());