@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
@@ -6,6 +6,22 @@ const tokens_1 = require("../../tokens");
6
6
  // A note / entity card — measured from a Notes grid: a bordered white card
7
7
  // with an author row (avatar + name) at the top, a title, a clamped excerpt,
8
8
  // and a footer row (avatar + name … date). The whole card is clickable.
9
+ //
10
+ // Re-measured directly against a live note card (#474): the author name
11
+ // renders at fgPrimary/text.sm (12px), not a muted 13px — it reads as
12
+ // regular body text, not a caption. The title is weight.medium (500), not
13
+ // semibold — "my" measured 500 on the live card, and semibold was an
14
+ // unmeasured guess. The excerpt is text.sm (12px)/16px line-height, and
15
+ // both the excerpt body and the footer date share one muted tone that is
16
+ // darker than `fgMuted` (measured `rgba(0,0,0,0.63)` on the excerpt
17
+ // paragraph AND, independently, on the footer date — the same value also
18
+ // turned up on Board's "29d" age pill in the same audit, so it's a real,
19
+ // reused Attio tone and not noise). `fgMuted` (gray400, #71717a) doesn't
20
+ // carry that ring, so this reads it as 63% of `fgPrimary` instead of adding
21
+ // an unmeasured new dark-mode literal (AUTHORING Lesson 47) — `fgPrimary`
22
+ // already flips correctly per theme, so the mix preserves the *relationship*
23
+ // Attio is expressing (a darker secondary text tone) in both themes.
24
+ const secondaryFg = `color-mix(in srgb, ${tokens_1.contract.fgPrimary} 63%, transparent)`;
9
25
  const config = (0, core_1.defineRecipe)({
10
26
  name: "entitycard",
11
27
  base: `display: flex; flex-direction: column; box-sizing: border-box; width: 100%; min-width: 0; text-align: start;
@@ -17,11 +33,11 @@ const config = (0, core_1.defineRecipe)({
17
33
  &:hover { box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDefault}, 0 1px 2px rgba(0,0,0,0.04), 0 6px 16px -6px rgba(28,40,64,0.12); }
18
34
  &:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 2px; }
19
35
 
20
- & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
21
- & .crisp-entitycard-title { margin-top: ${tokens_1.contract.space["2"]}; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.1px; color: ${tokens_1.contract.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
22
- & .crisp-entitycard-excerpt { margin-top: ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.md}; line-height: 18px; color: ${tokens_1.contract.fgMuted}; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
36
+ & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; }
37
+ & .crisp-entitycard-title { margin-top: ${tokens_1.contract.space["2"]}; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; letter-spacing: -0.1px; color: ${tokens_1.contract.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
38
+ & .crisp-entitycard-excerpt { margin-top: ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.sm}; line-height: 16px; color: ${secondaryFg}; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
23
39
  & .crisp-entitycard-spacer { flex: 1; min-height: ${tokens_1.contract.space["3"]}; }
24
- & .crisp-entitycard-foot { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; padding-top: ${tokens_1.contract.space["2.5"]}; margin-top: ${tokens_1.contract.space["1"]}; border-top: 1px solid ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; }
40
+ & .crisp-entitycard-foot { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; padding-top: ${tokens_1.contract.space["2.5"]}; margin-top: ${tokens_1.contract.space["1"]}; border-top: 1px solid ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; color: ${secondaryFg}; }
25
41
  & .crisp-entitycard-foot-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: ${tokens_1.contract.fgPrimary}; }
26
42
  & .crisp-entitycard-date { margin-inline-start: auto; flex-shrink: 0; white-space: nowrap; }`,
27
43
  variants: {},
@@ -20,6 +20,6 @@ function ListRow({ children, onClick, }) {
20
20
  */
21
21
  function ListPage({ leading, trailing, count, favorites, favoritesLabel = "Favorites", columns, groups, emptyLabel = "Nothing here yet", className, ...rest }) {
22
22
  const total = groups?.reduce((s, g) => s + g.rows.length, 0) ?? 0;
23
- return ((0, jsx_runtime_1.jsxs)("div", { className: (0, listpage_recipe_1.listpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-toolbar", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-toolbar-lead", children: leading }), count != null && (0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-count", children: count }), trailing != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-toolbar-trail", children: trailing }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-body", children: [favorites != null && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-section-label", children: favoritesLabel }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-favorites", "data-dashed": "", children: favorites })] })), columns && columns.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-colhead", children: columns.map((col, i) => ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-listpage-colhead-cell", children: [col.icon, col.label] }, i))) })), total === 0 && !groups?.length ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-empty", children: emptyLabel })) : (groups?.map((g) => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-group", children: [g.label, g.count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-listpage-group-count", children: g.count }))] }), g.rows] }, g.id))))] })] }));
23
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, listpage_recipe_1.listpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-toolbar", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-toolbar-lead", children: leading }), count != null && (0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-count", children: count }), trailing != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-toolbar-trail", children: trailing }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-body", children: [favorites != null && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-favorites-section", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-section-label", children: favoritesLabel }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-favorites", children: favorites })] })), columns && columns.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-colhead", children: columns.map((col, i) => ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-listpage-colhead-cell", children: [col.icon, col.label] }, i))) })), total === 0 && !groups?.length ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-listpage-empty", children: emptyLabel })) : (groups?.map((g) => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-listpage-group", children: [g.label, g.count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-listpage-group-count", children: g.count }))] }), g.rows] }, g.id))))] })] }));
24
24
  }
25
25
  ListPage.displayName = "ListPage";
