@crisp-ui-kit/crisp 0.54.10 → 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 (47) hide show
  1. package/dist/cjs/components/datatable/datatable.js +121 -10
  2. package/dist/cjs/components/datatable/datatable.recipe.js +24 -0
  3. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  4. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +93 -43
  5. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
  6. package/dist/cjs/product/recordpage/column.js +19 -0
  7. package/dist/cjs/product/recordpage/recordpage.js +5 -4
  8. package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
  9. package/dist/cjs/product/recordstable/index.js +1 -1
  10. package/dist/cjs/product/recordstable/recordstable.js +5 -0
  11. package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
  12. package/dist/cjs/product/taskstable/taskstable.js +5 -0
  13. package/dist/datatable.css +14 -0
  14. package/dist/datatable.layered.css +14 -0
  15. package/dist/esm/components/datatable/datatable.js +121 -10
  16. package/dist/esm/components/datatable/datatable.recipe.js +24 -0
  17. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  18. package/dist/esm/components/tabletoolbar/tabletoolbar.js +93 -43
  19. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
  20. package/dist/esm/product/recordpage/column.js +16 -0
  21. package/dist/esm/product/recordpage/recordpage.js +2 -1
  22. package/dist/esm/product/recordpage/test-fixtures.js +52 -0
  23. package/dist/esm/product/recordstable/index.js +1 -1
  24. package/dist/esm/product/recordstable/recordstable.js +5 -0
  25. package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
  26. package/dist/esm/product/taskstable/taskstable.js +5 -0
  27. package/dist/recordstable.css +19 -2
  28. package/dist/recordstable.layered.css +19 -2
  29. package/dist/savedviews.css +6 -18
  30. package/dist/savedviews.layered.css +6 -18
  31. package/dist/styles.css +62 -23
  32. package/dist/styles.layered.css +62 -23
  33. package/dist/tabletoolbar.css +23 -3
  34. package/dist/tabletoolbar.layered.css +23 -3
  35. package/dist/types/components/datatable/datatable.d.ts +30 -3
  36. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +26 -1
  37. package/dist/types/product/recordpage/column.d.ts +112 -0
  38. package/dist/types/product/recordpage/recordpage.d.ts +1 -1
  39. package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
  40. package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
  41. package/dist/types/product/recordstable/index.d.ts +1 -1
  42. package/dist/types/product/recordstable/recordstable.d.ts +6 -1
  43. package/dist/types/product/taskstable/taskstable.d.ts +5 -0
  44. package/package.json +1 -1
  45. /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
  46. /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
  47. /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("");
@@ -602,16 +645,23 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
602
645
  // worse than an imperfect word order); `crispMessages` rather than a
603
646
  // second `useCrispMessage` call, since this only fires on the branch that
604
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;
605
659
  const sortedLabel = sortTriggerLabel
606
660
  ? sortTriggerLabel(sorts, sortColumns)
607
661
  : sorts.length
608
- ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
609
- // #754: sorts[0].key may be a dot-joined nested path
610
- // ("team.jobTitle") — resolve it the same way the chip and sort
611
- // row do, rather than a flat find that fails on a nested key.
612
- resolveColumnPath(sortColumns, sorts[0].key).at(-1)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
662
+ ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortField}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
613
663
  : sortLabel;
614
- 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) => {
615
665
  if (dragSort !== null)
616
666
  e.preventDefault();
617
667
  }, onDrop: () => {
@@ -648,7 +698,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
648
698
  label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
649
699
  keywords: descendingLabel,
650
700
  },
651
- ], "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: () => {
652
702
  setFieldPickerPath([]);
653
703
  setOpenMenu(openMenu === "filter" ? null : "filter");
654
704
  }, "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
