@crisp-ui-kit/crisp 0.54.7 → 0.54.9

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 (130) 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 +36 -8
  6. package/dist/board.layered.css +36 -8
  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/contextmenu/contextmenu.js +2 -2
  13. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +24 -0
  14. package/dist/cjs/components/datatable/datatable.js +590 -41
  15. package/dist/cjs/components/datatable/datatable.recipe.js +118 -5
  16. package/dist/cjs/components/dialog/dialog.js +72 -5
  17. package/dist/cjs/components/dialog/dialog.recipe.js +15 -1
  18. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
  19. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +365 -49
  20. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +78 -0
  21. package/dist/cjs/components/tabs/tabs.recipe.js +11 -2
  22. package/dist/cjs/components/tooltip/tooltip.js +23 -9
  23. package/dist/cjs/components/tooltip/tooltip.recipe.js +10 -9
  24. package/dist/cjs/product/authshell/authshell.js +14 -1
  25. package/dist/cjs/product/authshell/authshell.recipe.js +13 -1
  26. package/dist/cjs/product/board/board.js +175 -46
  27. package/dist/cjs/product/board/board.recipe.js +56 -4
  28. package/dist/cjs/product/comments/comments.js +34 -12
  29. package/dist/cjs/product/comments/comments.recipe.js +31 -7
  30. package/dist/cjs/product/formdialog/formdialog.js +10 -4
  31. package/dist/cjs/product/navitem/navitem.js +7 -2
  32. package/dist/cjs/product/navitem/navitem.recipe.js +16 -0
  33. package/dist/cjs/product/objectsettings/index.js +1 -2
  34. package/dist/cjs/product/objectsettings/objectsettings.js +3 -13
  35. package/dist/cjs/product/recordstable/recordstable.js +1 -17
  36. package/dist/cjs/product/richtext/richtext.js +76 -1
  37. package/dist/cjs/product/savedviews/savedviews.js +55 -24
  38. package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
  39. package/dist/cjs/product/sidebar/sidebar.js +68 -15
  40. package/dist/cjs/product/sidebar/sidebar.recipe.js +6 -1
  41. package/dist/cjs/tokens/palette.js +32 -1
  42. package/dist/cjs/tokens/semantic.js +11 -0
  43. package/dist/comments.css +50 -14
  44. package/dist/comments.layered.css +50 -14
  45. package/dist/contextmenu.css +24 -0
  46. package/dist/contextmenu.layered.css +24 -0
  47. package/dist/datatable.css +101 -37
  48. package/dist/datatable.layered.css +101 -37
  49. package/dist/dialog.css +13 -1
  50. package/dist/dialog.layered.css +13 -1
  51. package/dist/esm/components/badge/badge.recipe.js +28 -5
  52. package/dist/esm/components/button/button.js +34 -3
  53. package/dist/esm/components/button/button.recipe.js +31 -5
  54. package/dist/esm/components/contextmenu/contextmenu.js +2 -2
  55. package/dist/esm/components/contextmenu/contextmenu.recipe.js +24 -0
  56. package/dist/esm/components/datatable/datatable.js +590 -42
  57. package/dist/esm/components/datatable/datatable.recipe.js +119 -6
  58. package/dist/esm/components/dialog/dialog.js +72 -5
  59. package/dist/esm/components/dialog/dialog.recipe.js +15 -1
  60. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +49 -5
  61. package/dist/esm/components/tabletoolbar/tabletoolbar.js +366 -50
  62. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +80 -2
  63. package/dist/esm/components/tabs/tabs.recipe.js +11 -2
  64. package/dist/esm/components/tooltip/tooltip.js +23 -9
  65. package/dist/esm/components/tooltip/tooltip.recipe.js +10 -9
  66. package/dist/esm/product/authshell/authshell.js +15 -2
  67. package/dist/esm/product/authshell/authshell.recipe.js +13 -1
  68. package/dist/esm/product/authshell/index.js +1 -1
  69. package/dist/esm/product/board/board.js +176 -47
  70. package/dist/esm/product/board/board.recipe.js +56 -4
  71. package/dist/esm/product/comments/comments.js +35 -13
  72. package/dist/esm/product/comments/comments.recipe.js +31 -7
  73. package/dist/esm/product/formdialog/formdialog.js +10 -4
  74. package/dist/esm/product/navitem/index.js +1 -1
  75. package/dist/esm/product/navitem/navitem.js +7 -2
  76. package/dist/esm/product/navitem/navitem.recipe.js +16 -0
  77. package/dist/esm/product/objectsettings/index.js +1 -1
  78. package/dist/esm/product/objectsettings/objectsettings.js +2 -12
  79. package/dist/esm/product/recordstable/recordstable.js +2 -18
  80. package/dist/esm/product/richtext/richtext.js +76 -1
  81. package/dist/esm/product/savedviews/savedviews.js +57 -26
  82. package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
  83. package/dist/esm/product/sidebar/sidebar.js +69 -16
  84. package/dist/esm/product/sidebar/sidebar.recipe.js +6 -1
  85. package/dist/esm/tokens/palette.js +32 -1
  86. package/dist/esm/tokens/semantic.js +11 -0
  87. package/dist/native/tokens.d.ts +1 -1
  88. package/dist/native/tokens.js +16 -2
  89. package/dist/navitem.css +7 -0
  90. package/dist/navitem.layered.css +7 -0
  91. package/dist/recordstable.css +34 -32
  92. package/dist/recordstable.layered.css +34 -32
  93. package/dist/savedviews.css +27 -0
  94. package/dist/savedviews.layered.css +27 -0
  95. package/dist/sidebar.css +6 -1
  96. package/dist/sidebar.layered.css +6 -1
  97. package/dist/styles.css +369 -106
  98. package/dist/styles.layered.css +369 -106
  99. package/dist/tabletoolbar.css +47 -3
  100. package/dist/tabletoolbar.layered.css +47 -3
  101. package/dist/tabs.css +1 -1
  102. package/dist/tabs.layered.css +1 -1
  103. package/dist/tokens.css +9 -1
  104. package/dist/tokens.json +16 -2
  105. package/dist/tooltip.css +1 -1
  106. package/dist/tooltip.layered.css +1 -1
  107. package/dist/types/components/badge/badge.recipe.d.ts +5 -1
  108. package/dist/types/components/button/button.recipe.d.ts +18 -5
  109. package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
  110. package/dist/types/components/datatable/datatable.d.ts +98 -1
  111. package/dist/types/components/dialog/dialog.d.ts +25 -2
  112. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +113 -3
  113. package/dist/types/components/tooltip/tooltip.d.ts +12 -4
  114. package/dist/types/components/tooltip/tooltip.recipe.d.ts +1 -1
  115. package/dist/types/product/authshell/authshell.d.ts +26 -2
  116. package/dist/types/product/authshell/index.d.ts +1 -1
  117. package/dist/types/product/board/board.d.ts +53 -6
  118. package/dist/types/product/comments/comments.d.ts +16 -3
  119. package/dist/types/product/formdialog/formdialog.d.ts +2 -1
  120. package/dist/types/product/navitem/index.d.ts +1 -1
  121. package/dist/types/product/navitem/navitem.d.ts +23 -0
  122. package/dist/types/product/objectsettings/index.d.ts +1 -1
  123. package/dist/types/product/objectsettings/objectsettings.d.ts +0 -10
  124. package/dist/types/product/recordstable/recordstable.d.ts +0 -1
  125. package/dist/types/product/savedviews/savedviews.d.ts +45 -8
  126. package/dist/types/product/sidebar/sidebar.d.ts +33 -5
  127. package/dist/types/tokens/contract.d.ts +4 -0
  128. package/dist/types/tokens/palette.d.ts +13 -1
  129. package/dist/types/tokens/semantic.d.ts +11 -0
  130. package/package.json +1 -1
