@crisp-ui-kit/crisp 0.54.10 → 0.55.0

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 (77) hide show
  1. package/dist/cjs/components/datatable/datatable.js +249 -46
  2. package/dist/cjs/components/datatable/datatable.recipe.js +24 -0
  3. package/dist/cjs/components/resizablepanel/resizablepanel.js +10 -1
  4. package/dist/cjs/components/table/table.js +8 -1
  5. package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  6. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +176 -43
  7. package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
  8. package/dist/cjs/index.js +0 -2
  9. package/dist/cjs/product/recordpage/column.js +19 -0
  10. package/dist/cjs/product/recordpage/index.js +6 -1
  11. package/dist/cjs/product/recordpage/recordpage.js +25 -8
  12. package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
  13. package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
  14. package/dist/datatable.css +14 -0
  15. package/dist/datatable.layered.css +14 -0
  16. package/dist/esm/components/datatable/datatable.js +250 -47
  17. package/dist/esm/components/datatable/datatable.recipe.js +24 -0
  18. package/dist/esm/components/resizablepanel/resizablepanel.js +10 -1
  19. package/dist/esm/components/table/table.js +8 -1
  20. package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
  21. package/dist/esm/components/tabletoolbar/tabletoolbar.js +177 -44
  22. package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
  23. package/dist/esm/index.js +0 -2
  24. package/dist/esm/product/recordpage/column.js +16 -0
  25. package/dist/esm/product/recordpage/index.js +2 -0
  26. package/dist/esm/product/recordpage/recordpage.js +22 -5
  27. package/dist/esm/product/recordpage/test-fixtures.js +52 -0
  28. package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
  29. package/dist/savedviews.css +6 -18
  30. package/dist/savedviews.layered.css +6 -18
  31. package/dist/styles.css +43 -276
  32. package/dist/styles.layered.css +43 -276
  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/table/table.d.ts +14 -0
  37. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +26 -1
  38. package/dist/types/index.d.ts +0 -2
  39. package/dist/types/product/recordpage/column.d.ts +112 -0
  40. package/dist/types/product/recordpage/index.d.ts +2 -0
  41. package/dist/types/product/recordpage/recordpage.d.ts +1 -1
  42. package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
  43. package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
  44. package/package.json +2 -12
  45. package/dist/cjs/product/recordstable/company-logos.js +0 -29
  46. package/dist/cjs/product/recordstable/index.js +0 -13
  47. package/dist/cjs/product/recordstable/recordstable.js +0 -1357
  48. package/dist/cjs/product/recordstable/recordstable.recipe.js +0 -175
  49. package/dist/cjs/product/recordstable/recordstable.tag.js +0 -26
  50. package/dist/cjs/product/taskstable/index.js +0 -7
  51. package/dist/cjs/product/taskstable/taskstable.js +0 -27
  52. package/dist/cjs/product/taskstable/taskstable.recipe.js +0 -56
  53. package/dist/esm/product/recordstable/company-logos.js +0 -26
  54. package/dist/esm/product/recordstable/index.js +0 -3
  55. package/dist/esm/product/recordstable/recordstable.js +0 -1317
  56. package/dist/esm/product/recordstable/recordstable.recipe.js +0 -172
  57. package/dist/esm/product/recordstable/recordstable.tag.js +0 -23
  58. package/dist/esm/product/taskstable/index.js +0 -2
  59. package/dist/esm/product/taskstable/taskstable.js +0 -24
  60. package/dist/esm/product/taskstable/taskstable.recipe.js +0 -53
  61. package/dist/recordstable.css +0 -223
  62. package/dist/recordstable.d.ts +0 -7
  63. package/dist/recordstable.layered.css +0 -225
  64. package/dist/taskstable.css +0 -30
  65. package/dist/taskstable.d.ts +0 -7
  66. package/dist/taskstable.layered.css +0 -32
  67. package/dist/types/product/recordstable/company-logos.d.ts +0 -1
  68. package/dist/types/product/recordstable/index.d.ts +0 -3
  69. package/dist/types/product/recordstable/recordstable.d.ts +0 -289
  70. package/dist/types/product/recordstable/recordstable.recipe.d.ts +0 -5
  71. package/dist/types/product/recordstable/recordstable.tag.d.ts +0 -15
  72. package/dist/types/product/taskstable/index.d.ts +0 -2
  73. package/dist/types/product/taskstable/taskstable.d.ts +0 -54
  74. package/dist/types/product/taskstable/taskstable.recipe.d.ts +0 -3
  75. /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
  76. /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
  77. /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 { isInsideLaterFloatingLayer, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
4
+ import { createPortal } from "react-dom";
5
+ import { FloatingPortal, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, 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,25 @@ 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]);
415
+ const rootRef = React.useRef(null);
416
+ const viewSettingsTriggerRef = React.useRef(null);
417
+ const viewSettingsContentRef = React.useRef(null);
418
+ const [viewSettingsPos, setViewSettingsPos] = React.useState({});
419
+ const importExportTriggerRef = React.useRef(null);
420
+ const [importExportPos, setImportExportPos] = React.useState({});
372
421
  const [openMenu, setOpenMenu] = React.useState(null);
373
422
  const [dragSort, setDragSort] = React.useState(null);
374
423
  const [filterQuery, setFilterQuery] = React.useState("");
@@ -396,6 +445,65 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
396
445
  // built here -- hiding an attribute is a trailing control instead, see
397
446
  // the hide button below).
398
447
  const [addingAttribute, setAddingAttribute] = React.useState(false);
448
+ // crisp/#807: `viewSettingsRef`/`importExportRef` (#766) relocate the
449
+ // TRIGGER out of this toolbar's own row, but their POPOVERS stayed
450
+ // `position: absolute` inside the (now relocated) trigger's own
451
+ // `.crisp-tabletoolbar-toolgroup` -- correct chrome-wise (#772 already
452
+ // fixed that), but its stacking is trapped inside whatever DOM branch the
453
+ // consumer's own `viewSettingsRef` slot lives in. A consumer header
454
+ // sitting in an EARLIER sibling branch than the toolbar/grid below it
455
+ // (GRC's own layout) has that later branch simply paint over the popover
456
+ // regardless of its own z-index: 30, because z-index only orders siblings
457
+ // WITHIN the same stacking context, and the two branches aren't one.
458
+ // Fixed the same way `Menu`/`Select`/`Popover` already solve this: the
459
+ // popover content itself portals to `document.body` (`FloatingPortal`),
460
+ // positioned with `placeFloating` against the trigger's own rect (a real
461
+ // `position: fixed` box, anchored by viewport coordinates, escapes ANY
462
+ // ancestor's stacking context) -- not just relocated alongside the
463
+ // trigger. Wrapped in its own `tabletoolbarRecipe()` span (the same
464
+ // reason `ViewBarSlot` wraps the trigger) so every nested
465
+ // `.crisp-tabletoolbar-*` class inside the popover still resolves once
466
+ // portaled outside the toolbar's own root. Sort/Filter's own popovers
467
+ // are untouched -- they never relocate, so they were never trapped.
468
+ const placeViewSettingsMenu = () => {
469
+ const rect = viewSettingsTriggerRef.current?.getBoundingClientRect();
470
+ if (!rect)
471
+ return;
472
+ const { style } = placeFloating({
473
+ rect,
474
+ gap: 4,
475
+ flipBelow: 280,
476
+ anchor: viewSettingsTriggerRef.current,
477
+ });
478
+ setViewSettingsPos(style);
479
+ };
480
+ // biome-ignore lint/correctness/useExhaustiveDependencies: reposition each time it opens (and when its own screen changes, since the two screens differ in height)
481
+ React.useLayoutEffect(() => {
482
+ if (openMenu === "viewsettings")
483
+ placeViewSettingsMenu();
484
+ }, [openMenu, addingAttribute]);
485
+ React.useEffect(() => {
486
+ if (openMenu !== "viewsettings" || !viewSettingsContentRef.current)
487
+ return;
488
+ return registerFloatingLayer(viewSettingsContentRef.current);
489
+ }, [openMenu]);
490
+ const placeImportExportMenu = () => {
491
+ const rect = importExportTriggerRef.current?.getBoundingClientRect();
492
+ if (!rect)
493
+ return;
494
+ const { style } = placeFloating({
495
+ rect,
496
+ gap: 4,
497
+ flipBelow: 280,
498
+ anchor: importExportTriggerRef.current,
499
+ });
500
+ setImportExportPos(style);
501
+ };
502
+ // biome-ignore lint/correctness/useExhaustiveDependencies: reposition each time it opens
503
+ React.useLayoutEffect(() => {
504
+ if (openMenu === "importexport")
505
+ placeImportExportMenu();
506
+ }, [openMenu]);
399
507
  // close any open menu on outside click (anything not inside a menu or its
400
508
  // trigger) or Escape
401
509
  React.useEffect(() => {
@@ -546,6 +654,13 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
546
654
  close: () => setOpenMenu(null),
547
655
  contentRef: importExportMenuRef,
548
656
  });
657
+ // crisp/#807: registers the (now-portaled) Import/Export menu on the
658
+ // shared floating-layer stack, the same reason `Menu` itself does.
659
+ React.useEffect(() => {
660
+ if (openMenu !== "importexport" || !importExportMenuRef.current)
661
+ return;
662
+ return registerFloatingLayer(importExportMenuRef.current);
663
+ }, [openMenu]);
549
664
  // #754: generalized from the Filter-only `filterFieldPicker` (#592) to
