@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
@@ -1,10 +1,19 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, CircleHelp, GripVertical, ListFilter, MoreVertical, Plus, SlidersHorizontal, Trash2, X, } from "lucide-react";
2
+ import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, ChevronDown, ChevronLeft, ChevronRight, CircleHelp, Download, GripVertical, ListFilter, MoreVertical, Plus, SlidersHorizontal, Trash2, Upload, X, } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { isInsideLaterFloatingLayer, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
5
+ import { DatePicker, } from "../datepicker/index.js";
5
6
  import { MenuCheckboxItem, useMenuKeyboard } from "../menu/index.js";
6
7
  import { Select } from "../select/index.js";
7
8
  import { tabletoolbarRecipe } from "./tabletoolbar.recipe.js";
9
+ // #592: shared by every direction-bearing ChevronRight usage below (the
10
+ // field picker's own disclosure chevron, a flattened search result's
11
+ // breadcrumb separator, and a filter chip's own breadcrumb separator) so
12
+ // the RTL mirror attribute lives in exactly one JSX tag — build/spec-check
13
+ // .mjs's icon sweep counts that attribute per icon NAME across the whole
14
+ // file, not per call site, and three independently attributed tags for
15
+ // the same name over-counts against that (it expects one per name).
16
+ const RtlChevronRight = ({ size, className, }) => (_jsx(ChevronRight, { "data-rtl-flip": "", size: size, strokeWidth: 1.75, className: className }));
8
17
  const OP_LABELS = {
9
18
  contains: "contains",
10
19
  doesnotcontain: "does not contain",
@@ -21,11 +30,24 @@ const OP_LABELS = {
21
30
  // review.
22
31
  empty: "empty",
23
32
  notempty: "not empty",
33
+ lessthan: "less than",
34
+ greaterthan: "greater than",
35
+ before: "before",
36
+ after: "after",
24
37
  };
25
38
  // #619: order and label set match Attio's own text-filter operator picker
26
39
  // verbatim (measured live, per RecordsTable's own #578 port of the same
27
40
  // set: contains · does not contain · starts with · ends with · is · is not ·
28
- // empty · not empty).
41
+ // empty · not empty). #593: number and date columns get their OWN shorter
42
+ // sets instead — also measured live (`app.attio.com`, a Twitter follower
43
+ // count / Foundation date column, each filter added then discarded): number
44
+ // is "is / is not / less than / greater than / empty / not empty" (104.86px
45
+ // wide, 6 rows); date is "is / is not / before / after / empty / not empty"
46
+ // (91.16px, 6 rows) — both narrower than the text set's own 156.5px, which
47
+ // is why `.crisp-tabletoolbar-menu--op`'s old fixed 134px floor is gone (see
48
+ // tabletoolbar.recipe.ts): every one of these three sets is real, live-
49
+ // measured content-driven width, and a floor sized for one of them
50
+ // overshoots the other two.
29
51
  const OP_ORDER = [
30
52
  "contains",
31
53
  "doesnotcontain",
@@ -36,15 +58,79 @@ const OP_ORDER = [
36
58
  "empty",
37
59
  "notempty",
38
60
  ];
61
+ const NUMBER_OP_ORDER = [
62
+ "is",
63
+ "isnot",
64
+ "lessthan",
65
+ "greaterthan",
66
+ "empty",
67
+ "notempty",
68
+ ];
69
+ const DATE_OP_ORDER = [
70
+ "is",
71
+ "isnot",
72
+ "before",
73
+ "after",
74
+ "empty",
75
+ "notempty",
76
+ ];
77
+ // #715: a closed-vocabulary column (GRC Audit's Action/Entity, e.g.) gets
78
+ // its own shorter set too — the text set's `contains`/`starts with`/… mean
79
+ // nothing against a fixed value list. Reuses the already-measured `is`/
80
+ // `is not`/`empty`/`not empty` labels and the same "is/isnot before the
81
+ // type-specific pair, empty/notempty last" ordering every other set here
82
+ // follows — not an independent pixel measurement the way #593's number/date
83
+ // sets were (a live re-check of Attio's own select-column filter menu is
84
+ // still open, see the PR).
85
+ const SELECT_OP_ORDER = ["is", "isnot", "empty", "notempty"];
39
86
  // "isanyof" is appended only for a column that supplies `values` — there is
40
- // nothing to check off otherwise.
41
- const opsFor = (col) => col?.values?.length ? [...OP_ORDER, "isanyof"] : OP_ORDER;
87
+ // nothing to check off otherwise. A column's `type` picks the base set
88
+ // first; "isanyof" can still follow any of the four (a column's own
89
+ // declared values aren't tied to its underlying number/date/text/select
90
+ // kind).
91
+ const opsFor = (col) => {
92
+ const base = col?.type === "number"
93
+ ? NUMBER_OP_ORDER
94
+ : col?.type === "date"
95
+ ? DATE_OP_ORDER
96
+ : col?.type === "select"
97
+ ? SELECT_OP_ORDER
98
+ : OP_ORDER;
99
+ return col?.values?.length ? [...base, "isanyof"] : base;
100
+ };
101
+ // #592: a condition's `key` is either a plain top-level column key, or a
102
+ // dot-joined path into `nestedColumns` ("team.twitterFollowerCount"). Walks
103
+ // the path one segment at a time, descending into each match's own
104
+ // `nestedColumns` for the next — returns the resolved column at each step,
105
+ // in order, so a caller has both the LEAF (its own `type`/`values`/`icon`
106
+ // govern the operator set and value editor, unchanged from a plain column)
107
+ // and the FULL PATH (for the breadcrumb the field segment/picker render).
108
+ // Empty when any segment fails to resolve, rather than a partial path — a
109
+ // stale key (its column was removed upstream) should render as "unknown",
110
+ // not as whatever prefix still happens to match.
111
+ const resolveColumnPath = (cols, key) => {
112
+ const segments = key.split(".");
113
+ const path = [];
114
+ let list = cols;
115
+ for (const seg of segments) {
116
+ const found = list.find((c) => c.key === seg);
117
+ if (!found)
118
+ return [];
119
+ path.push(found);
120
+ list = found.nestedColumns ?? [];
121
+ }
122
+ return path;
123
+ };
42
124
  const opNeedsValue = (op) => op === "contains" ||
43
125
  op === "doesnotcontain" ||
44
126
  op === "startswith" ||
45
127
  op === "endswith" ||
46
128
  op === "is" ||
47
- op === "isnot";
129
+ op === "isnot" ||
130
+ op === "lessthan" ||
131
+ op === "greaterthan" ||
132
+ op === "before" ||
133
+ op === "after";
48
134
  const opNeedsValues = (op) => op === "isanyof";
49
135
  // "Department is any of IT, Finance" past two values becomes "… +N": the
50
136
  // chip is one line, and a field can have far more than two facet values.
@@ -59,9 +145,10 @@ const asText = (v) => {
59
145
  return v.join(", ");
60
146
  // Before the generic object branch, which would look for `.name`, find none
61
147
  // and return "" — turning every Date into the empty string (#185). The ISO
62
- // date is stable and locale-independent, so `contains "2026-06"` works; it
63
- // will not match "16/06/2026", and a date-aware operator set needs a
64
- // date-picker UI, which has to be measured before it can be built.
148
+ // date is stable and locale-independent, so `contains "2026-06"` works and
149
+ // (#593) so does `is`/`is not` against a `type: "date"` column's own ISO
150
+ // condition value — `before`/`after` get a real chronological compare
151
+ // instead, below.
65
152
  if (v instanceof Date)
66
153
  return Number.isNaN(v.getTime()) ? "" : v.toISOString().slice(0, 10);
67
154
  if (typeof v === "object") {
@@ -70,13 +157,67 @@ const asText = (v) => {
70
157
  }
71
158
  return String(v);
72
159
  };
160
+ // #593: `lessthan`/`greaterthan` (number columns) and `before`/`after` (date
161
+ // columns) need a real numeric/chronological compare, not `asText`'s string
162
+ // one — `null` on anything that doesn't parse, so a condition against a
163
+ // malformed or missing value never matches rather than throwing.
164
+ const toNumberOrNull = (v) => {
165
+ if (typeof v === "number")
166
+ return Number.isNaN(v) ? null : v;
167
+ if (typeof v === "string" && v.trim() !== "") {
168
+ const n = Number(v);
169
+ return Number.isNaN(n) ? null : n;
170
+ }
171
+ return null;
172
+ };
173
+ const toDateOrNull = (v) => {
174
+ if (v instanceof Date)
175
+ return Number.isNaN(v.getTime()) ? null : v;
176
+ if (typeof v === "string" || typeof v === "number") {
177
+ const d = new Date(v);
178
+ return Number.isNaN(d.getTime()) ? null : d;
179
+ }
180
+ return null;
181
+ };
182
+ // A `type: "date"` condition's own value is stored as a plain ISO date
183
+ // string ("2026-09-25") — machine-comparable (`before`/`after` above,
184
+ // `is`/`is not` via `asText`) and locale-independent, the same choice #185
185
+ // already made for `asText`'s own `Date` branch. Parsed/formatted at local
186
+ // midnight, not UTC, so the picked calendar day and the displayed day are
187
+ // always the same one regardless of timezone offset.
188
+ const isoToDate = (iso) => {
189
+ if (!iso)
190
+ return null;
191
+ const d = new Date(`${iso}T00:00:00`);
192
+ return Number.isNaN(d.getTime()) ? null : d;
193
+ };
194
+ const dateToISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
195
+ // e.g. "Sep 25, 2026" — the same format `DatePickerField`'s own default
196
+ // uses, and what Attio's chip itself showed after picking "Today" live.
197
+ const formatFilterDate = (iso) => {
198
+ const d = isoToDate(iso);
199
+ return d
200
+ ? d.toLocaleDateString(undefined, {
201
+ month: "short",
202
+ day: "numeric",
203
+ year: "numeric",
204
+ })
205
+ : iso;
206
+ };
207
+ const toDatePickerDate = (d) => ({
208
+ year: d.getFullYear(),
209
+ month: d.getMonth() + 1,
210
+ day: d.getDate(),
211
+ });
212
+ const fromDatePickerDate = (d) => new Date(d.year, d.month - 1, d.day);
73
213
  /**
74
214
  * Applies a toolbar's filter conditions to any row array. Reads the raw value
75
215
  * at `row[key]` (override with `get` for derived fields) — never a formatted
76
216
  * display string, so a grouped-thousands "$1,200" still matches "1200".
77
217
  */
78
218
  export function applyFilters(rows, filters, get = (row, key) => row[key]) {
79
- const active = filters.filter((f) => (!opNeedsValue(f.op) || f.value.trim() !== "") &&
219
+ const active = filters.filter((f) => f.op != null &&
220
+ (!opNeedsValue(f.op) || f.value.trim() !== "") &&
80
221
  (!opNeedsValues(f.op) || (f.values?.length ?? 0) > 0));
81
222
  if (!active.length)
82
223
  return rows;
@@ -86,6 +227,20 @@ export function applyFilters(rows, filters, get = (row, key) => row[key]) {
86
227
  return isEmptyValue(raw);
87
228
  if (f.op === "notempty")
88
229
  return !isEmptyValue(raw);
230
+ if (f.op === "lessthan" || f.op === "greaterthan") {
231
+ const n = toNumberOrNull(raw);
232
+ const cn = toNumberOrNull(f.value);
233
+ if (n == null || cn == null)
234
+ return false;
235
+ return f.op === "lessthan" ? n < cn : n > cn;
236
+ }
237
+ if (f.op === "before" || f.op === "after") {
238
+ const d = toDateOrNull(raw);
239
+ const cd = toDateOrNull(f.value);
240
+ if (d == null || cd == null)
241
+ return false;
242
+ return f.op === "before" ? d < cd : d > cd;
243
+ }
89
244
  const t = asText(raw).toLowerCase();
90
245
  if (f.op === "isanyof")
91
246
  return (f.values ?? []).some((v) => v.toLowerCase() === t);
@@ -144,7 +299,7 @@ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
144
299
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
145
300
  * Created by), which one shared list can't reproduce.
146
301
  */
147
- export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, 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 }) {
302
+ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, 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 }) {
148
303
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
149
304
  // CrispMessagesProvider's translation of the English default below, or
150
305
  // that default itself with no provider — unchanged from before this
@@ -171,6 +326,12 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
171
326
  const reorderAttributeLabel = useCrispMessage(reorderAttributeLabelProp, "Reorder attribute");
172
327
  const addAttributeToViewLabel = useCrispMessage(addAttributeToViewLabelProp, "Add attribute to view");
173
328
  const searchAttributesPlaceholder = useCrispMessage(searchAttributesPlaceholderProp, "Search attributes…");
329
+ const importExportLabel = useCrispMessage(importExportLabelProp, "Import / Export");
330
+ const importCsvLabel = useCrispMessage(importCsvLabelProp, "Import CSV");
331
+ const exportCsvLabel = useCrispMessage(exportCsvLabelProp, "Export view as CSV");
332
+ const exportExcelLabel = useCrispMessage(exportExcelLabelProp, "Export view as Excel");
333
+ const learnImportExportLabel = useCrispMessage(learnImportExportLabelProp, "Learn about imports and exports");
334
+ const backLabel = useCrispMessage(backLabelProp, "Back");
174
335
  // OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
175
336
  // their own — a whole lookup table, not one string — so they translate
176
337
  // straight from the provider's dictionary at the two spots they render
@@ -211,6 +372,11 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
211
372
  const [openMenu, setOpenMenu] = React.useState(null);
212
373
  const [dragSort, setDragSort] = React.useState(null);
213
374
  const [filterQuery, setFilterQuery] = React.useState("");
375
+ // #592: which nested scope the Filter field picker is currently drilled
376
+ // into -- [] is the top level, [team] is inside Team's own attributes,
377
+ // [team, company] is Team -> Company, etc. Reset to [] wherever
378
+ // `filterQuery` already resets (opening the picker fresh), same lifetime.
379
+ const [fieldPickerPath, setFieldPickerPath] = React.useState([]);
214
380
  // filters the "isanyof" checkbox list, same lifetime as filterQuery above
215
381
  const [anyOfQuery, setAnyOfQuery] = React.useState("");
216
382
  const [dragAttr, setDragAttr] = React.useState(null);
@@ -278,11 +444,29 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
278
444
  };
279
445
  const addFilter = (key) => {
280
446
  const id = `f${Date.now()}${Math.random().toString(36).slice(2, 6)}`;
281
- putFilters([...filters, { id, key, op: "contains", value: "" }]);
447
+ putFilters([...filters, { id, key, value: "" }]);
282
448
  setFilterQuery("");
283
- setOpenMenu(`fcond:${id}`);
449
+ // matches Attio: picking a field opens the operator menu next, not a
450
+ // combined dialog — measured live, 2026-09-25.
451
+ setOpenMenu(`fop:${id}`);
284
452
  };
285
453
  const patchFilter = (id, patch) => putFilters(filters.map((c) => (c.id === id ? { ...c, ...patch } : c)));
454
+ // The field segment's own picker (#462 follow-up): changing the field
455
+ // replaces it in place — confirmed live, one chip, not a second — and
456
+ // resets op/value/values, since neither is guaranteed to make sense for
457
+ // the new field's type (a text op on a select field, e.g.).
458
+ const changeFilterField = (id, key) => {
459
+ patchFilter(id, { key, op: undefined, value: "", values: undefined });
460
+ setFilterQuery("");
461
+ setOpenMenu(`fop:${id}`);
462
+ };
463
+ // The operator segment's own menu: picking an operator that needs a value
464
+ // auto-opens the value popover, focused; "empty"/"not empty" need nothing
465
+ // further, so the menu just closes — both measured live, 2026-09-25.
466
+ const pickOp = (id, op) => {
467
+ patchFilter(id, { op });
468
+ setOpenMenu(opNeedsValue(op) || opNeedsValues(op) ? `fval:${id}` : null);
469
+ };
286
470
  const removeFilter = (id) => {
287
471
  putFilters(filters.filter((c) => c.id !== id));
288
472
  setOpenMenu(null);
@@ -321,6 +505,66 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
321
505
  close: () => setOpenMenu(null),
322
506
  contentRef: kebabMenuRef,
323
507
  });
508
+ // The operator popover is `role="menu"` too — confirmed live, 2026-09-25 —
509
+ // so it gets the same shared keyboard model as the kebab above, one hook
510
+ // call tracking whichever condition's operator menu is open.
511
+ const opMenuRef = React.useRef(null);
512
+ const { onKeyDown: onOpKeyDown } = useMenuKeyboard({
513
+ open: typeof openMenu === "string" && openMenu.startsWith("fop:"),
514
+ close: () => setOpenMenu(null),
515
+ contentRef: opMenuRef,
516
+ });
517
+ // The Import/Export menu is `role="menu"` too — confirmed live, 2026-09-25
518
+ // (an `<a>`/`<div>` mix, each `role="menuitem"`, two `role="separator"`
519
+ // dividers between the 3 groups) — same shared keyboard model.
520
+ const importExportMenuRef = React.useRef(null);
521
+ const { onKeyDown: onImportExportKeyDown } = useMenuKeyboard({
522
+ open: openMenu === "importexport",
523
+ close: () => setOpenMenu(null),
524
+ contentRef: importExportMenuRef,
525
+ });
526
+ // Shared between the "Filter" trigger's own add-condition picker and each
527
+ // chip's field segment (#462 follow-up) — both search the same
528
+ // `filterColumns` list and differ only in what a pick does. #592: also
529
+ // handles drilling into a `nestedColumns` column — `fieldPickerPath`
530
+ // tracks the current scope ([] is the top level), reset to [] wherever
531
+ // the picker opens fresh (both trigger onClicks, above).
532
+ const filterFieldPicker = (onSelect) => {
533
+ const scope = fieldPickerPath.at(-1);
534
+ const scopeColumns = scope ? (scope.nestedColumns ?? []) : filterColumns;
535
+ const query = filterQuery.trim().toLowerCase();
536
+ const direct = scopeColumns.filter((c) => c.label.toLowerCase().includes(query));
537
+ // #592: one level of flattening from wherever the picker is currently
538
+ // scoped — a nested column's own children get searched too, and a
539
+ // match renders breadcrumbed, alongside this level's direct matches.
540
+ // Matches the one case measured live at the TOP level ("Twitter
541
+ // follower" surfaces both the direct field and "Team › Twitter
542
+ // follower count"); applying the identical rule recursively from an
543
+ // already-drilled-into scope (so it also reaches Team → Company → X
544
+ // while already browsing inside Team) is the same construct, one level
545
+ // relative to wherever you are, but wasn't independently re-verified
546
+ // live from inside a nested scope — flagged on the PR.
547
+ const nested = query
548
+ ? scopeColumns.flatMap((c) => (c.nestedColumns ?? [])
549
+ .filter((n) => n.label.toLowerCase().includes(query))
550
+ .map((n) => ({ parent: c, col: n })))
551
+ : [];
552
+ const selectPath = (path) => onSelect(path.map((c) => c.key).join("."));
553
+ const drillIn = (col) => {
554
+ setFieldPickerPath([...fieldPickerPath, col]);
555
+ setFilterQuery("");
556
+ };
557
+ return (_jsxs(_Fragment, { children: [scope ? (_jsxs("div", { className: "crisp-tabletoolbar-menu-nesthead", children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-menu-back", "aria-label": backLabel, onClick: () => {
558
+ setFieldPickerPath(fieldPickerPath.slice(0, -1));
559
+ setFilterQuery("");
560
+ }, children: _jsx(ChevronLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) }), _jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: nestedFieldPlaceholder
561
+ ? nestedFieldPlaceholder(scope.nestedObjectLabel ?? scope.label.toLowerCase())
562
+ : `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": filterFieldLabel,
563
+ // biome-ignore lint/a11y/noAutofocus: focus the picker on open
564
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : (_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
565
+ // biome-ignore lint/a11y/noAutofocus: focus the picker on open
566
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })), scope && (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath(fieldPickerPath), children: [scope.icon ? (_jsx(scope.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), scope.label] })), direct.map((col) => col.nestedColumns?.length ? (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--expand", onClick: () => drillIn(col), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label, _jsx(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-chevron" })] }, col.key)) : (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, col]), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key))), nested.map(({ parent, col }) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => selectPath([...fieldPickerPath, parent, col]), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), _jsxs("span", { className: "crisp-tabletoolbar-menu-item-path", children: [_jsx("span", { className: "crisp-tabletoolbar-menu-item-path-parent", children: parent.label }), _jsx(RtlChevronRight, { size: 15, className: "crisp-tabletoolbar-menu-item-path-sep" }), _jsx("span", { children: col.label })] })] }, `${parent.key}.${col.key}`)))] }));
567
+ };
324
568
  const activeFilters = filters.length;
325
569
  // The default phrase only works in English word order, so a caller can
326
570
  // replace the whole thing rather than fill a template — crisp/#471's
@@ -335,7 +579,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
335
579
  : sorts.length
336
580
  ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
337
581
  : sortLabel;
338
- return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
582
+ return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
339
583
  if (dragSort !== null)
340
584
  e.preventDefault();
341
585
  }, onDrop: () => {
@@ -357,47 +601,106 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
357
601
  label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
358
602
  keywords: descendingLabel,
359
603
  },
360
- ], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] }), _jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "filter" ? null : "filter"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
604
+ ], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => {
605
+ setFieldPickerPath([]);
606
+ setOpenMenu(openMenu === "filter" ? null : "filter");
607
+ }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
361
608
  ? filterTriggerLabel(activeFilters)
362
609
  : activeFilters
363
610
  ? `${filterLabel} · ${activeFilters}`
364
- : filterLabel] }), openMenu === "filter" && (_jsxs("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
365
- // biome-ignore lint/a11y/noAutofocus: focus the picker on open
366
- autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
367
- .filter((col) => col.label
368
- .toLowerCase()
369
- .includes(filterQuery.trim().toLowerCase()))
370
- .map((col) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => addFilter(col.key), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key)))] }))] }), filters.map((cond) => {
371
- const col = filterColumns.find((c) => c.key === cond.key);
372
- const open = openMenu === `fcond:${cond.id}`;
611
+ : filterLabel] }), openMenu === "filter" && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
612
+ // #592: cond.key may be a dot-joined nested path
613
+ // ("team.twitterFollowerCount") — resolve the full path so the
614
+ // field segment can render its breadcrumb; `col` (the leaf) still
615
+ // drives the operator set and value editor exactly as before, for
616
+ // both a plain and a nested key.
617
+ const colPath = resolveColumnPath(filterColumns, cond.key);
618
+ const col = colPath.at(-1);
619
+ const fieldOpen = openMenu === `ffield:${cond.id}`;
620
+ const opOpen = openMenu === `fop:${cond.id}`;
621
+ const valOpen = openMenu === `fval:${cond.id}`;
373
622
  const kebabOpen = openMenu === `fkebab:${cond.id}`;
374
- const hasValue = (!opNeedsValue(cond.op) || cond.value.trim()) &&
623
+ // Gates the field/value segments' background (white once the
624
+ // condition is usable, the measured `f6f7f7` otherwise) — NOT
625
+ // whether the value popover renders; see `showValueSeg` below.
626
+ const complete = cond.op != null &&
627
+ (!opNeedsValue(cond.op) || cond.value.trim()) &&
375
628
  (!opNeedsValues(cond.op) || (cond.values?.length ?? 0) > 0);
376
- return (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("span", { className: "crisp-tabletoolbar-fchip", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-fchip-main", "data-menu-trigger": true, "aria-haspopup": "dialog", onClick: () => setOpenMenu(open ? null : `fcond:${cond.id}`), children: [_jsx("span", { className: "crisp-tabletoolbar-fchip-field", children: col?.label }), hasValue ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-fchip-op", children: crispMessages[OP_LABELS[cond.op]] ??
377
- OP_LABELS[cond.op] }), opNeedsValue(cond.op) && (_jsx("span", { className: "crisp-tabletoolbar-fchip-val", children: cond.value })), cond.op === "isanyof" && cond.values && (_jsx("span", { className: "crisp-tabletoolbar-fchip-val", children: isAnyOfSummary(cond.values) }))] })) : (_jsx("span", { className: "crisp-tabletoolbar-fchip-op", children: selectConditionLabel }))] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-fchip-x", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: _jsx(MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && (_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: _jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [_jsx(Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), open && (_jsxs("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "dialog", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, children: [_jsxs("div", { className: "crisp-tabletoolbar-menu-head", children: [col?.label, " ", crispMessages.condition ?? "condition"] }), opsFor(col).map((op) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => patchFilter(cond.id, { op }), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))), opNeedsValue(cond.op) && (_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
378
- // biome-ignore lint/a11y/noAutofocus: focus the value field
379
- autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })), cond.op === "isanyof" && col?.values && (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
380
- // biome-ignore lint/a11y/noAutofocus: focus the search field
381
- autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
382
- .filter((v) => v
383
- .toLowerCase()
384
- .includes(anyOfQuery.trim().toLowerCase()))
385
- .map((v) => {
386
- const checked = (cond.values ?? []).includes(v);
387
- return (_jsx(MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
388
- const set = new Set(cond.values ?? []);
389
- if (next)
390
- set.add(v);
391
- else
392
- set.delete(v);
393
- // the column's own order, not click order —
394
- // what the chip summary reads out
395
- patchFilter(cond.id, {
396
- values: col.values?.filter((x) => set.has(x)),
397
- });
398
- }, children: v }, v));
399
- })] }))] }))] }, cond.id));
400
- }), _jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" }), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost", onClick: () => {
629
+ // No value segment at all for "empty"/"not empty" (nothing to
630
+ // show) or before an operator is even chosen — matches Attio,
631
+ // which never renders a third box in either case.
632
+ const showValueSeg = cond.op != null &&
633
+ (opNeedsValue(cond.op) || opNeedsValues(cond.op));
634
+ return (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [_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: () => {
635
+ setFieldPickerPath([]);
636
+ setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`);
637
+ }, children: [col?.icon ? _jsx(col.icon, { size: 14, strokeWidth: 1.75 }) : null, colPath.length > 1 ? (_jsx("span", { className: "crisp-tabletoolbar-fcond-path", children: colPath.map((c, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), _jsx("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (col?.label)] }), _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
638
+ ? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
639
+ : selectConditionLabel }), showValueSeg && (_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"
640
+ ? cond.values?.length
641
+ ? isAnyOfSummary(cond.values)
642
+ : filterValuePlaceholder
643
+ : cond.value.trim()
644
+ ? col?.type === "date"
645
+ ? formatFilterDate(cond.value)
646
+ : cond.value
647
+ : filterValuePlaceholder })), _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: _jsx(MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && (_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: _jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [_jsx(Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker((key) => changeFilterField(cond.id, key)) })), opOpen && (_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) => (_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 ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && (_jsx("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
648
+ // biome-ignore lint/a11y/noAutofocus: focus the search field
649
+ autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
650
+ .filter((v) => v
651
+ .toLowerCase()
652
+ .includes(anyOfQuery.trim().toLowerCase()))
653
+ .map((v) => {
654
+ const checked = (cond.values ?? []).includes(v);
655
+ return (_jsx(MenuCheckboxItem, { checked: checked, onCheckedChange: (next) => {
656
+ const set = new Set(cond.values ?? []);
657
+ if (next)
658
+ set.add(v);
659
+ else
660
+ set.delete(v);
661
+ // the column's own order, not click order —
662
+ // what the chip summary reads out
663
+ patchFilter(cond.id, {
664
+ values: col.values?.filter((x) => set.has(x)),
665
+ });
666
+ }, children: v }, v));
667
+ })] })) : col?.type === "select" &&
668
+ col?.values &&
669
+ (cond.op === "is" || cond.op === "isnot") ? (_jsxs(_Fragment, { children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
670
+ // biome-ignore lint/a11y/noAutofocus: focus the search field
671
+ autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
672
+ .filter((v) => v
673
+ .toLowerCase()
674
+ .includes(anyOfQuery.trim().toLowerCase()))
675
+ .map((v) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.value === v ? "" : undefined, onClick: () => {
676
+ patchFilter(cond.id, { value: v });
677
+ setOpenMenu(null);
678
+ }, children: [v, cond.value === v ? (_jsx(Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, v)))] })) : col?.type === "date" ? (_jsxs("div", { className: "crisp-tabletoolbar-datepop", children: [_jsx(DatePicker, { ...{
679
+ selectionMode: "single",
680
+ value: isoToDate(cond.value)
681
+ ? toDatePickerDate(isoToDate(cond.value))
682
+ : undefined,
683
+ onValueChange: (next) => {
684
+ if (!next)
685
+ return;
686
+ patchFilter(cond.id, {
687
+ value: dateToISO(fromDatePickerDate(next)),
688
+ });
689
+ setOpenMenu(null);
690
+ },
691
+ } }), _jsx("div", { className: "crisp-tabletoolbar-datepop-presets", children: [
692
+ [crispMessages.Today ?? "Today", 0],
693
+ [crispMessages.Tomorrow ?? "Tomorrow", 1],
694
+ [crispMessages["Next week"] ?? "Next week", 7],
695
+ ].map(([label, addDays]) => (_jsx("button", { type: "button", className: "crisp-tabletoolbar-datepop-preset", onClick: () => {
696
+ const d = new Date();
697
+ d.setDate(d.getDate() + addDays);
698
+ patchFilter(cond.id, { value: dateToISO(d) });
699
+ setOpenMenu(null);
700
+ }, children: label }, label))) })] })) : (_jsx("input", { className: "crisp-tabletoolbar-menu-input", type: col?.type === "number" ? "number" : "text", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
701
+ // biome-ignore lint/a11y/noAutofocus: focus the value field
702
+ autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
703
+ }), showViewSettings && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost", onClick: () => {
401
704
  if (openMenu === "viewsettings") {
402
705
  setOpenMenu(null);
403
706
  }
@@ -421,7 +724,20 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
421
724
  moveAttr(dragAttr, i);
422
725
  setDragAttr(null);
423
726
  }, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), _jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
424
- `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] }), _jsx("span", { className: "crisp-tabletoolbar-spacer" }), count != null && (_jsx("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
727
+ `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (onImportCsv ||
728
+ onExportCsv ||
729
+ onExportExcel ||
730
+ importExportHelpHref) && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_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: [_jsx(Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, _jsx(ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && (_jsxs("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, children: [onImportCsv && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
731
+ onImportCsv();
732
+ setOpenMenu(null);
733
+ }, children: [_jsx(Upload, { size: 15, strokeWidth: 1.75 }), importCsvLabel] })), onImportCsv && (onExportCsv || onExportExcel) && (_jsx("hr", { className: "crisp-tabletoolbar-menu-sep" })), onExportCsv && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
734
+ onExportCsv();
735
+ setOpenMenu(null);
736
+ }, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
737
+ onExportExcel();
738
+ setOpenMenu(null);
739
+ }, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
740
+ (onImportCsv || onExportCsv || onExportExcel) && (_jsx("hr", { className: "crisp-tabletoolbar-menu-sep" })), importExportHelpHref && (_jsxs("a", { className: "crisp-tabletoolbar-menu-item", role: "menuitem", href: importExportHelpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 15, strokeWidth: 1.75 }), learnImportExportLabel] }))] }))] })), _jsx("span", { className: "crisp-tabletoolbar-spacer" }), count != null && (_jsx("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
425
741
  }
426
742
  /**
427
743
  * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone