@crisp-ui-kit/crisp 0.54.9 → 0.54.11

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 (52) hide show
  1. package/dist/cjs/components/datatable/datatable.js +175 -12
  2. package/dist/cjs/components/datatable/datatable.recipe.js +30 -0
  3. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  4. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +159 -63
  5. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +41 -9
  6. package/dist/cjs/components/text/text.recipe.js +17 -7
  7. package/dist/cjs/product/recordpage/column.js +19 -0
  8. package/dist/cjs/product/recordpage/recordpage.js +5 -4
  9. package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
  10. package/dist/cjs/product/recordstable/index.js +1 -1
  11. package/dist/cjs/product/recordstable/recordstable.js +5 -0
  12. package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
  13. package/dist/cjs/product/taskstable/taskstable.js +5 -0
  14. package/dist/datatable.css +17 -0
  15. package/dist/datatable.layered.css +17 -0
  16. package/dist/esm/components/datatable/datatable.js +175 -12
  17. package/dist/esm/components/datatable/datatable.recipe.js +30 -0
  18. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  19. package/dist/esm/components/tabletoolbar/tabletoolbar.js +159 -63
  20. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +41 -9
  21. package/dist/esm/components/text/text.recipe.js +17 -7
  22. package/dist/esm/product/recordpage/column.js +16 -0
  23. package/dist/esm/product/recordpage/recordpage.js +2 -1
  24. package/dist/esm/product/recordpage/test-fixtures.js +52 -0
  25. package/dist/esm/product/recordstable/index.js +1 -1
  26. package/dist/esm/product/recordstable/recordstable.js +5 -0
  27. package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
  28. package/dist/esm/product/taskstable/taskstable.js +5 -0
  29. package/dist/recordstable.css +19 -2
  30. package/dist/recordstable.layered.css +19 -2
  31. package/dist/savedviews.css +6 -18
  32. package/dist/savedviews.layered.css +6 -18
  33. package/dist/styles.css +78 -24
  34. package/dist/styles.layered.css +78 -24
  35. package/dist/tabletoolbar.css +35 -3
  36. package/dist/tabletoolbar.layered.css +35 -3
  37. package/dist/text.css +1 -1
  38. package/dist/text.layered.css +1 -1
  39. package/dist/types/components/datatable/datatable.d.ts +38 -3
  40. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +26 -1
  41. package/dist/types/components/text/text.recipe.d.ts +1 -1
  42. package/dist/types/product/recordpage/column.d.ts +112 -0
  43. package/dist/types/product/recordpage/recordpage.d.ts +1 -1
  44. package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
  45. package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
  46. package/dist/types/product/recordstable/index.d.ts +1 -1
  47. package/dist/types/product/recordstable/recordstable.d.ts +6 -1
  48. package/dist/types/product/taskstable/taskstable.d.ts +5 -0
  49. package/package.json +1 -1
  50. /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
  51. /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
  52. /package/dist/types/product/{recordstable → recordpage}/editable-field.d.ts +0 -0
@@ -1,11 +1,41 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
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
+ import { createPortal } from "react-dom";
4
5
  import { isInsideLaterFloatingLayer, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
5
6
  import { DatePicker, } from "../datepicker/index.js";
6
7
  import { MenuCheckboxItem, useMenuKeyboard } from "../menu/index.js";
7
8
  import { Select } from "../select/index.js";
8
9
  import { tabletoolbarRecipe } from "./tabletoolbar.recipe.js";
10
+ // crisp/#766, #772: portals View settings/Import-Export into a
11
+ // consumer-supplied target (e.g. a `<span>` rendered next to `SavedViews`,
12
+ // or another next to the primary action) instead of this toolbar's own row,
13
+ // when one is given -- a plain passthrough otherwise, so the default (no
14
+ // ref given) stays byte-identical to before this existed. Kept outside the
15
+ // main component (no closure over its state) so it's a stable reference
16
+ // across renders.
17
+ //
18
+ // #772 fix: every `.crisp-tabletoolbar-*` chrome rule is nested under this
19
+ // recipe's own root class (`& .crisp-tabletoolbar-tool { ... }`), which
20
+ // compiles to a real CSS descendant combinator -- it needs actual DOM
21
+ // containment under an element carrying that class. A portal target
22
+ // rendered by the CONSUMER, outside this component's own root `<div>`,
23
+ // breaks that containment even though React still considers the portaled
24
+ // content a child for event/context purposes: the classes were still
25
+ // applied, they just matched nothing, so the buttons rendered as unstyled
26
+ // native chrome. Same failure class `select.recipe.ts`'s own doc comment
27
+ // describes for its listbox portal to `document.body`. Fixed the same way
28
+ // `ToolbarTool` (below) already lets a bare `.crisp-tabletoolbar-tool` be
29
+ // used outside an actual `TableToolbar` tree: wrap the portaled content in
30
+ // its own `tabletoolbarRecipe()`-classed span. `display: contents` on that
31
+ // recipe's own base rule means the wrapper contributes no box of its own,
32
+ // so it changes nothing about layout -- only re-establishes the selector
33
+ // match.
34
+ function ViewBarSlot({ target, children, }) {
35
+ return target
36
+ ? createPortal(_jsx("span", { className: tabletoolbarRecipe(), children: children }), target)
37
+ : children;
38
+ }
9
39
  // #592: shared by every direction-bearing ChevronRight usage below (the
10
40
  // field picker's own disclosure chevron, a flattened search result's
11
41
  // breadcrumb separator, and a filter chip's own breadcrumb separator) so
@@ -299,7 +329,7 @@ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
299
329
  * this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
300
330
  * Created by), which one shared list can't reproduce.