@@ -747,43 +797,43 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
747
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`,
748
798
  // biome-ignore lint/a11y/noAutofocus: focus the value field
749
799
  autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
750
- }), 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: () => {
751
- if (openMenu === "viewsettings") {
752
- setOpenMenu(null);
753
- }
754
- else {
755
- setAddingAttribute(false);
756
- setFieldPickerPath([]);
757
- setFilterQuery("");
758
- setOpenMenu("viewsettings");
759
- }
760
- }, "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" &&
761
- (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) => {
762
- if (dragAttr !== null)
763
- e.preventDefault();
764
- }, onDrop: () => {
765
- if (dragAttr !== null)
766
- moveAttr(dragAttr, i);
767
- setDragAttr(null);
768
- }, 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) ??
769
- `${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: () => {
770
- setAddingAttribute(true);
771
- setFieldPickerPath([]);
772
- setFilterQuery("");
773
- }, 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 ||
774
824
  onExportCsv ||
775
825
  onExportExcel ||
776
- 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: () => {
777
- onImportCsv();
778
- setOpenMenu(null);
779
- }, 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: () => {
780
- onExportCsv();
781
- setOpenMenu(null);
782
- }, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
783
- onExportExcel();
784
- setOpenMenu(null);
785
- }, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
786
- (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] }) }));
787
837
  }
788
838
  /**
789
839
  * The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
@@ -71,14 +71,21 @@ const filterChipChrome = `
71
71
  & .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${c.space["1"]}; margin-top: ${c.space["2"]}; padding: 0 ${c.space["1"]}; }
72
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; }
73
73
  & .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); }`;
74
- // #595: the Import/Export trigger — measured live, `app.attio.com`'s own
75
- // view bar: `28px` tall, `radius.md` (8px), background `bgSurfaceMuted`
76
- // (`rgb(246,247,247)`, exact match) and `elevation.field`'s exact 3-layer
77
- // shadow (`inset 0 0 0 1px transparent, 0 0 2px rgba(28,40,64,.18), 0 1px 3px
78
- // rgba(0,0,0,.04)`) — a "raised chrome button" distinct from Sort/Filter/View
79
- // settings' plain ringed `.tool` (no background, no elevation shadow), and
80
- // the only trigger of the four with a trailing chevron (2 icons total,
81
- // 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
82
89
  // order (this block is appended after `toolbarChromeBody`, which defines
83
90
  // them), the same way `.tool--ghost` already does. `border`/`font-family`/
84
91
  // `cursor` are repeated here too, even though `.tool`'s own base rule (in
@@ -88,7 +95,18 @@ const filterChipChrome = `
88
95
  // behind `chrome.ts`'s function call is invisible to it; harmless repeats,
89
96
  // not a real gap.
90
97
  const importExportChrome = `
91
- & .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); }`;
92
110
  // The table toolbar, measured: 28px controls, the Sort/Filter buttons and
93
111
  // the menus they open, sort-condition rows, and the filter chips. The chrome
94
112
  // itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
@@ -107,6 +125,7 @@ ${toolbarChromeHead("tabletoolbar")}
107
125
  ${toolbarChromeBody("tabletoolbar")}
108
126
  ${filterChipChrome}
109
127
  ${importExportChrome}
128
+ ${sortedLabelChrome}
110
129
  ${rtlFlipCss}
111
130
  `,
112
131
  });
@@ -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";
@@ -512,6 +512,11 @@ function CreateRecordDialog({ open, onOpenChange, columns, entityLabel, onCreate
512
512
  * editing (double-click a text/number/url/date cell), add-row and add-column,
513
513
  * and a floating bulk-action bar (delete / deselect) when rows are selected.
514
514
  * Selection can be controlled via `selectedIds`/`onSelectionChange`.
515
+ *
516
+ * @deprecated Reach for `DataTable` instead — a single Attio-exact records
517
+ * grid replaces the separate table variants. `RecordsTable` has no
518
+ * consumers left (checked GRC and the fms/Mercury admin) and is slated for
519
+ * removal in a future minor.
515
520
  */
