@crisp-ui-kit/crisp 0.54.7 → 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 +12 -3
  17. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +231 -48
  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 +12 -3
  52. package/dist/esm/components/tabletoolbar/tabletoolbar.js +231 -48
  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 +1 -1
  80. package/dist/recordstable.layered.css +1 -1
  81. package/dist/sidebar.css +6 -1
  82. package/dist/sidebar.layered.css +6 -1
  83. package/dist/styles.css +163 -42
  84. package/dist/styles.layered.css +163 -42
  85. package/dist/tabletoolbar.css +16 -1
  86. package/dist/tabletoolbar.layered.css +16 -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 +43 -3
  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");
@@ -60,11 +61,24 @@ const OP_LABELS = {
60
61
  // review.
61
62
  empty: "empty",
62
63
  notempty: "not empty",
64
+ lessthan: "less than",
65
+ greaterthan: "greater than",
66
+ before: "before",
67
+ after: "after",
63
68
  };
64
69
  // #619: order and label set match Attio's own text-filter operator picker
65
70
  // verbatim (measured live, per RecordsTable's own #578 port of the same
66
71
  // set: contains · does not contain · starts with · ends with · is · is not ·
67
- // 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.
68
82
  const OP_ORDER = [
69
83
  "contains",
70
84
  "doesnotcontain",
@@ -75,15 +89,44 @@ const OP_ORDER = [
75
89
  "empty",
76
90
  "notempty",
77
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
+ ];
78
108
  // "isanyof" is appended only for a column that supplies `values` — there is
79
- // nothing to check off otherwise.
80
- 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
+ };
81
120
  const opNeedsValue = (op) => op === "contains" ||
82
121
  op === "doesnotcontain" ||
83
122
  op === "startswith" ||
84
123
  op === "endswith" ||
85
124
  op === "is" ||
86
- op === "isnot";
125
+ op === "isnot" ||
126
+ op === "lessthan" ||
127
+ op === "greaterthan" ||
128
+ op === "before" ||
129
+ op === "after";
87
130
  const opNeedsValues = (op) => op === "isanyof";
88
131
  // "Department is any of IT, Finance" past two values becomes "… +N": the
89
132
  // chip is one line, and a field can have far more than two facet values.
@@ -98,9 +141,10 @@ const asText = (v) => {
98
141
  return v.join(", ");
99
142
  // Before the generic object branch, which would look for `.name`, find none
100
143
  // and return "" — turning every Date into the empty string (#185). The ISO
101
- // date is stable and locale-independent, so `contains "2026-06"` works; it
102
- // will not match "16/06/2026", and a date-aware operator set needs a
103
- // 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.
104
148
  if (v instanceof Date)
105
149
  return Number.isNaN(v.getTime()) ? "" : v.toISOString().slice(0, 10);
106
150
  if (typeof v === "object") {
@@ -109,13 +153,67 @@ const asText = (v) => {
109
153
  }
110
154
  return String(v);
111
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);
112
209
  /**
113
210
  * Applies a toolbar's filter conditions to any row array. Reads the raw value
114
211
  * at `row[key]` (override with `get` for derived fields) — never a formatted
115
212
  * display string, so a grouped-thousands "$1,200" still matches "1200".
116
213
  */
117
214
  function applyFilters(rows, filters, get = (row, key) => row[key]) {
118
- 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() !== "") &&
119
217
  (!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
120
218
  if (!active.length)
121
219
  return rows;
@@ -125,6 +223,20 @@ function applyFilters(rows, filters, get = (row, key) => row[key]) {
125
223
  return isEmptyValue(raw);
126
224
  if (f.op === "notempty")
127
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
+ }
128
240
  const t = asText(raw).toLowerCase();
129
241
  if (f.op === "isanyof")
130
242
  return (f.values ?? []).some((v) => v.toLowerCase() === t);
@@ -183,7 +295,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
183
295
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
184
296
  * Created by), which one shared list can't reproduce.
185
297
  */
186
- 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 }) {
187
299
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
188
300
  // CrispMessagesProvider's translation of the English default below, or
189
301
  // that default itself with no provider — unchanged from before this
@@ -317,11 +429,29 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
317
429
  };
318
430
  const addFilter = (key) => {
319
431
  const id = `f${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
320
- putFilters([...filters, { id, key, op: "contains", value: "" }]);
432
+ putFilters([...filters, { id, key, value: "" }]);
321
433
  setFilterQuery("");
322
- 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}`);
323
437
  };
324
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
+ };
325
455
  const removeFilter = (id) => {
326
456
  putFilters(filters.filter((c) => c.id !== id));
327
457
  setOpenMenu(null);
@@ -360,6 +490,23 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
360
490
  close: () => setOpenMenu(null),
361
491
  contentRef: kebabMenuRef,
362
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)))] }));
363
510
  const activeFilters = filters.length;
364
511
  // The default phrase only works in English word order, so a caller can
365
512
  // replace the whole thing rather than fill a template — crisp/#471's
@@ -374,7 +521,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
374
521
  : sorts.length
375
522
  ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
376
523
  : sortLabel;
377
- 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) => {
378
525
  if (dragSort !== null)
379
526
  e.preventDefault();
380
527
  }, onDrop: () => {
@@ -396,47 +543,83 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
396
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] })),
397
544
  keywords: descendingLabel,
398
545
  },
399
- ], "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
400
547
  ? filterTriggerLabel(activeFilters)
401
548
  : activeFilters
402
549
  ? `${filterLabel} · ${activeFilters}`
403
- : 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,
404
- // biome-ignore lint/a11y/noAutofocus: focus the picker on open
405
- autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
406
- .filter((col) => col.label
407
- .toLowerCase()
408
- .includes(filterQuery.trim().toLowerCase()))
409
- .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) => {
410
551
  const col = filterColumns.find((c) => c.key === cond.key);
411
- 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}`;
412
555
  const kebabOpen = openMenu === `fkebab:${cond.id}`;
413
- 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()) &&
414
561
  (!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
415
- 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]] ??
416
- 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.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] }) })), open && ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", 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`,
417
- // biome-ignore lint/a11y/noAutofocus: focus the value field
418
- 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`,
419
- // biome-ignore lint/a11y/noAutofocus: focus the search field
420
- autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
421
- .filter((v) => v
422
- .toLowerCase()
423
- .includes(anyOfQuery.trim().toLowerCase()))
424
- .map((v) => {
425
- const checked = (cond.values ?? []).includes(v);
426
- return ((0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
427
- const set = new Set(cond.values ?? []);
428
- if (next)
429
- set.add(v);
430
- else
431
- set.delete(v);
432
- // the column's own order, not click order —
433
- // what the chip summary reads out
434
- patchFilter(cond.id, {
435
- values: col.values?.filter((x) => set.has(x)),
436
- });
437
- }, children: v }, v));
438
- })] }))] }))] }, cond.id));
439
- }), (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: () => {
440
623
  if (openMenu === "viewsettings") {
441
624
  setOpenMenu(null);
442
625
  }
@@ -460,7 +643,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
460
643
  moveAttr(dragAttr, i);
461
644
  setDragAttr(null);
462
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) ??
463
- `${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] }) }));
464
647
  }
465
648
  /**
466
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
  },