301
331
  */
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 }) {
332
+ 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, viewSettingsRef, importExportRef, 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 }) {
303
333
  // crisp/#471: an explicit prop always wins; missing, it reads the nearest
304
334
  // CrispMessagesProvider's translation of the English default below, or
305
335
  // that default itself with no provider — unchanged from before this
@@ -369,6 +399,19 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
369
399
  setShownKeysState(next);
370
400
  onShownKeysChange?.(next);
371
401
  };
402
+ // crisp/#766: a ref's `.current` is null during THIS render (a ref only
403
+ // attaches during commit, and the consumer's own target element may not
404
+ // even exist in the DOM yet on first paint) -- an effect after mount picks
405
+ // it up and re-renders once, the same one-render-late pattern the floating
406
+ // popovers elsewhere in this file already use for a portal target.
407
+ const [viewSettingsTarget, setViewSettingsTarget] = React.useState(null);
408
+ React.useEffect(() => {
409
+ setViewSettingsTarget(viewSettingsRef?.current ?? null);
410
+ }, [viewSettingsRef]);
411
+ const [importExportTarget, setImportExportTarget] = React.useState(null);
412
+ React.useEffect(() => {
413
+ setImportExportTarget(importExportRef?.current ?? null);
414
+ }, [importExportRef]);
372
415
  const [openMenu, setOpenMenu] = React.useState(null);
373
416
  const [dragSort, setDragSort] = React.useState(null);
374
417
  const [filterQuery, setFilterQuery] = React.useState("");
@@ -377,6 +420,12 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
377
420
  // [team, company] is Team -> Company, etc. Reset to [] wherever
378
421
  // `filterQuery` already resets (opening the picker fresh), same lifetime.
379
422
  const [fieldPickerPath, setFieldPickerPath] = React.useState([]);
423
+ // #754: which sort row's own field picker is open (its index into
424
+ // `sorts`), or `null` for the normal sort-row list — the same
425
+ // "one sub-screen behind a flag" split View settings' own
426
+ // `addingAttribute` already uses, parameterized by index since more than
427
+ // one sort row exists.
428
+ const [sortFieldPickerIndex, setSortFieldPickerIndex] = React.useState(null);
380
429
  // filters the "isanyof" checkbox list, same lifetime as filterQuery above
381
430
  const [anyOfQuery, setAnyOfQuery] = React.useState("");
382
431
  const [dragAttr, setDragAttr] = React.useState(null);
@@ -390,7 +439,6 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
390
439
  // built here -- hiding an attribute is a trailing control instead, see
391
440
  // the hide button below).
392
441
  const [addingAttribute, setAddingAttribute] = React.useState(false);
393
- const [addAttrQuery, setAddAttrQuery] = React.useState("");
394
442
  // close any open menu on outside click (anything not inside a menu or its
395
443
  // trigger) or Escape
396
444
  React.useEffect(() => {
@@ -473,10 +521,27 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
473
521
  };
474
522
  // #590: shown attributes, in `shownKeys`' order -- a key with no matching
475
523
  // `viewColumns` entry (stale state after a column is removed upstream)
476
- // drops out rather than rendering a blank row.
477
- const shownAttrs = shownKeys
478
- .map((k) => viewColumns.find((c) => c.key === k))
479
- .filter((c) => c != null);
524
+ // drops out rather than rendering a blank row. #754: `shownKeys` can now
525
+ // carry a dot-joined nested path ("team.jobTitle") from the add-attribute
526
+ // screen's own nested picker, so each key resolves via the same
527
+ // `resolveColumnPath` the filter chip uses -- the LEAF column's icon
528
+ // drives the row's own icon, and the label is the full breadcrumb
529
+ // ("Team › Job title"), joined as plain text rather than the chip's own
530
+ // muted/chevron treatment, which was measured specifically for the
531
+ // filter chip and wasn't re-verified for this row shape.
532
+ const shownAttrs = shownKeys.flatMap((k) => {
533
+ const path = resolveColumnPath(viewColumns, k);
534
+ const leaf = path.at(-1);
535
+ if (!leaf)
536
+ return [];
537
+ return [
538
+ {
539
+ key: k,
540
+ icon: leaf.icon,
541
+ label: path.length > 1 ? path.map((c) => c.label).join(" › ") : leaf.label,
542
+ },
543
+ ];
544
+ });
480
545
  const hiddenAttrs = viewColumns.filter((c) => !shownKeys.includes(c.key));
481
546
  const moveAttr = (from, to) => {
482
547
  if (from === to)
@@ -490,7 +555,8 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
490
555
  const showAttr = (key) => {
491
556
  putShownKeys([...shownKeys, key]);
492
557
  setAddingAttribute(false);
493
- setAddAttrQuery("");
558
+ setFieldPickerPath([]);
559
+ setFilterQuery("");
494
560
  };
495
561
  // #594: the filter chip's kebab is a real role="menu" (crisp/#188's shared
496
562
  // model — arrows, Home/End, typeahead, Escape), not a hand-rolled div like
@@ -523,15 +589,20 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
523
589
  close: () => setOpenMenu(null),
524
590
  contentRef: importExportMenuRef,
525
591
  });
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) => {
592
+ // #754: generalized from the Filter-only `filterFieldPicker` (#592) to
593
+ // also drive Sort's own field picker and View settings' add-attribute
594
+ // screen — all three offer the identical nested-drill-down construct on
595
+ // live Attio (confirmed: "Team" with a count badge appears, and drills
596
+ // into the same People-attribute screen, in all three pickers). Shared
597
+ // `fieldPickerPath`/`filterQuery` state across all three call sites is
598
+ // safe: `openMenu` already only ever names ONE open popover at a time,
599
+ // so only one of Filter/Sort/View-settings' own picker can be showing.
600
+ // `fieldPickerPath` tracks the current scope ([] is the top level),
601
+ // reset to [] wherever a picker opens fresh (every trigger's own
602
+ // onClick).
603
+ const nestedFieldPicker = (columns, onSelect, topLevelPlaceholder, topLevelLabel) => {
533
604
  const scope = fieldPickerPath.at(-1);
534
- const scopeColumns = scope ? (scope.nestedColumns ?? []) : filterColumns;
605
+ const scopeColumns = scope ? (scope.nestedColumns ?? []) : columns;
535
606
  const query = filterQuery.trim().toLowerCase();
536
607
  const direct = scopeColumns.filter((c) => c.label.toLowerCase().includes(query));
537
608
  // #592: one level of flattening from wherever the picker is currently
@@ -559,9 +630,9 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
559
630
  setFilterQuery("");
560
631
  }, children: _jsx(ChevronLeft, { "data-rtl-flip": "", size: 15, strokeWidth: 1.75 }) }), _jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: nestedFieldPlaceholder
561
632
  ? nestedFieldPlaceholder(scope.nestedObjectLabel ?? scope.label.toLowerCase())