516
521
  export function RecordsTable({ labels, columns: columnsProp = defaultColumns, rows: rowsProp = defaultRows, selectedIds, onSelectionChange, showNew = true, allowAddColumn = true, onOpenRecord, onOpenRecordPage, avatarTone = "brand", emptyStateExtra, className, ...rest }) {
517
522
  const [cols, setCols] = React.useState(columnsProp);
@@ -23,24 +23,26 @@ const config = defineRecipe({
23
23
  position: relative; display: inline-flex; font-family: ${c.font.sans};
24
24
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
25
25
 
26
- /* Trigger fill measured rgb(246,247,247); bgRaised's light value is
27
- #f4f4f5 (244,244,245) — a 2-unit-per-channel difference, inside
28
- screenshot/anti-aliasing noise, not a second real colour. Reusing the
29
- token (Lesson 3 — tokens, not raw px/hex) gets a correct,
30
- already-established dark-mode fill for free instead of a light-only
31
- literal; AUTHORING.md Lesson 29 is the case where that shortcut went
32
- wrong (a flat colour that stayed visible where the reference had
33
- none) — this is the opposite risk (a token that already flips per
34
- theme, applied to the closest measured match). */
26
+ /* crisp/#772 follow-up, re-measured live 2026-09-25 in the trigger's
27
+ actual position (Attio's real view bar, next to View settings --
28
+ fixed there in #766/#768/#778): white (bgSurface, rgb(255,255,255)
29
+ exact) with elevation.field's 3-layer shadow, the same raised
30
+ chrome View settings and Import/Export both carry
31
+ (tabletoolbar.recipe.ts's own --raised, not reused here directly
32
+ since this is a separate recipe/component, but the identical measured
33
+ values). Supersedes the original #555 measurement (rgb(246,247,247),
34
+ reused as bgRaised -- a flat grey fill with no shadow at all), which
35
+ was wrong: this trigger was never re-checked once it moved into its
36
+ real view-bar position. */
35
37
  & .crisp-savedviews-trigger {
36
38
  box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1.5"]};
37
39
  height: ${c.size.row}; margin: 0; padding: ${c.space["1"]} ${c.space["1.5"]};
38
- border: none; border-radius: ${c.radius.md}; background: ${c.bgRaised};
40
+ border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: ${e.field};
39
41
  font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary};
40
42
  cursor: pointer; white-space: nowrap; transition: background-color .12s ease;
41
43
  }
42
44
  & .crisp-savedviews-trigger:hover,
43
- & .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, ${c.fgPrimary} 4%, ${c.bgRaised}); }
45
+ & .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, ${c.fgPrimary} 4%, ${c.bgSurface}); }
44
46
  & .crisp-savedviews-trigger-icon { flex-shrink: 0; display: inline-flex; width: ${c.size.icon}; height: ${c.size.icon}; }
45
47
  & .crisp-savedviews-trigger-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
46
48
  & .crisp-savedviews-trigger-chevron { flex-shrink: 0; color: ${c.fgMuted}; }
@@ -11,6 +11,11 @@ function Cell({ flex, className, children, }) {
11
11
  * taskstable.recipe.ts) with a typed column/cell layer: a title+checkbox
12
12
  * row grouped into due-date sections, a due-date cell that goes red past
13
13
  * due, and consumer-supplied record/assignee chip slots.
14
+ *
15
+ * @deprecated Reach for `DataTable`'s `groupBy` instead — a single
16
+ * Attio-exact records grid replaces the separate table variants.
17
+ * `TasksTable` has no consumers left (checked GRC and the fms/Mercury
18
+ * admin) and is slated for removal in a future minor.
14
19
  */
15
20
  export function TasksTable({ columns, groups, leading, trailing, emptyLabel: emptyLabelProp, completeLabel: completeLabelProp, incompleteLabel: incompleteLabelProp, className, ...rest }) {
16
21
  const crispMessages = useCrispMessages();
@@ -8,12 +8,29 @@
8
8
  & .crisp-recordstable-search svg { flex-shrink: 0; }
9
9
  & .crisp-recordstable-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-primary); width: 180px; }
10
10
  & .crisp-recordstable-search input::placeholder { color: var(--crisp-fg-muted); }
11
- & .crisp-recordstable-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px var(--crisp-border-default); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
11
+ & .crisp-recordstable-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-divider); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
12
12
  & .crisp-recordstable-tool:hover { background: var(--crisp-bg-raised); }
13
13
  & .crisp-recordstable-tool svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
14
- & .crisp-recordstable-tool--active { background: var(--crisp-bg-raised); }
14
+ /* crisp/#772: re-measured live on Sort with an actual sort applied -- an active trigger's own fill is white (bgSurface), the same as the
15
+ base .tool rule above now already sets; this override existed to
16
+ distinguish active from a base state that used to be transparent.
17
+ Kept explicit (not just deleted) so a future change to the base
18
+ rule's background can't silently change this one too. */
19
+ & .crisp-recordstable-tool--active { background: var(--crisp-bg-surface); }
15
20
  & .crisp-recordstable-tool--ghost { box-shadow: none; background: color-mix(in srgb, var(--crisp-fg-primary) 3%, transparent); color: var(--crisp-fg-muted); }
16
21
  & .crisp-recordstable-tool--ghost:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
22
+ /* crisp/#772 follow-up, re-measured live 2026-09-25: an EMPTY Sort/Filter
23
+ trigger (no sort/condition set) isn't a solid-ring chip at all -- it's
24
+ a dashed ::before (1px dashed, border-radius: inherit, absolutely
25
+ positioned inset: 0) over a barely-there fill, confirmed on Attio's own empty Filter chip (its ::before border computes to exactly "1px dashed rgba(0, 0, 0, 0.06)"). Once a value IS set, the trigger drops the dashed pseudo and picks up the base tool's own solid ring
26
+ instead (the --active modifier, unchanged) -- confirmed on Sort with
27
+ an actual sort applied. color-mix (not the raw rgba Attio computes
28
+ to) so both the border and the fill flip correctly in dark mode, the
29
+ same reasoning --ghost above already uses. Only wired into
30
+ TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's own Sort/Filter triggers share this same chrome function but are deprecated, not touched here. */
31
+ & .crisp-recordstable-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); }
32
+ & .crisp-recordstable-tool--dashed::before { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 1px dashed color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); pointer-events: none; }
33
+ & .crisp-recordstable-tool--dashed:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
17
34
  & .crisp-recordstable-toolgroup { position: relative; display: inline-flex; }
18
35
  & .crisp-recordstable-divider { flex-shrink: 0; width: 1px; height: 20px; background: var(--crisp-border-subtle); }
19
36
  & .crisp-recordstable-menu { position: absolute; top: calc(100% + var(--crisp-space-1)); left: 0; z-index: 30; min-width: 232px; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); display: flex; flex-direction: column; gap: var(--crisp-space-px); transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
@@ -9,12 +9,29 @@
9
9
  & .crisp-recordstable-search svg { flex-shrink: 0; }
10
10
  & .crisp-recordstable-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-primary); width: 180px; }
11
11
  & .crisp-recordstable-search input::placeholder { color: var(--crisp-fg-muted); }
12
- & .crisp-recordstable-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: transparent; box-shadow: inset 0 0 0 1px var(--crisp-border-default); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
12
+ & .crisp-recordstable-tool { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: var(--crisp-size-row); margin: 0; padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-divider); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); white-space: nowrap; cursor: pointer; transition: background-color .12s; }
13
13
  & .crisp-recordstable-tool:hover { background: var(--crisp-bg-raised); }
14
14
  & .crisp-recordstable-tool svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