550
665
  // also drive Sort's own field picker and View settings' add-attribute
551
666
  // screen — all three offer the identical nested-drill-down construct on
@@ -602,16 +717,23 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
602
717
  // worse than an imperfect word order); `crispMessages` rather than a
603
718
  // second `useCrispMessage` call, since this only fires on the branch that
604
719
  // needs it.
720
+ // crisp/#788: "Sorted by" and the field name are NOT a font-weight split
721
+ // (both measure 500/medium live) -- the difference is colour/opacity only:
722
+ // "Sorted by" at 63% black, the field name at full-strength near-black
723
+ // (rgb(16,17,18)). The "+N" suffix is smaller (12px vs 14px) and its own
724
+ // mid-grey (rgb(80,81,85)), not muted-63%-black either -- three distinct
725
+ // treatments, not two. `sortField` isolates the resolved label so the
726
+ // default (non-`sortTriggerLabel`) render can wrap each part in its own
727
+ // span; `sortedLabel` stays a single string for the `sortTriggerLabel`
728
+ // escape hatch (a caller-supplied formatter can't be split, by definition)
729
+ // and the empty-state `sortLabel`.
730
+ const sortField = resolveColumnPath(sortColumns, sorts[0]?.key ?? "").at(-1)?.label;
605
731
  const sortedLabel = sortTriggerLabel
606
732
  ? sortTriggerLabel(sorts, sortColumns)
607
733
  : 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}` : ""}`
