@crisp-ui-kit/crisp 0.54.6 → 0.54.8

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 (113) hide show
  1. package/dist/authshell.css +5 -1
  2. package/dist/authshell.layered.css +5 -1
  3. package/dist/badge.css +5 -5
  4. package/dist/badge.layered.css +5 -5
  5. package/dist/board.css +13 -9
  6. package/dist/board.layered.css +13 -9
  7. package/dist/button.css +3 -1
  8. package/dist/button.layered.css +3 -1
  9. package/dist/cjs/components/badge/badge.recipe.js +28 -5
  10. package/dist/cjs/components/button/button.js +33 -2
  11. package/dist/cjs/components/button/button.recipe.js +31 -5
  12. package/dist/cjs/components/datatable/datatable.js +66 -7
  13. package/dist/cjs/components/datatable/datatable.recipe.js +57 -2
  14. package/dist/cjs/components/dialog/dialog.js +8 -5
  15. package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
  16. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +19 -1
  17. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +239 -50
  18. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +44 -0
  19. package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
  20. package/dist/cjs/components/tooltip/tooltip.js +23 -9
  21. package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
  22. package/dist/cjs/product/authshell/authshell.js +14 -1
  23. package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
  24. package/dist/cjs/product/board/board.js +122 -27
  25. package/dist/cjs/product/board/board.recipe.js +14 -2
  26. package/dist/cjs/product/comments/comments.js +23 -10
  27. package/dist/cjs/product/comments/comments.recipe.js +17 -3
  28. package/dist/cjs/product/formdialog/formdialog.js +10 -4
  29. package/dist/cjs/product/navitem/navitem.js +7 -2
  30. package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
  31. package/dist/cjs/product/objectsettings/index.js +1 -2
  32. package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
  33. package/dist/cjs/product/richtext/richtext.js +76 -1
  34. package/dist/cjs/product/sidebar/sidebar.js +68 -15
  35. package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
  36. package/dist/cjs/tokens/palette.js +32 -1
  37. package/dist/cjs/tokens/semantic.js +11 -0
  38. package/dist/comments.css +36 -10
  39. package/dist/comments.layered.css +36 -10
  40. package/dist/datatable.css +47 -9
  41. package/dist/datatable.layered.css +47 -9
  42. package/dist/dialog.css +13 -1
  43. package/dist/dialog.layered.css +13 -1
  44. package/dist/esm/components/badge/badge.recipe.js +28 -5
  45. package/dist/esm/components/button/button.js +34 -3
  46. package/dist/esm/components/button/button.recipe.js +31 -5
  47. package/dist/esm/components/datatable/datatable.js +67 -8
  48. package/dist/esm/components/datatable/datatable.recipe.js +57 -2
  49. package/dist/esm/components/dialog/dialog.js +8 -5
  50. package/dist/esm/components/dialog/dialog.recipe.js +15 -1
  51. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +19 -1
  52. package/dist/esm/components/tabletoolbar/tabletoolbar.js +240 -51
  53. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +44 -0
  54. package/dist/esm/components/tabs/tabs.recipe.js +11 -2
  55. package/dist/esm/components/tooltip/tooltip.js +23 -9
  56. package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
  57. package/dist/esm/product/authshell/authshell.js +15 -2
  58. package/dist/esm/product/authshell/authshell.recipe.js +13 -1
  59. package/dist/esm/product/authshell/index.js +1 -1
  60. package/dist/esm/product/board/board.js +122 -27
  61. package/dist/esm/product/board/board.recipe.js +14 -2
  62. package/dist/esm/product/comments/comments.js +23 -10
  63. package/dist/esm/product/comments/comments.recipe.js +17 -3
  64. package/dist/esm/product/formdialog/formdialog.js +10 -4
  65. package/dist/esm/product/navitem/index.js +1 -1
  66. package/dist/esm/product/navitem/navitem.js +7 -2
  67. package/dist/esm/product/navitem/navitem.recipe.js +16 -0
  68. package/dist/esm/product/objectsettings/index.js +1 -1
  69. package/dist/esm/product/objectsettings/objectsettings.js +2 -12
  70. package/dist/esm/product/richtext/richtext.js +76 -1
  71. package/dist/esm/product/sidebar/sidebar.js +69 -16
  72. package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
  73. package/dist/esm/tokens/palette.js +32 -1
  74. package/dist/esm/tokens/semantic.js +11 -0
  75. package/dist/native/tokens.d.ts +1 -1
  76. package/dist/native/tokens.js +16 -2
  77. package/dist/navitem.css +7 -0
  78. package/dist/navitem.layered.css +7 -0
  79. package/dist/recordstable.css +2 -1
  80. package/dist/recordstable.layered.css +2 -1
  81. package/dist/sidebar.css +6 -1
  82. package/dist/sidebar.layered.css +6 -1
  83. package/dist/styles.css +165 -42
  84. package/dist/styles.layered.css +165 -42
  85. package/dist/tabletoolbar.css +17 -1
  86. package/dist/tabletoolbar.layered.css +17 -1
  87. package/dist/tabs.css +1 -1
  88. package/dist/tabs.layered.css +1 -1
  89. package/dist/tokens.css +9 -1
  90. package/dist/tokens.json +16 -2
  91. package/dist/tooltip.css +1 -1
  92. package/dist/tooltip.layered.css +1 -1
  93. package/dist/types/components/badge/badge.recipe.d.ts +5 -1
  94. package/dist/types/components/button/button.recipe.d.ts +18 -5
  95. package/dist/types/components/datatable/datatable.d.ts +9 -0
  96. package/dist/types/components/dialog/dialog.d.ts +25 -2
  97. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +47 -5
  98. package/dist/types/components/tooltip/tooltip.d.ts +12 -4
  99. package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
  100. package/dist/types/product/authshell/authshell.d.ts +26 -2
  101. package/dist/types/product/authshell/index.d.ts +1 -1
  102. package/dist/types/product/board/board.d.ts +5 -4
  103. package/dist/types/product/comments/comments.d.ts +10 -2
  104. package/dist/types/product/formdialog/formdialog.d.ts +2 -1
  105. package/dist/types/product/navitem/index.d.ts +1 -1
  106. package/dist/types/product/navitem/navitem.d.ts +23 -0
  107. package/dist/types/product/objectsettings/index.d.ts +1 -1
  108. package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
  109. package/dist/types/product/sidebar/sidebar.d.ts +33 -5
  110. package/dist/types/tokens/contract.d.ts +4 -0
  111. package/dist/types/tokens/palette.d.ts +13 -1
  112. package/dist/types/tokens/semantic.d.ts +11 -0
  113. package/package.json +1 -1