15
- & .crisp-recordstable-tool--active { background: var(--crisp-bg-raised); }
15
+ /* crisp/#772: re-measured live on Sort with an actual sort applied -- an active trigger's own fill is white (bgSurface), the same as the
16
+ base .tool rule above now already sets; this override existed to
17
+ distinguish active from a base state that used to be transparent.
18
+ Kept explicit (not just deleted) so a future change to the base
19
+ rule's background can't silently change this one too. */
20
+ & .crisp-recordstable-tool--active { background: var(--crisp-bg-surface); }
16
21
  & .crisp-recordstable-tool--ghost { box-shadow: none; background: color-mix(in srgb, var(--crisp-fg-primary) 3%, transparent); color: var(--crisp-fg-muted); }
17
22
  & .crisp-recordstable-tool--ghost:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
23
+ /* crisp/#772 follow-up, re-measured live 2026-09-25: an EMPTY Sort/Filter
24
+ trigger (no sort/condition set) isn't a solid-ring chip at all -- it's
25
+ a dashed ::before (1px dashed, border-radius: inherit, absolutely
26
+ positioned inset: 0) over a barely-there fill, confirmed on Attio's own empty Filter chip (its ::before border computes to exactly "1px dashed rgba(0, 0, 0, 0.06)"). Once a value IS set, the trigger drops the dashed pseudo and picks up the base tool's own solid ring
27
+ instead (the --active modifier, unchanged) -- confirmed on Sort with
28
+ an actual sort applied. color-mix (not the raw rgba Attio computes
29
+ to) so both the border and the fill flip correctly in dark mode, the
30
+ same reasoning --ghost above already uses. Only wired into
31
+ TableToolbar itself (tabletoolbar.tsx) this pass -- RecordsTable's own Sort/Filter triggers share this same chrome function but are deprecated, not touched here. */
32
+ & .crisp-recordstable-tool--dashed { position: relative; box-shadow: none; background: color-mix(in srgb, var(--crisp-fg-primary) 2%, transparent); }
33
+ & .crisp-recordstable-tool--dashed::before { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 1px dashed color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); pointer-events: none; }
34
+ & .crisp-recordstable-tool--dashed:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); }
18
35
  & .crisp-recordstable-toolgroup { position: relative; display: inline-flex; }
19
36
  & .crisp-recordstable-divider { flex-shrink: 0; width: 1px; height: 20px; background: var(--crisp-border-subtle); }
20
37
  & .crisp-recordstable-menu { position: absolute; top: calc(100% + var(--crisp-space-1)); left: 0; z-index: 30; min-width: 232px; padding: var(--crisp-space-1); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-popover); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); display: flex; flex-direction: column; gap: var(--crisp-space-px); transform-origin: top; opacity: 1; transform: none; transition: opacity .13s ease, transform .13s ease; }
@@ -73,24 +73,12 @@
73
73
  }
74
74
 
75
75
  .crisp-savedviews {
76
- position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; /* Trigger fill measured rgb(246,247,247); bgRaised's light value is
77
- #f4f4f5 (244,244,245) — a 2-unit-per-channel difference, inside
78
- screenshot/anti-aliasing noise, not a second real colour. Reusing the
79
- token (Lesson 3 — tokens, not raw px/hex) gets a correct,
80
- already-established dark-mode fill for free instead of a light-only
81
- literal; AUTHORING.md Lesson 29 is the case where that shortcut went
82
- wrong (a flat colour that stayed visible where the reference had
83
- none) — this is the opposite risk (a token that already flips per
84
- theme, applied to the closest measured match). */
85
- & .crisp-savedviews-trigger {
86
- box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5);
87
- height: var(--crisp-size-row); margin: 0; padding: var(--crisp-space-1) var(--crisp-space-1-5);
88
- border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-raised);
89
- font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary);
90
- cursor: pointer; white-space: nowrap; transition: background-color .12s ease;
91
- }
92
- & .crisp-savedviews-trigger:hover,
93
- & .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-raised)); }
76
+ position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; /* crisp/#772 follow-up, re-measured live 2026-09-25 in the trigger's
77
+ actual position (Attio's real view bar, next to View settings -- fixed there in #766/#768/#778): white (bgSurface, rgb(255,255,255) exact) with elevation.field's 3-layer shadow, the same raised
78
+ chrome View settings and Import/Export both carry
79
+ (tabletoolbar.recipe.ts's own --raised, not reused here directly since this is a separate recipe/component, but the identical measured values). Supersedes the original #555 measurement (rgb(246,247,247), reused as bgRaised -- a flat grey fill with no shadow at all), which was wrong: this trigger was never re-checked once it moved into its real view-bar position. */
80
+ & .crisp-savedviews-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-row); margin: 0; padding: var(--crisp-space-1) var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); cursor: pointer; white-space: nowrap; transition: background-color .12s ease; }
81
+ & .crisp-savedviews-trigger:hover, & .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
94
82
  & .crisp-savedviews-trigger-icon { flex-shrink: 0; display: inline-flex; width: var(--crisp-size-icon); height: var(--crisp-size-icon); }
95
83
  & .crisp-savedviews-trigger-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
96
84
  & .crisp-savedviews-trigger-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); }
@@ -74,24 +74,12 @@
74
74
  }
75
75
 
76
76
  .crisp-savedviews {
77
- position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; /* Trigger fill measured rgb(246,247,247); bgRaised's light value is
78
- #f4f4f5 (244,244,245) — a 2-unit-per-channel difference, inside
79
- screenshot/anti-aliasing noise, not a second real colour. Reusing the
80
- token (Lesson 3 — tokens, not raw px/hex) gets a correct,
81
- already-established dark-mode fill for free instead of a light-only
82
- literal; AUTHORING.md Lesson 29 is the case where that shortcut went
83
- wrong (a flat colour that stayed visible where the reference had
84
- none) — this is the opposite risk (a token that already flips per
85
- theme, applied to the closest measured match). */
86
- & .crisp-savedviews-trigger {
87
- box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5);
88
- height: var(--crisp-size-row); margin: 0; padding: var(--crisp-space-1) var(--crisp-space-1-5);
89
- border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-raised);
90
- font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary);
91
- cursor: pointer; white-space: nowrap; transition: background-color .12s ease;
92
- }
93
- & .crisp-savedviews-trigger:hover,
94
- & .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-raised)); }
77
+ position: relative; display: inline-flex; font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; /* crisp/#772 follow-up, re-measured live 2026-09-25 in the trigger's
78
+ actual position (Attio's real view bar, next to View settings -- fixed there in #766/#768/#778): white (bgSurface, rgb(255,255,255) exact) with elevation.field's 3-layer shadow, the same raised
79
+ chrome View settings and Import/Export both carry
80
+ (tabletoolbar.recipe.ts's own --raised, not reused here directly since this is a separate recipe/component, but the identical measured values). Supersedes the original #555 measurement (rgb(246,247,247), reused as bgRaised -- a flat grey fill with no shadow at all), which was wrong: this trigger was never re-checked once it moved into its real view-bar position. */
81
+ & .crisp-savedviews-trigger { box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-row); margin: 0; padding: var(--crisp-space-1) var(--crisp-space-1-5); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); cursor: pointer; white-space: nowrap; transition: background-color .12s ease; }
82
+ & .crisp-savedviews-trigger:hover, & .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, var(--crisp-fg-primary) 4%, var(--crisp-bg-surface)); }
95
83
  & .crisp-savedviews-trigger-icon { flex-shrink: 0; display: inline-flex; width: var(--crisp-size-icon); height: var(--crisp-size-icon); }
96
84
  & .crisp-savedviews-trigger-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
97
85
  & .crisp-savedviews-trigger-chevron { flex-shrink: 0; color: var(--crisp-fg-muted); }