562
- : `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": filterFieldLabel,
633
+ : `Search ${scope.nestedObjectLabel ?? scope.label.toLowerCase()} attributes…`, "aria-label": topLevelLabel,
563
634
  // 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,
635
+ autoFocus: true, value: filterQuery, onChange: (e) => setFilterQuery(e.target.value) })] })) : (_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: topLevelPlaceholder, "aria-label": topLevelLabel,
565
636
  // biome-ignore lint/a11y/noAutofocus: focus the picker on open
566
637
  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
638
  };
@@ -574,23 +645,49 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
574
645
  // worse than an imperfect word order); `crispMessages` rather than a
575
646
  // second `useCrispMessage` call, since this only fires on the branch that
576
647
  // needs it.
648
+ // crisp/#788: "Sorted by" and the field name are NOT a font-weight split
649
+ // (both measure 500/medium live) -- the difference is colour/opacity only:
650
+ // "Sorted by" at 63% black, the field name at full-strength near-black
651
+ // (rgb(16,17,18)). The "+N" suffix is smaller (12px vs 14px) and its own
652
+ // mid-grey (rgb(80,81,85)), not muted-63%-black either -- three distinct
653
+ // treatments, not two. `sortField` isolates the resolved label so the
654
+ // default (non-`sortTriggerLabel`) render can wrap each part in its own
655
+ // span; `sortedLabel` stays a single string for the `sortTriggerLabel`
656
+ // escape hatch (a caller-supplied formatter can't be split, by definition)
657
+ // and the empty-state `sortLabel`.
658
+ const sortField = resolveColumnPath(sortColumns, sorts[0]?.key ?? "").at(-1)?.label;
577
659
  const sortedLabel = sortTriggerLabel
578
660
  ? sortTriggerLabel(sorts, sortColumns)
579
661
  : sorts.length
580
- ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortColumns.find((c) => c.key === sorts[0].key)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
662
+ ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortField}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
581
663
  : sortLabel;
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) => {
664
+ 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" : " crisp-tabletoolbar-tool--dashed"}`, 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" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", _jsx("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && (_jsxs("span", { className: "crisp-tabletoolbar-sorted-count", children: [" ", "+", sorts.length - 1] }))] }))] }), 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) => {
583
665
  if (dragSort !== null)
584
666
  e.preventDefault();
585
667
  }, onDrop: () => {
586
668
  if (dragSort !== null)
587
669
  moveSort(dragSort, i);
588
670
  setDragSort(null);
589
- }, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), _jsx(Select, { className: "crisp-tabletoolbar-sort-field", searchable: true, value: s.key, onValueChange: (k) => setSortField(i, k), options: sortColumns.map((c) => ({
590
- value: c.key,
591
- label: c.icon ? (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(c.icon, { size: 15, strokeWidth: 1.75 }), c.label] })) : (c.label),
592
- keywords: c.label,
593
- })), "aria-label": sortFieldLabel }), _jsx(Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
671
+ }, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderSortLabel, draggable: true, onDragStart: () => setDragSort(i), onDragEnd: () => setDragSort(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), (() => {
672
+ // #754: the sort row's own field picker -- the
673
+ // same nested-drill-down construct Filter's field
674
+ // picker already has (#592), measured live:
675
+ // clicking a sort row's field (or "Add sort") opens
676
+ // an identical picker, "Team" (a relationship
677
+ // field) drills into the same People-attribute
678
+ // screen Filter's own "Team" does.
679
+ const sortColPath = resolveColumnPath(sortColumns, s.key);
680
+ const sortCol = sortColPath.at(-1);
681
+ const pickerOpen = sortFieldPickerIndex === i;
682
+ return (_jsxs("span", { className: "crisp-tabletoolbar-sort-field-picker", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-field-trigger", "aria-haspopup": "dialog", "aria-expanded": pickerOpen, "aria-label": sortFieldLabel, onClick: () => {
683
+ setFieldPickerPath([]);
684
+ setFilterQuery("");
685
+ setSortFieldPickerIndex(pickerOpen ? null : i);
686
+ }, children: [sortCol?.icon ? (_jsx(sortCol.icon, { size: 15, strokeWidth: 1.75 })) : null, sortColPath.length > 1 ? (_jsx("span", { className: "crisp-tabletoolbar-fcond-path", children: sortColPath.map((c, pi) => (_jsxs(React.Fragment, { children: [pi > 0 && (_jsx(RtlChevronRight, { size: 14, className: "crisp-tabletoolbar-fcond-path-sep" })), _jsx("span", { className: "crisp-tabletoolbar-fcond-path-seg", children: c.label })] }, c.key))) })) : (_jsx("span", { className: "crisp-tabletoolbar-sort-field-label", children: sortCol?.label }))] }), pickerOpen && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": sortFieldLabel, children: nestedFieldPicker(sortColumns, (key) => {
687
+ setSortField(i, key);
688
+ setSortFieldPickerIndex(null);
689
+ }, searchAttributesPlaceholder, sortFieldLabel) }))] }));
690
+ })(), _jsx(Select, { className: "crisp-tabletoolbar-sort-dir", value: s.dir, onValueChange: (d) => setSortDir(i, d), options: [
594
691
  {
595
692
  value: "asc",
596
693
  label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowUpNarrowWide, { size: 15, strokeWidth: 1.75 }), ascendingLabel] })),
@@ -601,14 +698,14 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
601
698
  label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
602
699
  keywords: descendingLabel,
603
700
  },
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: () => {
701
+ ], "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${activeFilters ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => {
605
702
  setFieldPickerPath([]);
606
703
  setOpenMenu(openMenu === "filter" ? null : "filter");
607
704
  }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
608
705
  ? filterTriggerLabel(activeFilters)
609
706
  : activeFilters
610
707
  ? `${filterLabel} · ${activeFilters}`
611
- : filterLabel] }), openMenu === "filter" && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: filterFieldPicker(addFilter) }))] }), filters.map((cond) => {
708
+ : filterLabel] }), openMenu === "filter" && (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": filterFieldLabel, children: nestedFieldPicker(filterColumns, addFilter, filterFieldPlaceholder, filterFieldLabel) }))] }), filters.map((cond) => {
612
709
  // #592: cond.key may be a dot-joined nested path
613
710
  // ("team.twitterFollowerCount") — resolve the full path so the
614
711
  // field segment can render its breadcrumb; `col` (the leaf) still
@@ -644,7 +741,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
644
741
  ? col?.type === "date"
645
742
  ? formatFilterDate(cond.value)
646
743
  : 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`,
744
+ : 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: nestedFieldPicker(filterColumns, (key) => changeFilterField(cond.id, key), filterFieldPlaceholder, filterFieldLabel) })), 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
745
  // biome-ignore lint/a11y/noAutofocus: focus the search field
649
746
  autoFocus: true, value: anyOfQuery, onChange: (e) => setAnyOfQuery(e.target.value) }), col.values
650
747
  .filter((v) => v
@@ -700,44 +797,43 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
700
797
  }, 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
798
  // biome-ignore lint/a11y/noAutofocus: focus the value field
702
799
  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: () => {
704
- if (openMenu === "viewsettings") {
705
- setOpenMenu(null);
706
- }
707
- else {
708
- setAddingAttribute(false);
709
- setAddAttrQuery("");
710
- setOpenMenu("viewsettings");
711
- }
712
- }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel] }), openMenu === "viewsettings" &&
713
- (addingAttribute ? (_jsxs("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: [_jsx("input", { className: "crisp-tabletoolbar-menu-input", placeholder: searchAttributesPlaceholder, "aria-label": searchAttributesPlaceholder,
714
- // biome-ignore lint/a11y/noAutofocus: focus the search box on open, same as Filter's field picker
715
- autoFocus: true, value: addAttrQuery, onChange: (e) => setAddAttrQuery(e.target.value) }), hiddenAttrs
716
- .filter((col) => col.label
717
- .toLowerCase()
718
- .includes(addAttrQuery.trim().toLowerCase()))
719
- .map((col) => (_jsxs("button", { type: "button", className: "crisp-tabletoolbar-menu-item", onClick: () => showAttr(col.key), children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : (_jsx("span", { style: { width: 15 } })), col.label] }, col.key)))] })) : (_jsxs("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
720
- if (dragAttr !== null)
721
- e.preventDefault();
722
- }, onDrop: () => {
723
- if (dragAttr !== null)
724
- moveAttr(dragAttr, i);
725
- setDragAttr(null);
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) ??
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 ||
800
+ }), showViewSettings && !viewSettingsTarget && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && (_jsx(ViewBarSlot, { target: viewSettingsTarget, children: _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => {
801
+ if (openMenu === "viewsettings") {
802
+ setOpenMenu(null);
803
+ }
804
+ else {
805
+ setAddingAttribute(false);
806
+ setFieldPickerPath([]);
807
+ setFilterQuery("");
808
+ setOpenMenu("viewsettings");
809
+ }
810
+ }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel, _jsx(ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "viewsettings" &&
811
+ (addingAttribute ? (_jsx("div", { className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : (_jsxs("div", { className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, children: [shownAttrs.map((col, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
812
+ if (dragAttr !== null)
813
+ e.preventDefault();
814
+ }, onDrop: () => {
815
+ if (dragAttr !== null)
816
+ moveAttr(dragAttr, i);
817
+ setDragAttr(null);
818
+ }, 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) ??
819
+ `${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: () => {
820
+ setAddingAttribute(true);
821
+ setFieldPickerPath([]);
822
+ setFilterQuery("");
823
+ }, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })))] }) })), (onImportCsv ||
728
824
  onExportCsv ||
729
825
  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] }) }));
826
+ importExportHelpHref) && (_jsx(ViewBarSlot, { target: importExportTarget, children: _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: () => {
827
+ onImportCsv();
828
+ setOpenMenu(null);
829
+ }, 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: () => {
830
+ onExportCsv();
831
+ setOpenMenu(null);
832
+ }, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
833
+ onExportExcel();
834
+ setOpenMenu(null);
835
+ }, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
836
+ (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] }) }));
741
837
  }
