@crisp-ui-kit/crisp 0.54.8 → 0.54.10

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 (59) hide show
  1. package/dist/board.css +32 -8
  2. package/dist/board.layered.css +32 -8
  3. package/dist/cjs/components/contextmenu/contextmenu.js +2 -2
  4. package/dist/cjs/components/contextmenu/contextmenu.recipe.js +24 -0
  5. package/dist/cjs/components/datatable/datatable.js +593 -51
  6. package/dist/cjs/components/datatable/datatable.recipe.js +67 -3
  7. package/dist/cjs/components/dialog/dialog.js +65 -1
  8. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +37 -2
  9. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +218 -39
  10. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +48 -1
  11. package/dist/cjs/components/text/text.recipe.js +17 -7
  12. package/dist/cjs/product/board/board.js +62 -28
  13. package/dist/cjs/product/board/board.recipe.js +42 -2
  14. package/dist/cjs/product/comments/comments.js +11 -2
  15. package/dist/cjs/product/comments/comments.recipe.js +14 -4
  16. package/dist/cjs/product/recordstable/recordstable.js +1 -17
  17. package/dist/cjs/product/savedviews/savedviews.js +55 -24
  18. package/dist/cjs/product/savedviews/savedviews.recipe.js +27 -0
  19. package/dist/comments.css +14 -4
  20. package/dist/comments.layered.css +14 -4
  21. package/dist/contextmenu.css +24 -0
  22. package/dist/contextmenu.layered.css +24 -0
  23. package/dist/datatable.css +106 -77
  24. package/dist/datatable.layered.css +106 -77
  25. package/dist/esm/components/contextmenu/contextmenu.js +2 -2
  26. package/dist/esm/components/contextmenu/contextmenu.recipe.js +24 -0
  27. package/dist/esm/components/datatable/datatable.js +593 -52
  28. package/dist/esm/components/datatable/datatable.recipe.js +68 -4
  29. package/dist/esm/components/dialog/dialog.js +65 -1
  30. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +37 -2
  31. package/dist/esm/components/tabletoolbar/tabletoolbar.js +219 -40
  32. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +50 -3
  33. package/dist/esm/components/text/text.recipe.js +17 -7
  34. package/dist/esm/product/board/board.js +63 -29
  35. package/dist/esm/product/board/board.recipe.js +42 -2
  36. package/dist/esm/product/comments/comments.js +12 -3
  37. package/dist/esm/product/comments/comments.recipe.js +14 -4
  38. package/dist/esm/product/recordstable/recordstable.js +2 -18
  39. package/dist/esm/product/savedviews/savedviews.js +57 -26
  40. package/dist/esm/product/savedviews/savedviews.recipe.js +27 -0
  41. package/dist/recordstable.css +33 -31
  42. package/dist/recordstable.layered.css +33 -31
  43. package/dist/savedviews.css +27 -0
  44. package/dist/savedviews.layered.css +27 -0
  45. package/dist/styles.css +282 -125
  46. package/dist/styles.layered.css +282 -125
  47. package/dist/tabletoolbar.css +45 -4
  48. package/dist/tabletoolbar.layered.css +45 -4
  49. package/dist/text.css +1 -1
  50. package/dist/text.layered.css +1 -1
  51. package/dist/types/components/contextmenu/contextmenu.d.ts +8 -1
  52. package/dist/types/components/datatable/datatable.d.ts +98 -2
  53. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +72 -2
  54. package/dist/types/components/text/text.recipe.d.ts +1 -1
  55. package/dist/types/product/board/board.d.ts +48 -2
  56. package/dist/types/product/comments/comments.d.ts +6 -1
  57. package/dist/types/product/recordstable/recordstable.d.ts +0 -1
  58. package/dist/types/product/savedviews/savedviews.d.ts +45 -8
  59. package/package.json +1 -1
@@ -45,6 +45,14 @@ const datepicker_1 = require("../datepicker");
45
45
  const menu_1 = require("../menu");
46
46
  const select_1 = require("../select");
47
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 }));
48
56
  const OP_LABELS = {
49
57
  contains: "contains",
50
58
  doesnotcontain: "does not contain",
@@ -105,18 +113,53 @@ const DATE_OP_ORDER = [
105
113
  "empty",
106
114
  "notempty",
107
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"];
108
125
  // "isanyof" is appended only for a column that supplies `values` — there is
109
126
  // nothing to check off otherwise. A column's `type` picks the base set
110
- // first; "isanyof" can still follow any of the three (a column's own
111
- // declared values aren't tied to its underlying number/date/text kind).
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).
112
130
  const opsFor = (col) => {
113
131
  const base = col?.type === "number"
114
132
  ? NUMBER_OP_ORDER
115
133
  : col?.type === "date"
116
134
  ? DATE_OP_ORDER
117
- : OP_ORDER;
135
+ : col?.type === "select"
136
+ ? SELECT_OP_ORDER
137
+ : OP_ORDER;
118
138
  return col?.values?.length ? [...base, "isanyof"] : base;
119
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
+ };
120
163
  const opNeedsValue = (op) => op === "contains" ||
121
164
  op === "doesnotcontain" ||
122
165
  op === "startswith" ||
@@ -295,7 +338,7 @@ function applySorts(rows, sorts, get = (row, key) => row[key]) {
295
338
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
296
339
  * Created by), which one shared list can't reproduce.
297
340
  */
298
- function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, ...rest }) {
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 }) {
299
342
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
300
343
  // CrispMessagesProvider's translation of the English default below, or
301
344
  // that default itself with no provider — unchanged from before this
@@ -322,6 +365,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
322
365
  const reorderAttributeLabel = (0, core_1.useCrispMessage)(reorderAttributeLabelProp, "Reorder attribute");
323
366
  const addAttributeToViewLabel = (0, core_1.useCrispMessage)(addAttributeToViewLabelProp, "Add attribute to view");
324
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");
325
374
  // OP_LABELS' own words ("contains", "is", "is any of", …) have no prop of
326
375
  // their own — a whole lookup table, not one string — so they translate
327
376
  // straight from the provider's dictionary at the two spots they render
@@ -362,6 +411,17 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
362
411
  const [openMenu, setOpenMenu] = React.useState(null);
363
412
  const [dragSort, setDragSort] = React.useState(null);
364
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([]);
419
+ // #754: which sort row's own field picker is open (its index into
420
+ // `sorts`), or `null` for the normal sort-row list — the same
421
+ // "one sub-screen behind a flag" split View settings' own
422
+ // `addingAttribute` already uses, parameterized by index since more than
423
+ // one sort row exists.
424
+ const [sortFieldPickerIndex, setSortFieldPickerIndex] = React.useState(null);
365
425
  // filters the "isanyof" checkbox list, same lifetime as filterQuery above
366
426
  const [anyOfQuery, setAnyOfQuery] = React.useState("");
367
427
  const [dragAttr, setDragAttr] = React.useState(null);
@@ -375,7 +435,6 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
375
435
  // built here -- hiding an attribute is a trailing control instead, see
376
436
  // the hide button below).
377
437
  const [addingAttribute, setAddingAttribute] = React.useState(false);
378
- const [addAttrQuery, setAddAttrQuery] = React.useState("");
379
438
  // close any open menu on outside click (anything not inside a menu or its
380
439
  // trigger) or Escape
381
440
  React.useEffect(() => {
@@ -458,10 +517,27 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
458
517
  };
459
518
  // #590: shown attributes, in `shownKeys`' order -- a key with no matching
460
519
  // `viewColumns` entry (stale state after a column is removed upstream)
461
- // drops out rather than rendering a blank row.
462
- const shownAttrs = shownKeys
463
- .map((k) => viewColumns.find((c) => c.key === k))
464
- .filter((c) => c != null);
520
+ // drops out rather than rendering a blank row. #754: `shownKeys` can now
521
+ // carry a dot-joined nested path ("team.jobTitle") from the add-attribute
522
+ // screen's own nested picker, so each key resolves via the same
523
+ // `resolveColumnPath` the filter chip uses -- the LEAF column's icon
524
+ // drives the row's own icon, and the label is the full breadcrumb
525
+ // ("Team › Job title"), joined as plain text rather than the chip's own
526
+ // muted/chevron treatment, which was measured specifically for the
527
+ // filter chip and wasn't re-verified for this row shape.
528
+ const shownAttrs = shownKeys.flatMap((k) => {
529
+ const path = resolveColumnPath(viewColumns, k);
530
+ const leaf = path.at(-1);
531
+ if (!leaf)
532
+ return [];
533
+ return [
534
+ {
535
+ key: k,
536
+ icon: leaf.icon,
537
+ label: path.length > 1 ? path.map((c) => c.label).join(" › ") : leaf.label,
538
+ },
539
+ ];
540
+ });
465
541
  const hiddenAttrs = viewColumns.filter((c) => !shownKeys.includes(c.key));
466
542
  const moveAttr = (from, to) => {
467
543
  if (from === to)
@@ -475,7 +551,8 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
475
551
  const showAttr = (key) => {
476
552
  putShownKeys([...shownKeys, key]);
477
553
  setAddingAttribute(false);
478
- setAddAttrQuery("");
554
+ setFieldPickerPath([]);
555
+ setFilterQuery("");
479
556
  };
480
557
  // #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
481
558
  // model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
@@ -499,14 +576,62 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
499
576
  close: () => setOpenMenu(null),
500
577
  contentRef: opMenuRef,
501
578
  });
502
- // Shared between the "Filter" trigger's own add-condition picker and each
503
- // chip's field segment (#462 follow-up) — both search the same
504
- // `filterColumns` list and differ only in what a pick does.
505
- const filterFieldPicker = (onSelect) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterFieldPlaceholder, "aria-label": filterFieldLabel,
506
- // biome-ignore lint/a11y/noAutofocus: focus the picker on open
507
- autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) }), filterColumns
508
- .filter((col) => col.label.toLowerCase().includes(filterQuery.trim().toLowerCase()))
509
- .map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => onSelect(col.key), children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)("span", { style: { width: 15 } })), col.label] }, col.key)))] }));
579
+ // The Import/Export menu is `role="menu"` too — confirmed live, 2026-09-25
580
+ // (an `<a>`/`<div>` mix, each `role="menuitem"`, two `role="separator"`
581
+ // dividers between the 3 groups) — same shared keyboard model.
582
+ const importExportMenuRef = React.useRef(null);
583
+ const { onKeyDown: onImportExportKeyDown } = (0, menu_1.useMenuKeyboard)({
584
+ open: openMenu === "importexport",
585
+ close: () => setOpenMenu(null),
586
+ contentRef: importExportMenuRef,
587
+ });
588
+ // #754: generalized from the Filter-only `filterFieldPicker` (#592) to
589
+ // also drive Sort's own field picker and View settings' add-attribute
590
+ // screen — all three offer the identical nested-drill-down construct on
591
+ // live Attio (confirmed: "Team" with a count badge appears, and drills
592
+ // into the same People-attribute screen, in all three pickers). Shared
593
+ // `fieldPickerPath`/`filterQuery` state across all three call sites is
594
+ // safe: `openMenu` already only ever names ONE open popover at a time,
595
+ // so only one of Filter/Sort/View-settings' own picker can be showing.
596
+ // `fieldPickerPath` tracks the current scope ([] is the top level),
597
+ // reset to [] wherever a picker opens fresh (every trigger's own
598
+ // onClick).
599
+ const nestedFieldPicker = (columns, onSelect, topLevelPlaceholder, topLevelLabel) => {
600
+ const scope = fieldPickerPath.at(-1);
601
+ const scopeColumns = scope ? (scope.nestedColumns ?? []) : columns;
602
+ const query = filterQuery.trim().toLowerCase();
603
+ const direct = scopeColumns.filter((c) => c.label.toLowerCase().includes(query));
604
+ // #592: one level of flattening from wherever the picker is currently
605
+ // scoped — a nested column's own children get searched too, and a
606
+ // match renders breadcrumbed, alongside this level's direct matches.
607
+ // Matches the one case measured live at the TOP level ("Twitter
608
+ // follower" surfaces both the direct field and "Team › Twitter
609
+ // follower count"); applying the identical rule recursively from an
610
+ // already-drilled-into scope (so it also reaches Team → Company → X
611
+ // while already browsing inside Team) is the same construct, one level
612
+ // relative to wherever you are, but wasn't independently re-verified
613
+ // live from inside a nested scope — flagged on the PR.
614
+ const nested = query
615
+ ? scopeColumns.flatMap((c) => (c.nestedColumns ?? [])
616
+ .filter((n) => n.label.toLowerCase().includes(query))
617
+ .map((n) => ({ parent: c, col: n })))
618
+ : [];
619
+ const selectPath = (path) => onSelect(path.map((c) => c.key).join("."));
620
+ const drillIn = (col) => {
621
+ setFieldPickerPath([...fieldPickerPath, col]);
622
+ setFilterQuery("");
623
+ };
624
+ 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: () => {
625
+ setFieldPickerPath(fieldPickerPath.slice(0, -1));
626
+ setFilterQuery("");
627
+ }, 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
628
+ ? nestedFieldPlaceholder(scope.nestedObjectLabel ?? scope.label.toLowerCase())
629
+ : `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": topLevelLabel,
630
+ // biome-ignore lint/a11y/noAutofocus: focus the picker on open
631
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : ((0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: topLevelPlaceholder, "aria-label": topLevelLabel,
632
+ // biome-ignore lint/a11y/noAutofocus: focus the picker on open
633
+ 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}`)))] }));
634
+ };
510
635
  const activeFilters = filters.length;
511
636
  // The default phrase only works in English word order, so a caller can
512
637
  // replace the whole thing rather than fill a template — crisp/#471's
@@ -519,7 +644,11 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
519
644
  const sortedLabel = sortTriggerLabel
520
645
  ? sortTriggerLabel(sorts, sortColumns)
521
646
  : sorts.length
522
- ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
647
+ ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
648
+ // #754: sorts[0].key may be a dot-joined nested path
649
+ // ("team.jobTitle") — resolve it the same way the chip and sort
650
+ // row do, rather than a flat find that fails on a nested key.
651
+ resolveColumnPath(sortColumns, sorts[0].key).at(-1)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
523
652
  : sortLabel;
524
653
  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) => {
525
654
  if (dragSort !== null)
@@ -528,11 +657,26 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
528
657
  if (dragSort !== null)
529
658
  moveSort(dragSort, i);
530
659
  setDragSort(null);
531
- }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)(select_1.Select, { className: "crisp-tabletoolbar-sort-field", searchable: true, value: s.key, onValueChange: (k) => setSortField(i, k), options: sortColumns.map((c) => ({
532
- value: c.key,
533
- label: c.icon ? ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(c.icon, { size: 15, strokeWidth: 1.75 }), c.label] })) : (c.label),
534
- keywords: c.label,
535
- })), "aria-label": sortFieldLabel }), (0, jsx_runtime_1.jsx)(select_1.Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
660
+ }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: (0, jsx_runtime_1.jsx)(lucide_react_1.GripVertical, { size: 15, strokeWidth: 1.75 }) }), (() => {
661
+ // #754: the sort row's own field picker -- the
662
+ // same nested-drill-down construct Filter's field
663
+ // picker already has (#592), measured live:
664
+ // clicking a sort row's field (or "Add sort") opens
665
+ // an identical picker, "Team" (a relationship
666
+ // field) drills into the same People-attribute
667
+ // screen Filter's own "Team" does.
668
+ const sortColPath = resolveColumnPath(sortColumns, s.key);
669
+ const sortCol = sortColPath.at(-1);
670
+ const pickerOpen = sortFieldPickerIndex === i;
671
+ return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-field-picker", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-field-trigger", "aria-haspopup": "dialog", "aria-expanded": pickerOpen, "aria-label": sortFieldLabel, onClick: () => {
672
+ setFieldPickerPath([]);
673
+ setFilterQuery("");
674
+ setSortFieldPickerIndex(pickerOpen ? null : i);
675
+ }, children: [sortCol?.icon ? ((0, jsx_runtime_1.jsx)(sortCol.icon, { size: 15, strokeWidth: 1.75 })) : null, sortColPath.length > 1 ? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-fcond-path", children: sortColPath.map((c, pi) => ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [pi > 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))) })) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-sort-field-label", children: sortCol?.label }))] }), pickerOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": sortFieldLabel, children: nestedFieldPicker(sortColumns, (key) => {
676
+ setSortField(i, key);
677
+ setSortFieldPickerIndex(null);
678
+ }, searchAttributesPlaceholder, sortFieldLabel) }))] }));
679
+ })(), (0, jsx_runtime_1.jsx)(select_1.Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
536
680
  {
537
681
  value: "asc",
538
682
  label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpNarrowWide, { size: 15, strokeWidth: 1.75 }), ascendingLabel] })),
@@ -543,12 +687,21 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
543
687
  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] })),
544
688
  keywords: descendingLabel,
545
689
  },
546
- ], "aria-label": sortDirectionLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-foot", children: (0, jsx_runtime_1.jsxs)("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: `crisp-tabletoolbar-tool crisp-tabletoolbar-tool--ghost${activeFilters ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "filter" ? null : "filter"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
690
+ ], "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: () => {
691
+ setFieldPickerPath([]);
692
+ setOpenMenu(openMenu === "filter" ? null : "filter");
693
+ }, "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
547
694
  ? filterTriggerLabel(activeFilters)
548
695
  : activeFilters
549
696
  ? `${filterLabel} · ${activeFilters}`
550
- : filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
551
- const col = filterColumns.find((c) => c.key === cond.key);
697
+ : filterLabel] }), openMenu === "filter" && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, addFilter, filterFieldPlaceholder, filterFieldLabel) }))] }), filters.map((cond) => {
698
+ // #592: cond.key may be a dot-joined nested path
699
+ // ("team.twitterFollowerCount") — resolve the full path so the
700
+ // field segment can render its breadcrumb; `col` (the leaf) still
701
+ // drives the operator set and value editor exactly as before, for
702
+ // both a plain and a nested key.
703
+ const colPath = resolveColumnPath(filterColumns, cond.key);
704
+ const col = colPath.at(-1);
552
705
  const fieldOpen = openMenu === `ffield:${cond.id}`;
553
706
  const opOpen = openMenu === `fop:${cond.id}`;
554
707
  const valOpen = openMenu === `fval:${cond.id}`;
@@ -564,7 +717,10 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
564
717
  // which never renders a third box in either case.
565
718
  const showValueSeg = cond.op != null &&
566
719
  (opNeedsValue(cond.op) || opNeedsValues(cond.op));
567
- return ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-toolgroup", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-tabletoolbar-fcond", "data-complete": complete ? "" : undefined, children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--field", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": fieldOpen, onClick: () => setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`), children: [col?.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 14, strokeWidth: 1.75 }) : null, col?.label] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--op", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": opOpen, onClick: () => setOpenMenu(opOpen ? null : `fop:${cond.id}`), children: cond.op
720
+ 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: () => {
721
+ setFieldPickerPath([]);
722
+ setOpenMenu(fieldOpen ? null : `ffield:${cond.id}`);
723
+ }, 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
568
724
  ? (crispMessages[OP_LABELS[cond.op]] ?? OP_LABELS[cond.op])
569
725
  : selectConditionLabel }), showValueSeg && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--val", "data-menu-trigger": true, "aria-haspopup": "dialog", "aria-expanded": valOpen, onClick: () => setOpenMenu(valOpen ? null : `fval:${cond.id}`), children: cond.op === "isanyof"
570
726
  ? cond.values?.length
@@ -574,7 +730,7 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
574
730
  ? col?.type === "date"
575
731
  ? formatFilterDate(cond.value)
576
732
  : cond.value
577
- : filterValuePlaceholder })), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-tabletoolbar-fcond-seg crisp-tabletoolbar-fcond-seg--kebab", "data-menu-trigger": true, "aria-haspopup": "menu", "aria-expanded": kebabOpen, "aria-label": `${col?.label} ${filterOptionsLabel}`, onClick: () => setOpenMenu(kebabOpen ? null : `fkebab:${cond.id}`), children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreVertical, { size: 13, strokeWidth: 2 }) })] }), kebabOpen && ((0, jsx_runtime_1.jsx)("div", { ref: kebabMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--right crisp-tabletoolbar-menu--hdr", role: "menu", "aria-label": `${col?.label} ${filterOptionsLabel}`, onKeyDown: onKebabKeyDown, children: (0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item crisp-tabletoolbar-menu-item--danger", onClick: () => removeFilter(cond.id), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 16, strokeWidth: 1.75 }), deleteFilterLabel] }) })), fieldOpen && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker((key) => changeFilterField(cond.id, key)) })), opOpen && ((0, jsx_runtime_1.jsx)("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
733
+ : 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: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), opOpen && ((0, jsx_runtime_1.jsx)("div", { ref: opMenuRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--op", role: "menu", "aria-label": `${col?.label} ${crispMessages.condition ?? "condition"}`, onKeyDown: onOpKeyDown, children: opsFor(col).map((op) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", "data-on": cond.op === op ? "" : undefined, onClick: () => pickOp(cond.id, op), children: [crispMessages[OP_LABELS[op]] ?? OP_LABELS[op], cond.op === op ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "crisp-tabletoolbar-menu-check", size: 15, strokeWidth: 1.75 })) : null] }, op))) })), valOpen && showValueSeg && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-menu--val", role: "dialog", "aria-label": `${col?.label} filter value`, children: cond.op === "isanyof" && col?.values ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter values`,
578
734
  // biome-ignore lint/a11y/noAutofocus: focus the search field
579
735
  autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
580
736
  .filter((v) => v
@@ -594,7 +750,18 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
594
750
  values: col.values?.filter((x) => set.has(x)),
595
751
  });
596
752
  }, children: v }, v));
597
- })] })) : col?.type === "date" ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-datepop", children: [(0, jsx_runtime_1.jsx)(datepicker_1.DatePicker, { ...{
753
+ })] })) : col?.type === "select" &&
754
+ col?.values &&
755
+ (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`,
756
+ // biome-ignore lint/a11y/noAutofocus: focus the search field
757
+ autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
758
+ .filter((v) => v
759
+ .toLowerCase()
760
+ .includes(anyOfQuery.trim().toLowerCase()))
761
+ .map((v) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", "data-on": cond.value === v ? "" : undefined, onClick: () => {
762
+ patchFilter(cond.id, { value: v });
763
+ setOpenMenu(null);
764
+ }, 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, { ...{
598
765
  selectionMode: "single",
599
766
  value: isoToDate(cond.value)
600
767
  ? toDatePickerDate(isoToDate(cond.value))
@@ -625,17 +792,12 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
625
792
  }
626
793
  else {
627
794
  setAddingAttribute(false);
628
- setAddAttrQuery("");
795
+ setFieldPickerPath([]);
796
+ setFilterQuery("");
629
797
  setOpenMenu("viewsettings");
630
798
  }
631
799
  }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel] }), openMenu === "viewsettings" &&
632
- (addingAttribute ? ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-tabletoolbar-menu-input", placeholder: searchAttributesPlaceholder, "aria-label": searchAttributesPlaceholder,
633
- // biome-ignore lint/a11y/noAutofocus: focus the search box on open, same as Filter's field picker
634
- autoFocus: true, value: addAttrQuery, onChange: (e) => setAddAttrQuery(e.target.value) }), hiddenAttrs
635
- .filter((col) => col.label
636
- .toLowerCase()
637
- .includes(addAttrQuery.trim().toLowerCase()))
638
- .map((col) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => showAttr(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)))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
800
+ (addingAttribute ? ((0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
639
801
  if (dragAttr !== null)
640
802
  e.preventDefault();
641
803
  }, onDrop: () => {
@@ -643,7 +805,24 @@ function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: fi
643
805
  moveAttr(dragAttr, i);
644
806
  setDragAttr(null);
645
807
  }, 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) ??
646
- `${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), (0, jsx_runtime_1.jsx)("div", { className: "crisp-tabletoolbar-sort-addrow", children: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => setAddingAttribute(true), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-spacer" }), count != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
808
+ `${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: () => {
809
+ setAddingAttribute(true);
810
+ setFieldPickerPath([]);
811
+ setFilterQuery("");
812
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] })), (onImportCsv ||
813
+ onExportCsv ||
814
+ onExportExcel ||
815
+ 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: () => {
816
+ onImportCsv();
817
+ setOpenMenu(null);
818
+ }, 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: () => {
819
+ onExportCsv();
820
+ setOpenMenu(null);
821
+ }, 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: () => {
822
+ onExportExcel();
823
+ setOpenMenu(null);
824
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
825
+ (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] }) }));
647
826
  }
648
827
  /**
649
828
  * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
@@ -36,17 +36,62 @@ const filterChipChrome = `
36
36
  & .crisp-tabletoolbar-fcond-seg svg { flex-shrink: 0; }
37
37
  & .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--field,
38
38
  & .crisp-tabletoolbar-fcond[data-complete] .crisp-tabletoolbar-fcond-seg--val { background: ${tokens_1.contract.bgSurface}; }
39
- & .crisp-tabletoolbar-fcond-seg--field { border-radius: ${tokens_1.contract.radius.md} 0 0 ${tokens_1.contract.radius.md}; font-weight: ${tokens_1.contract.weight.medium}; }
39
+ & .crisp-tabletoolbar-fcond-seg--field { border-radius: ${tokens_1.contract.radius.md} 0 0 ${tokens_1.contract.radius.md}; font-weight: ${tokens_1.contract.weight.medium}; max-width: 200px; }
40
40
  & .crisp-tabletoolbar-fcond-seg--field svg { color: ${tokens_1.contract.fgMuted}; }
41
+ /* #592: a nested pick's field segment ("Team › Twitter follower
42
+ count") — measured live: BOTH path segments the same colour (chip
43
+ context only; the picker's own menu rows keep the parent muted, see
44
+ tabletoolbar.chrome.ts), each independently truncated. Attio's own
45
+ layout gives the leaf segment priority (it stays fully readable
46
+ longer, the parent truncates first) via a two-part min-content
47
+ measurement this reproduces qualitatively with a flex-shrink
48
+ differential rather than byte-for-byte (the exact px split Attio
49
+ computes is content-length-dependent, not a fixed value) — not
50
+ independently re-verified at other path-segment lengths. */
51
+ & .crisp-tabletoolbar-fcond-path { display: inline-flex; align-items: center; gap: 2px; min-width: 0; max-width: 178px; }
52
+ & .crisp-tabletoolbar-fcond-path-seg { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
53
+ & .crisp-tabletoolbar-fcond-path-seg:first-child { flex: 0 1 auto; }
54
+ & .crisp-tabletoolbar-fcond-path-sep { flex-shrink: 0; color: inherit; }
41
55
  & .crisp-tabletoolbar-fcond-seg--op { background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgMuted}; }
42
56
  & .crisp-tabletoolbar-fcond-seg--val { font-weight: ${tokens_1.contract.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
43
57
  & .crisp-tabletoolbar-fcond-seg--kebab { background: ${tokens_1.contract.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${tokens_1.contract.radius.md} ${tokens_1.contract.radius.md} 0; color: ${tokens_1.contract.fgMuted}; }
44
58
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${tokens_1.contract.fgPrimary}; }
59
+ /* #754: the sort row's own field trigger, replacing a plain Select
60
+ with the same nested-drill-down popover Filter's field picker
61
+ already has — reuses Select's own already-measured trigger chrome
62
+ verbatim (its class is scoped under Select's own root class, so it
63
+ can't be reused as a bare class name outside an actual Select tree;
64
+ these values are copied, not re-measured). */
65
+ & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
66
+ & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: ${tokens_1.contract.size.menuRow}; width: 100%; min-width: 0; margin: 0; padding: 0 ${tokens_1.contract.space["1.5"]} 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 7%, transparent); font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
67
+ & .crisp-tabletoolbar-sort-field-trigger:hover { background: ${tokens_1.contract.bgRaised}; }
68
+ & .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
69
+ & .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; background: ${tokens_1.contract.bgSurface}; }
70
+ & .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
71
+ & .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
45
72
  & .crisp-tabletoolbar-menu--val { min-width: 134px; }
46
73
  & .crisp-tabletoolbar-datepop { width: 300px; }
47
74
  & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${tokens_1.contract.space["1"]}; margin-top: ${tokens_1.contract.space["2"]}; padding: 0 ${tokens_1.contract.space["1"]}; }
48
75
  & .crisp-tabletoolbar-datepop-preset { height: ${tokens_1.contract.size.chip}; padding: 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.chip}; background: ${tokens_1.contract.bgRaised}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.sm}; cursor: pointer; }
49
76
  & .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${tokens_1.contract.fgPrimary} 8%, transparent); }`;
77
+ // #595: the Import/Export trigger — measured live, `app.attio.com`'s own
78
+ // view bar: `28px` tall, `radius.md` (8px), background `bgSurfaceMuted`
79
+ // (`rgb(246,247,247)`, exact match) and `elevation.field`'s exact 3-layer
80
+ // shadow (`inset 0 0 0 1px transparent, 0 0 2px rgba(28,40,64,.18), 0 1px 3px
81
+ // rgba(0,0,0,.04)`) — a "raised chrome button" distinct from Sort/Filter/View
82
+ // settings' plain ringed `.tool` (no background, no elevation shadow), and
83
+ // the only trigger of the four with a trailing chevron (2 icons total,
84
+ // confirmed live). Overrides `.tool`'s own base background/shadow by cascade
85
+ // order (this block is appended after `toolbarChromeBody`, which defines
86
+ // them), the same way `.tool--ghost` already does. `border`/`font-family`/
87
+ // `cursor` are repeated here too, even though `.tool`'s own base rule (in
88
+ // the shared `tabletoolbar.chrome.ts`) already sets them on every button
89
+ // carrying this modifier — `spec-check.mjs`'s button-reset audit only reads
90
+ // each component's own `<name>.recipe.ts` file verbatim, so a reset living
91
+ // behind `chrome.ts`'s function call is invisible to it; harmless repeats,
92
+ // not a real gap.
93
+ const importExportChrome = `
94
+ & .crisp-tabletoolbar-tool--raised { border: none; background: ${tokens_1.contract.bgSurfaceMuted}; box-shadow: ${tokens_1.elevation.field}; font-family: inherit; cursor: pointer; }`;
50
95
  // The table toolbar, measured: 28px controls, the Sort/Filter buttons and
51
96
  // the menus they open, sort-condition rows, and the filter chips. The chrome
52
97
  // itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
@@ -64,6 +109,8 @@ const config = (0, core_1.defineRecipe)({
64
109
  ${(0, tabletoolbar_chrome_1.toolbarChromeHead)("tabletoolbar")}
65
110
  ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("tabletoolbar")}
66
111
  ${filterChipChrome}
112
+ ${importExportChrome}
113
+ ${core_1.rtlFlipCss}
67
114
  `,
68
115
  });
69
116
  exports.tabletoolbarRecipe = (0, core_1.resolveRecipe)(config);
@@ -13,13 +13,23 @@ const tokens_1 = require("../../tokens");
13
13
  // the same reason (see its own doc).
14
14
  const spec = (0, core_1.defineSpec)({
15
15
  name: "text",
16
- // `display: inline` -- a span's own default, stated rather than left
17
- // implicit: an EMPTY base compiles to an empty string, and
18
- // `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats that
19
- // falsy string as "no base rule", which drops the `crisp-text` class
20
- // itself, silently, from every instance (caught by this file's own
21
- // tests going red on that exact class).
22
- base: { display: "inline" },
16
+ // Needs a real, always-present declaration so `resolveRecipe` emits the
17
+ // `crisp-text` class at all -- an EMPTY base compiles to an empty string,
18
+ // and `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
19
+ // that falsy string as "no base rule", dropping the class itself,
20
+ // silently, from every instance (this file's own tests catch that).
21
+ // `display: "inline"` filled that role until a real bug: `asChild`
22
+ // (Primitive.span -> Radix Slot) merges this class onto WHATEVER element
23
+ // the caller passes, not a `<span>` -- a `<div>`/`<p>`/`<li>` child's own
24
+ // block default lost to `.crisp-text`'s class-specificity `display:
25
+ // inline`, silently collapsing its layout (internal/grc's own Settings.tsx
26
+ // ran two rows onto one line this way). `vertical-align: baseline` fills
27
+ // the same "always-present, never empty" role without that risk: it's a
28
+ // span's own already-inherited default (so nothing changes for the plain,
29
+ // non-asChild case `Primitive.span` renders), and it has NO effect at all
30
+ // on a block-level box, so an `asChild` div/p/li keeps its own display
31
+ // exactly as its own tag already gives it.
32
+ base: { verticalAlign: "baseline" },
23
33
  variants: {
24
34
  tone: {
25
35
  default: { color: tokens_1.contract.fgPrimary },