@@ -41,9 +41,18 @@ 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");
48
+ // #592: shared by every direction-bearing ChevronRight usage below (the
49
+ // field picker's own disclosure chevron, a flattened search result's
50
+ // breadcrumb separator, and a filter chip's own breadcrumb separator) so
51
+ // the RTL mirror attribute lives in exactly one JSX tag — build/spec-check
52
+ // .mjs's icon sweep counts that attribute per icon NAME across the whole
53
+ // file, not per call site, and three independently attributed tags for
54
+ // the same name over-counts against that (it expects one per name).
55
+ const RtlChevronRight = ({ size, className, }) => ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { "data-rtl-flip": "", size: size, strokeWidth: 1.75, className: className }));
47
56
  const OP_LABELS = {
48
57
  contains: "contains",
49
58
  doesnotcontain: "does not contain",
@@ -60,11 +69,24 @@ const OP_LABELS = {
60
69
  // review.
61
70
  empty: "empty",
62
71
  notempty: "not empty",
72
+ lessthan: "less than",
73
+ greaterthan: "greater than",
74
+ before: "before",
75
+ after: "after",
63
76
  };
64
77
  // #619: order and label set match Attio's own text-filter operator picker
65
78
  // verbatim (measured live, per RecordsTable's own #578 port of the same
66
79
  // set: contains · does not contain · starts with · ends with · is · is not ·
67
- // empty · not empty).
80
+ // empty · not empty). #593: number and date columns get their OWN shorter
81
+ // sets instead — also measured live (`app.attio.com`, a Twitter follower
82
+ // count / Foundation date column, each filter added then discarded): number
83
+ // is "is / is not / less than / greater than / empty / not empty" (104.86px
84
+ // wide, 6 rows); date is "is / is not / before / after / empty / not empty"
85
+ // (91.16px, 6 rows) — both narrower than the text set's own 156.5px, which
86
+ // is why `.crisp-tabletoolbar-menu--op`'s old fixed 134px floor is gone (see
87
+ // tabletoolbar.recipe.ts): every one of these three sets is real, live-
88
+ // measured content-driven width, and a floor sized for one of them
89
+ // overshoots the other two.
68
90
  const OP_ORDER = [
69
91
  "contains",
70
92
  "doesnotcontain",
@@ -75,15 +97,79 @@ const OP_ORDER = [
75
97
  "empty",
76
98
  "notempty",
77
99
  ];
100
+ const NUMBER_OP_ORDER = [
101
+ "is",
102
+ "isnot",
103
+ "lessthan",
104
+ "greaterthan",
105
+ "empty",
106
+ "notempty",
107
+ ];
108
+ const DATE_OP_ORDER = [
109
+ "is",
110
+ "isnot",
111
+ "before",
112
+ "after",
113
+ "empty",
114
+ "notempty",
115
+ ];
116
+ // #715: a closed-vocabulary column (GRC Audit's Action/Entity, e.g.) gets
117
+ // its own shorter set too — the text set's `contains`/`starts with`/… mean
118
+ // nothing against a fixed value list. Reuses the already-measured `is`/
119
+ // `is not`/`empty`/`not empty` labels and the same "is/isnot before the
120
+ // type-specific pair, empty/notempty last" ordering every other set here
121
+ // follows — not an independent pixel measurement the way #593's number/date
122
+ // sets were (a live re-check of Attio's own select-column filter menu is
123
+ // still open, see the PR).
124
+ const SELECT_OP_ORDER = ["is", "isnot", "empty", "notempty"];
78
125
  // "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;
126
+ // nothing to check off otherwise. A column's `type` picks the base set
127
+ // first; "isanyof" can still follow any of the four (a column's own
128
+ // declared values aren't tied to its underlying number/date/text/select
129
+ // kind).
130
+ const opsFor = (col) => {
131
+ const base = col?.type === "number"
132
+ ? NUMBER_OP_ORDER
133
+ : col?.type === "date"
134
+ ? DATE_OP_ORDER
135
+ : col?.type === "select"
136
+ ? SELECT_OP_ORDER
137
+ : OP_ORDER;
138
+ return col?.values?.length ? [...base, "isanyof"] : base;
139
+ };
140
+ // #592: a condition's `key` is either a plain top-level column key, or a
141
+ // dot-joined path into `nestedColumns` ("team.twitterFollowerCount"). Walks
142
+ // the path one segment at a time, descending into each match's own
143
+ // `nestedColumns` for the next — returns the resolved column at each step,
144
+ // in order, so a caller has both the LEAF (its own `type`/`values`/`icon`
145
+ // govern the operator set and value editor, unchanged from a plain column)
146
+ // and the FULL PATH (for the breadcrumb the field segment/picker render).
147
+ // Empty when any segment fails to resolve, rather than a partial path — a
148
+ // stale key (its column was removed upstream) should render as "unknown",
149
+ // not as whatever prefix still happens to match.
150
+ const resolveColumnPath = (cols, key) => {
151
+ const segments = key.split(".");
152
+ const path = [];
153
+ let list = cols;
154
+ for (const seg of segments) {
155
+ const found = list.find((c) => c.key === seg);
156
+ if (!found)
157
+ return [];
158
+ path.push(found);
159
+ list = found.nestedColumns ?? [];
160
+ }
161
+ return path;
162
+ };
81
163
  const opNeedsValue = (op) => op === "contains" ||
82
164
  op === "doesnotcontain" ||
83
165
  op === "startswith" ||
84
166
  op === "endswith" ||
85
167
  op === "is" ||
86
- op === "isnot";
168
+ op === "isnot" ||
169
+ op === "lessthan" ||
170
+ op === "greaterthan" ||
171
+ op === "before" ||
172
+ op === "after";
87
173
  const opNeedsValues = (op) => op === "isanyof";
88
174
  // "Department is any of IT, Finance" past two values becomes "… +N": the
89
175
  // chip is one line, and a field can have far more than two facet values.
@@ -98,9 +184,10 @@ const asText = (v) => {
98
184
  return v.join(", ");
99
185
  // Before the generic object branch, which would look for `.name`, find none
100
186
  // 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.
187
+ // date is stable and locale-independent, so `contains "2026-06"` works and
188
+ // (#593) so does `is`/`is not` against a `type: "date"` column's own ISO
189
+ // condition value — `before`/`after` get a real chronological compare
190
+ // instead, below.
104
191
  if (v instanceof Date)
105
192
  return Number.isNaN(v.getTime()) ? "" : v.toISOString().slice(0, 10);
106
193
  if (typeof v === "object") {
@@ -109,13 +196,67 @@ const asText = (v) => {
109
196
  }
110
197
  return String(v);
111
198
  };
199
+ // #593: `lessthan`/`greaterthan` (number columns) and `before`/`after` (date
200
+ // columns) need a real numeric/chronological compare, not `asText`'s string
201
+ // one — `null` on anything that doesn't parse, so a condition against a
202
+ // malformed or missing value never matches rather than throwing.
203
+ const toNumberOrNull = (v) => {
204
+ if (typeof v === "number")
205
+ return Number.isNaN(v) ? null : v;
206
+ if (typeof v === "string" && v.trim() !== "") {
207
+ const n = Number(v);
208
+ return Number.isNaN(n) ? null : n;
209
+ }
210
+ return null;
211
+ };
212
+ const toDateOrNull = (v) => {
213
+ if (v instanceof Date)
214
+ return Number.isNaN(v.getTime()) ? null : v;
215
+ if (typeof v === "string" || typeof v === "number") {
216
+ const d = new Date(v);
217
+ return Number.isNaN(d.getTime()) ? null : d;
218
+ }
219
+ return null;
220
+ };
221
+ // A `type: "date"` condition's own value is stored as a plain ISO date
222
+ // string ("2026-09-25") — machine-comparable (`before`/`after` above,
223
+ // `is`/`is not` via `asText`) and locale-independent, the same choice #185
224
+ // already made for `asText`'s own `Date` branch. Parsed/formatted at local
225
+ // midnight, not UTC, so the picked calendar day and the displayed day are
226
+ // always the same one regardless of timezone offset.
227
+ const isoToDate = (iso) => {
228
+ if (!iso)
229
+ return null;
230
+ const d = new Date(`${iso}T00:00:00`);
231
+ return Number.isNaN(d.getTime()) ? null : d;
232
+ };
233
+ const dateToISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
234
+ // e.g. "Sep 25, 2026" — the same format `DatePickerField`'s own default
235
+ // uses, and what Attio's chip itself showed after picking "Today" live.
236
+ const formatFilterDate = (iso) => {
237
+ const d = isoToDate(iso);
238
+ return d
239
+ ? d.toLocaleDateString(undefined, {
240
+ month: "short",
241
+ day: "numeric",
242
+ year: "numeric",
243
+ })
244
+ : iso;
245
+ };
246
+ const toDatePickerDate = (d) => ({
247
+ year: d.getFullYear(),
248
+ month: d.getMonth() + 1,
249
+ day: d.getDate(),
250
+ });
251
+ const fromDatePickerDate = (d) => new Date(d.year, d.month - 1, d.day);
112
252
  /**
113
253
  * Applies a toolbar's filter conditions to any row array. Reads the raw value
114
254
  * at `row[key]` (override with `get` for derived fields) — never a formatted
115
255
  * display string, so a grouped-thousands "$1,200" still matches "1200".
116
256
  */
117
257
  function applyFilters(rows, filters, get = (row, key) => row[key]) {
118
- const active = filters.filter((f) => (!opNeedsValue(f.op) || f.value.trim() !== "") &&
258
+ const active = filters.filter((f) => f.op != null &&
259
+ (!opNeedsValue(f.op) || f.value.trim() !== "") &&
119
260
  (!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
120
261
  if (!active.length)
121
262
  return rows;
@@ -125,6 +266,20 @@ function applyFilters(rows, filters, get = (row, key) => row[key]) {
125
266
  return isEmptyValue(raw);
126
267
  if (f.op === "notempty")
127
268
  return !isEmptyValue(raw);
269
+ if (f.op === "lessthan" || f.op === "greaterthan") {
270
+ const n = toNumberOrNull(raw);
271
+ const cn = toNumberOrNull(f.value);
272
+ if (n == null || cn == null)
273
+ return false;
274
+ return f.op === "lessthan" ? n < cn : n > cn;
275
+ }
276
+ if (f.op === "before" || f.op === "after") {
277
+ const d = toDateOrNull(raw);
278
+ const cd = toDateOrNull(f.value);
279
+ if (d == null || cd == null)
280
+ return false;
281
+ return f.op === "before" ? d < cd : d > cd;
282
+ }
128
283
  const t = asText(raw).toLowerCase();
129
284
  if (f.op === "isanyof")
130
285
  return (f.values ?? []).some((v) => v.toLowerCase() === t);
@@ -183,7 +338,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
183
338
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
184
339
  * Created by), which one shared list can't reproduce.
185
340
  */
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 }) {
341
+ 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, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
187
342
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
188
343
  // CrispMessagesProvider's translation of the English default below, or
189
344
  // that default itself with no provider — unchanged from before this
@@ -210,6 +365,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
210
365
  const reorderAttributeLabel = (0, core_1.useCrispMessage)(reorderAttributeLabelProp, "Reorder attribute");
211
366
  const addAttributeToViewLabel = (0, core_1.useCrispMessage)(addAttributeToViewLabelProp, "Add attribute to view");
212
367
  const searchAttributesPlaceholder = (0, core_1.useCrispMessage)(searchAttributesPlaceholderProp, "Search attributes…");
368
+ const importExportLabel = (0, core_1.useCrispMessage)(importExportLabelProp, "Import / Export");
369
+ const importCsvLabel = (0, core_1.useCrispMessage)(importCsvLabelProp, "Import CSV");
370
+ const exportCsvLabel = (0, core_1.useCrispMessage)(exportCsvLabelProp, "Export view as CSV");
371
+ const exportExcelLabel = (0, core_1.useCrispMessage)(exportExcelLabelProp, "Export view as Excel");
372
+ const learnImportExportLabel = (0, core_1.useCrispMessage)(learnImportExportLabelProp, "Learn about imports and exports");
373
+ const backLabel = (0, core_1.useCrispMessage)(backLabelProp, "Back");
213
374
  // OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
214
375
  // their own — a whole lookup table, not one string — so they translate
215
376
  // straight from the provider's dictionary at the two spots they render
@@ -250,6 +411,11 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
250
411
  const [openMenu, setOpenMenu] = React.useState(null);
251
412
  const [dragSort, setDragSort] = React.useState(null);
252
413
  const [filterQuery, setFilterQuery] = React.useState("");
414
+ // #592: which nested scope the Filter field picker is currently drilled
415
+ // into -- [] is the top level, [team] is inside Team's own attributes,
416
+ // [team, company] is Team -> Company, etc. Reset to [] wherever
417
+ // `filterQuery` already resets (opening the picker fresh), same lifetime.
418
+ const [fieldPickerPath, setFieldPickerPath] = React.useState([]);
253
419
  // filters the "isanyof" checkbox list, same lifetime as filterQuery above
254
420
  const [anyOfQuery, setAnyOfQuery] = React.useState("");
255
421
  const [dragAttr, setDragAttr] = React.useState(null);
@@ -317,11 +483,29 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
317
483
  };
318
484
  const addFilter = (key) => {
319
485
  const id = `f${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
320
- putFilters([...filters, { id, key, op: "contains", value: "" }]);
486
+ putFilters([...filters, { id, key, value: "" }]);
321
487
  setFilterQuery("");
322
- setOpenMenu(`fcond:${id}`);
488
+ // matches Attio: picking a field opens the operator menu next, not a
489
+ // combined dialog — measured live, 2026-09-25.
490
+ setOpenMenu(`fop:${id}`);
323
491
  };
324
492
  const patchFilter = (id, patch) => putFilters(filters.map((c) => (c.id === id ? { ...c, ...patch } : c)));
493
+ // The field segment's own picker (#462 follow-up): changing the field
494
+ // replaces it in place — confirmed live, one chip, not a second — and
495
+ // resets op/value/values, since neither is guaranteed to make sense for
496
+ // the new field's type (a text op on a select field, e.g.).
497
+ const changeFilterField = (id, key) => {
498
+ patchFilter(id, { key, op: undefined, value: "", values: undefined });
499
+ setFilterQuery("");
500
+ setOpenMenu(`fop:${id}`);
501
+ };
502
+ // The operator segment's own menu: picking an operator that needs a value
503
+ // auto-opens the value popover, focused; "empty"/"not empty" need nothing
504
+ // further, so the menu just closes — both measured live, 2026-09-25.
505
+ const pickOp = (id, op) => {
506
+ patchFilter(id, { op });
507
+ setOpenMenu(opNeedsValue(op) || opNeedsValues(op) ? `fval:${id}` : null);
508
+ };
325
509
  const removeFilter = (id) => {
326
510
  putFilters(filters.filter((c) => c.id !== id));
327
511
  setOpenMenu(null);
@@ -360,6 +544,66 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
360
544
  close: () => setOpenMenu(null),
361
545
  contentRef: kebabMenuRef,
362
546
  });
547
+ // The operator popover is `role="menu"` too — confirmed live, 2026-09-25 —
548
+ // so it gets the same shared keyboard model as the kebab above, one hook
549
+ // call tracking whichever condition's operator menu is open.
550
+ const opMenuRef = React.useRef(null);
551
+ const { onKeyDown: onOpKeyDown } = (0, menu_1.useMenuKeyboard)({
552
+ open: typeof openMenu === "string" && openMenu.startsWith("fop:"),
553
+ close: () => setOpenMenu(null),
554
+ contentRef: opMenuRef,
555
+ });
556
+ // The Import/Export menu is `role="menu"` too — confirmed live, 2026-09-25
557
+ // (an `<a>`/`<div>` mix, each `role="menuitem"`, two `role="separator"`
558
+ // dividers between the 3 groups) — same shared keyboard model.
559
+ const importExportMenuRef = React.useRef(null);
560
+ const { onKeyDown: onImportExportKeyDown } = (0, menu_1.useMenuKeyboard)({
561
+ open: openMenu === "importexport",
562
+ close: () => setOpenMenu(null),
563
+ contentRef: importExportMenuRef,
564
+ });
565
+ // Shared between the "Filter" trigger's own add-condition picker and each
566
+ // chip's field segment (#462 follow-up) — both search the same
567
+ // `filterColumns` list and differ only in what a pick does. #592: also
568
+ // handles drilling into a `nestedColumns` column — `fieldPickerPath`
569
+ // tracks the current scope ([] is the top level), reset to [] wherever
570
+ // the picker opens fresh (both trigger onClicks, above).
571
+ const filterFieldPicker = (onSelect) => {
572
+ const scope = fieldPickerPath.at(-1);
573
+ const scopeColumns = scope ? (scope.nestedColumns ?? []) : filterColumns;
574
+ const query = filterQuery.trim().toLowerCase();
575
+ const direct = scopeColumns.filter((c) => c.label.toLowerCase().includes(query));
576
+ // #592: one level of flattening from wherever the picker is currently
577
+ // scoped — a nested column's own children get searched too, and a
578
+ // match renders breadcrumbed, alongside this level's direct matches.
579
+ // Matches the one case measured live at the TOP level ("Twitter
580
+ // follower" surfaces both the direct field and "Team › Twitter
581
+ // follower count"); applying the identical rule recursively from an
582
+ // already-drilled-into scope (so it also reaches Team → Company → X
583
+ // while already browsing inside Team) is the same construct, one level
584
+ // relative to wherever you are, but wasn't independently re-verified
585
+ // live from inside a nested scope — flagged on the PR.
586
+ const nested = query
587
+ ? scopeColumns.flatMap((c) => (c.nestedColumns ?? [])
588
+ .filter((n) => n.label.toLowerCase().includes(query))
589
+ .map((n) => ({ parent: c, col: n })))
590
+ : [];
591
+ const selectPath = (path) => onSelect(path.map((c) => c.key).join("."));
592
+ const drillIn = (col) => {
593
+ setFieldPickerPath([...fieldPickerPath, col]);
594
+ setFilterQuery("");
595
+ };
596
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [scope ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu-nesthead", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-menu-back", "aria-label": backLabel, onClick: () => {
597
+ setFieldPickerPath(fieldPickerPath.slice(0, -1));
598
+ setFilterQuery("");
599
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: nestedFieldPlaceholder
600
+ ? nestedFieldPlaceholder(scope.nestedObjectLabel ?? scope.label.toLowerCase())
601
+ : `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": filterFieldLabel,
602
+ // biome-ignore lint/a11y/noAutofocus: focus the picker on open
603
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
604
+ // biome-ignore lint/a11y/noAutofocus: focus the picker on open
605
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })), scope && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath(fieldPickerPath), children: [scope.icon ? ((0, jsx_runtime_1.jsx)(scope.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), scope.label] })), direct.map((col) => col.nestedColumns?.length ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--expand", onClick: () => drillIn(col), 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, (0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-chevron" })] }, col.key)) : ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, col]), 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))), nested.map(({ parent, col }) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, parent, col]), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-menu-item-path", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-menu-item-path-parent", children: parent.label }), (0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-path-sep" }), (0, jsx_runtime_1.jsx)("span", { children: col.label })] })] }, `${parent.key}.${col.key}`)))] }));
606
+ };
363
607
  const activeFilters = filters.length;