742
838
  /**
743
839
  * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
@@ -53,19 +53,39 @@ const filterChipChrome = `
53
53
  & .crisp-tabletoolbar-fcond-seg--val { font-weight: ${c.weight.medium}; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
54
54
  & .crisp-tabletoolbar-fcond-seg--kebab { background: ${c.bgSurface}; justify-content: center; width: 28px; padding: 0; border-radius: 0 ${c.radius.md} ${c.radius.md} 0; color: ${c.fgMuted}; }
55
55
  & .crisp-tabletoolbar-fcond-seg--kebab:hover { color: ${c.fgPrimary}; }
56
+ /* #754: the sort row's own field trigger, replacing a plain Select
57
+ with the same nested-drill-down popover Filter's field picker
58
+ already has — reuses Select's own already-measured trigger chrome
59
+ verbatim (its class is scoped under Select's own root class, so it
60
+ can't be reused as a bare class name outside an actual Select tree;
61
+ these values are copied, not re-measured). */
62
+ & .crisp-tabletoolbar-sort-field-picker { position: relative; display: inline-flex; flex: 1; min-width: 0; }
63
+ & .crisp-tabletoolbar-sort-field-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.menuRow}; width: 100%; min-width: 0; margin: 0; padding: 0 ${c.space["1.5"]} 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: transparent; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 7%, transparent); font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; cursor: pointer; transition: background-color .12s ease, box-shadow .12s ease; }
64
+ & .crisp-tabletoolbar-sort-field-trigger:hover { background: ${c.bgRaised}; }
65
+ & .crisp-tabletoolbar-sort-field-trigger[aria-expanded="true"],
66
+ & .crisp-tabletoolbar-sort-field-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; background: ${c.bgSurface}; }
67
+ & .crisp-tabletoolbar-sort-field-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: start; }
68
+ & .crisp-tabletoolbar-sort-field-picker .crisp-tabletoolbar-menu { min-width: 200px; }
56
69
  & .crisp-tabletoolbar-menu--val { min-width: 134px; }
57
70
  & .crisp-tabletoolbar-datepop { width: 300px; }
58
71
  & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${c.space["1"]}; margin-top: ${c.space["2"]}; padding: 0 ${c.space["1"]}; }
59
72
  & .crisp-tabletoolbar-datepop-preset { height: ${c.size.chip}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.chip}; background: ${c.bgRaised}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.sm}; cursor: pointer; }
60
73
  & .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); }`;
61
- // #595: the Import/Export trigger — measured live, `app.attio.com`'s own
62
- // view bar: `28px` tall, `radius.md` (8px), background `bgSurfaceMuted`
63
- // (`rgb(246,247,247)`, exact match) and `elevation.field`'s exact 3-layer
64
- // shadow (`inset 0 0 0 1px transparent, 0 0 2px rgba(28,40,64,.18), 0 1px 3px
65
- // rgba(0,0,0,.04)`) — a "raised chrome button" distinct from Sort/Filter/View
66
- // settings' plain ringed `.tool` (no background, no elevation shadow), and
67
- // the only trigger of the four with a trailing chevron (2 icons total,
68
- // confirmed live). Overrides `.tool`'s own base background/shadow by cascade
74
+ // #595, corrected #772 (re-measured live 2026-09-25): the view bar's raised
75
+ // trigger chrome — `app.attio.com`'s own view bar: `28px` tall, `radius.md`
76
+ // (8px), background `bgSurface` (white, `rgb(255,255,255)` exact match — the
77
+ // #595 pass originally measured `bgSurfaceMuted`/`rgb(246,247,247)`, which
78
+ // re-measuring live this pass shows was wrong, not a later Attio change: ALL
79
+ // THREE of the switcher, View settings and Import/Export read pure white)
80
+ // and `elevation.field`'s exact 3-layer shadow (`inset 0 0 0 1px transparent,
81
+ // 0 0 2px rgba(28,40,64,.18), 0 1px 3px rgba(0,0,0,.04)`) — distinct from
82
+ // Sort/Filter's plain ringed `.tool` (no elevation shadow; see `--dashed` in
83
+ // `tabletoolbar.chrome.ts` for their own OWN empty-state chrome). Originally
84
+ // built for Import/Export alone ("the only trigger... with a trailing
85
+ // chevron") — that was also wrong: View settings and the view switcher both
86
+ // carry one too, confirmed live (3 children each: icon, label, chevron).
87
+ // Now shared by View settings' own trigger (`tabletoolbar.tsx`) as well as
88
+ // Import/Export's. Overrides `.tool`'s own base background/shadow by cascade
69
89
  // order (this block is appended after `toolbarChromeBody`, which defines
70
90
  // them), the same way `.tool--ghost` already does. `border`/`font-family`/
71
91
  // `cursor` are repeated here too, even though `.tool`'s own base rule (in
@@ -75,7 +95,18 @@ const filterChipChrome = `
75
95
  // behind `chrome.ts`'s function call is invisible to it; harmless repeats,
76
96
  // not a real gap.
77
97
  const importExportChrome = `
78
- & .crisp-tabletoolbar-tool--raised { border: none; background: ${c.bgSurfaceMuted}; box-shadow: ${e.field}; font-family: inherit; cursor: pointer; }`;
98
+ & .crisp-tabletoolbar-tool--raised { border: none; background: ${c.bgSurface}; box-shadow: ${e.field}; font-family: inherit; cursor: pointer; }`;
99
+ // crisp/#788: the Sort trigger's own "Sorted by X (+N)" label, measured live
100
+ // against Attio's own chip -- three parts, not one flat string, and NOT a
101
+ // font-weight split (all three read 500/medium): "Sorted by" is muted (63%
102
+ // black), the resolved field name is full-strength near-black, and a
103
+ // trailing "+N" (when more than one sort is active) is both smaller (12px
104
+ // vs 14px) and its own mid-grey, distinct from the "Sorted by" muted tone.
105
+ // TableToolbar-only, same reasoning as `filterChipChrome` above (RecordsTable
106
+ // keeps its own older Sort label markup, not touched here).
107
+ const sortedLabelChrome = `
108
+ & .crisp-tabletoolbar-sorted-prefix { color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
109
+ & .crisp-tabletoolbar-sorted-count { font-size: ${c.text.sm}; color: color-mix(in srgb, ${c.fgPrimary} 73%, transparent); }`;
79
110
  // The table toolbar, measured: 28px controls, the Sort/Filter buttons and
80
111
  // the menus they open, sort-condition rows, and the filter chips. The chrome
81
112
  // itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
@@ -94,6 +125,7 @@ ${toolbarChromeHead("tabletoolbar")}
94
125
  ${toolbarChromeBody("tabletoolbar")}
95
126
  ${filterChipChrome}
96
127
  ${importExportChrome}
128
+ ${sortedLabelChrome}
97
129
  ${rtlFlipCss}