734
+ ? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortField}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
613
735
  : 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) => {
736
+ return (_jsx("div", { ref: rootRef, 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
737
  if (dragSort !== null)
616
738
  e.preventDefault();
617
739
  }, onDrop: () => {
@@ -648,7 +770,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
648
770
  label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
649
771
  keywords: descendingLabel,
650
772
  },
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: () => {
773
+ ], "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
774
  setFieldPickerPath([]);
653
775
  setOpenMenu(openMenu === "filter" ? null : "filter");
654
776
  }, "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 +869,54 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
747
869
  }, 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
870
  // biome-ignore lint/a11y/noAutofocus: focus the value field
749
871
  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 ||
872
+ }), 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", { ref: viewSettingsTriggerRef, type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => {
873
+ if (openMenu === "viewsettings") {
874
+ setOpenMenu(null);
875
+ }
876
+ else {
877
+ setAddingAttribute(false);
878
+ setFieldPickerPath([]);
879
+ setFilterQuery("");
880
+ setOpenMenu("viewsettings");
881
+ }
882
+ }, "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" && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("span", { className: tabletoolbarRecipe(), children: addingAttribute ? (_jsx("div", { ref: viewSettingsContentRef, className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, style: {
883
+ position: "fixed",
884
+ zIndex: 70,
885
+ ...viewSettingsPos,
886
+ }, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : (_jsxs("div", { ref: viewSettingsContentRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, style: {
887
+ position: "fixed",
888
+ zIndex: 70,
889
+ ...viewSettingsPos,
890
+ }, children: [shownAttrs.map((col, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
891
+ if (dragAttr !== null)
892
+ e.preventDefault();
893
+ }, onDrop: () => {
894
+ if (dragAttr !== null)
895
+ moveAttr(dragAttr, i);
896
+ setDragAttr(null);
897
+ }, 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) ??
898
+ `${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: () => {
899
+ setAddingAttribute(true);
900
+ setFieldPickerPath([]);
901
+ setFilterQuery("");
902
+ }, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })) }) }))] }) })), (onImportCsv ||
774
903
  onExportCsv ||
775
904
  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] }) }));
905
+ importExportHelpHref) && (_jsx(ViewBarSlot, { target: importExportTarget, children: _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { ref: importExportTriggerRef, 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" && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("span", { className: tabletoolbarRecipe(), children: _jsxs("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, style: {
906
+ position: "fixed",
907
+ zIndex: 70,
908
+ ...importExportPos,
909
+ }, children: [onImportCsv && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
910
+ onImportCsv();
911
+ setOpenMenu(null);
912
+ }, 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: () => {
913
+ onExportCsv();
914
+ setOpenMenu(null);
915
+ }, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
916
+ onExportExcel();
917
+ setOpenMenu(null);
918
+ }, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
919
+ (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
920
  }
788
921
  /**
789
922
  * 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
  });
package/dist/esm/index.js CHANGED
@@ -86,7 +86,6 @@ export * from "./product/objectsettings/index.js";
86
86
  export * from "./product/onboarding/index.js";
87
87
  export * from "./product/recordpage/index.js";
88
88
  export * from "./product/recordpanel/index.js";
89
- export * from "./product/recordstable/index.js";
90
89
  export * from "./product/richtext/index.js";
91
90
  export * from "./product/savedviews/index.js";
92
91
  export * from "./product/settingssection/index.js";
@@ -94,7 +93,6 @@ export * from "./product/settingsshell/index.js";
94
93
  export * from "./product/sidebar/index.js";
95
94
  export * from "./product/statlist/index.js";
96
95
  export * from "./product/tasklist/index.js";
97
- export * from "./product/taskstable/index.js";
98
96
  export * from "./product/themepicker/index.js";
99
97
  export * from "./product/trialbanner/index.js";
100
98
  export * from "./product/viewtoggle/index.js";
@@ -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
+ }
@@ -1,2 +1,4 @@
1
+ export { tagTone, } from "./column.js";
2
+ export { editableInputType, useEditableField, } from "./editable-field.js";
1
3
  export { RecordDetails, RecordPage, recordPageLabels, } from "./recordpage.js";
2
4
  export { recordpageRecipe } from "./recordpage.recipe.js";
@@ -9,8 +9,23 @@ 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";
15
+ // crisp/#798: the details column's own drag clamp -- re-measured live on
16
+ // Attio's own record page (a real drag test, not a source read): min 619px,
17
+ // max 1170px, default (also the double-click-reset target) 734px. These
18
+ // replace an earlier, much narrower 300/560/320 that was never actually
19
+ // checked against Attio and turned out roughly half the real max. Confirmed
20
+ // live these are literal px, not a viewport ratio: at a narrower emulated
21
+ // width the already-dragged-to-max column shrank below 1170px, but its OWN
22
+ // `getComputedStyle(...).width` still read a plain px number the whole
23
+ // time (not a `%`) — that shrink is ordinary flex-shrink (both panels
24
+ // share `flex: 1 1 auto` and can't both keep their preferred width once the
25
+ // window is too narrow), not the clamp itself changing with viewport width.
26
+ const DETAILS_MIN_W = 619;
27
+ const DETAILS_MAX_W = 1170;
28
+ const DETAILS_DEFAULT_W = 734;
14
29
  function valueEmpty(v) {
15
30
  return v == null || v === "" || (Array.isArray(v) && v.length === 0);
16
31
  }
@@ -418,7 +433,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
418
433
  const tasks = tasksProp ?? internalTasks;
419
434
  const [showAllActivity, setShowAllActivity] = React.useState(false);
420
435
  // Drag-resizable left details column.
421
- const [detailsW, setDetailsW] = React.useState(320);
436
+ const [detailsW, setDetailsW] = React.useState(DETAILS_DEFAULT_W);
422
437
  const [resizing, setResizing] = React.useState(false);
423
438
  const dragRef = React.useRef(null);
424
439
  const onResizeDown = (e) => {
@@ -436,7 +451,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
436
451
  if (!dragRef.current)
437
452
  return;
438
453
  const next = dragRef.current.w + dragRef.current.dir * (e.clientX - dragRef.current.x);
439
- setDetailsW(Math.max(300, Math.min(560, next)));
454
+ setDetailsW(Math.max(DETAILS_MIN_W, Math.min(DETAILS_MAX_W, next)));
440
455
  };
441
456
  const onResizeUp = (e) => {
442
457
  dragRef.current = null;
@@ -621,7 +636,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
621
636
  const effectivePanel = tabsProp
622
637
  ? (active) => tabsProp.find((tb) => tb.id === active)?.render()
623
638
  : builtinPanel;
624
- return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: _jsx(Copy, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: _jsx(FileText, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: _jsx(Share2, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: _jsx(SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
639
+ return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: _jsx(Copy, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: _jsx(FileText, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: _jsx(Share2, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: _jsx(SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
625
640
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
626
641
  return;
627
642
  // The pointer drag learned its direction in #212; this did not.
@@ -630,7 +645,9 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
630
645
  // half-conversion the drag had, one handler away (#255).
631
646
  const rtl = isRtlElement(e.currentTarget);
632
647
  const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
633
- setDetailsW((w) => grow ? Math.min(560, w + 16) : Math.max(300, w - 16));
648
+ setDetailsW((w) => grow
649
+ ? Math.min(DETAILS_MAX_W, w + 16)
650
+ : Math.max(DETAILS_MIN_W, w - 16));
634
651
  } }), _jsx("main", { className: "crisp-recordpage-main", children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] })] }));
635
652
  }
636
653
  RecordPage.displayName = "RecordPage";
@@ -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
+ ];
@@ -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}; }