@@ -41,6 +41,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
41
41
  const lucide_react_1 = require("lucide-react");
42
42
  const React = __importStar(require("react"));
43
43
  const core_1 = require("../../core");
44
+ const datepicker_1 = require("../datepicker");
44
45
  const menu_1 = require("../menu");
45
46
  const select_1 = require("../select");
46
47
  const tabletoolbar_recipe_1 = require("./tabletoolbar.recipe");
@@ -52,13 +53,32 @@ const OP_LABELS = {
52
53
  is: "is",
53
54
  isnot: "is not",
54
55
  isanyof: "is any of",
55
- empty: "is empty",
56
- notempty: "is not empty",
56
+ // Attio's own text says "empty"/"not empty", not "is empty"/"is not empty"
57
+ // -- #619's own issue body quotes it verbatim this way, and #593's number/
58
+ // date operator sets ("is / is not / less than / greater than / empty /
59
+ // not empty") repeat it. #619 ported the 3 new ops but left these two
60
+ // untouched from the pre-existing 5-op set, which had drifted; caught in
61
+ // review.
62
+ empty: "empty",
63
+ notempty: "not empty",
64
+ lessthan: "less than",
65
+ greaterthan: "greater than",
66
+ before: "before",
67
+ after: "after",
57
68
  };
58
69
  // #619: order and label set match Attio's own text-filter operator picker
59
70
  // verbatim (measured live, per RecordsTable's own #578 port of the same
60
71
  // set: contains · does not contain · starts with · ends with · is · is not ·