98
130
  `,
99
131
  });
@@ -10,13 +10,23 @@ import { contract as c } from "../../tokens/index.js";
10
10
  // the same reason (see its own doc).
11
11
  const spec = defineSpec({
12
12
  name: "text",
13
- // `display: inline` -- a span's own default, stated rather than left
14
- // implicit: an EMPTY base compiles to an empty string, and
15
- // `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats that
16
- // falsy string as "no base rule", which drops the `crisp-text` class
17
- // itself, silently, from every instance (caught by this file's own
18
- // tests going red on that exact class).
19
- base: { display: "inline" },
13
+ // Needs a real, always-present declaration so `resolveRecipe` emits the
14
+ // `crisp-text` class at all -- an EMPTY base compiles to an empty string,
15
+ // and `core/resolve.ts`'s `if (base) classes.push(prefixedName)` treats
16
+ // that falsy string as "no base rule", dropping the class itself,
17
+ // silently, from every instance (this file's own tests catch that).
18
+ // `display: "inline"` filled that role until a real bug: `asChild`
19
+ // (Primitive.span -> Radix Slot) merges this class onto WHATEVER element
20
+ // the caller passes, not a `<span>` -- a `<div>`/`<p>`/`<li>` child's own
21
+ // block default lost to `.crisp-text`'s class-specificity `display:
22
+ // inline`, silently collapsing its layout (internal/grc's own Settings.tsx
23
+ // ran two rows onto one line this way). `vertical-align: baseline` fills
24
+ // the same "always-present, never empty" role without that risk: it's a
25
+ // span's own already-inherited default (so nothing changes for the plain,
26
+ // non-asChild case `Primitive.span` renders), and it has NO effect at all
27
+ // on a block-level box, so an `asChild` div/p/li keeps its own display
28
+ // exactly as its own tag already gives it.
29
+ base: { verticalAlign: "baseline" },
20
30
  variants: {
21
31
  tone: {
22
32
  default: { color: c.fgPrimary },
@@ -0,0 +1,16 @@
1
+ // deterministic pastel colour per tag value (a select option keeps its colour)
2
+ const TAG_TONES = [
3
+ "brand",
4
+ "success",
5
+ "warning",
6
+ "danger",
7
+ "purple",
8
+ "cyan",
9
+ "orange",
10
+ ];
11
+ export function tagTone(t) {
12
+ let h = 0;
13
+ for (let i = 0; i < t.length; i++)
14
+ h = (h * 31 + t.charCodeAt(i)) >>> 0;
15
+ return TAG_TONES[h % TAG_TONES.length];
16
+ }
@@ -9,7 +9,8 @@ import { Spinner } from "../../components/spinner/index.js";
9
9
  import { Switch } from "../../components/switch/index.js";
10
10
  import { Tabs } from "../../components/tabs/index.js";
11
11
  import { cx, isRtlElement, useCrispComponentDefaults } from "../../core/index.js";
12
- import { editableInputType, tagTone, useEditableField, } from "../recordstable/index.js";
12
+ import { tagTone } from "./column.js";
13
+ import { editableInputType, useEditableField } from "./editable-field.js";
13
14
  import { recordpageRecipe } from "./recordpage.recipe.js";
14
15
  function valueEmpty(v) {
15
16
  return v == null || v === "" || (Array.isArray(v) && v.length === 0);
@@ -0,0 +1,52 @@
1
+ import { AtSign, Briefcase, Building2, Calendar, Globe, Hash, MapPin, Tag, TrendingUp, Users, } from "lucide-react";
2
+ // Moved here from product/recordstable/recordstable.tsx's `defaultColumns`/
3
+ // `defaultRows` (crisp#786): RecordsTable is deprecated and its whole
4
+ // directory is being deleted, but RecordPage's own tests (and the shared
5
+ // RTL-keyboard-convention tests that live alongside them) still need a
6
+ // real, 12-column-deep dataset to exercise "a field past the first 5 needs
7
+ // View all values" and similar shape-dependent assertions. Simplified from
8
+ // the original: no baked-in company-logo data URIs (nothing here asserts on
9
+ // avatars), a plain favicon URL instead.
10
+ export const testColumns = [
11
+ { key: "company", label: "Company", type: "entity", icon: Building2 },
12
+ { key: "domain", label: "Domain", type: "url", icon: Globe },
13
+ {
14
+ key: "categories",
15
+ label: "Categories",
16
+ type: "tags",
17
+ icon: Tag,
18
+ width: 240,
19
+ },
20
+ {
21
+ key: "employees",
22
+ label: "Employees",
23
+ type: "number",
24
+ icon: Users,
25
+ width: 120,
26
+ },
27
+ { key: "location", label: "Location", type: "text", icon: MapPin },
28
+ { key: "stage", label: "Stage", type: "tags", icon: TrendingUp, width: 150 },
29
+ { key: "ceo", label: "CEO", type: "text", icon: Briefcase, width: 170 },
30
+ { key: "twitter", label: "Twitter", type: "url", icon: AtSign, width: 150 },
31
+ {
32
+ key: "followers",
33
+ label: "Followers",
34
+ type: "number",
35
+ icon: Users,
36
+ width: 120,
37
+ },
38
+ { key: "linkedin", label: "LinkedIn", type: "url", icon: Globe, width: 180 },
39
+ {
40
+ key: "founded",
41
+ label: "Founded",
42
+ type: "date",
43
+ icon: Calendar,
44
+ width: 110,
45
+ },
46
+ { key: "arr", label: "Est. ARR", type: "number", icon: Hash, width: 130 },
47
+ ];
48
+ // biome-ignore format: one row per line keeps the wide demo dataset readable
49
+ export const testRows = [
50
+ { id: "google", company: { name: "Google", avatar: "https://www.google.com/s2/favicons?domain=google.com&sz=64" }, domain: "google.com", categories: ["Search", "Ads", "Cloud", "AI", "Android", "Maps", "Hardware", "Workspace", "Chrome"], employees: 182000, location: "Mountain View, CA", stage: ["Public"], ceo: "Sundar Pichai", twitter: "twitter.com/google", followers: 29800000, linkedin: "linkedin.com/company/google", founded: "1998", arr: 307000000000 },
51
+ { id: "stripe", company: { name: "Stripe", avatar: "https://www.google.com/s2/favicons?domain=stripe.com&sz=64" }, domain: "stripe.com", categories: ["Payments", "Fintech"], employees: 8000, location: "San Francisco, CA", stage: ["Series I"], ceo: "Patrick Collison", twitter: "twitter.com/stripe", followers: 1200000, linkedin: "linkedin.com/company/stripe", founded: "2010", arr: 16000000000 },
52
+ ];
@@ -1,3 +1,3 @@
1
- export { editableInputType, useEditableField, } from "./editable-field.js";
1
+ export { editableInputType, useEditableField, } from "../recordpage/editable-field.js";
2
2
  export { defaultColumns, defaultRows, RecordsTable, tagTone, } from "./recordstable.js";
3
3
  export { recordstableRecipe } from "./recordstable.recipe.js";