@@ -12,7 +12,7 @@ const config = (0, core_1.defineRecipe)({
12
12
  base: `display: flex; flex-direction: column; min-height: 0; box-sizing: border-box; font-family: ${tokens_1.contract.font.sans}; color: ${tokens_1.contract.fgPrimary};
13
13
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
14
14
 
15
- & .crisp-listpage-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: 52px; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; }
15
+ & .crisp-listpage-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.header}; flex-shrink: 0; padding: 0 ${tokens_1.contract.space["3"]}; }
16
16
  & .crisp-listpage-toolbar-lead { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; }
17
17
  & .crisp-listpage-count { margin-inline-start: auto; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; white-space: nowrap; }
18
18
  & .crisp-listpage-toolbar-trail { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; }
@@ -21,15 +21,13 @@ const config = (0, core_1.defineRecipe)({
21
21
  & .crisp-listpage-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 ${tokens_1.contract.space["3"]} ${tokens_1.contract.space["3"]}; }
22
22
 
23
23
  & .crisp-listpage-section-label { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
24
- & .crisp-listpage-favorites { display: flex; align-items: center; justify-content: center; min-height: 200px; padding: ${tokens_1.contract.space["6"]}; border-radius: ${tokens_1.contract.radius.xl};
25
- background: repeating-linear-gradient(0deg, transparent, transparent) padding-box,
26
- border-box; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); }
27
- & .crisp-listpage-favorites[data-dashed] { box-shadow: none; border: 1px dashed color-mix(in srgb, ${tokens_1.contract.fgPrimary} 14%, transparent); }
24
+ & .crisp-listpage-favorites-section { background: ${tokens_1.contract.bgSubtle}; box-shadow: inset 0 -1px 0 ${tokens_1.contract.borderSubtle}; }
25
+ & .crisp-listpage-favorites { display: flex; align-items: center; justify-content: center; min-height: 200px; padding: ${tokens_1.contract.space["6"]}; }
28
26
 
29
- & .crisp-listpage-colhead { display: flex; align-items: center; gap: ${tokens_1.contract.space["2.5"]}; height: 40px; padding: 0 ${tokens_1.contract.space["2"]}; box-shadow: inset 0 -1px 0 ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; }
27
+ & .crisp-listpage-colhead { display: flex; align-items: center; gap: ${tokens_1.contract.space["2.5"]}; height: ${tokens_1.contract.size.menuRow}; padding: 0 ${tokens_1.contract.space["4"]}; box-shadow: inset 0 -1px 0 ${tokens_1.contract.borderSubtle}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.fgMuted}; }
30
28
  & .crisp-listpage-colhead-cell { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; }
31
29
 
32
- & .crisp-listpage-group { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 36px; padding: 0 ${tokens_1.contract.space["2"]}; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
30
+ & .crisp-listpage-group { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: ${tokens_1.contract.size.menuRow}; padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["4"]}; box-shadow: inset 0 -1px 0 ${tokens_1.contract.borderSubtle}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 2%, transparent); font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
33
31
  & .crisp-listpage-group-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: ${tokens_1.contract.radius.sm}; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 6%, transparent); font-size: ${tokens_1.contract.text.xs}; color: ${tokens_1.contract.fgMuted}; }
34
32
 
35
33
  & .crisp-listpage-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["2.5"]}; width: 100%; height: 52px; padding: 0 ${tokens_1.contract.space["2"]}; margin: 0; border: none; border-top: 1px solid ${tokens_1.contract.borderSubtle}; background: none; font-family: inherit; color: inherit; text-align: start; cursor: pointer; transition: background-color .12s; }
@@ -2,11 +2,11 @@
2
2
  display: flex; flex-direction: column; box-sizing: border-box; width: 100%; min-width: 0; text-align: start; padding: var(--crisp-space-3) var(--crisp-space-4); margin: 0; border: none; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); color: var(--crisp-fg-primary); font-family: var(--crisp-font-sans); cursor: pointer; transition: box-shadow .12s ease, background-color .12s ease; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
3
  &:hover { box-shadow: inset 0 0 0 1px var(--crisp-border-default), 0 1px 2px rgba(0,0,0,0.04), 0 6px 16px -6px rgba(28,40,64,0.12); }
4
4
  &:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 2px; }
5
- & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
6
- & .crisp-entitycard-title { margin-top: var(--crisp-space-2); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.1px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
7
- & .crisp-entitycard-excerpt { margin-top: var(--crisp-space-1); font-size: var(--crisp-text-md); line-height: 18px; color: var(--crisp-fg-muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
5
+ & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
6
+ & .crisp-entitycard-title { margin-top: var(--crisp-space-2); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.1px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
7
+ & .crisp-entitycard-excerpt { margin-top: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
8
8
  & .crisp-entitycard-spacer { flex: 1; min-height: var(--crisp-space-3); }
9
- & .crisp-entitycard-foot { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding-top: var(--crisp-space-2-5); margin-top: var(--crisp-space-1); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
9
+ & .crisp-entitycard-foot { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding-top: var(--crisp-space-2-5); margin-top: var(--crisp-space-1); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-sm); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
10
10
  & .crisp-entitycard-foot-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--crisp-fg-primary); }
11
11
  & .crisp-entitycard-date { margin-inline-start: auto; flex-shrink: 0; white-space: nowrap; }
12
12
  }
@@ -3,11 +3,11 @@
3
3
  display: flex; flex-direction: column; box-sizing: border-box; width: 100%; min-width: 0; text-align: start; padding: var(--crisp-space-3) var(--crisp-space-4); margin: 0; border: none; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-subtle); color: var(--crisp-fg-primary); font-family: var(--crisp-font-sans); cursor: pointer; transition: box-shadow .12s ease, background-color .12s ease; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
4
  &:hover { box-shadow: inset 0 0 0 1px var(--crisp-border-default), 0 1px 2px rgba(0,0,0,0.04), 0 6px 16px -6px rgba(28,40,64,0.12); }
5
5
  &:focus-visible { outline: 2px solid var(--crisp-bg-brand-solid); outline-offset: 2px; }
6
- & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
7
- & .crisp-entitycard-title { margin-top: var(--crisp-space-2); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.1px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
8
- & .crisp-entitycard-excerpt { margin-top: var(--crisp-space-1); font-size: var(--crisp-text-md); line-height: 18px; color: var(--crisp-fg-muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
6
+ & .crisp-entitycard-author { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
7
+ & .crisp-entitycard-title { margin-top: var(--crisp-space-2); font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); letter-spacing: -0.1px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
8
+ & .crisp-entitycard-excerpt { margin-top: var(--crisp-space-1); font-size: var(--crisp-text-sm); line-height: 16px; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
9
9
  & .crisp-entitycard-spacer { flex: 1; min-height: var(--crisp-space-3); }
10
- & .crisp-entitycard-foot { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding-top: var(--crisp-space-2-5); margin-top: var(--crisp-space-1); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-sm); color: var(--crisp-fg-muted); }
10
+ & .crisp-entitycard-foot { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding-top: var(--crisp-space-2-5); margin-top: var(--crisp-space-1); border-top: 1px solid var(--crisp-border-subtle); font-size: var(--crisp-text-sm); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
11
11
  & .crisp-entitycard-foot-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--crisp-fg-primary); }
12
12
  & .crisp-entitycard-date { margin-inline-start: auto; flex-shrink: 0; white-space: nowrap; }
13
13
  }
@@ -7,7 +7,7 @@ import { bulkbarRecipe } from "./bulkbar.recipe.js";
7
7
  * positioned ancestor when a selection is active; render it conditionally
8
8
  * (e.g. `count > 0 && <BulkBar …/>`). Measured from a records table.
9
9
  */
10
- export function BulkBar({ count, onClear, countLabel = "selected", countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
10
+ export function BulkBar({ count, onClear, countLabel = "Selected", countText = (count, label) => (_jsxs(_Fragment, { children: [count, " ", label] })), children, className, clearLabel = "Clear selection", ...rest }) {
11
11
  return (_jsx("div", { className: bulkbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-bulkbar-bar", children: [count != null && (_jsxs("button", { type: "button", className: "crisp-bulkbar-count", onClick: onClear, title: clearLabel, children: [countText(count, countLabel), _jsx("span", { className: "crisp-bulkbar-clearlabel", children: clearLabel }), _jsx(X, { size: 13, strokeWidth: 2, "aria-hidden": "true" })] })), children] }) }));
12
12
  }
13
13
  BulkBar.displayName = "BulkBar";
@@ -12,7 +12,7 @@ const config = defineRecipe({
12
12
  base: `position: absolute; left: 0; right: 0; bottom: ${c.space["4"]}; display: flex; justify-content: center; pointer-events: none; z-index: 10;
13
13
  & .crisp-bulkbar-bar { pointer-events: auto; display: flex; align-items: center; gap: ${c.space["0.5"]}; height: 48px; padding: 0 ${c.space["1"]} 0 ${c.space["1.5"]}; border-radius: 14px; background: ${c.bgSurface}; box-shadow: ${e.floating}; color: ${c.fgPrimary}; transition: opacity .16s ease, transform .16s ease; }
14
14
  & .crisp-bulkbar-bar { @starting-style { opacity: 0; transform: translateY(10px); } }
15
- & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: color-mix(in srgb, ${c.fgPrimary} 4%, transparent); font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
15
+ & .crisp-bulkbar-count { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.lg}; background: color-mix(in srgb, ${c.fgPrimary} 2%, transparent); font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
16
16
  & .crisp-bulkbar-count:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); color: ${c.fgPrimary}; }
17
17
  & .crisp-bulkbar-count svg { flex-shrink: 0; }
18
18
  /* the ", Clear selection" ARIA concatenates onto the pill's name (#324)
@@ -21,7 +21,7 @@ const config = defineRecipe({
21
21
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
22
22
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
23
23
  }
24
- & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: transparent; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
24
+ & .crisp-bulkbar-btn { display: inline-flex; align-items: center; gap: ${c.space["1"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.lg}; background: ${c.bgSurface}; box-shadow: ${e.field}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium}; white-space: nowrap; cursor: pointer; transition: background-color .12s; }
25
25
  & .crisp-bulkbar-btn:hover { background: ${c.bgRaised}; }
26
26
  & .crisp-bulkbar-btn--danger { color: ${c.bgDanger}; }
27
27
  & .crisp-bulkbar-btn--danger:hover { background: color-mix(in srgb, ${c.bgDanger} 10%, transparent); }
@@ -25,7 +25,12 @@ const spec = defineSpec({
25
25
  decls: {
26
26
  background: c.bgBrandSolid,
27
27
  color: c.fgOnBrand,
28
- 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)",
28
+ // crisp#494: the inset ring is a black tint, not white -- live-
29
+ // measured on Attio's own "New Company" button as
30
+ // rgba(0,0,0,0.06), not the rgba(255,255,255,0.07) both solid
31
+ // intents shared. The other two shadow layers already matched
32
+ // exactly.
33
+ 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)",
29
34
  },
30
35
  parts: [
31
36
  { selector: "&:hover", decls: { background: c.bgBrandSolidPressed } },
@@ -47,7 +52,10 @@ const spec = defineSpec({
47
52
  decls: {
48
53
  background: c.bgDangerSolid,
49
54
  color: c.fgOnBrand,
50
- 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)",
55
+ // Same corrected inset ring as primary (crisp#494) -- shared
56
+ // literal, same solid-fill button family; the coloured layers
57
+ // themselves weren't independently re-measured this round.
58
+ 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)",
51
59
  },
52
60
  parts: [
53
61
  { selector: "&:hover", decls: { filter: "brightness(0.96)" } },
@@ -1,7 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ArrowDown, ArrowUp, Check, ChevronsUpDown, CircleUserRound, GripVertical, Plus, Search, SlidersHorizontal, } from "lucide-react";
3
3
  import * as React from "react";
4
- import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, } from "../../core/index.js";
4
+ import { cellDatePrecision, formatCellDate, isRtlElement, parseCellDate, useCrispMessage, useCrispMessages, } from "../../core/index.js";
5
5
  import { Avatar } from "../avatar/index.js";
6
6
  import { Checkbox } from "../checkbox/index.js";
7
7
  import { Menu, MenuItem } from "../menu/index.js";
@@ -294,9 +294,47 @@ function DataReferenceEditor({ col, value, isPending, onCommit, onCancel, }) {
294
294
  onCommit(v);
295
295
  } }) }));
296
296
  }
297
- export 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
298
- ? `${total} ${total === 1 ? "row" : "rows"}`
299
- : `${shown} of ${total}`, toolbar, className, reorderRowLabel = "Reorder row", ...rest }) {
297
+ export 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 }) {
298
+ // crisp/#471: same resolution order as TableToolbar's own — an explicit
299
+ // prop wins, then the provider's translation of the built-in English
300
+ // default, then that default itself with no provider in the tree.
301
+ const crispMessages = useCrispMessages();
302
+ const selectAllLabel = useCrispMessage(selectAllLabelProp, "Select all rows");
303
+ const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search…");
304
+ const searchLabel = useCrispMessage(searchLabelProp, "Search table");
305
+ // Also `React.ReactNode`-typed, same reasoning as `empty` below.
306
+ const addRowLabel = addRowLabelProp ?? crispMessages["New record"] ?? "New record";
307
+ // `empty` is typed `React.ReactNode`, not `string` — a consumer can pass a
308
+ // whole custom empty-state element, not just replacement text, so it
309
+ // can't go through `useCrispMessage` (string-only). A ReactNode override
310
+ // passes straight through unchanged (it's already whatever the consumer
311
+ // wants shown); only the built-in string default translates.
312
+ const empty = emptyProp ?? crispMessages["No results."] ?? "No results.";
313
+ const reorderRowLabel = useCrispMessage(reorderRowLabelProp, "Reorder row");
314
+ const sortAscendingLabel = useCrispMessage(sortAscendingLabelProp, "Sort ascending");
315
+ const sortDescendingLabel = useCrispMessage(sortDescendingLabelProp, "Sort descending");
316
+ const moveColumnLeftLabel = useCrispMessage(moveColumnLeftLabelProp, "Move left");
317
+ const moveColumnRightLabel = useCrispMessage(moveColumnRightLabelProp, "Move right");
318
+ const editColumnLabel = useCrispMessage(editColumnLabelProp, "Edit column label");
319
+ const hideColumnLabel = useCrispMessage(hideColumnLabelProp, "Hide from view");
320
+ const renameColumnLabel = useCrispMessage(renameColumnLabelProp, "Rename column");
321
+ const viewSettingsLabel = useCrispMessage(viewSettingsLabelProp, "View settings");
322
+ const columnsGroupLabel = useCrispMessage(columnsGroupLabelProp, "Columns");
323
+ // countLabel/selectionAnnouncement stay formatter props, same escape hatch
324
+ // as TableToolbar's sortTriggerLabel — word order and pluralisation rules
325
+ // differ too much across languages for a template. Their BUILT-IN default
326
+ // (when the consumer supplies neither) still translates its own static
327
+ // fragments through the provider, same as TableToolbar's "Sorted by".
328
+ const resolvedCountLabel = countLabel ??
329
+ ((shown, total) => shown === total
330
+ ? `${total} ${total === 1
331
+ ? (crispMessages.row ?? "row")
332
+ : (crispMessages.rows ?? "rows")}`
333
+ : `${shown} ${crispMessages.of ?? "of"} ${total}`);
334
+ const resolvedSelectionAnnouncement = selectionAnnouncement ??
335
+ ((count) => count === 0
336
+ ? (crispMessages["Selection cleared"] ?? "Selection cleared")
337
+ : `${count} ${crispMessages.selected ?? "selected"}`);
300
338
  // Two different defaults on purpose. FORMATTING falls back to "en-US", which
301
339
  // is what the built-in date/number/currency renderers have always produced,
302
340
  // so no existing output moves. SORTING falls back to `undefined`, which is
@@ -390,7 +428,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
390
428
  const [selectionText, setSelectionText] = React.useState("");
391
429
  if (prevSelectedCount.current !== selected.size) {
392
430
  prevSelectedCount.current = selected.size;
393
- setSelectionText(selectionAnnouncement(selected.size));
431
+ setSelectionText(resolvedSelectionAnnouncement(selected.size));
394
432
  }
395
433
  const [editing, setEditing] = React.useState(null);
396
434
  // Set only while an async `onEdit` is in flight (#385) -- a sync `onEdit`
@@ -736,7 +774,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
736
774
  return dir === "asc" ? (_jsx(ArrowUp, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 })) : (_jsx(ArrowDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
737
775
  return (_jsx(ChevronsUpDown, { className: "crisp-datatable-chevron", size: 13, strokeWidth: 2 }));
738
776
  }
739
- return (_jsxs("div", { ref: gridRef, className: datatableRecipe({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [_jsx("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && (_jsxs("div", { className: "crisp-datatable-toolbar", children: [columnMenu && (_jsxs(Menu, { align: "start", trigger: _jsxs("button", { type: "button", className: "crisp-datatable-viewsettings", children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75 }), "View settings"] }), children: [_jsx("div", { className: "crisp-datatable-group-label", children: "Columns" }), allColumns.map((c) => (_jsxs(MenuItem, { onSelect: () => c.hidden ? showColumn(c.key) : hideColumn(c.key), children: [_jsx(Check, { size: 14, strokeWidth: 2, style: { opacity: c.hidden ? 0 : 1 } }), typeof c.header === "string" ? c.header : c.key] }, c.key)))] })), filterable && (_jsxs("span", { className: "crisp-datatable-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchLabel })] })), showCount && (_jsx("span", { className: "crisp-datatable-count", children: countLabel(rows.length, data.length) })), toolbar] })), _jsxs(Table, { interactive: interactive, surface: surface, style: { minWidth }, scrollProps: {
777
+ return (_jsxs("div", { ref: gridRef, className: datatableRecipe({ className }), tabIndex: -1, onKeyDown: onGridKeyDown, ...rest, children: [_jsx("div", { className: "crisp-datatable-live", role: "status", "aria-live": "polite", children: selectionText }), (filterable || showCount || toolbar || columnMenu) && (_jsxs("div", { className: "crisp-datatable-toolbar", children: [columnMenu && (_jsxs(Menu, { align: "start", trigger: _jsxs("button", { type: "button", className: "crisp-datatable-viewsettings", children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75 }), viewSettingsLabel] }), children: [_jsx("div", { className: "crisp-datatable-group-label", children: columnsGroupLabel }), allColumns.map((c) => (_jsxs(MenuItem, { onSelect: () => c.hidden ? showColumn(c.key) : hideColumn(c.key), children: [_jsx(Check, { size: 14, strokeWidth: 2, style: { opacity: c.hidden ? 0 : 1 } }), typeof c.header === "string" ? c.header : c.key] }, c.key)))] })), filterable && (_jsxs("span", { className: "crisp-datatable-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchLabel })] })), showCount && (_jsx("span", { className: "crisp-datatable-count", children: resolvedCountLabel(rows.length, data.length) })), toolbar] })), _jsxs(Table, { interactive: interactive, surface: surface, style: { minWidth }, scrollProps: {
740
778
  ref: scrollRef,
741
779
  onScroll: (e) => {
742
780
  const el = e.currentTarget;
@@ -766,7 +804,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
766
804
  const inner = (_jsxs("span", { className: "crisp-datatable-th-inner", children: [Icon ? _jsx(Icon, { size: 16, strokeWidth: 1.75 }) : null, _jsx("span", { className: "crisp-datatable-th-label", children: col.header }), col.sortable ? _jsx(ChevronFor, { colKey: col.key }) : null] }));
767
805
  return (_jsxs(TableHead, { align: alignFor(col), "aria-sort": ariaSort, ...dataStickyCol(i), children: [renaming === col.key ? (_jsx("input", {
768
806
  // biome-ignore lint/a11y/noAutofocus: the rename item opens this field
769
- autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": `Rename column`, onBlur: (e) => {
807
+ autoFocus: true, className: "crisp-datatable-th-rename", defaultValue: typeof col.header === "string" ? col.header : "", "aria-label": renameColumnLabel, onBlur: (e) => {
770
808
  renameColumn(col.key, e.currentTarget.value);
771
809
  setRenaming(null);
772
810
  }, onKeyDown: (e) => {
@@ -778,7 +816,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
778
816
  setRenaming(null);
779
817
  } })) : columnMenu ? (_jsxs(Menu, { align: "start", trigger: _jsx("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), "aria-label": `${typeof col.header === "string"
780
818
  ? col.header
781
- : col.key} column options`, children: inner }), children: [_jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: "Sort ascending" }), _jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: "Sort descending" }), col.managed === false ? null : (_jsxs(_Fragment, { children: [_jsx(MenuItem, { onSelect: () => moveColumn(col.key, -1), children: "Move left" }), _jsx(MenuItem, { onSelect: () => moveColumn(col.key, 1), children: "Move right" }), _jsx(MenuItem, { onSelect: () => setRenaming(col.key), children: "Edit column label" }), _jsx(MenuItem, { onSelect: () => hideColumn(col.key), children: "Hide from view" })] }))] })) : col.sortable ? (_jsx("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.resizable !== false && (_jsx("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
819
+ : col.key} ${crispMessages["column options"] ?? "column options"}`, children: inner }), children: [_jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "asc" }), children: sortAscendingLabel }), _jsx(MenuItem, { onSelect: () => setSort({ key: col.key, dir: "desc" }), children: sortDescendingLabel }), col.managed === false ? null : (_jsxs(_Fragment, { children: [_jsx(MenuItem, { onSelect: () => moveColumn(col.key, -1), children: moveColumnLeftLabel }), _jsx(MenuItem, { onSelect: () => moveColumn(col.key, 1), children: moveColumnRightLabel }), _jsx(MenuItem, { onSelect: () => setRenaming(col.key), children: editColumnLabel }), _jsx(MenuItem, { onSelect: () => hideColumn(col.key), children: hideColumnLabel })] }))] })) : col.sortable ? (_jsx("button", { type: "button", className: "crisp-datatable-sort", "data-sorted": sortDirByKey.get(col.key), onClick: () => cycleSort(col.key), children: inner })) : (inner), col.resizable !== false && (_jsx("span", { className: "crisp-datatable-resize", "data-resizing": resizingKey === col.key ? "" : undefined, onPointerDown: (e) => startResize(col, e), onPointerMove: moveResize, onPointerUp: endResize }))] }, col.key));
782
820
  })] }) }), _jsx(TableBody, { children: error ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: totalCols, children: _jsx("div", { className: "crisp-datatable-empty", children: error }) }) })) : rows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: totalCols, children: _jsx("div", { className: "crisp-datatable-empty", children: empty }) }) })) : (groupedRows.map(({ group, rows: groupRows }) => (_jsxs(React.Fragment, { children: [group !== null && (_jsx(TableRow, { children: _jsxs(TableCell, { colSpan: totalCols, className: "crisp-datatable-group", children: [_jsx("span", { className: "crisp-datatable-group-label", children: group }), _jsx("span", { className: "crisp-datatable-group-count", children: groupRows.length })] }) })), groupRows.map((row) => (_jsxs(TableRow, { "data-selected": selected.has(row.id) ? "" : undefined, "data-drop-target": showGrip &&
783
821
  dragRowId &&
784
822
  dragRowId !== row.id &&
@@ -166,7 +166,7 @@ id, placeholder = "Select…", searchable = true, searchPlaceholder = "Search…
166
166
  e.preventDefault();
167
167
  openMenu();
168
168
  }
169
- }, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _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 && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
169
+ }, children: [_jsx("span", { className: "crisp-multiselect-chips", children: selectedOptions.length === 0 ? (_jsx("span", { className: "crisp-multiselect-placeholder", children: placeholder })) : (selectedOptions.map((o) => _jsx(Chip, { opt: o }, o.value))) }), _jsx(ChevronDown, { className: "crisp-multiselect-chevron", size: 16, strokeWidth: 1.75 })] }), open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _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 && (_jsxs("div", { className: "crisp-multiselect-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { ref: searchRef, type: "text", placeholder: searchPlaceholder, "aria-label": searchPlaceholder, "aria-controls": listboxId, "aria-activedescendant": activeId, value: query, onChange: (e) => {
170
170
  setQuery(e.target.value);
171
171
  setHighlight(0);
172
172
  }, onKeyDown: onNavKeys })] })), filtered.length === 0 ? (_jsx("div", { className: "crisp-multiselect-empty", children: emptyLabel })) : (filtered.map((opt, i) => {
@@ -62,7 +62,7 @@ const config = defineRecipe({
62
62
  }
63
63
  .crisp-multiselect-listbox[data-up] { transform-origin: bottom left; }
64
64
  .crisp-multiselect-listbox { @starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); } }
65
- .crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 34px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: 0 ${c.space["2.5"]}; border-bottom: 1px solid ${c.borderSubtle}; color: ${c.fgMuted}; }
65
+ .crisp-multiselect-search { display: flex; align-items: center; gap: ${c.space["1.5"]}; height: 36px; flex-shrink: 0; margin: calc(-1 * ${c.space["1"]}) calc(-1 * ${c.space["1"]}) ${c.space["1"]}; padding: ${c.space["2"]} ${c.space["3"]}; color: ${c.fgMuted}; }
66
66
  .crisp-multiselect-search svg { flex-shrink: 0; }
67
67
  .crisp-multiselect-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
68
68
  .crisp-multiselect-search input::placeholder { color: ${c.fgMuted}; }
@@ -14,7 +14,7 @@ import { contract as c, elevation as e } from "../../tokens/index.js";
14
14
  // block, and those depend on following `.menu` in source order. Keeping the
15
15
  // split preserves that cascade exactly, which is what lets the generated CSS
16
16
  // stay byte-for-byte identical to what shipped before the extraction.
17
- export const toolbarChromeHead = (p) => ` & .${CLASS_PREFIX}${p}-toolbar { display: flex; align-items: center; gap: ${c.space["1.5"]}; padding: 0 ${c.space["3"]} ${c.space["2"]}; }
17
+ export const toolbarChromeHead = (p) => ` & .${CLASS_PREFIX}${p}-toolbar { display: flex; align-items: center; gap: ${c.space["2"]}; padding: 0 ${c.space["3"]} ${c.space["2"]}; }
18
18
  & .${CLASS_PREFIX}${p}-toolbar[data-inline] { padding-bottom: 0; }
19
19
  & .${CLASS_PREFIX}${p}-search { display: flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.row}; padding: 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; color: ${c.fgMuted}; }
20
20
  & .${CLASS_PREFIX}${p}-search svg { flex-shrink: 0; }
@@ -27,10 +27,11 @@ export const toolbarChromeHead = (p) => ` & .${CLASS_PREFIX}${p}-toolbar { di
27
27
  & .${CLASS_PREFIX}${p}-tool--ghost { box-shadow: none; background: color-mix(in srgb, ${c.fgPrimary} 3%, transparent); color: ${c.fgMuted}; }
28
28
  & .${CLASS_PREFIX}${p}-tool--ghost:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); }
29
29
  & .${CLASS_PREFIX}${p}-toolgroup { position: relative; display: inline-flex; }
30
- & .${CLASS_PREFIX}${p}-menu { position: absolute; top: calc(100% + ${c.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${c.space["1"]}; border-radius: ${c.radius.lg}; background: ${c.bgPopover}; box-shadow: ${e.floating}; display: flex; flex-direction: column; gap: ${c.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
30
+ & .${CLASS_PREFIX}${p}-divider { flex-shrink: 0; width: 1px; height: 20px; background: ${c.borderSubtle}; }
31
+ & .${CLASS_PREFIX}${p}-menu { position: absolute; top: calc(100% + ${c.space["1"]}); left: 0; z-index: 30; min-width: 232px; padding: ${c.space["1"]}; border-radius: ${c.radius.xl}; background: ${c.bgPopover}; box-shadow: ${e.floating}; display: flex; flex-direction: column; gap: ${c.space.px}; transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
31
32
  & .${CLASS_PREFIX}${p}-menu { @starting-style { opacity: 0; transform: scale(0.96); } }`;
32
33
  export const toolbarChromeBody = (p) => ` & .${CLASS_PREFIX}${p}-menu-head { padding: ${c.space["1"]} ${c.space["2"]} ${c.space["0.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
33
- & .${CLASS_PREFIX}${p}-sortpop { min-width: 460px; overflow: visible; padding: ${c.space["1.5"]}; gap: ${c.space["1"]}; }
34
+ & .${CLASS_PREFIX}${p}-sortpop { min-width: 480px; overflow: visible; padding: ${c.space["3"]} ${c.space["1.5"]}; gap: ${c.space["2"]}; }
34
35
  & .${CLASS_PREFIX}${p}-sort-empty { padding: ${c.space["1"]} ${c.space["1"]} ${c.space["2"]}; font-size: ${c.text.md}; color: ${c.fgMuted}; }
35
36
  & .${CLASS_PREFIX}${p}-sort-row { display: flex; align-items: center; gap: ${c.space["1.5"]}; }
36
37
  & .${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 * ${c.space["1"]}); padding: 0; border: none; background: none; color: ${c.fgMuted}; cursor: grab; }
@@ -51,7 +52,7 @@ export const toolbarChromeBody = (p) => ` & .${CLASS_PREFIX}${p}-menu-head {
51
52
  & .${CLASS_PREFIX}${p}-sort-learn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.lg}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); text-decoration: none; }
52
53
  & .${CLASS_PREFIX}${p}-sort-learn svg { flex-shrink: 0; }
53
54
  & .${CLASS_PREFIX}${p}-sort-learn:hover { color: ${c.fgPrimary}; }
54
- & .${CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; height: ${c.size.row}; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: left; cursor: pointer; }
55
+ & .${CLASS_PREFIX}${p}-menu-item { display: flex; align-items: center; gap: ${c.space["1.5"]}; width: 100%; height: ${c.size.menuRow}; margin: 0; padding: 0 ${c.space["2"]}; border: none; background: none; border-radius: ${c.radius.md}; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: left; cursor: pointer; }
55
56
  & .${CLASS_PREFIX}${p}-menu-item:hover { background: ${c.bgRaised}; }
56
57
  & .${CLASS_PREFIX}${p}-menu-item:disabled { opacity: 0.4; cursor: default; }
57
58
  & .${CLASS_PREFIX}${p}-menu-item:disabled:hover { background: none; }
@@ -1,7 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, CircleHelp, GripVertical, ListFilter, Plus, X, } from "lucide-react";
3
3
  import * as React from "react";
4
- import { isInsideLaterFloatingLayer, useFocusAfterRemoval } from "../../core/index.js";
4
+ import { isInsideLaterFloatingLayer, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
5
5
  import { MenuCheckboxItem } from "../menu/index.js";
6
6
  import { Select } from "../select/index.js";
7
7
  import { tabletoolbarRecipe } from "./tabletoolbar.recipe.js";
@@ -117,7 +117,33 @@ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
117
117
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
118
118
  * Created by), which one shared list can't reproduce.
119
119
  */
120
- export 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 }) {
120
+ export 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 }) {
121
+ // crisp/#471: an explicit prop always wins; missing, it reads the nearest
122
+ // CrispMessagesProvider's translation of the English default below, or
123
+ // that default itself with no provider — unchanged from before this
124
+ // existed. One resolved constant per prop, same name every existing usage
125
+ // below already reads, so nothing past this block changes.
126
+ const sortLabel = useCrispMessage(sortLabelProp, "Sort");
127
+ const filterLabel = useCrispMessage(filterLabelProp, "Filter");
128
+ const reorderSortLabel = useCrispMessage(reorderSortLabelProp, "Reorder sort");
129
+ const noSortsLabel = useCrispMessage(noSortsLabelProp, "No sorts applied to this view.");
130
+ const addSortLabel = useCrispMessage(addSortLabelProp, "Add sort");
131
+ const learnSortingLabel = useCrispMessage(learnSortingLabelProp, "Learn about sorting");
132
+ const selectConditionLabel = useCrispMessage(selectConditionLabelProp, "Select condition");
133
+ const sortFieldLabel = useCrispMessage(sortFieldLabelProp, "Sort field");
134
+ const sortDirectionLabel = useCrispMessage(sortDirectionLabelProp, "Sort direction");
135
+ const removeSortLabel = useCrispMessage(removeSortLabelProp, "Remove sort");
136
+ const filterFieldLabel = useCrispMessage(filterFieldLabelProp, "Filter by field");
137
+ const filterFieldPlaceholder = useCrispMessage(filterFieldPlaceholderProp, "Filter by…");
138
+ const filterValuePlaceholder = useCrispMessage(filterValuePlaceholderProp, "Value…");
139
+ const ascendingLabel = useCrispMessage(ascendingLabelProp, "Ascending");
140
+ const descendingLabel = useCrispMessage(descendingLabelProp, "Descending");
141
+ // OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
142
+ // their own — a whole lookup table, not one string — so they translate
143
+ // straight from the provider's dictionary at the two spots they render
144
+ // (#628, #673 below), same fallback order as every resolved constant
145
+ // above.
146
+ const crispMessages = useCrispMessages();
121
147
  // Each side falls back to the shared `columns` independently — a caller
122
148
  // that only overrides one (Filter's own Record/Created by, say) still
123
149
  // gets `columns` on the other, unchanged from before either prop existed.
@@ -206,12 +232,18 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
206
232
  };
207
233
  const activeFilters = filters.length;
208
234
  // The default phrase only works in English word order, so a caller can
209
- // replace the whole thing rather than fill a template.
235
+ // replace the whole thing rather than fill a template — crisp/#471's
236
+ // `sortTriggerLabel` formatter is exactly that escape hatch. Short of a
237
+ // full formatter, the "Sorted by" fragment itself still translates (the
238
+ // sort trigger reading "Sort" in Mongolian and "Sorted by X" in English is
239
+ // worse than an imperfect word order); `crispMessages` rather than a
240
+ // second `useCrispMessage` call, since this only fires on the branch that
241
+ // needs it.
210
242
  const sortedLabel = sortTriggerLabel
211
243
  ? sortTriggerLabel(sorts, sortColumns)
212
244
  : sorts.length
213
- ? `Sorted by ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
214
- : "Sort";
245
+ ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
246
+ : sortLabel;
215
247
  return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
216
248
  if (dragSort !== null)
217
249
  e.preventDefault();
@@ -234,11 +266,11 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
234
266
  label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
235
267
  keywords: descendingLabel,
236
268
  },
237
- ], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
269
+ ], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), _jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
238
270
  ? filterTriggerLabel(activeFilters)
239
271
  : activeFilters
240
- ? `Filter · ${activeFilters}`
241
- : "Filter"] }), openMenu === "filter" && (_jsxs("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
272
+ ? `${filterLabel} · ${activeFilters}`
273
+ : filterLabel] }), openMenu === "filter" && (_jsxs("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
242
274
  // biome-ignore lint/a11y/noAutofocus: focus the picker on open
243
275
  autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
244
276
  .filter((col) => col.label
@@ -249,7 +281,8 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
249
281
  const open = openMenu === `fcond:${cond.id}`;
250
282
  const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
251
283
  (!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
252
- return (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("span", { className: "crisp-tabletoolbar-fchip", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [_jsx("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-fchip-op", children: OP_LABELS[cond.op] }), opNeedsValue(cond.op) && (_jsx("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && (_jsx("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : (_jsx("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "aria-label": `Remove ${col?.label} filter`, onClick: () => removeFilter(cond.id), children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }), open && (_jsxs("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} condition`, children: [_jsxs("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " condition"] }), opsFor(col).map((op) => (_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 ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && (_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
284
+ return (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("span", { className: "crisp-tabletoolbar-fchip", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [_jsx("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-fchip-op", children: crispMessages[OP_LABELS[cond.op]] ??
285
+ OP_LABELS[cond.op] }), opNeedsValue(cond.op) && (_jsx("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && (_jsx("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : (_jsx("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "aria-label": `${crispMessages.Remove ?? "Remove"} ${col?.label} ${crispMessages.filter ?? "filter"}`, onClick: () => removeFilter(cond.id), children: _jsx(X, { size: 13, strokeWidth: 2 }) })] }), open && (_jsxs("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, children: [_jsxs("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " ", crispMessages.condition ?? "condition"] }), opsFor(col).map((op) => (_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 ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && (_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
253
286
  // biome-ignore lint/a11y/noAutofocus: focus the value field
254
287
  autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })), cond.op === "isanyof" && col?.values && (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
255
288
  // biome-ignore lint/a11y/noAutofocus: focus the search field
@@ -2,6 +2,15 @@ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // The floating bubble for a Tooltip — a dark hint chip. Applied to the
4
4
  // portaled bubble itself (not a wrapper), so its styles survive the portal.
5
+ //
6
+ // Re-measured live against Attio's own tooltip (crisp#465): background,
7
+ // text colour, vertical padding and radius were all off — #20242c/#fff/4px
8
+ // vertical/6px radius were an earlier, uncorroborated guess (not the same
9
+ // surface Lesson 24 in AUTHORING.md describes for the RichText toolbar,
10
+ // despite the similar wording). The real values are gray900's light hex
11
+ // (#101112, the same "dark pill" ink NavItem's own text uses) and
12
+ // line100's light hex (#eeeff1) — both theme-constant, same as before,
13
+ // just the wrong pair.
5
14
  const spec = defineSpec({
6
15
  name: "tooltip",
7
16
  base: {
@@ -10,10 +19,10 @@ const spec = defineSpec({
10
19
  boxSizing: "border-box",
11
20
  pointerEvents: "none",
12
21
  maxWidth: "260px",
13
- padding: `${c.space["1"]} ${c.space["2"]}`,
14
- borderRadius: c.radius.sm,
15
- background: "#20242c",
16
- color: "#fff",
22
+ padding: `${c.space["1.5"]} ${c.space["2"]}`,
23
+ borderRadius: c.radius.md,
24
+ background: "#101112",
25
+ color: "#eeeff1",
17
26
  fontFamily: c.font.sans,
18
27
  fontSize: c.text.sm,
19
28
  fontWeight: c.weight.medium,
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ const CrispMessagesContext = React.createContext({});
4
+ export function CrispMessagesProvider({ messages, children, }) {
5
+ return (_jsx(CrispMessagesContext.Provider, { value: messages, children: children }));
6
+ }
7
+ /**
8
+ * Resolves one translatable string for a component: `explicit` is whatever
9
+ * the caller passed for that prop (`undefined` when they didn't), and
10
+ * `englishDefault` is the literal that shipped as that prop's default value
11
+ * before crisp/#471 — still what renders with no provider in the tree.
12
+ *
13
+ * A plain function, not a second context read inside a loop or a render
14
+ * helper — components call this once per string, the same shape
15
+ * `useLabelAssociation`/`useFormReset` and this codebase's other small
16
+ * `core/` hooks already take.
17
+ */
18
+ export function useCrispMessage(explicit, englishDefault) {
19
+ const messages = React.useContext(CrispMessagesContext);
20
+ if (explicit !== undefined)
21
+ return explicit;
22
+ return messages[englishDefault] ?? englishDefault;
23
+ }
24
+ /**
25
+ * The same resolution as `useCrispMessage`, for a component that builds its
26
+ * own default text internally (TableToolbar's "Sorted by {field}", a
27
+ * DatePicker's month/weekday names) rather than taking one string prop per
28
+ * piece — every literal fragment of that built-in text still needs its own
29
+ * translation lookup, and a component with many such fragments (twelve
30
+ * month names, seven weekday names) would otherwise call `useCrispMessage`
31
+ * in a loop, which breaks the rules of hooks. `useCrispMessages` returns the
32
+ * raw dictionary once; call sites do their own `dict[english] ?? english`
33
+ * per fragment, same fallback order, no hook-in-a-loop.
34
+ */
35
+ export function useCrispMessages() {
36
+ return React.useContext(CrispMessagesContext);
37
+ }
@@ -7,6 +7,7 @@ export { isRtlElement } from "./direction.js";
7
7
  export { FloatingPortal } from "./floating-portal.js";
8
8
  export { hasOpenFloatingLayer, isInsideLaterFloatingLayer, registerFloatingLayer, } from "./floating-stack.js";
9
9
  export { formValueStyle } from "./form-value.js";
10
+ export { CrispMessagesProvider, useCrispMessage, useCrispMessages, } from "./i18n.js";
10
11
  export { matchMentionTrigger, } from "./mention.js";
11
12
  export { placeFloating, } from "./place-floating.js";
12
13
  export { Primitive } from "./primitive.js";