61
- // empty · not empty).
72
+ // empty · not empty). #593: number and date columns get their OWN shorter
73
+ // sets instead — also measured live (`app.attio.com`, a Twitter follower
74
+ // count / Foundation date column, each filter added then discarded): number
75
+ // is "is / is not / less than / greater than / empty / not empty" (104.86px
76
+ // wide, 6 rows); date is "is / is not / before / after / empty / not empty"
77
+ // (91.16px, 6 rows) — both narrower than the text set's own 156.5px, which
78
+ // is why `.crisp-tabletoolbar-menu--op`'s old fixed 134px floor is gone (see
79
+ // tabletoolbar.recipe.ts): every one of these three sets is real, live-
80
+ // measured content-driven width, and a floor sized for one of them
81
+ // overshoots the other two.
62
82
  const OP_ORDER = [
63
83
  "contains",
64
84
  "doesnotcontain",
@@ -69,15 +89,44 @@ const OP_ORDER = [
69
89
  "empty",
70
90
  "notempty",
71
91
  ];
92
+ const NUMBER_OP_ORDER = [
93
+ "is",
94
+ "isnot",
95
+ "lessthan",
96
+ "greaterthan",
97
+ "empty",
98
+ "notempty",
99
+ ];
100
+ const DATE_OP_ORDER = [
101
+ "is",
102
+ "isnot",
103
+ "before",
104
+ "after",
105
+ "empty",
106
+ "notempty",
107
+ ];
72
108
  // "isanyof" is appended only for a column that supplies `values` — there is
73
- // nothing to check off otherwise.
74
- const opsFor = (col) => col?.values?.length ? [...OP_ORDER, "isanyof"] : OP_ORDER;
109
+ // nothing to check off otherwise. A column's `type` picks the base set
110
+ // first; "isanyof" can still follow any of the three (a column's own
111
+ // declared values aren't tied to its underlying number/date/text kind).
112
+ const opsFor = (col) => {
113
+ const base = col?.type === "number"
114
+ ? NUMBER_OP_ORDER
115
+ : col?.type === "date"
116
+ ? DATE_OP_ORDER
117
+ : OP_ORDER;
118
+ return col?.values?.length ? [...base, "isanyof"] : base;
119
+ };
75
120
  const opNeedsValue = (op) => op === "contains" ||
76
121
  op === "doesnotcontain" ||
77
122
  op === "startswith" ||
78
123
  op === "endswith" ||
79
124
  op === "is" ||
80
- op === "isnot";
125
+ op === "isnot" ||
126
+ op === "lessthan" ||
127
+ op === "greaterthan" ||
128
+ op === "before" ||
129
+ op === "after";
81
130
  const opNeedsValues = (op) => op === "isanyof";
82
131
  // "Department is any of IT, Finance" past two values becomes "… +N": the
83
132
  // chip is one line, and a field can have far more than two facet values.
@@ -92,9 +141,10 @@ const asText = (v) => {
92
141
  return v.join(", ");
93
142
  // Before the generic object branch, which would look for `.name`, find none
94
143
  // and return "" — turning every Date into the empty string (#185). The ISO
95
- // date is stable and locale-independent, so `contains "2026-06"` works; it
96
- // will not match "16/06/2026", and a date-aware operator set needs a
97
- // date-picker UI, which has to be measured before it can be built.
144
+ // date is stable and locale-independent, so `contains "2026-06"` works and
145
+ // (#593) so does `is`/`is not` against a `type: "date"` column's own ISO
146
+ // condition value — `before`/`after` get a real chronological compare
147
+ // instead, below.
98
148
  if (v instanceof Date)
99
149
  return Number.isNaN(v.getTime()) ? "" : v.toISOString().slice(0, 10);
100
150
  if (typeof v === "object") {
@@ -103,13 +153,67 @@ const asText = (v) => {
103
153
  }
104
154
  return String(v);
105
155
  };
156
+ // #593: `lessthan`/`greaterthan` (number columns) and `before`/`after` (date
157
+ // columns) need a real numeric/chronological compare, not `asText`'s string
158
+ // one — `null` on anything that doesn't parse, so a condition against a
159
+ // malformed or missing value never matches rather than throwing.
160
+ const toNumberOrNull = (v) => {
161
+ if (typeof v === "number")
162
+ return Number.isNaN(v) ? null : v;
163
+ if (typeof v === "string" && v.trim() !== "") {
164
+ const n = Number(v);
165
+ return Number.isNaN(n) ? null : n;
166
+ }
167
+ return null;
168
+ };
169
+ const toDateOrNull = (v) => {
170
+ if (v instanceof Date)
171
+ return Number.isNaN(v.getTime()) ? null : v;
172
+ if (typeof v === "string" || typeof v === "number") {
173
+ const d = new Date(v);
174
+ return Number.isNaN(d.getTime()) ? null : d;
175
+ }
176
+ return null;
177
+ };
178
+ // A `type: "date"` condition's own value is stored as a plain ISO date
179
+ // string ("2026-09-25") — machine-comparable (`before`/`after` above,
180
+ // `is`/`is not` via `asText`) and locale-independent, the same choice #185
181
+ // already made for `asText`'s own `Date` branch. Parsed/formatted at local
182
+ // midnight, not UTC, so the picked calendar day and the displayed day are
183
+ // always the same one regardless of timezone offset.
184
+ const isoToDate = (iso) => {
185
+ if (!iso)
186
+ return null;
187
+ const d = new Date(`${iso}T00:00:00`);
188
+ return Number.isNaN(d.getTime()) ? null : d;
189
+ };
190
+ const dateToISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
191
+ // e.g. "Sep 25, 2026" — the same format `DatePickerField`'s own default
192
+ // uses, and what Attio's chip itself showed after picking "Today" live.
193
+ const formatFilterDate = (iso) => {
194
+ const d = isoToDate(iso);
195
+ return d
196
+ ? d.toLocaleDateString(undefined, {
197
+ month: "short",
198
+ day: "numeric",
199
+ year: "numeric",
200
+ })
201
+ : iso;
202
+ };
203
+ const toDatePickerDate = (d) => ({
204
+ year: d.getFullYear(),
205
+ month: d.getMonth() + 1,
206
+ day: d.getDate(),
207
+ });
208
+ const fromDatePickerDate = (d) => new Date(d.year, d.month - 1, d.day);
106
209
  /**
107
210
  * Applies a toolbar's filter conditions to any row array. Reads the raw value
108
211
  * at `row[key]` (override with `get` for derived fields) — never a formatted
109
212
  * display string, so a grouped-thousands "$1,200" still matches "1200".
110
213
  */
111
214
  function applyFilters(rows, filters, get = (row, key) => row[key]) {
112
- const active = filters.filter((f) => (!opNeedsValue(f.op) || f.value.trim() !== "") &&
215
+ const active = filters.filter((f) => f.op != null &&
216
+ (!opNeedsValue(f.op) || f.value.trim() !== "") &&
113
217
  (!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
114
218
  if (!active.length)
115
219
  return rows;
@@ -119,6 +223,20 @@ function applyFilters(rows, filters, get = (row, key) => row[key]) {
119
223
  return isEmptyValue(raw);
120
224
  if (f.op === "notempty")
121
225
  return !isEmptyValue(raw);
226
+ if (f.op === "lessthan" || f.op === "greaterthan") {
227
+ const n = toNumberOrNull(raw);
228
+ const cn = toNumberOrNull(f.value);
229
+ if (n == null || cn == null)
230
+ return false;
231
+ return f.op === "lessthan" ? n < cn : n > cn;
232
+ }
233
+ if (f.op === "before" || f.op === "after") {
234
+ const d = toDateOrNull(raw);
235
+ const cd = toDateOrNull(f.value);
236
+ if (d == null || cd == null)
237
+ return false;
238
+ return f.op === "before" ? d < cd : d > cd;
239
+ }
122
240
  const t = asText(raw).toLowerCase();
123
241
  if (f.op === "isanyof")
124
242
  return (f.values ?? []).some((v) => v.toLowerCase() === t);
@@ -177,7 +295,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
177
295
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
178
296
  * Created by), which one shared list can't reproduce.
179
297
  */
180
- 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, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, ...rest }) {
298
+ 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, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, ...rest }) {
181
299
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
182
300
  // CrispMessagesProvider's translation of the English default below, or
183
301
  // that default itself with no provider — unchanged from before this
@@ -311,11 +429,29 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
311
429
  };
312
430
  const addFilter = (key) => {
313
431
  const id = `f${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
314
- putFilters([...filters, { id, key, op: "contains", value: "" }]);
432
+ putFilters([...filters, { id, key, value: "" }]);
315
433
  setFilterQuery("");
316
- setOpenMenu(`fcond:${id}`);
434
+ // matches Attio: picking a field opens the operator menu next, not a
435
+ // combined dialog — measured live, 2026-09-25.
436
+ setOpenMenu(`fop:${id}`);
317
437
  };
318
438
  const patchFilter = (id, patch) => putFilters(filters.map((c) => (c.id === id ? { ...c, ...patch } : c)));
439
+ // The field segment's own picker (#462 follow-up): changing the field
440
+ // replaces it in place — confirmed live, one chip, not a second — and
441
+ // resets op/value/values, since neither is guaranteed to make sense for
442
+ // the new field's type (a text op on a select field, e.g.).
443
+ const changeFilterField = (id, key) => {
444
+ patchFilter(id, { key, op: undefined, value: "", values: undefined });
445
+ setFilterQuery("");
446
+ setOpenMenu(`fop:${id}`);
447
+ };
448
+ // The operator segment's own menu: picking an operator that needs a value
449
+ // auto-opens the value popover, focused; "empty"/"not empty" need nothing
450
+ // further, so the menu just closes — both measured live, 2026-09-25.
451
+ const pickOp = (id, op) => {
452
+ patchFilter(id, { op });
453
+ setOpenMenu(opNeedsValue(op) || opNeedsValues(op) ? `fval:${id}` : null);
454
+ };
319
455
  const removeFilter = (id) => {
320
456
  putFilters(filters.filter((c) => c.id !== id));
321
457
  setOpenMenu(null);
@@ -354,6 +490,23 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
354
490
  close: () => setOpenMenu(null),
355
491
  contentRef: kebabMenuRef,
356
492
  });
493
+ // The operator popover is `role="menu"` too — confirmed live, 2026-09-25 —
494
+ // so it gets the same shared keyboard model as the kebab above, one hook
495
+ // call tracking whichever condition's operator menu is open.
496
+ const opMenuRef = React.useRef(null);
497
+ const { onKeyDown: onOpKeyDown } = (0, menu_1.useMenuKeyboard)({
498
+ open: typeof openMenu === "string" && openMenu.startsWith("fop:"),
499
+ close: () => setOpenMenu(null),
500
+ contentRef: opMenuRef,
501
+ });
502
+ // Shared between the "Filter" trigger's own add-condition picker and each
503
+ // chip's field segment (#462 follow-up) — both search the same
504
+ // `filterColumns` list and differ only in what a pick does.
505
+ const filterFieldPicker = (onSelect) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
506
+ // biome-ignore lint/a11y/noAutofocus: focus the picker on open
507
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
508
+ .filter((col) => col.label.toLowerCase().includes(filterQuery.trim().toLowerCase()))
509
+ .map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => onSelect(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] }));
357
510
  const activeFilters = filters.length;
358
511
  // The default phrase only works in English word order, so a caller can
359
512
  // replace the whole thing rather than fill a template — crisp/#471's
@@ -368,7 +521,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
368
521
  : sorts.length
369
522
  ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
370
523
  : sortLabel;
371
- 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) => {
524
+ 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: [showSort && ((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) => {
372
525
  if (dragSort !== null)
373
526
  e.preventDefault();
374
527
  }, onDrop: () => {
@@ -390,47 +543,83 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
390
543
  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] })),
391
544
  keywords: descendingLabel,
392
545
  },
393
- ], "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
546
+ ], "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] }) }))] }))] })), showSort && ((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
394
547
  ? filterTriggerLabel(activeFilters)
395
548
  : activeFilters
396
549
  ? `${filterLabel} · ${activeFilters}`
397
- : 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,
398
- // biome-ignore lint/a11y/noAutofocus: focus the picker on open
399
- autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
400
- .filter((col) => col.label
401
- .toLowerCase()
402
- .includes(filterQuery.trim().toLowerCase()))
403
- .map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => addFilter(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] }))] }), filters.map((cond) => {
550
+ : filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
404
551
  const col = filterColumns.find((c) => c.key === cond.key);
405
- const open = openMenu === `fcond:${cond.id}`;
552
+ const fieldOpen = openMenu === `ffield:${cond.id}`;
553
+ const opOpen = openMenu === `fop:${cond.id}`;
554
+ const valOpen = openMenu === `fval:${cond.id}`;
406
555
  const kebabOpen = openMenu === `fkebab:${cond.id}`;
407
- const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
556
+ // Gates the field/value segments' background (white once the
557
+ // condition is usable, the measured `f6f7f7` otherwise) — NOT
558
+ // whether the value popover renders; see `showValueSeg` below.
559
+ const complete = cond.op != null &&
560
+ (!opNeedsValue(cond.op) || cond.value.trim()) &&
408
561
  (!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
409
- 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]] ??
410
- 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", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsx)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => removeFilter(cond.id), children: deleteFilterLabel }) })), 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`,
411
- // biome-ignore lint/a11y/noAutofocus: focus the value field
412
- 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`,
413
- // biome-ignore lint/a11y/noAutofocus: focus the search field
414
- autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
415
- .filter((v) => v
416
- .toLowerCase()
417
- .includes(anyOfQuery.trim().toLowerCase()))
418
- .map((v) => {
419
- const checked = (cond.values ?? []).includes(v);
420
- return ((0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
421
- const set = new Set(cond.values ?? []);
422
- if (next)
423
- set.add(v);
424
- else
425
- set.delete(v);
426
- // the column's own order, not click order —
427
- // what the chip summary reads out
428
- patchFilter(cond.id, {
429
- values: col.values?.filter((x) => set.has(x)),
430
- });
431
- }, children: v }, v));
432
- })] }))] }))] }, cond.id));
433
- }), (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", onClick: () => {
562
+ // No value segment at all for "empty"/"not empty" (nothing to
563
+ // show) or before an operator is even chosen — matches Attio,
564
+ // which never renders a third box in either case.
565
+ const showValueSeg = cond.op != null &&
566
+ (opNeedsValue(cond.op) || opNeedsValues(cond.op));
567
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--field", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": fieldOpen, onClick: () => setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`), children: [col?.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 14, strokeWidth: 1.75 }) : null, col?.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--op", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": opOpen, onClick: () => setOpenMenu(opOpen ? null : `fop:${cond.id}`), children: cond.op
568
+ ? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
569
+ : selectConditionLabel }), showValueSeg && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--val", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": valOpen, onClick: () => setOpenMenu(valOpen ? null : `fval:${cond.id}`), children: cond.op === "isanyof"
570
+ ? cond.values?.length
571
+ ? isAnyOfSummary(cond.values)
572
+ : filterValuePlaceholder
573
+ : cond.value.trim()
574
+ ? col?.type === "date"
575
+ ? formatFilterDate(cond.value)
576
+ : cond.value
577
+ : filterValuePlaceholder })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker((key) => changeFilterField(cond.id, key)) })), opOpen && ((0, jsx_runtime_1.jsx)("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(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))) })), valOpen && showValueSeg && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: 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`,
578
+ // biome-ignore lint/a11y/noAutofocus: focus the search field
579
+ autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
580
+ .filter((v) => v
581
+ .toLowerCase()
582
+ .includes(anyOfQuery.trim().toLowerCase()))
583
+ .map((v) => {
584
+ const checked = (cond.values ?? []).includes(v);
585
+ return ((0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
586
+ const set = new Set(cond.values ?? []);
587
+ if (next)
588
+ set.add(v);
589
+ else
590
+ set.delete(v);
591
+ // the column's own order, not click order —
592
+ // what the chip summary reads out
593
+ patchFilter(cond.id, {
594
+ values: col.values?.filter((x) => set.has(x)),
595
+ });
596
+ }, children: v }, v));
597
+ })] })) : col?.type === "date" ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-datepop", children: [(0, jsx_runtime_1.jsx)(datepicker_1.DatePicker, { ...{
598
+ selectionMode: "single",
599
+ value: isoToDate(cond.value)
600
+ ? toDatePickerDate(isoToDate(cond.value))
601
+ : undefined,
602
+ onValueChange: (next) => {
603
+ if (!next)
604
+ return;
605
+ patchFilter(cond.id, {
606
+ value: dateToISO(fromDatePickerDate(next)),
607
+ });
608
+ setOpenMenu(null);
609
+ },
610
+ } }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-datepop-presets", children: [
611
+ [crispMessages.Today ?? "Today", 0],
612
+ [crispMessages.Tomorrow ?? "Tomorrow", 1],
613
+ [crispMessages["Next week"] ?? "Next week", 7],
614
+ ].map(([label, addDays]) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-datepop-preset", onClick: () => {
615
+ const d = new Date();
616
+ d.setDate(d.getDate() + addDays);
617
+ patchFilter(cond.id, { value: dateToISO(d) });
618
+ setOpenMenu(null);
619
+ }, children: label }, label))) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", type: col?.type === "number" ? "number" : "text", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
620
+ // biome-ignore lint/a11y/noAutofocus: focus the value field
621
+ autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
622
+ }), showViewSettings && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && ((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", onClick: () => {
434
623
  if (openMenu === "viewsettings") {
435
624
  setOpenMenu(null);
436
625
  }
@@ -454,7 +643,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
454
643
  moveAttr(dragAttr, i);
455
644
  setDragAttr(null);
456
645
  }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
457
- `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
646
+ `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
458
647
  }
459
648
  /**
460
649
  * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
@@ -4,6 +4,49 @@ exports.tabletoolbarRecipe = void 0;
4
4
  const core_1 = require("../../core");
5
5
  const tokens_1 = require("../../tokens");
6
6
  const tabletoolbar_chrome_1 = require("./tabletoolbar.chrome");
7
+ // The filter condition chip, TableToolbar-only (crisp/#462 follow-up,
8
+ // re-measured live 2026-09-25). Deliberately NOT added to the shared
9
+ // `tabletoolbar.chrome.ts` functions: those also render under RecordsTable's
10
+ // own `crisp-recordstable-` prefix, whose still-tested (84 cases) chip
11
+ // predates this re-measurement and keeps its original `.fchip`/`.fchip-main`
12
+ // markup (one combined field+op+value button, plus a small floating kebab)
13
+ // until RecordsTable is rebuilt onto DataTable + TableToolbar and inherits
14
+ // this one instead. New, disjoint class names here so nothing shared changes
15
+ // meaning under the older markup.
16
+ //
17
+ // Measured on Attio's own chip (`app.attio.com/<ws>/companies`, a text-field
18
+ // condition): 4 independently-clickable, edge-to-edge segments, each 28px
19
+ // tall with its own `inset 0 0 0 1px` ring — field (radius 8/0/0/8), operator
20
+ // (flat), value (flat), kebab (radius 0/8/8/0). Background is NOT fixed per
21
+ // segment: the field and value segments (the two that show the condition's
22
+ // own text) read `rgb(246,247,247)` while the condition has no value yet and
23
+ // flip to white the instant it does (typing a value, or picking an operator
24
+ // like "empty" that needs none, both trigger it); the operator and kebab
25
+ // segments (pure controls, no text of the condition's own) stay white
26
+ // throughout. Click targets, confirmed live: the field segment reopens the
27
+ // attribute picker and replaces this condition's field in place (not a
28
+ // second chip); the operator segment opens an operator-only menu (closing it
29
+ // after a pick, then auto-opening the value popover, focused, when that
30
+ // operator needs one — "empty"/"not empty" just close); the value segment
31
+ // opens a value-only popover/input, independent of the operator menu (Attio
32
+ // never combines the two, unlike crisp's older single dialog).
33
+ const filterChipChrome = `
34
+ & .crisp-tabletoolbar-fcond { display: inline-flex; align-items: center; height: ${tokens_1.contract.size.row}; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgPrimary}; white-space: nowrap; }
35
+ & .crisp-tabletoolbar-fcond-seg { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: 100%; margin: 0; padding: 0 ${tokens_1.contract.space["2"]}; border: none; background: ${tokens_1.contract.bgSurfaceMuted}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDivider}; font-family: inherit; font-size: inherit; color: inherit; cursor: pointer; }
36
+ & .crisp-tabletoolbar-fcond-seg svg { flex-shrink: 0; }
37
+ & .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--field,
38
+ & .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: ${tokens_1.contract.bgSurface}; }
39
+ & .crisp-tabletoolbar-fcond-seg--field { border-radius: ${tokens_1.contract.radius.md} 0 0 ${tokens_1.contract.radius.md}; font-weight: ${tokens_1.contract.weight.medium}; }
40
+ & .crisp-tabletoolbar-fcond-seg--field svg { color: ${tokens_1.contract.fgMuted}; }
41
+ & .crisp-tabletoolbar-fcond-seg--op { background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgMuted}; }
42
+ & .crisp-tabletoolbar-fcond-seg--val { font-weight: ${tokens_1.contract.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
43
+ & .crisp-tabletoolbar-fcond-seg--kebab { background: ${tokens_1.contract.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${tokens_1.contract.radius.md} ${tokens_1.contract.radius.md} 0; color: ${tokens_1.contract.fgMuted}; }
44
+ & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${tokens_1.contract.fgPrimary}; }
45
+ & .crisp-tabletoolbar-menu--val { min-width: 134px; }
46
+ & .crisp-tabletoolbar-datepop { width: 300px; }
47
+ & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${tokens_1.contract.space["1"]}; margin-top: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["1"]}; }
48
+ & .crisp-tabletoolbar-datepop-preset { height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.chip}; background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.sm}; cursor: pointer; }
49
+ & .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); }`;
7
50
  // The table toolbar, measured: 28px controls, the Sort/Filter buttons and
8
51
  // the menus they open, sort-condition rows, and the filter chips. The chrome
9
52
  // itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
@@ -20,6 +63,7 @@ const config = (0, core_1.defineRecipe)({
20
63
  & * { box-sizing: border-box; }
21
64
  ${(0, tabletoolbar_chrome_1.toolbarChromeHead)("tabletoolbar")}
22
65
  ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("tabletoolbar")}
66
+ ${filterChipChrome}
23
67
  `,
24
68
  });
25
69
  exports.tabletoolbarRecipe = (0, core_1.resolveRecipe)(config);
@@ -14,7 +14,16 @@ const tokens_1 = require("../../tokens");
14
14
  // inactive tabs are muted and fill to a subtle pill on hover. Each tab carries
15
15
  // a 16px leading icon and an optional count badge. Measured from the live
16
16
  // app: bar `padding: 0 12px 8px`, 28px pills, 4/8 padding, 6px icon-label
17
- // gap, 14/500 labels, 10px badges (rgba(0,0,0,.04)).
17
+ // gap, 14/500 labels. The count badge (crisp#697, re-measured against a
18
+ // record's tab counts and a settings "Attributes 35" count) is 14px
19
+ // tall/wide-min, radius 4px (no scale token lands there -- a recipe-local
20
+ // literal, same precedent as this file's own icon/gap literals), 10/500/14
21
+ // text on a 4% `fgPrimary` tint (`rgba(0,0,0,.04)`) with a measured
22
+ // `inset 0 0 0 1px borderSubtle` ring, text `fgNeutralSoft` (rgb(80,81,85),
23
+ // the same token Badge's neutral tone uses, #696) -- was 16px/r6/11px/
24
+ // `fgMuted`/6% tint with no ring at all. The selected tab's own badge
25
+ // colour (`fgPrimary`) wasn't measured (the live Overview tab carries no
26
+ // count) and stays as-is.
18
27
  //
19
28
  // The panel's own `padding-top` (`c.space["8"]`, 32px) is measured
20
29
  // separately, live, on a company record page (Overview tab): the gap from
@@ -45,7 +54,7 @@ const config = (0, core_1.defineRecipe)({
45
54
  & .crisp-tabs-tab:hover:not([data-selected]) { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); color: ${tokens_1.contract.fgPrimary}; }
46
55
  & .crisp-tabs-tab[data-selected] { color: ${tokens_1.contract.fgPrimary}; background: ${tokens_1.contract.bgSubtle}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; }
47
56
  & .crisp-tabs-tab:focus-visible { outline: 2px solid ${tokens_1.contract.bgBrandSolid}; outline-offset: 1px; }
48
- & .crisp-tabs-tab-badge { 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}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
57
+ & .crisp-tabs-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; padding: 0 ${tokens_1.contract.space["1"]}; border-radius: 4px; background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 4%, transparent); box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; font-size: 10px; line-height: 14px; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgNeutralSoft}; }
49
58
  & .crisp-tabs-tab[data-selected] .crisp-tabs-tab-badge { color: ${tokens_1.contract.fgPrimary}; }
50
59
  & .crisp-tabs-indicator { position: absolute; bottom: 0; left: 0; z-index: 1; height: 1px; background: ${tokens_1.contract.fgPrimary}; border-radius: 2px 2px 0 0; transition: transform .2s cubic-bezier(.4,0,.2,1), width .2s cubic-bezier(.4,0,.2,1); pointer-events: none; }
51
60
  & .crisp-tabs-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; padding-top: ${tokens_1.contract.space["8"]}; }
@@ -103,12 +103,24 @@ style: _style, ...rest }, forwardedRef) => {
103
103
  if (!el)
104
104
  return;
105
105
  const r = el.getBoundingClientRect();
106
- const below = side === "bottom" || r.top < 48;
107
- setPos({
108
- left: Math.round(r.left + r.width / 2),
109
- top: Math.round(below ? r.bottom + 6 : r.top - 6),
110
- below,
111
- });
106
+ const placement = side === "right"
107
+ ? "right"
108
+ : side === "bottom" || r.top < 48
109
+ ? "bottom"
110
+ : "top";
111
+ setPos(placement === "right"
112
+ ? {
113
+ // Measured (crisp#633): the bubble's left edge sits 8px past
114
+ // the trigger's right edge, vertically centred on it.
115
+ left: Math.round(r.right + 8),
116
+ top: Math.round(r.top + r.height / 2),
117
+ placement,
118
+ }
119
+ : {
120
+ left: Math.round(r.left + r.width / 2),
121
+ top: Math.round(placement === "bottom" ? r.bottom + 6 : r.top - 6),
122
+ placement,
123
+ });
112
124
  }, [open, side]);
113
125
  const openAfterDelay = () => {
114
126
  clearTimeout(timer.current);
@@ -155,9 +167,11 @@ style: _style, ...rest }, forwardedRef) => {
155
167
  (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { id: tipId, role: "tooltip", className: (0, tooltip_recipe_1.tooltipRecipe)({ variant }), style: {
156
168
  left: pos.left,
157
169
  top: pos.top,
158
- transform: pos.below
159
- ? "translate(-50%, 0)"
160
- : "translate(-50%, -100%)",
170
+ transform: pos.placement === "right"
171
+ ? "translate(0, -50%)"
172
+ : pos.placement === "bottom"
173
+ ? "translate(-50%, 0)"
174
+ : "translate(-50%, -100%)",
161
175
  }, children: content }), document.body);
162
176
  if (asChild) {
163
177
  // No wrapper at all: the child carries the hover/focus/Escape
@@ -16,14 +16,15 @@ const tokens_1 = require("../../tokens");
16
16
  // just the wrong pair.
17
17
  //
18
18
  // crisp#563: this `boxShadow` is Attio's PLAIN tooltip (a truncated header
19
- // label, measured on "Twitter follower count") — still an uncorroborated
20
- // single sample, flagged for its own re-measurement pass, not touched here.
21
- // Attio has a SECOND, visually similar but structurally different tooltip
22
- // for a reference-column header (icon + the referenced type's name after
23
- // the label, crisp#580) with its own measured shadow — an inset ring plus
24
- // two offset shadows, `variant: "reference"` below. Background, text
25
- // colour, radius, padding and font are identical between the two measured
26
- // samples; only the shadow differs.
19
+ // label, measured on "Twitter follower count"). Re-measured live and
20
+ // corroborated against a second sample (hovering the same field a second
21
+ // time) per AUTHORING.md Lesson 22 — the value below is an inset ring plus
22
+ // two offset shadows, not the single offset shadow this recipe carried
23
+ // before. Attio has a SECOND, visually similar but structurally different
24
+ // tooltip for a reference-column header (icon + the referenced type's name
25
+ // after the label, crisp#580) with its own measured shadow, `variant:
26
+ // "reference"` below. Background, text colour, radius, padding and font are
27
+ // identical between the two measured samples; only the shadow differs.
27
28
  const spec = (0, core_1.defineSpec)({
28
29
  name: "tooltip",
29
30
  base: {
@@ -40,7 +41,7 @@ const spec = (0, core_1.defineSpec)({
40
41
  fontSize: tokens_1.contract.text.sm,
41
42
  fontWeight: tokens_1.contract.weight.medium,
42
43
  lineHeight: "1.45",
43
- boxShadow: "0 4px 14px -2px rgba(28,40,64,0.28), 0 0 0 1px rgba(255,255,255,0.04)",
44
+ boxShadow: "rgb(33,34,36) 0 0 0 1px inset, rgba(0,0,0,0.04) 0 0 2px 0, rgba(0,0,0,0.12) 0 1px 3px 0",
44
45
  opacity: 1,
45
46
  transition: "opacity 0.12s ease",
46
47
  },