@crisp-ui-kit/crisp 0.52.5 → 0.53.0

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 (54) hide show
  1. package/dist/bulkbar.css +2 -2
  2. package/dist/bulkbar.layered.css +2 -2
  3. package/dist/button.css +2 -2
  4. package/dist/button.layered.css +2 -2
  5. package/dist/cjs/components/bulkbar/bulkbar.js +1 -1
  6. package/dist/cjs/components/bulkbar/bulkbar.recipe.js +2 -2
  7. package/dist/cjs/components/button/button.recipe.js +10 -2
  8. package/dist/cjs/components/datatable/datatable.js +45 -7
  9. package/dist/cjs/components/multiselect/multiselect.js +1 -1
  10. package/dist/cjs/components/multiselect/multiselect.recipe.js +1 -1
  11. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +5 -4
  12. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +41 -8
  13. package/dist/cjs/components/tooltip/tooltip.recipe.js +13 -4
  14. package/dist/cjs/core/i18n.js +75 -0
  15. package/dist/cjs/core/index.js +5 -1
  16. package/dist/cjs/product/entitycard/entitycard.recipe.js +20 -4
  17. package/dist/cjs/product/listpage/listpage.js +1 -1
  18. package/dist/cjs/product/listpage/listpage.recipe.js +5 -7
  19. package/dist/entitycard.css +4 -4
  20. package/dist/entitycard.layered.css +4 -4
  21. package/dist/esm/components/bulkbar/bulkbar.js +1 -1
  22. package/dist/esm/components/bulkbar/bulkbar.recipe.js +2 -2
  23. package/dist/esm/components/button/button.recipe.js +10 -2
  24. package/dist/esm/components/datatable/datatable.js +46 -8
  25. package/dist/esm/components/multiselect/multiselect.js +1 -1
  26. package/dist/esm/components/multiselect/multiselect.recipe.js +1 -1
  27. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +5 -4
  28. package/dist/esm/components/tabletoolbar/tabletoolbar.js +42 -9
  29. package/dist/esm/components/tooltip/tooltip.recipe.js +13 -4
  30. package/dist/esm/core/i18n.js +37 -0
  31. package/dist/esm/core/index.js +1 -0
  32. package/dist/esm/product/entitycard/entitycard.recipe.js +20 -4
  33. package/dist/esm/product/listpage/listpage.js +2 -2
  34. package/dist/esm/product/listpage/listpage.recipe.js +5 -7
  35. package/dist/listpage.css +5 -5
  36. package/dist/listpage.layered.css +5 -5
  37. package/dist/multiselect.css +1 -1
  38. package/dist/multiselect.layered.css +1 -1
  39. package/dist/recordstable.css +5 -4
  40. package/dist/recordstable.layered.css +5 -4
  41. package/dist/styles.css +25 -23
  42. package/dist/styles.layered.css +25 -23
  43. package/dist/tabletoolbar.css +5 -4
  44. package/dist/tabletoolbar.layered.css +5 -4
  45. package/dist/tooltip.css +1 -1
  46. package/dist/tooltip.layered.css +1 -1
  47. package/dist/types/components/bulkbar/bulkbar.d.ts +3 -3
  48. package/dist/types/components/button/button.recipe.d.ts +2 -2
  49. package/dist/types/components/datatable/datatable.d.ts +21 -1
  50. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +5 -1
  51. package/dist/types/components/tooltip/tooltip.recipe.d.ts +2 -2
  52. package/dist/types/core/i18n.d.ts +58 -0
  53. package/dist/types/core/index.d.ts +1 -0
  54. package/package.json +1 -1
package/dist/bulkbar.css CHANGED
@@ -2,13 +2,13 @@
2
2
  position: absolute; left: 0; right: 0; bottom: var(--crisp-space-4); display: flex; justify-content: center; pointer-events: none; z-index: 10;
3
3
  & .crisp-bulkbar-bar { pointer-events: auto; display: flex; align-items: center; gap: var(--crisp-space-0-5); height: 48px; padding: 0 var(--crisp-space-1) 0 var(--crisp-space-1-5); border-radius: 14px; background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); color: var(--crisp-fg-primary); transition: opacity .16s ease, transform .16s ease; }
4
4
  & .crisp-bulkbar-bar { @starting-style { opacity: 0; transform: translateY(10px); } }
5
- & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
5
+ & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-lg); background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
6
6
  & .crisp-bulkbar-count:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); color: var(--crisp-fg-primary); }
7
7
  & .crisp-bulkbar-count svg { flex-shrink: 0; }
8
8
  /* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
9
9
  -- read, never drawn. Same technique as Slider's thumblabel. */
10
10
  & .crisp-bulkbar-clearlabel { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
11
- & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
11
+ & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-lg); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
12
12
  & .crisp-bulkbar-btn:hover { background: var(--crisp-bg-raised); }
13
13
  & .crisp-bulkbar-btn--danger { color: var(--crisp-bg-danger); }
14
14
  & .crisp-bulkbar-btn--danger:hover { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
@@ -3,13 +3,13 @@
3
3
  position: absolute; left: 0; right: 0; bottom: var(--crisp-space-4); display: flex; justify-content: center; pointer-events: none; z-index: 10;
4
4
  & .crisp-bulkbar-bar { pointer-events: auto; display: flex; align-items: center; gap: var(--crisp-space-0-5); height: 48px; padding: 0 var(--crisp-space-1) 0 var(--crisp-space-1-5); border-radius: 14px; background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); color: var(--crisp-fg-primary); transition: opacity .16s ease, transform .16s ease; }
5
5
  & .crisp-bulkbar-bar { @starting-style { opacity: 0; transform: translateY(10px); } }
6
- & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
6
+ & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-lg); background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
7
7
  & .crisp-bulkbar-count:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 8%, transparent); color: var(--crisp-fg-primary); }
8
8
  & .crisp-bulkbar-count svg { flex-shrink: 0; }
9
9
  /* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
10
10
  -- read, never drawn. Same technique as Slider's thumblabel. */
11
11
  & .crisp-bulkbar-clearlabel { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
12
- & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
12
+ & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-lg); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
13
13
  & .crisp-bulkbar-btn:hover { background: var(--crisp-bg-raised); }
14
14
  & .crisp-bulkbar-btn--danger { color: var(--crisp-bg-danger); }
15
15
  & .crisp-bulkbar-btn--danger:hover { background: color-mix(in srgb, var(--crisp-bg-danger) 10%, transparent); }
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
  }
@@ -44,7 +44,7 @@ const bulkbar_recipe_1 = require("./bulkbar.recipe");
44
44
  * positioned ancestor when a selection is active; render it conditionally
45
45
  * (e.g. `count > 0 && <BulkBar …/>`). Measured from a records table.
46
46
  */
47
- function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
47
+ function BulkBar({ count, onClear, countLabel = "Selected", countText = (count, label) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
48
48
  return ((0, jsx_runtime_1.jsx)("div", { className: (0, bulkbar_recipe_1.bulkbarRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-bulkbar-bar", children: [count != null && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), (0, jsx_runtime_1.jsx)("span", { className: "crisp-bulkbar-clearlabel", children: clearLabel }), (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
49
49
  }
50
50
  BulkBar.displayName = "BulkBar";
@@ -15,7 +15,7 @@ const config = (0, core_1.defineRecipe)({
15
15
  base: `position: absolute; left: 0; right: 0; bottom: ${tokens_1.contract.space["4"]}; display: flex; justify-content: center; pointer-events: none; z-index: 10;
16
16
  & .crisp-bulkbar-bar { pointer-events: auto; display: flex; align-items: center; gap: ${tokens_1.contract.space["0.5"]}; height: 48px; padding: 0 ${tokens_1.contract.space["1"]} 0 ${tokens_1.contract.space["1.5"]}; border-radius: 14px; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.floating}; color: ${tokens_1.contract.fgPrimary}; transition: opacity .16s ease, transform .16s ease; }
17
17
  & .crisp-bulkbar-bar { @starting-style { opacity: 0; transform: translateY(10px); } }
18
- & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.menuRow}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
18
+ & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.menuRow}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.lg}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 2%, transparent); font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
19
19
  & .crisp-bulkbar-count:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); color: ${tokens_1.contract.fgPrimary}; }
20
20
  & .crisp-bulkbar-count svg { flex-shrink: 0; }
21
21
  /* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
@@ -24,7 +24,7 @@ const config = (0, core_1.defineRecipe)({
24
24
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
25
25
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
26
26
  }
27
- & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.menuRow}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
27
+ & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; height: ${tokens_1.contract.size.menuRow}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.lg}; background: ${tokens_1.contract.bgSurface}; box-shadow: ${tokens_1.elevation.field}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
28
28
  & .crisp-bulkbar-btn:hover { background: ${tokens_1.contract.bgRaised}; }
29
29
  & .crisp-bulkbar-btn--danger { color: ${tokens_1.contract.bgDanger}; }
30
30
  & .crisp-bulkbar-btn--danger:hover { background: color-mix(in srgb, ${tokens_1.contract.bgDanger} 10%, transparent); }
@@ -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)" } },
@@ -330,9 +330,47 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
330
330
  onCommit(v);
331
331
  } }) }));
332
332
  }
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 }) {
333
+ 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 }) {
334
+ // crisp/#471: same resolution order as TableToolbar's own — an explicit
335
+ // prop wins, then the provider's translation of the built-in English
336
+ // default, then that default itself with no provider in the tree.
337
+ const crispMessages = (0, core_1.useCrispMessages)();
338
+ const selectAllLabel = (0, core_1.useCrispMessage)(selectAllLabelProp, "Select all rows");
339
+ const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search…");
340
+ const searchLabel = (0, core_1.useCrispMessage)(searchLabelProp, "Search table");
341
+ // Also `React.ReactNode`-typed, same reasoning as `empty` below.
342
+ const addRowLabel = addRowLabelProp ?? crispMessages["New record"] ?? "New record";
343
+ // `empty` is typed `React.ReactNode`, not `string` — a consumer can pass a
344
+ // whole custom empty-state element, not just replacement text, so it
345
+ // can't go through `useCrispMessage` (string-only). A ReactNode override
346
+ // passes straight through unchanged (it's already whatever the consumer
347
+ // wants shown); only the built-in string default translates.
348
+ const empty = emptyProp ?? crispMessages["No results."] ?? "No results.";
349
+ const reorderRowLabel = (0, core_1.useCrispMessage)(reorderRowLabelProp, "Reorder row");
350
+ const sortAscendingLabel = (0, core_1.useCrispMessage)(sortAscendingLabelProp, "Sort ascending");
351
+ const sortDescendingLabel = (0, core_1.useCrispMessage)(sortDescendingLabelProp, "Sort descending");
352
+ const moveColumnLeftLabel = (0, core_1.useCrispMessage)(moveColumnLeftLabelProp, "Move left");
353
+ const moveColumnRightLabel = (0, core_1.useCrispMessage)(moveColumnRightLabelProp, "Move right");
354
+ const editColumnLabel = (0, core_1.useCrispMessage)(editColumnLabelProp, "Edit column label");
355
+ const hideColumnLabel = (0, core_1.useCrispMessage)(hideColumnLabelProp, "Hide from view");
356
+ const renameColumnLabel = (0, core_1.useCrispMessage)(renameColumnLabelProp, "Rename column");
357
+ const viewSettingsLabel = (0, core_1.useCrispMessage)(viewSettingsLabelProp, "View settings");
358
+ const columnsGroupLabel = (0, core_1.useCrispMessage)(columnsGroupLabelProp, "Columns");
359
+ // countLabel/selectionAnnouncement stay formatter props, same escape hatch
360
+ // as TableToolbar's sortTriggerLabel — word order and pluralisation rules
361
+ // differ too much across languages for a template. Their BUILT-IN default
362
+ // (when the consumer supplies neither) still translates its own static
363
+ // fragments through the provider, same as TableToolbar's "Sorted by".
364
+ const resolvedCountLabel = countLabel ??
365
+ ((shown, total) => shown === total
366
+ ? `${total} ${total === 1
367
+ ? (crispMessages.row ?? "row")
368
+ : (crispMessages.rows ?? "rows")}`
369
+ : `${shown} ${crispMessages.of ?? "of"} ${total}`);
370
+ const resolvedSelectionAnnouncement = selectionAnnouncement ??
371
+ ((count) => count === 0
372
+ ? (crispMessages["Selection cleared"] ?? "Selection cleared")
373
+ : `${count} ${crispMessages.selected ?? "selected"}`);
336
374
  // Two different defaults on purpose. FORMATTING falls back to "en-US", which
337
375
  // is what the built-in date/number/currency renderers have always produced,
338
376
  // so no existing output moves. SORTING falls back to `undefined`, which is
@@ -426,7 +464,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
426
464
  const [selectionText, setSelectionText] = React.useState("");
427
465
  if (prevSelectedCount.current !== selected.size) {
428
466
  prevSelectedCount.current = selected.size;
429
- setSelectionText(selectionAnnouncement(selected.size));
467
+ setSelectionText(resolvedSelectionAnnouncement(selected.size));
430
468
  }
431
469
  const [editing, setEditing] = React.useState(null);
432
470
  // Set only while an async `onEdit` is in flight (#385) -- a sync `onEdit`
@@ -772,7 +810,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
772
810
  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
811
  return ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronsUpDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
774
812
  }
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: {
813
+ 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
814
  ref: scrollRef,
777
815
  onScroll: (e) => {
778
816
  const el = e.currentTarget;
@@ -802,7 +840,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
802
840
  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
841
  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
842
  // 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) => {
843
+ autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": renameColumnLabel, onBlur: (e) => {
806
844
  renameColumn(col.key, e.currentTarget.value);
807
845
  setRenaming(null);
808
846
  }, onKeyDown: (e) => {
@@ -814,7 +852,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
814
852
  setRenaming(null);
815
853
  } })) : 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
854
  ? 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));
855
+ : 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
856
  })] }) }), (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
857
  dragRowId &&
820
858
  dragRowId !== row.id &&
@@ -202,7 +202,7 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
202
202
  e.preventDefault();
203
203
  openMenu();
204
204
  }
205
- }, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-multiselect-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
205
+ }, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => (0, jsx_runtime_1.jsx)(Chip, { opt: o }, o.value))) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && ((0, jsx_runtime_1.jsx)(core_1.FloatingPortal, { sourceRef: rootRef, children: (0, jsx_runtime_1.jsxs)("div", { ref: listboxRef, id: listboxId, className: "crisp-multiselect-listbox", role: "listbox", "aria-multiselectable": "true", "aria-label": ariaLabel, "data-up": up ? "" : undefined, style: pos, children: [searchable && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-multiselect-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 16, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
206
206
  setQuery(e.target.value);
207
207
  setHighlight(0);
208
208
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
@@ -65,7 +65,7 @@ const config = (0, core_1.defineRecipe)({
65
65
  }
66
66
  .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
67
67
  .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
68
- .crisp-multiselect-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["1"]}) calc(-1 * ${tokens_1.contract.space["1"]}) ${tokens_1.contract.space["1"]}; padding: 0 ${tokens_1.contract.space["2.5"]}; border-bottom: 1px solid ${tokens_1.contract.borderSubtle}; color: ${tokens_1.contract.fgMuted}; }
68
+ .crisp-multiselect-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${tokens_1.contract.space["1"]}) calc(-1 * ${tokens_1.contract.space["1"]}) ${tokens_1.contract.space["1"]}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["3"]}; color: ${tokens_1.contract.fgMuted}; }
69
69
  .crisp-multiselect-search svg { flex-shrink: 0; }
70
70
  .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
71
71
  .crisp-multiselect-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
@@ -17,7 +17,7 @@ const tokens_1 = require("../../tokens");
17
17
  // block, and those depend on following `.menu` in source order. Keeping the
18
18
  // split preserves that cascade exactly, which is what lets the generated CSS
19
19
  // stay byte-for-byte identical to what shipped before the extraction.
20
- const toolbarChromeHead = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; padding: 0 ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; }
20
+ const toolbarChromeHead = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["2"]}; }
21
21
  & .${namespace_1.CLASS_PREFIX}${p}-toolbar[data-inline] { padding-bottom: 0; }
22
22
  & .${namespace_1.CLASS_PREFIX}${p}-search { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: ${tokens_1.contract.size.row}; padding: 0 ${tokens_1.contract.space["2.5"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; color: ${tokens_1.contract.fgMuted}; }
23
23
  & .${namespace_1.CLASS_PREFIX}${p}-search svg { flex-shrink: 0; }
@@ -30,11 +30,12 @@ const toolbarChromeHead = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-toolbar
30
30
  & .${namespace_1.CLASS_PREFIX}${p}-tool--ghost { box-shadow: none; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 3%, transparent); color: ${tokens_1.contract.fgMuted}; }
31
31
  & .${namespace_1.CLASS_PREFIX}${p}-tool--ghost:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); }
32
32
  & .${namespace_1.CLASS_PREFIX}${p}-toolgroup { position: relative; display: inline-flex; }
33
- & .${namespace_1.CLASS_PREFIX}${p}-menu { position: absolute; top: calc(100% + ${tokens_1.contract.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.lg}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
33
+ & .${namespace_1.CLASS_PREFIX}${p}-divider { flex-shrink: 0; width: 1px; height: 20px; background: ${tokens_1.contract.borderSubtle}; }
34
+ & .${namespace_1.CLASS_PREFIX}${p}-menu { position: absolute; top: calc(100% + ${tokens_1.contract.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.xl}; background: ${tokens_1.contract.bgPopover}; box-shadow: ${tokens_1.elevation.floating}; display: flex; flex-direction: column; gap: ${tokens_1.contract.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
34
35
  & .${namespace_1.CLASS_PREFIX}${p}-menu { @starting-style { opacity: 0; transform: scale(0.96); } }`;
35
36
  exports.toolbarChromeHead = toolbarChromeHead;
36
37
  const toolbarChromeBody = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-menu-head { padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
37
- & .${namespace_1.CLASS_PREFIX}${p}-sortpop { min-width: 460px; overflow: visible; padding: ${tokens_1.contract.space["1.5"]}; gap: ${tokens_1.contract.space["1"]}; }
38
+ & .${namespace_1.CLASS_PREFIX}${p}-sortpop { min-width: 480px; overflow: visible; padding: ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["1.5"]}; gap: ${tokens_1.contract.space["2"]}; }
38
39
  & .${namespace_1.CLASS_PREFIX}${p}-sort-empty { padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["2"]}; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
39
40
  & .${namespace_1.CLASS_PREFIX}${p}-sort-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; }
40
41
  & .${namespace_1.CLASS_PREFIX}${p}-sort-grip { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 28px; flex-shrink: 0; margin-right: calc(-1 * ${tokens_1.contract.space["1"]}); padding: 0; border: none; background: none; color: ${tokens_1.contract.fgMuted}; cursor: grab; }
@@ -55,7 +56,7 @@ const toolbarChromeBody = (p) => ` & .${namespace_1.CLASS_PREFIX}${p}-menu-he
55
56
  & .${namespace_1.CLASS_PREFIX}${p}-sort-learn { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; font-size: ${tokens_1.contract.text.lg}; color: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent); text-decoration: none; }
56
57
  & .${namespace_1.CLASS_PREFIX}${p}-sort-learn svg { flex-shrink: 0; }
57
58
  & .${namespace_1.CLASS_PREFIX}${p}-sort-learn:hover { color: ${tokens_1.contract.fgPrimary}; }
58
- & .${namespace_1.CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; width: 100%; height: ${tokens_1.contract.size.row}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: none; border-radius: ${tokens_1.contract.radius.md}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; text-align: left; cursor: pointer; }
59
+ & .${namespace_1.CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; width: 100%; height: ${tokens_1.contract.size.menuRow}; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: none; border-radius: ${tokens_1.contract.radius.md}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; text-align: left; cursor: pointer; }
59
60
  & .${namespace_1.CLASS_PREFIX}${p}-menu-item:hover { background: ${tokens_1.contract.bgRaised}; }
60
61
  & .${namespace_1.CLASS_PREFIX}${p}-menu-item:disabled { opacity: 0.4; cursor: default; }
61
62
  & .${namespace_1.CLASS_PREFIX}${p}-menu-item:disabled:hover { background: none; }
@@ -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();
@@ -273,11 +305,11 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
273
305
  label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
274
306
  keywords: descendingLabel,
275
307
  },
276
- ], "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.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
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
@@ -5,6 +5,15 @@ const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
6
  // The floating bubble for a Tooltip — a dark hint chip. Applied to the
7
7
  // portaled bubble itself (not a wrapper), so its styles survive the portal.
8
+ //
9
+ // Re-measured live against Attio's own tooltip (crisp#465): background,
10
+ // text colour, vertical padding and radius were all off — #20242c/#fff/4px
11
+ // vertical/6px radius were an earlier, uncorroborated guess (not the same
12
+ // surface Lesson 24 in AUTHORING.md describes for the RichText toolbar,
13
+ // despite the similar wording). The real values are gray900's light hex
14
+ // (#101112, the same "dark pill" ink NavItem's own text uses) and
15
+ // line100's light hex (#eeeff1) — both theme-constant, same as before,
16
+ // just the wrong pair.
8
17
  const spec = (0, core_1.defineSpec)({
9
18
  name: "tooltip",
10
19
  base: {
@@ -13,10 +22,10 @@ const spec = (0, core_1.defineSpec)({
13
22
  boxSizing: "border-box",
14
23
  pointerEvents: "none",
15
24
  maxWidth: "260px",
16
- padding: `${tokens_1.contract.space["1"]} ${tokens_1.contract.space["2"]}`,
17
- borderRadius: tokens_1.contract.radius.sm,
18
- background: "#20242c",
19
- color: "#fff",
25
+ padding: `${tokens_1.contract.space["1.5"]} ${tokens_1.contract.space["2"]}`,
26
+ borderRadius: tokens_1.contract.radius.md,
27
+ background: "#101112",
28
+ color: "#eeeff1",
20
29
  fontFamily: tokens_1.contract.font.sans,
21
30
  fontSize: tokens_1.contract.text.sm,
22
31
  fontWeight: tokens_1.contract.weight.medium,
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CrispMessagesProvider = CrispMessagesProvider;
37
+ exports.useCrispMessage = useCrispMessage;
38
+ exports.useCrispMessages = useCrispMessages;
39
+ const jsx_runtime_1 = require("react/jsx-runtime");
40
+ const React = __importStar(require("react"));
41
+ const CrispMessagesContext = React.createContext({});
42
+ function CrispMessagesProvider({ messages, children, }) {
43
+ return ((0, jsx_runtime_1.jsx)(CrispMessagesContext.Provider, { value: messages, children: children }));
44
+ }
45
+ /**
46
+ * Resolves one translatable string for a component: `explicit` is whatever
47
+ * the caller passed for that prop (`undefined` when they didn't), and
48
+ * `englishDefault` is the literal that shipped as that prop's default value
49
+ * before crisp/#471 — still what renders with no provider in the tree.
50
+ *
51
+ * A plain function, not a second context read inside a loop or a render
52
+ * helper — components call this once per string, the same shape
53
+ * `useLabelAssociation`/`useFormReset` and this codebase's other small
54
+ * `core/` hooks already take.
55
+ */
56
+ function useCrispMessage(explicit, englishDefault) {
57
+ const messages = React.useContext(CrispMessagesContext);
58
+ if (explicit !== undefined)
59
+ return explicit;
60
+ return messages[englishDefault] ?? englishDefault;
61
+ }
62
+ /**
63
+ * The same resolution as `useCrispMessage`, for a component that builds its
64
+ * own default text internally (TableToolbar's "Sorted by {field}", a
65
+ * DatePicker's month/weekday names) rather than taking one string prop per
66
+ * piece — every literal fragment of that built-in text still needs its own
67
+ * translation lookup, and a component with many such fragments (twelve
68
+ * month names, seven weekday names) would otherwise call `useCrispMessage`
69
+ * in a loop, which breaks the rules of hooks. `useCrispMessages` returns the
70
+ * raw dictionary once; call sites do their own `dict[english] ?? english`
71
+ * per fragment, same fallback order, no hook-in-a-loop.
72
+ */
73
+ function useCrispMessages() {
74
+ return React.useContext(CrispMessagesContext);
75
+ }
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.usePrefersReducedMotion = exports.useMentionTrigger = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.matchMentionTrigger = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = exports.cellDatePrecision = exports.caretRectInField = void 0;
17
+ exports.usePrefersReducedMotion = exports.useMentionTrigger = exports.useLabelAssociation = exports.useFormReset = exports.useFocusAfterRemoval = exports.specToRecipe = exports.defineSpec = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.matchMentionTrigger = exports.useCrispMessages = exports.useCrispMessage = exports.CrispMessagesProvider = exports.formValueStyle = exports.registerFloatingLayer = exports.isInsideLaterFloatingLayer = exports.hasOpenFloatingLayer = exports.FloatingPortal = exports.isRtlElement = exports.withDescribedBy = exports.defineRecipe = exports.cx = exports.parseCellDate = exports.formatCellDate = exports.cellDatePrecision = exports.caretRectInField = void 0;
18
18
  var caret_rect_1 = require("./caret-rect");
19
19
  Object.defineProperty(exports, "caretRectInField", { enumerable: true, get: function () { return caret_rect_1.caretRectInField; } });
20
20
  var cell_date_1 = require("./cell-date");
@@ -37,6 +37,10 @@ Object.defineProperty(exports, "isInsideLaterFloatingLayer", { enumerable: true,
37
37
  Object.defineProperty(exports, "registerFloatingLayer", { enumerable: true, get: function () { return floating_stack_1.registerFloatingLayer; } });
38
38
  var form_value_1 = require("./form-value");
39
39
  Object.defineProperty(exports, "formValueStyle", { enumerable: true, get: function () { return form_value_1.formValueStyle; } });
40
+ var i18n_1 = require("./i18n");
41
+ Object.defineProperty(exports, "CrispMessagesProvider", { enumerable: true, get: function () { return i18n_1.CrispMessagesProvider; } });
42
+ Object.defineProperty(exports, "useCrispMessage", { enumerable: true, get: function () { return i18n_1.useCrispMessage; } });
43
+ Object.defineProperty(exports, "useCrispMessages", { enumerable: true, get: function () { return i18n_1.useCrispMessages; } });
40
44
  var mention_1 = require("./mention");
41
45
  Object.defineProperty(exports, "matchMentionTrigger", { enumerable: true, get: function () { return mention_1.matchMentionTrigger; } });
42
46
  var place_floating_1 = require("./place-floating");