364
608
  // The default phrase only works in English word order, so a caller can
365
609
  // replace the whole thing rather than fill a template — crisp/#471's
@@ -374,7 +618,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
374
618
  : sorts.length
375
619
  ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
376
620
  : 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) => {
621
+ 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
622
  if (dragSort !== null)
379
623
  e.preventDefault();
380
624
  }, onDrop: () => {
@@ -396,47 +640,106 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
396
640
  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
641
  keywords: descendingLabel,
398
642
  },
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
643
+ ], "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: () => {
644
+ setFieldPickerPath([]);
645
+ setOpenMenu(openMenu === "filter" ? null : "filter");
646
+ }, "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
647
  ? filterTriggerLabel(activeFilters)
401
648
  : activeFilters
402
649
  ? `${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) => {
410
- const col = filterColumns.find((c) => c.key === cond.key);
411
- const open = openMenu === `fcond:${cond.id}`;
650
+ : filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
651
+ // #592: cond.key may be a dot-joined nested path
652
+ // ("team.twitterFollowerCount") — resolve the full path so the
653
+ // field segment can render its breadcrumb; `col` (the leaf) still
654
+ // drives the operator set and value editor exactly as before, for
655
+ // both a plain and a nested key.
656
+ const colPath = resolveColumnPath(filterColumns, cond.key);
657
+ const col = colPath.at(-1);
658
+ const fieldOpen = openMenu === `ffield:${cond.id}`;
659
+ const opOpen = openMenu === `fop:${cond.id}`;
660
+ const valOpen = openMenu === `fval:${cond.id}`;
412
661
  const kebabOpen = openMenu === `fkebab:${cond.id}`;
413
- const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
662
+ // Gates the field/value segments' background (white once the
663
+ // condition is usable, the measured `f6f7f7` otherwise) — NOT
664
+ // whether the value popover renders; see `showValueSeg` below.
665
+ const complete = cond.op != null &&
666
+ (!opNeedsValue(cond.op) || cond.value.trim()) &&
414
667
  (!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: () => {
668
+ // No value segment at all for "empty"/"not empty" (nothing to
669
+ // show) or before an operator is even chosen — matches Attio,
670
+ // which never renders a third box in either case.
671
+ const showValueSeg = cond.op != null &&
672
+ (opNeedsValue(cond.op) || opNeedsValues(cond.op));
673
+ 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: () => {
674
+ setFieldPickerPath([]);
675
+ setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`);
676
+ }, children: [col?.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 14, strokeWidth: 1.75 }) : null, colPath.length > 1 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path", children: colPath.map((c, i) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [i > 0 && ((0, jsx_runtime_1.jsx)(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (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
677
+ ? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
678
+ : 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"
679
+ ? cond.values?.length
680
+ ? isAnyOfSummary(cond.values)
681
+ : filterValuePlaceholder
682
+ : cond.value.trim()
683
+ ? col?.type === "date"
684
+ ? formatFilterDate(cond.value)
685
+ : cond.value
686
+ : 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`,
687
+ // biome-ignore lint/a11y/noAutofocus: focus the search field
688
+ autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
689
+ .filter((v) => v
690
+ .toLowerCase()
691
+ .includes(anyOfQuery.trim().toLowerCase()))
692
+ .map((v) => {
693
+ const checked = (cond.values ?? []).includes(v);
694
+ return ((0, jsx_runtime_1.jsx)(menu_1.MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
695
+ const set = new Set(cond.values ?? []);
696
+ if (next)
697
+ set.add(v);
698
+ else
699
+ set.delete(v);
700
+ // the column's own order, not click order —
701
+ // what the chip summary reads out
702
+ patchFilter(cond.id, {
703
+ values: col.values?.filter((x) => set.has(x)),
704
+ });
705
+ }, children: v }, v));
706
+ })] })) : col?.type === "select" &&
707
+ col?.values &&
708
+ (cond.op === "is" || cond.op === "isnot") ? ((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`,
709
+ // biome-ignore lint/a11y/noAutofocus: focus the search field
710
+ autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
711
+ .filter((v) => v
712
+ .toLowerCase()
713
+ .includes(anyOfQuery.trim().toLowerCase()))
714
+ .map((v) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.value === v ? "" : undefined, onClick: () => {
715
+ patchFilter(cond.id, { value: v });
716
+ setOpenMenu(null);
717
+ }, children: [v, cond.value === v ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, v)))] })) : col?.type === "date" ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-datepop", children: [(0, jsx_runtime_1.jsx)(datepicker_1.DatePicker, { ...{
718
+ selectionMode: "single",
719
+ value: isoToDate(cond.value)
720
+ ? toDatePickerDate(isoToDate(cond.value))
721
+ : undefined,
722
+ onValueChange: (next) => {
723
+ if (!next)
724
+ return;
725
+ patchFilter(cond.id, {
726
+ value: dateToISO(fromDatePickerDate(next)),
727
+ });
728
+ setOpenMenu(null);
729
+ },
730
+ } }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-datepop-presets", children: [
731
+ [crispMessages.Today ?? "Today", 0],
732
+ [crispMessages.Tomorrow ?? "Tomorrow", 1],
733
+ [crispMessages["Next week"] ?? "Next week", 7],
734
+ ].map(([label, addDays]) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-datepop-preset", onClick: () => {
735
+ const d = new Date();
736
+ d.setDate(d.getDate() + addDays);
737
+ patchFilter(cond.id, { value: dateToISO(d) });
738
+ setOpenMenu(null);
739
+ }, 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`,
740
+ // biome-ignore lint/a11y/noAutofocus: focus the value field
741
+ autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
742
+ }), 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
743
  if (openMenu === "viewsettings") {
441
744
  setOpenMenu(null);
442
745
  }
@@ -460,7 +763,20 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
460
763
  moveAttr(dragAttr, i);
461
764
  setDragAttr(null);
462
765
  }, 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] }) }));
766
+ `${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] }) })] })))] })), (onImportCsv ||
767
+ onExportCsv ||
768
+ onExportExcel ||
769
+ importExportHelpHref) && ((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--raised", onClick: () => setOpenMenu(openMenu === "importexport" ? null : "importexport"), "aria-haspopup": "menu", "aria-expanded": openMenu === "importexport", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && ((0, jsx_runtime_1.jsxs)("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, children: [onImportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
770
+ onImportCsv();
771
+ setOpenMenu(null);
772
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Upload, { size: 15, strokeWidth: 1.75 }), importCsvLabel] })), onImportCsv && (onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), onExportCsv && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
773
+ onExportCsv();
774
+ setOpenMenu(null);
775
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
776
+ onExportExcel();
777
+ setOpenMenu(null);
778
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
779
+ (onImportCsv || onExportCsv || onExportExcel) && ((0, jsx_runtime_1.jsx)("hr", { className: "crisp-tabletoolbar-menu-sep" })), importExportHelpHref && ((0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-menu-item", role: "menuitem", href: importExportHelpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 15, strokeWidth: 1.75 }), learnImportExportLabel] }))] }))] })), (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
780
  }
465
781
  /**
466
782
  * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone