@crispy-seed/data-table 0.10.4 → 0.11.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.
package/dist/index.cjs CHANGED
@@ -17,6 +17,9 @@ var reactCheckbox = require('@seed-design/react-checkbox');
17
17
  var reactScaleFeedback = require('@seed-design/react-scale-feedback');
18
18
  var checkboxGroup = require('@seed-design/css/recipes/checkbox-group');
19
19
  var reactDom = require('react-dom');
20
+ var reactMenu = require('@seed-design/react-menu');
21
+ var menu = require('@seed-design/css/recipes/menu');
22
+ var menuItem = require('@seed-design/css/recipes/menu-item');
20
23
  var reactImage = require('@seed-design/react-image');
21
24
  var avatar = require('@seed-design/css/recipes/avatar');
22
25
  require('@seed-design/css/recipes/avatar-stack');
@@ -31,9 +34,6 @@ var _switch = require('@seed-design/css/recipes/switch');
31
34
  var switchmark = require('@seed-design/css/recipes/switchmark');
32
35
  var reactTextField = require('@seed-design/react-text-field');
33
36
  var textInput = require('@seed-design/css/recipes/text-input');
34
- var reactMenu = require('@seed-design/react-menu');
35
- var menu = require('@seed-design/css/recipes/menu');
36
- var menuItem = require('@seed-design/css/recipes/menu-item');
37
37
  var tabs = require('@seed-design/css/recipes/tabs');
38
38
 
39
39
  function _interopNamespace(e) {
@@ -780,7 +780,10 @@ function isFunction$1(value) {
780
780
  const DATA_TABLE_UNIVERSAL_CALC_OPS = [
781
781
  "count",
782
782
  "empty",
783
- "notEmpty"
783
+ "notEmpty",
784
+ "percentEmpty",
785
+ "percentFilled",
786
+ "none"
784
787
  ];
785
788
  const DATA_TABLE_NUMERIC_CALC_OPS = [
786
789
  "sum",
@@ -795,8 +798,11 @@ const DATA_TABLE_NUMERIC_CALC_OPS = [
795
798
  ];
796
799
  const DATA_TABLE_CALC_LABEL = {
797
800
  count: "Count",
798
- empty: "Empty",
799
- notEmpty: "Not empty",
801
+ empty: "Count empty",
802
+ notEmpty: "Count filled",
803
+ percentEmpty: "Percent empty",
804
+ percentFilled: "Percent filled",
805
+ none: "None",
800
806
  sum: "Sum",
801
807
  average: "Average",
802
808
  median: "Median",
@@ -826,7 +832,12 @@ const DATA_TABLE_CALC_LABEL = {
826
832
  * (`JSON.stringify`-normalized, so two arrays/objects with the same shape count
827
833
  * once) -- empties are already their own op (`empty`), so folding them into "one
828
834
  * more unique value" here would double-count the same information two different
829
- * ways in the same menu.
835
+ * ways in the same menu. `percentEmpty`/`percentFilled` are `empty`/`notEmpty` as a
836
+ * share of the row count (0-100, unrounded -- formatting rounds it for display) --
837
+ * `null` on zero rows, since 0/0 has no meaningful percentage (not 0%, not 100%).
838
+ * `none` always renders nothing: it is Attio's own "no calculation" menu entry, not a
839
+ * separate always-appended "Clear" item, and picking it is a real, storable op so it
840
+ * overrides a `summaryDefaultOp` instead of falling back to it.
830
841
  */ function computeColumnCalc(values, op) {
831
842
  switch(op){
832
843
  case "count":
@@ -835,6 +846,14 @@ const DATA_TABLE_CALC_LABEL = {
835
846
  return values.filter(isEmptyValueForCalc).length;
836
847
  case "notEmpty":
837
848
  return values.filter((value)=>!isEmptyValueForCalc(value)).length;
849
+ case "percentEmpty":
850
+ if (values.length === 0) return null;
851
+ return values.filter(isEmptyValueForCalc).length / values.length * 100;
852
+ case "percentFilled":
853
+ if (values.length === 0) return null;
854
+ return values.filter((value)=>!isEmptyValueForCalc(value)).length / values.length * 100;
855
+ case "none":
856
+ return null;
838
857
  case "countUnique":
839
858
  {
840
859
  const seen = new Set(values.filter((value)=>!isEmptyValueForCalc(value)).map((value)=>JSON.stringify(value)));
@@ -1653,6 +1672,17 @@ function useDataTableSelection(props) {
1653
1672
  rowIds,
1654
1673
  setSelectedIds
1655
1674
  ]);
1675
+ // Bulk-action bar's own "×" (gap #4, DATATABLE-ATTIO-SPEC's Attio review) --
1676
+ // deliberately its own function rather than reusing `toggleAll`: `toggleAll` only
1677
+ // clears from an "all" selection, and otherwise SELECTS every row (`selectionState
1678
+ // === "some"` -> `rowIds`, above) -- exactly the wrong direction for a "clear
1679
+ // selection" affordance shown while `selectionState` is "some". This always empties
1680
+ // the set, regardless of `selectionState`.
1681
+ const clearSelection = React.useCallback(()=>{
1682
+ setSelectedIds([]);
1683
+ }, [
1684
+ setSelectedIds
1685
+ ]);
1656
1686
  const getRowProps = React.useCallback((rowId)=>{
1657
1687
  const isRowSelected = isSelected(rowId);
1658
1688
  return domUtils.elementProps({
@@ -1671,6 +1701,7 @@ function useDataTableSelection(props) {
1671
1701
  isSelected,
1672
1702
  toggleRow,
1673
1703
  toggleAll,
1704
+ clearSelection,
1674
1705
  getRowProps
1675
1706
  };
1676
1707
  }
@@ -1696,24 +1727,33 @@ function useDataTableSort(props) {
1696
1727
  // already passes its "cleared" shape through unconditionally; this now matches.
1697
1728
  onChange: onSortChange
1698
1729
  });
1730
+ // 3-state cycle (DATATABLE-ATTIO-SPEC.md: "asc/desc/none cycle") -- clicking a
1731
+ // DIFFERENT column always starts it fresh at asc, discarding whatever the previous
1732
+ // column's direction was (this stays a SINGLE-column secondary affordance, never a
1733
+ // second multi-sort list of its own). Clicking the SAME column steps asc -> desc ->
1734
+ // undefined ("none", i.e. this column drops out of the sort entirely) -> asc again.
1735
+ // Before this, the cycle wrapped desc -> asc forever, with no click-driven way back
1736
+ // to unsorted at all (a reported gap: "sort asc/desc has no option for no sort on
1737
+ // columns") -- `clearSort` below was the ONLY way there, now it's one of two.
1699
1738
  const toggleSort = React.useCallback((key)=>{
1700
1739
  setSort((current)=>{
1701
1740
  if (!current || current.key !== key) return {
1702
1741
  key,
1703
1742
  dir: "asc"
1704
1743
  };
1705
- return {
1744
+ if (current.dir === "asc") return {
1706
1745
  key,
1707
- dir: current.dir === "asc" ? "desc" : "asc"
1746
+ dir: "desc"
1708
1747
  };
1748
+ return undefined;
1709
1749
  });
1710
1750
  }, [
1711
1751
  setSort
1712
1752
  ]);
1713
- /** Attio/GRC's "Remove sort" -- the header click cycle (above) only ever toggles
1714
- * asc/desc once a column has been clicked once; this is the only way back to no
1715
- * sort at all. Exposed for a column's own header menu (`column-menu.tsx`'s
1716
- * `ColumnMenu`) to offer as a "Clear sort" item when that column is the sorted one. */ const clearSort = React.useCallback(()=>setSort(undefined), [
1753
+ /** Attio/GRC's "Remove sort" -- a second way back to no sort at all (the header
1754
+ * click cycle above reaches it too, on a column's third click), exposed for a
1755
+ * column's own header menu (`column-menu.tsx`'s `ColumnMenu`) to offer as a
1756
+ * "Clear sort" item when that column is the sorted one, without three clicks. */ const clearSort = React.useCallback(()=>setSort(undefined), [
1717
1757
  setSort
1718
1758
  ]);
1719
1759
  const getHeaderCellProps = React.useCallback((columnKey, sortable)=>{
@@ -1746,7 +1786,12 @@ function useDataTableSort(props) {
1746
1786
  sort,
1747
1787
  toggleSort,
1748
1788
  clearSort,
1749
- getHeaderCellProps
1789
+ getHeaderCellProps,
1790
+ // Exposed for `useDataTable.ts`'s own composition ONLY (unifying this single
1791
+ // click-to-sort column with `useDataTableToolbarSort`'s ordered list) -- not part
1792
+ // of the documented header-click contract (`toggleSort`/`clearSort` above are),
1793
+ // so no consumer-facing doc comment promotes it as a control of its own.
1794
+ setSort
1750
1795
  };
1751
1796
  }
1752
1797
 
@@ -1850,11 +1895,14 @@ const EMPTY_SORTS = [];
1850
1895
  * concern rather than folded into `useDataTableSort` (DATATABLE-ATTIO-SPEC.md: "compose,
1851
1896
  * don't fatten one hook"). `useDataTableSort` still owns the single click-to-sort
1852
1897
  * column/direction a header's own click toggles -- that stays Attio's SECONDARY sort
1853
- * affordance, untouched by this hook. This one is the PRIMARY toolbar Sort control: add
1854
- * a condition, pick its field/direction, reorder, remove -- multiple conditions apply in
1855
- * list order (sort[0] wins, later ones break ties; see the styled layer's
1856
- * `applyToolbarSorts`). The styled layer decides how the two combine (DataTable.tsx:
1857
- * toolbar sorts win when any exist, the single click-to-sort column otherwise).
1898
+ * affordance, a separate hook from this one. They are NOT independent state, though:
1899
+ * `useDataTable.ts`'s own composition keeps a header-driven column's entry in THIS
1900
+ * array in sync (added/updated/removed as that column's header cycles asc/desc/none),
1901
+ * so a consumer controlling only `toolbarSorts`/`onToolbarSortsChange` still sees a
1902
+ * header click, and the toolbar's own "Remove sort" x on that same entry clears the
1903
+ * header's side too. Multiple conditions apply in list order (sort[0] wins, later ones
1904
+ * break ties; see the styled layer's `applyToolbarSorts`). The styled layer
1905
+ * (`DataTable.tsx`) decides how the merged result actually sorts `data`.
1858
1906
  */ function useDataTableToolbarSort(props) {
1859
1907
  const { toolbarSorts: sortsProp, defaultToolbarSorts = EMPTY_SORTS, onToolbarSortsChange } = props;
1860
1908
  const [toolbarSorts = EMPTY_SORTS, setToolbarSorts] = useControllableState$1({
@@ -1939,8 +1987,13 @@ const EMPTY_SORTS = [];
1939
1987
  // header "is/is not" picker) -- DATATABLE-ATTIO-SPEC.md's Attio/crisp `TableToolbar`
1940
1988
  // model is now the PRIMARY filter/sort UX, not a per-column header affordance. They stay
1941
1989
  // SEPARATE sub-hooks from `useDataTableSort` rather than folded into it ("compose, don't
1942
- // fatten one hook") -- the styled layer decides how the toolbar's multi-condition sort
1943
- // and the header's single click-to-sort combine (DataTable.tsx).
1990
+ // fatten one hook") -- but this composition point below DOES reconcile the two into one
1991
+ // state (the two `useEffect`s after `infiniteScroll`), so a header click and the
1992
+ // toolbar's own Sort popover always agree on what's sorted, regardless of which prop
1993
+ // pair (`sort`/`onSortChange` or `toolbarSorts`/`onToolbarSortsChange`) a consumer
1994
+ // actually controls. `DataTable.tsx` (styled) still decides how the RESULT actually
1995
+ // sorts `data` (`effectiveSorts`/`applyToolbarSorts`) -- this layer only keeps the two
1996
+ // STATE pools in agreement.
1944
1997
  function useDataTable(props) {
1945
1998
  const sort = useDataTableSort(props);
1946
1999
  const selection = useDataTableSelection(props);
@@ -1963,6 +2016,105 @@ function useDataTable(props) {
1963
2016
  ...props,
1964
2017
  scrollRootRef: scroll.elementRef
1965
2018
  });
2019
+ // Unifies `sort` (the header's own single click-to-sort column, above) with
2020
+ // `toolbarSort.toolbarSorts` (the toolbar Sort popover's ordered list, above) into
2021
+ // ONE state instead of two -- before this, a header click only ever moved `sort`,
2022
+ // invisible to a consumer controlling `toolbarSorts`/`onToolbarSortsChange` (the
2023
+ // reported bug: clicking a header did nothing to the array the consumer's own Sort
2024
+ // popover reads, so the toolbar's "Sort" trigger/count and its popover rows silently
2025
+ // ignored every header click).
2026
+ //
2027
+ // ONE effect, not two -- an earlier version split this into a forward
2028
+ // (`sort` -> `toolbarSorts`) and a reverse (`toolbarSorts` -> `sort`) effect, each
2029
+ // keyed off its own dependency. That raced on the very first render: `useEffect`
2030
+ // runs every effect once on mount regardless of what "changed", so with
2031
+ // `defaultSort` set, the reverse effect ran in the SAME commit as the forward one,
2032
+ // read `toolbarSorts` BEFORE the forward effect's own (still-queued, not yet
2033
+ // re-rendered) write had landed, saw no entry for the column yet, and immediately
2034
+ // cleared `sort` right back out -- the defaultSort silently never took effect.
2035
+ // `settledRef` sidesteps this: it remembers the last `{key,dir}` this effect itself
2036
+ // confirmed, so on every run it can tell WHICH side moved since then, instead of
2037
+ // guessing from which dependency happens to have triggered the re-run.
2038
+ const settledRef = React.useRef({
2039
+ key: undefined,
2040
+ dir: undefined
2041
+ });
2042
+ React.useEffect(()=>{
2043
+ const curSort = sort.sort;
2044
+ const curEntry = curSort ? toolbarSort.toolbarSorts.find((s)=>s.key === curSort.key) : undefined;
2045
+ const settled = settledRef.current;
2046
+ const sortUnchangedSinceSettled = settled.key === curSort?.key && settled.dir === curSort?.dir;
2047
+ if (sortUnchangedSinceSettled) {
2048
+ // `sort` itself hasn't moved since we last settled -- if this effect is running
2049
+ // at all, `toolbarSorts` is what changed, so REVERSE-sync: the toolbar Sort
2050
+ // popover's own "Remove sort" x, or its direction select, touching this exact
2051
+ // column. Never reaches across to adopt some OTHER column added independently
2052
+ // via that popover -- only the column `sort` is already tracking.
2053
+ if (!curSort) return;
2054
+ if (!curEntry) {
2055
+ sort.clearSort();
2056
+ settledRef.current = {
2057
+ key: undefined,
2058
+ dir: undefined
2059
+ };
2060
+ } else if (curEntry.dir !== curSort.dir) {
2061
+ sort.setSort({
2062
+ key: curSort.key,
2063
+ dir: curEntry.dir
2064
+ });
2065
+ settledRef.current = {
2066
+ key: curSort.key,
2067
+ dir: curEntry.dir
2068
+ };
2069
+ }
2070
+ return;
2071
+ }
2072
+ // `sort` moved (a header click, Enter/Space, or `clearSort` -- column-menu.tsx's
2073
+ // menu item calls that directly, bypassing `toggleSort`) -- FORWARD-sync:
2074
+ // add/update/remove THIS column's own entry in `toolbarSorts`, leaving every
2075
+ // OTHER entry (added independently via the toolbar's own "+ Add sort") untouched.
2076
+ // The functional updater reads the CURRENT array off `current`, never off
2077
+ // `toolbarSort.toolbarSorts` in this closure, so a stale render can never clobber
2078
+ // an entry the toolbar's own popover added a moment earlier.
2079
+ const prevKey = settled.key;
2080
+ toolbarSort.setToolbarSorts((current)=>{
2081
+ let updated = current;
2082
+ if (prevKey && prevKey !== curSort?.key) {
2083
+ updated = updated.filter((s)=>s.key !== prevKey);
2084
+ }
2085
+ if (curSort) {
2086
+ const idx = updated.findIndex((s)=>s.key === curSort.key);
2087
+ if (idx === -1) {
2088
+ updated = [
2089
+ ...updated,
2090
+ {
2091
+ key: curSort.key,
2092
+ dir: curSort.dir
2093
+ }
2094
+ ];
2095
+ } else if (updated[idx].dir !== curSort.dir) {
2096
+ updated = updated.map((s, i)=>i === idx ? {
2097
+ ...s,
2098
+ dir: curSort.dir
2099
+ } : s);
2100
+ }
2101
+ }
2102
+ return updated;
2103
+ });
2104
+ settledRef.current = curSort ? {
2105
+ key: curSort.key,
2106
+ dir: curSort.dir
2107
+ } : {
2108
+ key: undefined,
2109
+ dir: undefined
2110
+ };
2111
+ }, [
2112
+ sort.sort,
2113
+ toolbarSort.toolbarSorts,
2114
+ toolbarSort.setToolbarSorts,
2115
+ sort.clearSort,
2116
+ sort.setSort
2117
+ ]);
1966
2118
  return {
1967
2119
  ...sort,
1968
2120
  ...selection,
@@ -2558,11 +2710,70 @@ CheckboxIndicator.displayName = "CheckboxIndicator";
2558
2710
  const CheckboxLabel = withContext$6(withStateProps$4(Primitive.span), "label");
2559
2711
  const CheckboxHiddenInput = reactCheckbox.Checkbox.HiddenInput;
2560
2712
 
2561
- const { withProvider: withProvider$2, withContext: withContext$5 } = createSlotRecipeContext(avatar.avatar);
2713
+ const { ClassNamesProvider: ClassNamesProvider$2, withContext: withContext$5, useClassNames: useClassNames$2 } = createSlotRecipeContext(menu.menu);
2714
+ const { PropsProvider: ItemPropsProvider$1, useProps: useItemProps$1, withContext: withItemContext$1, ClassNamesProvider: ItemClassNamesProvider$1 } = createSlotRecipeContext(menuItem.menuItem);
2715
+ const withStateProps$3 = createWithStateProps([
2716
+ reactMenu.useMenuContext
2717
+ ]);
2718
+ const withItemStateProps$1 = createWithStateProps([
2719
+ reactMenu.useMenuItemContext
2720
+ ]);
2721
+ const MenuRoot = (props)=>{
2722
+ const [variantProps, otherProps] = menu.menu.splitVariantProps(props);
2723
+ const classNames = menu.menu(variantProps);
2724
+ return /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider$2, {
2725
+ value: classNames,
2726
+ children: /*#__PURE__*/ jsxRuntime.jsx(ItemPropsProvider$1, {
2727
+ value: {
2728
+ size: variantProps.size
2729
+ },
2730
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactMenu.Menu.Root, {
2731
+ ...otherProps
2732
+ })
2733
+ })
2734
+ });
2735
+ };
2736
+ const MenuAnchor = reactMenu.Menu.Anchor;
2737
+ const MenuTrigger = reactMenu.Menu.Trigger;
2738
+ const MenuPositioner = /*#__PURE__*/ React__namespace.forwardRef(({ className, ...props }, ref)=>{
2739
+ const classNames = useClassNames$2();
2740
+ return /*#__PURE__*/ jsxRuntime.jsx(reactMenu.Menu.Positioner, {
2741
+ ref: ref,
2742
+ className: clsx(classNames.positioner, className),
2743
+ ...props
2744
+ });
2745
+ });
2746
+ MenuPositioner.displayName = "MenuPositioner";
2747
+ const MenuContent = withContext$5(reactMenu.Menu.Content, "content");
2748
+ const MenuScrollArea = withContext$5(withStateProps$3(Primitive.div), "scrollArea");
2749
+ withContext$5(reactMenu.Menu.Group, "group");
2750
+ withContext$5(reactMenu.Menu.GroupLabel, "groupLabel");
2751
+ const MenuItem = /*#__PURE__*/ React__namespace.forwardRef(({ className: propClassName, ...props }, ref)=>{
2752
+ const [variantProps, otherProps] = menuItem.menuItem.splitVariantProps(props);
2753
+ const parentProps = useItemProps$1();
2754
+ const classNames = menuItem.menuItem({
2755
+ ...parentProps,
2756
+ ...variantProps
2757
+ });
2758
+ return /*#__PURE__*/ jsxRuntime.jsx(ItemClassNamesProvider$1, {
2759
+ value: classNames,
2760
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactMenu.Menu.Item, {
2761
+ ref: ref,
2762
+ className: clsx(classNames.root, propClassName),
2763
+ ...otherProps
2764
+ })
2765
+ });
2766
+ });
2767
+ MenuItem.displayName = "MenuItem";
2768
+ withItemContext$1(withItemStateProps$1(Primitive.div), "body");
2769
+ const MenuItemLabel = withItemContext$1(withItemStateProps$1(Primitive.span), "label");
2770
+ withItemContext$1(withItemStateProps$1(Primitive.span), "description");
2771
+
2772
+ const { withProvider: withProvider$2, withContext: withContext$4 } = createSlotRecipeContext(avatar.avatar);
2562
2773
  const AvatarRoot = withProvider$2(reactImage.Image.Root, "root");
2563
- const AvatarImage = withContext$5(reactImage.Image.Content, "image");
2564
- const AvatarFallback = withContext$5(reactImage.Image.Fallback, "fallback");
2565
- withContext$5(Primitive.div, "badge");
2774
+ const AvatarImage = withContext$4(reactImage.Image.Content, "image");
2775
+ const AvatarFallback = withContext$4(reactImage.Image.Fallback, "fallback");
2776
+ withContext$4(Primitive.div, "badge");
2566
2777
 
2567
2778
  const Badge = /*#__PURE__*/ React.forwardRef(({ className, children, ...props }, ref)=>{
2568
2779
  const [variantProps, restProps] = badge.badge.splitVariantProps(props);
@@ -3106,6 +3317,141 @@ function RatingCell({ value, maxRating = 5 }) {
3106
3317
  });
3107
3318
  }
3108
3319
 
3320
+ /** Clear is withheld for `chip-list`: that column already owns its own per-tag remove
3321
+ * affordance (`ChipListEditor`) and commits a JSON-STRINGIFIED array through `onEdit`
3322
+ * (`DataTable.chips.test.tsx`'s own `TagsHarness` -- and this package's own Storybook
3323
+ * demo, `CellFeaturesDemo`, does exactly `JSON.parse(value)` on commit) -- committing
3324
+ * a bare `""` through that same path throws in the consumer's `onEdit`, it doesn't
3325
+ * clear anything. Every OTHER editable shape's commit value is a plain string
3326
+ * (`"true"`/`"false"`, a stringified rating, an option's own `value`, free text) that
3327
+ * a consumer's `onEdit` already has to accept a possibly-empty version of. */ function isClearableEditableCell(column) {
3328
+ return !!column.editable && column.type !== "chip-list";
3329
+ }
3330
+ /** Paste is narrower still -- restricted to a PLAIN `editable` column (no `editor`,
3331
+ * and not one of the always-live widget types) whose committed value IS the raw
3332
+ * text/number a user typed. `select`/`reference`/`actor` expect one of THEIR OWN
3333
+ * option values (not arbitrary clipboard text), `switch`/`rating`/`checkbox` are
3334
+ * always-live widgets with their own encoding, and `chip-list` is excluded for the
3335
+ * same JSON reason `isClearableEditableCell` documents above. This is the task's own
3336
+ * "only for simple editable text/number cells" guard -- committing pasted text
3337
+ * through any of the excluded shapes would corrupt the row rather than edit it
3338
+ * (crispy-seed's own #1094 lesson: never assume an editor's commit shape).
3339
+ */ function isSimpleEditableCell(column) {
3340
+ if (!column.editable) return false;
3341
+ if (column.editor) return false;
3342
+ if (column.type === "chip-list" || column.type === "checkbox" || column.type === "rating") {
3343
+ return false;
3344
+ }
3345
+ return true;
3346
+ }
3347
+ /** Copy's own text representation -- deliberately mirrors `DisplayCellContent`'s
3348
+ * (body-cells.tsx) switch closely enough to read the same as what's on screen, but
3349
+ * returns a plain string (`navigator.clipboard.writeText`) rather than JSX. Not
3350
+ * exported -- it exists only to feed Copy, not as a general-purpose formatter. */ function cellValueAsText(column, value, locale) {
3351
+ if (value == null) return "";
3352
+ switch(column.type){
3353
+ case "entity":
3354
+ if (typeof value === "object") return String(value.name ?? "");
3355
+ return String(value);
3356
+ case "tags":
3357
+ case "chip-list":
3358
+ return Array.isArray(value) ? value.join(", ") : String(value);
3359
+ case "number":
3360
+ return typeof value === "number" ? String(formatNumberCell(value, locale) ?? "") : String(value);
3361
+ case "currency":
3362
+ return typeof value === "number" ? String(formatCurrencyCell(value, column.symbol ?? "$", locale) ?? "") : String(value);
3363
+ case "date":
3364
+ return String(formatDateCell(value, locale) ?? "");
3365
+ case "datetime":
3366
+ return String(formatDateTimeCell(value, locale) ?? "");
3367
+ case "checkbox":
3368
+ return value === true || value === "true" ? "true" : "false";
3369
+ default:
3370
+ return String(value);
3371
+ }
3372
+ }
3373
+ /**
3374
+ * One cell's right-click menu -- mounted by `body-cells.tsx`'s `DataTableBodyCell`
3375
+ * only while actually open (the same "don't keep an unused floating-ui instance alive
3376
+ * per cell" reasoning as that file's own portaled chip-list popover), so this never
3377
+ * costs anything for the row×column cells a user never right-clicks.
3378
+ */ function CellContextMenu({ column, row, value, locale, point, open, onOpenChange, onCommit, onViewEditHistory, cellContextMenuItems }) {
3379
+ const clearable = isClearableEditableCell(column);
3380
+ const pasteable = isSimpleEditableCell(column);
3381
+ return /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
3382
+ open: open,
3383
+ onOpenChange: onOpenChange,
3384
+ placement: "bottom-start",
3385
+ strategy: "fixed",
3386
+ gutter: 2,
3387
+ children: [
3388
+ /*#__PURE__*/ jsxRuntime.jsx(MenuAnchor, {
3389
+ "aria-hidden": true,
3390
+ style: {
3391
+ position: "fixed",
3392
+ top: point.y,
3393
+ left: point.x,
3394
+ width: 0,
3395
+ height: 0,
3396
+ pointerEvents: "none"
3397
+ }
3398
+ }),
3399
+ /*#__PURE__*/ jsxRuntime.jsx(MenuPositioner, {
3400
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuContent, {
3401
+ style: {
3402
+ minWidth: 180
3403
+ },
3404
+ children: /*#__PURE__*/ jsxRuntime.jsxs(MenuScrollArea, {
3405
+ children: [
3406
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
3407
+ onClick: ()=>{
3408
+ const text = cellValueAsText(column, value, locale);
3409
+ // Best-effort: an insecure context or a denied permission rejects
3410
+ // this silently (no `MenuItem` state depends on the outcome) --
3411
+ // there's nothing else for a plain "Copy" item to do about a
3412
+ // browser that refused the write.
3413
+ void navigator.clipboard?.writeText?.(text);
3414
+ },
3415
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
3416
+ children: "Copy"
3417
+ })
3418
+ }),
3419
+ pasteable && /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
3420
+ onClick: ()=>{
3421
+ void navigator.clipboard?.readText?.().then((text)=>onCommit(text)).catch(()=>{
3422
+ // Clipboard read denied/unavailable -- best-effort, same
3423
+ // reasoning as Copy above.
3424
+ });
3425
+ },
3426
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
3427
+ children: "Paste"
3428
+ })
3429
+ }),
3430
+ clearable && /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
3431
+ onClick: ()=>onCommit(""),
3432
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
3433
+ children: "Clear value"
3434
+ })
3435
+ }),
3436
+ onViewEditHistory && /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
3437
+ onClick: ()=>onViewEditHistory(row, column.key),
3438
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
3439
+ children: "View edit history"
3440
+ })
3441
+ }),
3442
+ cellContextMenuItems?.({
3443
+ row,
3444
+ column,
3445
+ value
3446
+ })
3447
+ ]
3448
+ })
3449
+ })
3450
+ })
3451
+ ]
3452
+ });
3453
+ }
3454
+
3109
3455
  /** Fallback width (px) used to compute a pinned column's cumulative sticky offset when
3110
3456
  * it has no explicit `width` and hasn't been drag-resized -- pinning needs a STATIC
3111
3457
  * number to put in an inline `insetInlineStart`, and `table-layout: fixed` only
@@ -3264,11 +3610,11 @@ function RatingCell({ value, maxRating = 5 }) {
3264
3610
  // recipe, so a per-instance variant can override the root's default.
3265
3611
  const { ClassNamesProvider: TriggerClassNamesProvider, PropsProvider: TriggerPropsProvider, useProps: useTriggerProps, withContext: withTriggerContext, useClassNames: useTriggerClassNames } = createSlotRecipeContext(selectTrigger.selectTrigger);
3266
3612
  const { ClassNamesProvider: ContentClassNamesProvider, withContext: withContentContext, useClassNames: useContentClassNames } = createSlotRecipeContext(select.select);
3267
- const { ClassNamesProvider: ItemClassNamesProvider$1, PropsProvider: ItemPropsProvider$1, useProps: useItemProps$1, withContext: withItemContext$1, useClassNames: useItemClassNames } = createSlotRecipeContext(selectItem.selectItem);
3268
- const withStateProps$3 = createWithStateProps([
3613
+ const { ClassNamesProvider: ItemClassNamesProvider, PropsProvider: ItemPropsProvider, useProps: useItemProps, withContext: withItemContext, useClassNames: useItemClassNames } = createSlotRecipeContext(selectItem.selectItem);
3614
+ const withStateProps$2 = createWithStateProps([
3269
3615
  reactSelect.useSelectContext
3270
3616
  ]);
3271
- const withItemStateProps$1 = createWithStateProps([
3617
+ const withItemStateProps = createWithStateProps([
3272
3618
  reactSelect.useSelectItemContext
3273
3619
  ]);
3274
3620
  // NOTE: `gutter` and `overflowPadding` are specified in Rootage (`select.yaml`,
@@ -3289,7 +3635,7 @@ const SelectRoot = (props)=>{
3289
3635
  value: contentClassNames,
3290
3636
  children: /*#__PURE__*/ jsxRuntime.jsx(TriggerPropsProvider, {
3291
3637
  value: triggerVariantProps,
3292
- children: /*#__PURE__*/ jsxRuntime.jsx(ItemPropsProvider$1, {
3638
+ children: /*#__PURE__*/ jsxRuntime.jsx(ItemPropsProvider, {
3293
3639
  value: itemVariantProps,
3294
3640
  children: /*#__PURE__*/ jsxRuntime.jsx(reactSelect.Select.Root, {
3295
3641
  ...otherProps
@@ -3329,8 +3675,8 @@ const SelectTrigger = /*#__PURE__*/ React__namespace.forwardRef(({ className, ..
3329
3675
  });
3330
3676
  });
3331
3677
  SelectTrigger.displayName = "SelectTrigger";
3332
- const SelectValue = withTriggerContext(withStateProps$3(reactSelect.Select.Value), "value");
3333
- const SelectPlaceholder = withTriggerContext(withStateProps$3(reactSelect.Select.Placeholder), "placeholder");
3678
+ const SelectValue = withTriggerContext(withStateProps$2(reactSelect.Select.Value), "value");
3679
+ const SelectPlaceholder = withTriggerContext(withStateProps$2(reactSelect.Select.Placeholder), "placeholder");
3334
3680
  const SelectPrefixIcon = /*#__PURE__*/ React__namespace.forwardRef(({ fallback, className, ...otherProps }, ref)=>{
3335
3681
  const { selectedItem, stateProps } = reactSelect.useSelectContext();
3336
3682
  const classNames = useTriggerClassNames();
@@ -3345,7 +3691,7 @@ const SelectPrefixIcon = /*#__PURE__*/ React__namespace.forwardRef(({ fallback,
3345
3691
  });
3346
3692
  });
3347
3693
  SelectPrefixIcon.displayName = "SelectPrefixIcon";
3348
- const SelectSuffixIcon = withTriggerContext(withStateProps$3(InternalIcon), "suffixIcon");
3694
+ const SelectSuffixIcon = withTriggerContext(withStateProps$2(InternalIcon), "suffixIcon");
3349
3695
  const SelectPositioner = /*#__PURE__*/ React__namespace.forwardRef(({ className, ...props }, ref)=>{
3350
3696
  const classNames = useContentClassNames();
3351
3697
  return /*#__PURE__*/ jsxRuntime.jsx(reactSelect.Select.Positioner, {
@@ -3376,12 +3722,12 @@ withContentContext(reactSelect.Select.Group, "group");
3376
3722
  withContentContext(reactSelect.Select.GroupLabel, "groupLabel");
3377
3723
  const SelectItem = /*#__PURE__*/ React__namespace.forwardRef(({ prefixIcon, className, ...props }, ref)=>{
3378
3724
  const [variantProps, otherProps] = selectItem.selectItem.splitVariantProps(props);
3379
- const rootProps = useItemProps$1();
3725
+ const rootProps = useItemProps();
3380
3726
  const classNames = selectItem.selectItem({
3381
3727
  ...rootProps,
3382
3728
  ...variantProps
3383
3729
  });
3384
- return /*#__PURE__*/ jsxRuntime.jsx(ItemClassNamesProvider$1, {
3730
+ return /*#__PURE__*/ jsxRuntime.jsx(ItemClassNamesProvider, {
3385
3731
  value: classNames,
3386
3732
  children: /*#__PURE__*/ jsxRuntime.jsx(reactSelect.Select.Item, {
3387
3733
  ref: ref,
@@ -3406,7 +3752,7 @@ const SelectItemPrefixIcon = /*#__PURE__*/ React__namespace.forwardRef(({ svg: s
3406
3752
  });
3407
3753
  });
3408
3754
  SelectItemPrefixIcon.displayName = "SelectItemPrefixIcon";
3409
- const SelectItemBody = withItemContext$1(withItemStateProps$1(Primitive.div), "body");
3755
+ const SelectItemBody = withItemContext(withItemStateProps(Primitive.div), "body");
3410
3756
  const SelectItemLabel = /*#__PURE__*/ React__namespace.forwardRef(({ children, className, ...otherProps }, ref)=>{
3411
3757
  const { label, stateProps } = reactSelect.useSelectItemContext();
3412
3758
  const classNames = useItemClassNames();
@@ -3421,7 +3767,7 @@ const SelectItemLabel = /*#__PURE__*/ React__namespace.forwardRef(({ children, c
3421
3767
  });
3422
3768
  });
3423
3769
  SelectItemLabel.displayName = "SelectItemLabel";
3424
- withItemContext$1(withItemStateProps$1(Primitive.span), "description");
3770
+ withItemContext(withItemStateProps(Primitive.span), "description");
3425
3771
  const SelectItemIndicator = /*#__PURE__*/ React__namespace.forwardRef(({ selected: selectedSvg, unselected: unselectedSvg, className, ...otherProps }, ref)=>{
3426
3772
  const { isSelected, stateProps } = reactSelect.useSelectItemContext();
3427
3773
  const classNames = useItemClassNames();
@@ -3453,9 +3799,9 @@ const SelectHiddenSelect = /*#__PURE__*/ React__namespace.forwardRef((props, ref
3453
3799
  });
3454
3800
  SelectHiddenSelect.displayName = "SelectHiddenSelect";
3455
3801
 
3456
- const { withContext: withContext$4, ClassNamesProvider: ClassNamesProvider$2 } = createSlotRecipeContext(_switch.switchStyle);
3802
+ const { withContext: withContext$3, ClassNamesProvider: ClassNamesProvider$1 } = createSlotRecipeContext(_switch.switchStyle);
3457
3803
  const { withContext: withControlContext, PropsProvider: ControlPropsProvider, withProvider: withControlProvider } = createSlotRecipeContext(switchmark.switchmark);
3458
- const withStateProps$2 = createWithStateProps([
3804
+ const withStateProps$1 = createWithStateProps([
3459
3805
  reactSwitch.useSwitchContext
3460
3806
  ]);
3461
3807
  const SwitchRoot = /*#__PURE__*/ React__namespace.default.forwardRef(({ className, ...props }, ref)=>{
@@ -3466,7 +3812,7 @@ const SwitchRoot = /*#__PURE__*/ React__namespace.default.forwardRef(({ classNam
3466
3812
  const classNames = _switch.switchStyle(switchVariantProps);
3467
3813
  return /*#__PURE__*/ jsxRuntime.jsx(ControlPropsProvider, {
3468
3814
  value: switchmarkVariantProps,
3469
- children: /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider$2, {
3815
+ children: /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider$1, {
3470
3816
  value: classNames,
3471
3817
  children: /*#__PURE__*/ jsxRuntime.jsx(reactSwitch.Switch.Root, {
3472
3818
  ref: ref,
@@ -3479,20 +3825,20 @@ const SwitchRoot = /*#__PURE__*/ React__namespace.default.forwardRef(({ classNam
3479
3825
  SwitchRoot.displayName = "SwitchRoot";
3480
3826
  const SwitchControl = withScaleFeedback(withControlProvider(reactSwitch.Switch.Control, "root"));
3481
3827
  const SwitchThumb = withControlContext(reactSwitch.Switch.Thumb, "thumb");
3482
- withContext$4(withStateProps$2(Primitive.span), "label");
3828
+ withContext$3(withStateProps$1(Primitive.span), "label");
3483
3829
  const SwitchHiddenInput = reactSwitch.Switch.HiddenInput;
3484
3830
 
3485
3831
  // src/use-layout-effect.tsx
3486
3832
  var useLayoutEffect2 = globalThis?.document ? React__namespace.useLayoutEffect : ()=>{};
3487
3833
 
3488
- const { withProvider: withProvider$1, withContext: withContext$3, useClassNames: useClassNames$2 } = createSlotRecipeContext(textInput.textInput);
3834
+ const { withProvider: withProvider$1, withContext: withContext$2, useClassNames: useClassNames$1 } = createSlotRecipeContext(textInput.textInput);
3489
3835
  const withFieldStateProps = createWithStateProps([
3490
3836
  {
3491
3837
  useContext: reactField.useFieldContext,
3492
3838
  strict: false
3493
3839
  }
3494
3840
  ]);
3495
- const withStateProps$1 = createWithStateProps([
3841
+ const withStateProps = createWithStateProps([
3496
3842
  reactTextField.useTextFieldContext,
3497
3843
  {
3498
3844
  useContext: reactField.useFieldContext,
@@ -3500,12 +3846,12 @@ const withStateProps$1 = createWithStateProps([
3500
3846
  }
3501
3847
  ]);
3502
3848
  const TextFieldRoot = withProvider$1(withFieldStateProps(reactTextField.TextField.Root), "root");
3503
- withContext$3(withStateProps$1(InternalIcon), "prefixIcon");
3504
- withContext$3(withStateProps$1(Primitive.span), "prefixText");
3505
- withContext$3(withStateProps$1(InternalIcon), "suffixIcon");
3506
- withContext$3(withStateProps$1(Primitive.span), "suffixText");
3849
+ withContext$2(withStateProps(InternalIcon), "prefixIcon");
3850
+ withContext$2(withStateProps(Primitive.span), "prefixText");
3851
+ withContext$2(withStateProps(InternalIcon), "suffixIcon");
3852
+ withContext$2(withStateProps(Primitive.span), "suffixText");
3507
3853
  const TextFieldInput = /*#__PURE__*/ React.forwardRef(({ className, ...otherProps }, ref)=>{
3508
- const classNames = useClassNames$2();
3854
+ const classNames = useClassNames$1();
3509
3855
  const textFieldContext = reactTextField.useTextFieldContext();
3510
3856
  const fieldContext = reactField.useFieldContext({
3511
3857
  strict: false
@@ -3523,7 +3869,7 @@ const TextFieldInput = /*#__PURE__*/ React.forwardRef(({ className, ...otherProp
3523
3869
  });
3524
3870
  TextFieldInput.displayName = "TextFieldInput";
3525
3871
  const TextFieldTextarea = /*#__PURE__*/ React.forwardRef(({ className, autoresize = true, ...otherProps }, ref)=>{
3526
- const classNames = useClassNames$2();
3872
+ const classNames = useClassNames$1();
3527
3873
  const textFieldContext = reactTextField.useTextFieldContext();
3528
3874
  const fieldContext = reactField.useFieldContext({
3529
3875
  strict: false
@@ -3580,7 +3926,7 @@ const TextFieldTextarea = /*#__PURE__*/ React.forwardRef(({ className, autoresiz
3580
3926
  });
3581
3927
  TextFieldTextarea.displayName = "TextFieldTextarea";
3582
3928
 
3583
- __insertCSS(".seed-data-table__root{box-sizing:border-box;display:flex;flex-direction:column;width:100%;border-width:1px;border-style:solid;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}.seed-data-table__table{width:100%;border-collapse:separate;border-spacing:0;table-layout:fixed}.seed-data-table__headerCell{box-sizing:border-box;position:sticky;top:0;z-index:2;vertical-align:middle;text-align:start;padding:0;height:var(--seed-dimension-x10);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none;color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium);letter-spacing:-.14px}.seed-data-table__headerCell:last-child{border-inline-end:none}.seed-data-table__headerCell[data-sortable]{cursor:pointer;user-select:none}.seed-data-table__headerCell [data-chevron]{opacity:0;transition:opacity .12s}.seed-data-table__headerCell[data-sortable]:hover [data-chevron]{opacity:.6}.seed-data-table__headerCell[data-sorted] [data-chevron]{opacity:1;color:var(--seed-color-fg-neutral)}.seed-data-table__headerCell:hover [data-column-menu]{opacity:1}.seed-data-table__headerCell [data-column-menu]:focus-visible,.seed-data-table__headerCell [data-column-menu][data-open]{opacity:1}.seed-data-table__headerCell[data-dragging]{opacity:.5}.seed-data-table__headerCell[data-sticky]{position:sticky;z-index:3;background-color:var(--seed-color-bg-layer-default)}.seed-data-table__headerCell{outline:var(--seed-dimension-x0_5) solid transparent;outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell:is(:focus-visible,[data-focus-visible]){outline:var(--seed-dimension-x0_5) solid var(--seed-color-stroke-focus-ring);outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell .seed-data-table-resize-handle{position:absolute;top:0;inset-inline-end:calc(7px / -2);width:7px;height:100%;z-index:4;display:flex;align-items:center;justify-content:center;cursor:col-resize;touch-action:none}.seed-data-table__headerCell .seed-data-table-resize-handle-inner{width:3px;height:100%;border-radius:3px;background-color:#0298d9;opacity:0;transition:opacity .1s}.seed-data-table__headerCell .seed-data-table-resize-handle:hover .seed-data-table-resize-handle-inner,.seed-data-table__headerCell .seed-data-table-resize-handle[data-resizing] .seed-data-table-resize-handle-inner{opacity:1}.seed-data-table__headerCellInner{box-sizing:border-box;display:flex;align-items:center;width:100%;height:100%;min-width:0;gap:var(--seed-dimension-x1_5);padding-inline:var(--seed-dimension-x3)}.seed-data-table__headerCellLabel{min-width:0;flex:1 1 0%;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__headerCellIcon{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;justify-content:center;width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-subtle);border:none;background:0 0;padding:0;font:inherit;cursor:pointer}.seed-data-table__headerCellIcon[data-active]{color:var(--seed-color-fg-neutral)}.seed-data-table__headerCellIcon[data-column-menu]{opacity:0;transition:opacity .12s}.seed-data-table__row{position:relative;background-color:var(--seed-color-bg-layer-default);--seed-data-table-row-bg:var(--seed-color-bg-layer-default)}.seed-data-table__row[data-clickable]{cursor:pointer}.seed-data-table__row:is([aria-selected=true],[data-selected]){background-color:color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default));--seed-data-table-row-bg:color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default))}.seed-data-table__row:is([aria-selected=true],[data-selected]):is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default)));--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default)))}.seed-data-table__cell{box-sizing:border-box;position:relative;vertical-align:middle;text-align:start;height:var(--seed-dimension-x9);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-top:none;border-inline-start:none;padding-block:0;padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-regular);letter-spacing:-.14px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cell:not([data-editable]){user-select:none}.seed-data-table__cell:last-child{border-inline-end:none}tbody tr:last-child .seed-data-table__cell{border-bottom:none}.seed-data-table__cell[data-numeric]{text-align:end;font-variant-numeric:tabular-nums}.seed-data-table__cell[data-sticky]{position:sticky;z-index:1;background-color:var(--seed-data-table-row-bg)}.seed-data-table__cell[data-overlay-open]{overflow:visible;z-index:20}.seed-data-table__cell[data-editable]{cursor:default}.seed-data-table__cell .seed-data-table-edit-picker{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);min-width:0}.seed-data-table__cell .seed-data-table-edit-pending{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);animation:rotate .6s linear infinite}.seed-data-table__cell .seed-data-table-edit-error{position:absolute;top:100%;inset-inline-start:var(--seed-dimension-x3);inset-inline-end:var(--seed-dimension-x3);z-index:2;padding-top:2px;font-size:12px;line-height:1.3;color:var(--seed-color-fg-critical);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__entity{display:flex;align-items:center;min-width:0;gap:var(--seed-dimension-x1_5)}.seed-data-table__entityName{min-width:0;font-weight:var(--seed-font-weight-medium);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__tagList{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);overflow:hidden;min-width:0}.seed-data-table__entityActions{display:flex;align-items:center;gap:6px;margin-inline-start:auto;flex-shrink:0;opacity:0;transition:opacity .15s}tr:hover .seed-data-table__entityActions{opacity:1}.seed-data-table__entityActions:focus-within{opacity:1}.seed-data-table__entityAction{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin:0;padding:2px;border:none;border-radius:6px;background:0 0;font-family:inherit;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent);cursor:pointer;flex-shrink:0;transition:background-color .2s,color .2s,box-shadow .2s}.seed-data-table__entityAction:hover{background:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__link{color:var(--seed-color-fg-neutral);text-decoration:none;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__link:is(:hover,[data-hover]){text-decoration:underline}.seed-data-table__chip{box-sizing:border-box;display:inline-flex;align-items:center;flex-shrink:0;max-width:100%;height:22px;padding:0 4px;border-radius:7px;font-size:10px;font-weight:var(--seed-font-weight-regular);line-height:15px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__chipList{position:relative;display:flex;align-items:center;flex-wrap:nowrap;gap:3px;overflow:hidden;min-width:0;width:100%}.seed-data-table__chipOverflow{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;height:20px;padding:0 4px;border-radius:6px;background-color:#f6f7f7;color:#000;font-size:10px;font-weight:var(--seed-font-weight-regular);line-height:15px;white-space:nowrap}.seed-data-table__chipListEditor{display:flex;align-items:center;flex-wrap:wrap;gap:3px;min-width:0;width:100%}.seed-data-table__chipRemove{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:12px;height:12px;margin:0;margin-inline-start:2px;padding:0;border:none;border-radius:999px;background:0 0;color:inherit;opacity:.6;cursor:pointer;transition:opacity .12s,background-color .12s}.seed-data-table__chipRemove:hover{opacity:1;background-color:rgba(0,0,0,.08)}.seed-data-table__chipInput{flex:1 1 auto;min-width:40px;height:22px;margin:0;padding:0;border:none;outline:0;background:0 0;font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);letter-spacing:-.14px}.seed-data-table__expandOverlay{box-sizing:border-box;position:absolute;top:-1px;inset-inline-start:-1px;inset-inline-end:-1px;z-index:10;min-height:calc(100% + 2px);display:flex;align-items:flex-start;padding-inline:var(--seed-dimension-x3);padding-block:var(--seed-dimension-x3);background-color:var(--seed-color-bg-layer-floating);border-radius:4px;box-shadow:none;border:1px solid #266df0;color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);letter-spacing:-.14px;cursor:default}.seed-data-table__expandOverlayText{width:100%;min-width:0;white-space:normal;word-break:break-word}.seed-data-table__cellSuffixRow{display:flex;align-items:center;gap:4px;min-width:0;width:100%}.seed-data-table__cellSuffixText{flex:1 1 0%;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cellSuffixIcon{flex-shrink:0;display:inline-flex;align-items:center;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent)}.seed-data-table__toolbar{box-sizing:border-box;display:flex;align-items:center;width:100%;flex-shrink:0;min-height:49px;gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x3);border-top:1px solid var(--seed-color-stroke-neutral-muted)}.seed-data-table__search{position:relative;display:inline-flex;align-items:center;flex:1 1 0%;max-width:280px}.seed-data-table__search svg{position:absolute;inset-inline-start:var(--seed-dimension-x2);width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-muted);pointer-events:none}.seed-data-table__search input{width:100%;height:var(--seed-dimension-x8);padding-inline-start:var(--seed-dimension-x7);padding-inline-end:var(--seed-dimension-x2);border:none;border-radius:var(--seed-radius-r2);background-color:var(--seed-color-bg-layer-default);box-shadow:inset 0 0 0 1px var(--seed-color-stroke-neutral-weak);font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);outline:0;transition:box-shadow .1s var(--seed-timing-function-easing)}.seed-data-table__search input::placeholder{color:var(--seed-color-fg-neutral-subtle)}.seed-data-table__search input:focus{box-shadow:inset 0 0 0 2px var(--seed-color-stroke-neutral-contrast)}.seed-data-table__count{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t3);font-variant-numeric:tabular-nums}.seed-data-table__toolbarEnd{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;gap:var(--seed-dimension-x2)}.seed-data-table__scrollArea{position:relative;display:flex;flex-direction:column;width:100%;min-width:0;flex:1 1 auto;min-height:0;overflow-x:auto;overflow-y:auto}.seed-data-table__scrollArea[data-scrolled-start] [data-sticky-edge]{clip-path:inset(0 -38px 0 0);box-shadow:6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__scrollArea[data-scrolled-end] [data-sticky-edge-end]{clip-path:inset(0 0 0 -38px);box-shadow:-6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__footerCell{box-sizing:border-box;position:sticky;bottom:0;z-index:2;vertical-align:middle;padding:0;height:var(--seed-dimension-x9);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none}.seed-data-table__footerCell:last-child{border-inline-end:none}.seed-data-table__footerCell .seed-data-table-column-calc-trigger{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);width:100%;height:100%;min-width:0;margin:0;padding:0 var(--seed-dimension-x3);border:none;background:0 0;color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);font-weight:var(--seed-font-weight-medium);white-space:nowrap;overflow:clip;cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__footerCell .seed-data-table-column-calc-trigger:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__footerCell .seed-data-table-column-calc-trigger[data-align=right]{justify-content:flex-end}.seed-data-table__footerCell .seed-data-table-column-calc-trigger svg{flex-shrink:0;opacity:.7}.seed-data-table__footerCell .seed-data-table-column-calc-placeholder{overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-value{min-width:0;flex-shrink:1;color:var(--seed-color-fg-neutral);font-variant-numeric:tabular-nums;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-op{min-width:0;flex-shrink:1;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__emptyState{box-sizing:border-box;text-align:center;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t4)}.seed-data-table__loadingState{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:10px;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium)}.seed-data-table__loadingState[data-inline]{padding-block:0;padding-inline:0;height:var(--seed-dimension-x9)}.seed-data-table__loadingState .seed-data-table-loading-spinner{flex-shrink:0;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);animation:rotate .6s linear infinite}.seed-data-table__addRowCell{box-sizing:border-box;padding:0;border-top:none}.seed-data-table__addRow{box-sizing:border-box;display:flex;align-items:center;width:100%;margin:0;border:none;border-top:1px solid var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);height:var(--seed-dimension-x9);gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x2);color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__addRow:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,var(--seed-color-bg-layer-default));color:var(--seed-color-fg-neutral)}.seed-data-table__addRow svg{flex-shrink:0}.seed-data-table__selectionCell{text-align:center;padding:0}.seed-data-table__root--surface_card{border-color:var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);border-radius:var(--seed-radius-r3)}.seed-data-table__root--surface_bleed{border-color:var(--seed-color-bg-transparent);background-color:var(--seed-color-bg-transparent)}.seed-data-table__row--interactive_true{transition:background-color .12s}.seed-data-table__row--interactive_true:is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,transparent);--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, var(--seed-color-bg-layer-default))}");
3929
+ __insertCSS(".seed-data-table__root{box-sizing:border-box;position:relative;display:flex;flex-direction:column;width:100%;border-width:1px;border-style:solid;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}.seed-data-table__table{width:100%;border-collapse:separate;border-spacing:0;table-layout:fixed}.seed-data-table__headerCell{box-sizing:border-box;position:sticky;top:0;z-index:2;vertical-align:middle;text-align:start;padding:0;height:var(--seed-dimension-x10);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none;color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium);letter-spacing:-.14px}.seed-data-table__headerCell:last-child{border-inline-end:none}.seed-data-table__headerCell[data-sortable]{cursor:pointer;user-select:none}.seed-data-table__headerCell [data-chevron]{opacity:0;transition:opacity .12s}.seed-data-table__headerCell[data-sortable]:hover [data-chevron]{opacity:.6}.seed-data-table__headerCell[data-sorted] [data-chevron]{opacity:1;color:var(--seed-color-fg-neutral)}.seed-data-table__headerCell:hover [data-column-menu]{opacity:1}.seed-data-table__headerCell [data-column-menu]:focus-visible,.seed-data-table__headerCell [data-column-menu][data-open]{opacity:1}.seed-data-table__headerCell[data-dragging]{opacity:.5}.seed-data-table__headerCell[data-sticky]{position:sticky;z-index:3;background-color:var(--seed-color-bg-layer-default)}.seed-data-table__headerCell{outline:var(--seed-dimension-x0_5) solid transparent;outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell:is(:focus-visible,[data-focus-visible]){outline:var(--seed-dimension-x0_5) solid var(--seed-color-stroke-focus-ring);outline-offset:calc(var(--seed-dimension-x0_5) * -1)}.seed-data-table__headerCell .seed-data-table-resize-handle{position:absolute;top:0;inset-inline-end:calc(7px / -2);width:7px;height:100%;z-index:4;display:flex;align-items:center;justify-content:center;cursor:col-resize;touch-action:none}.seed-data-table__headerCell .seed-data-table-resize-handle-inner{width:3px;height:100%;border-radius:3px;background-color:#0298d9;opacity:0;transition:opacity .1s}.seed-data-table__headerCell .seed-data-table-resize-handle:hover .seed-data-table-resize-handle-inner,.seed-data-table__headerCell .seed-data-table-resize-handle[data-resizing] .seed-data-table-resize-handle-inner{opacity:1}.seed-data-table__headerCellInner{box-sizing:border-box;display:flex;align-items:center;width:100%;height:100%;min-width:0;gap:var(--seed-dimension-x1_5);padding-inline:var(--seed-dimension-x3)}.seed-data-table__headerCellLabel{min-width:0;flex:1 1 0%;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__headerCellIcon{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;justify-content:center;width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-subtle);border:none;background:0 0;padding:0;font:inherit;cursor:pointer}.seed-data-table__headerCellIcon[data-active]{color:var(--seed-color-fg-neutral)}.seed-data-table__headerCellIcon[data-column-menu]{opacity:0;transition:opacity .12s}.seed-data-table__row{position:relative;background-color:var(--seed-color-bg-layer-default);--seed-data-table-row-bg:var(--seed-color-bg-layer-default)}.seed-data-table__row[data-clickable]{cursor:pointer}.seed-data-table__row:is([aria-selected=true],[data-selected]){background-color:color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default));--seed-data-table-row-bg:color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default))}.seed-data-table__row:is([aria-selected=true],[data-selected]):is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,color-mix(in srgb,#0298d9 10%,var(--seed-color-bg-layer-default)));--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, color-mix(in srgb, #0298d9 10%, var(--seed-color-bg-layer-default)))}.seed-data-table__cell{box-sizing:border-box;position:relative;vertical-align:middle;text-align:start;height:var(--seed-dimension-x9);border-bottom:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-top:none;border-inline-start:none;padding-block:0;padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-regular);letter-spacing:-.14px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cell:not([data-editable]){user-select:none}.seed-data-table__cell:last-child{border-inline-end:none}tbody tr:last-child .seed-data-table__cell{border-bottom:none}.seed-data-table__cell[data-numeric]{text-align:end;font-variant-numeric:tabular-nums}.seed-data-table__cell[data-sticky]{position:sticky;z-index:1;background-color:var(--seed-data-table-row-bg)}.seed-data-table__cell[data-overlay-open]{overflow:visible;z-index:20}.seed-data-table__cell[data-editable]{cursor:default}.seed-data-table__cell .seed-data-table-edit-picker{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);min-width:0}.seed-data-table__cell .seed-data-table-edit-pending{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);animation:rotate .6s linear infinite}.seed-data-table__cell .seed-data-table-edit-error{position:absolute;top:100%;inset-inline-start:var(--seed-dimension-x3);inset-inline-end:var(--seed-dimension-x3);z-index:2;padding-top:2px;font-size:12px;line-height:1.3;color:var(--seed-color-fg-critical);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__entity{display:flex;align-items:center;min-width:0;gap:var(--seed-dimension-x1_5)}.seed-data-table__entityName{min-width:0;font-weight:var(--seed-font-weight-medium);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__tagList{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);overflow:hidden;min-width:0}.seed-data-table__entityActions{display:flex;align-items:center;gap:6px;margin-inline-start:auto;flex-shrink:0;opacity:0;transition:opacity .15s}tr:hover .seed-data-table__entityActions{opacity:1}.seed-data-table__entityActions:focus-within{opacity:1}.seed-data-table__entityAction{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin:0;padding:2px;border:none;border-radius:6px;background:0 0;font-family:inherit;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent);cursor:pointer;flex-shrink:0;transition:background-color .2s,color .2s,box-shadow .2s}.seed-data-table__entityAction:hover{background:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__link{color:var(--seed-color-fg-neutral);text-decoration:none;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__link:is(:hover,[data-hover]){text-decoration:underline}.seed-data-table__chip{box-sizing:border-box;display:inline-flex;align-items:center;flex-shrink:0;max-width:100%;height:22px;padding:0 4px;border-radius:7px;font-size:10px;font-weight:var(--seed-font-weight-regular);line-height:15px;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__chipList{position:relative;display:flex;align-items:center;flex-wrap:nowrap;gap:3px;overflow:hidden;min-width:0;width:100%}.seed-data-table__chipOverflow{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;height:20px;padding:0 4px;border-radius:6px;background-color:#f6f7f7;color:#000;font-size:10px;font-weight:var(--seed-font-weight-regular);line-height:15px;white-space:nowrap}.seed-data-table__chipListEditor{display:flex;align-items:center;flex-wrap:wrap;gap:3px;min-width:0;width:100%}.seed-data-table__chipRemove{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:12px;height:12px;margin:0;margin-inline-start:2px;padding:0;border:none;border-radius:999px;background:0 0;color:inherit;opacity:.6;cursor:pointer;transition:opacity .12s,background-color .12s}.seed-data-table__chipRemove:hover{opacity:1;background-color:rgba(0,0,0,.08)}.seed-data-table__chipInput{flex:1 1 auto;min-width:40px;height:22px;margin:0;padding:0;border:none;outline:0;background:0 0;font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);letter-spacing:-.14px}.seed-data-table__expandOverlay{box-sizing:border-box;position:absolute;top:-1px;inset-inline-start:-1px;inset-inline-end:-1px;z-index:10;min-height:calc(100% + 2px);display:flex;align-items:flex-start;padding-inline:var(--seed-dimension-x3);padding-block:var(--seed-dimension-x3);background-color:var(--seed-color-bg-layer-floating);border-radius:4px;box-shadow:none;border:1px solid #266df0;color:var(--seed-color-fg-neutral);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);letter-spacing:-.14px;cursor:default}.seed-data-table__expandOverlayText{width:100%;min-width:0;white-space:normal;word-break:break-word}.seed-data-table__cellSuffixRow{display:flex;align-items:center;gap:4px;min-width:0;width:100%}.seed-data-table__cellSuffixText{flex:1 1 0%;min-width:0;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__cellSuffixIcon{flex-shrink:0;display:inline-flex;align-items:center;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent)}.seed-data-table__toolbar{box-sizing:border-box;display:flex;align-items:center;width:100%;flex-shrink:0;min-height:49px;gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x3);border-top:1px solid var(--seed-color-stroke-neutral-muted)}.seed-data-table__search{position:relative;display:inline-flex;align-items:center;flex:1 1 0%;max-width:280px}.seed-data-table__search svg{position:absolute;inset-inline-start:var(--seed-dimension-x2);width:var(--seed-dimension-x4);height:var(--seed-dimension-x4);color:var(--seed-color-fg-neutral-muted);pointer-events:none}.seed-data-table__search input{width:100%;height:var(--seed-dimension-x8);padding-inline-start:var(--seed-dimension-x7);padding-inline-end:var(--seed-dimension-x2);border:none;border-radius:var(--seed-radius-r2);background-color:var(--seed-color-bg-layer-default);box-shadow:inset 0 0 0 1px var(--seed-color-stroke-neutral-weak);font-family:inherit;font-size:var(--seed-font-size-t4);color:var(--seed-color-fg-neutral);outline:0;transition:box-shadow .1s var(--seed-timing-function-easing)}.seed-data-table__search input::placeholder{color:var(--seed-color-fg-neutral-subtle)}.seed-data-table__search input:focus{box-shadow:inset 0 0 0 2px var(--seed-color-stroke-neutral-contrast)}.seed-data-table__count{flex-shrink:0;color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t3);font-variant-numeric:tabular-nums}.seed-data-table__toolbarEnd{flex-shrink:0;margin-inline-start:auto;display:inline-flex;align-items:center;gap:var(--seed-dimension-x2)}.seed-data-table__scrollArea{position:relative;display:flex;flex-direction:column;width:100%;min-width:0;flex:1 1 auto;min-height:0;overflow-x:auto;overflow-y:auto}.seed-data-table__scrollArea[data-scrolled-start] [data-sticky-edge]{clip-path:inset(0 -38px 0 0);box-shadow:6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__scrollArea[data-scrolled-end] [data-sticky-edge-end]{clip-path:inset(0 0 0 -38px);box-shadow:-6px 0 16px 4px rgba(0,0,0,.12)}.seed-data-table__footerCell{box-sizing:border-box;position:sticky;bottom:0;z-index:2;vertical-align:middle;padding:0;height:var(--seed-dimension-x9);background-color:var(--seed-color-bg-layer-default);border-top:1px solid var(--seed-color-stroke-neutral-muted);border-inline-end:1px solid var(--seed-color-stroke-neutral-muted);border-inline-start:none}.seed-data-table__footerCell:last-child{border-inline-end:none}.seed-data-table__footerCell .seed-data-table-column-calc-trigger{display:flex;align-items:center;gap:var(--seed-dimension-x1_5);width:100%;height:100%;min-width:0;margin:0;padding:0 var(--seed-dimension-x3);border:none;background:0 0;color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);font-weight:var(--seed-font-weight-medium);white-space:nowrap;overflow:clip;cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__footerCell .seed-data-table-column-calc-trigger:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__footerCell .seed-data-table-column-calc-trigger[data-align=right]{justify-content:flex-end}.seed-data-table__footerCell .seed-data-table-column-calc-trigger svg{flex-shrink:0;opacity:.7}.seed-data-table__footerCell .seed-data-table-column-calc-placeholder{overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-value{min-width:0;flex-shrink:1;color:var(--seed-color-fg-neutral);font-variant-numeric:tabular-nums;overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__footerCell .seed-data-table-column-calc-op{min-width:0;flex-shrink:1;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);overflow:clip;text-overflow:ellipsis;white-space:nowrap}.seed-data-table__emptyState{box-sizing:border-box;text-align:center;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:var(--seed-color-fg-neutral-subtle);font-size:var(--seed-font-size-t4)}.seed-data-table__loadingState{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:10px;padding-block:var(--seed-dimension-x8);padding-inline:var(--seed-dimension-x3);color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);font-size:var(--seed-font-size-t4);line-height:var(--seed-line-height-t4);font-weight:var(--seed-font-weight-medium)}.seed-data-table__loadingState[data-inline]{padding-block:0;padding-inline:0;height:var(--seed-dimension-x9)}.seed-data-table__loadingState .seed-data-table-loading-spinner{flex-shrink:0;color:color-mix(in srgb,var(--seed-color-fg-neutral) 63%,transparent);animation:rotate .6s linear infinite}.seed-data-table__addRowCell{box-sizing:border-box;padding:0;border-top:none}.seed-data-table__addRow{box-sizing:border-box;display:flex;align-items:center;width:100%;margin:0;border:none;border-top:1px solid var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);height:var(--seed-dimension-x9);gap:var(--seed-dimension-x2);padding-inline:var(--seed-dimension-x2);color:var(--seed-color-fg-neutral-subtle);font-family:inherit;font-size:var(--seed-font-size-t4);cursor:pointer;transition:background-color .12s,color .12s}.seed-data-table__addRow:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 4%,var(--seed-color-bg-layer-default));color:var(--seed-color-fg-neutral)}.seed-data-table__addRow svg{flex-shrink:0}.seed-data-table__selectionCell{text-align:center;padding:0}.seed-data-table__bulkBar{box-sizing:border-box;position:absolute;inset-inline-start:50%;transform:translateX(-50%);bottom:var(--seed-dimension-x4);z-index:30;display:flex;align-items:center;gap:var(--seed-dimension-x3);min-height:36px;padding-inline:var(--seed-dimension-x3_5);padding-block:var(--seed-dimension-x2);border-radius:var(--seed-radius-full);box-shadow:var(--seed-shadow-s3);background-color:var(--seed-color-bg-layer-floating);color:var(--seed-color-fg-neutral);font-family:inherit;font-size:var(--seed-font-size-t4);font-weight:var(--seed-font-weight-medium);white-space:nowrap}.seed-data-table__bulkBarCount{flex-shrink:0;color:var(--seed-color-fg-neutral);font-variant-numeric:tabular-nums}.seed-data-table__bulkBarClear{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:20px;height:20px;margin:0;padding:2px;border:none;border-radius:6px;background:0 0;font-family:inherit;color:color-mix(in srgb,var(--seed-color-fg-neutral) 55%,transparent);cursor:pointer;transition:background-color .2s,color .2s}.seed-data-table__bulkBarClear:hover{background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 8%,transparent);color:var(--seed-color-fg-neutral)}.seed-data-table__bulkBarActions{display:flex;align-items:center;flex-shrink:0;gap:var(--seed-dimension-x3);padding-inline-start:var(--seed-dimension-x3);border-inline-start:1px solid color-mix(in srgb,var(--seed-color-fg-neutral) 16%,transparent)}.seed-data-table__root--surface_card{border-color:var(--seed-color-stroke-neutral-muted);background-color:var(--seed-color-bg-layer-default);border-radius:var(--seed-radius-r3)}.seed-data-table__root--surface_bleed{border-color:var(--seed-color-bg-transparent);background-color:var(--seed-color-bg-transparent)}.seed-data-table__row--interactive_true{transition:background-color .12s}.seed-data-table__row--interactive_true:is(:hover,[data-hover]){background-color:color-mix(in srgb,var(--seed-color-fg-neutral) 3%,transparent);--seed-data-table-row-bg:color-mix(in srgb, var(--seed-color-fg-neutral) 3%, var(--seed-color-bg-layer-default))}");
3584
3930
 
3585
3931
  const createClassName = (className, variants, compoundVariants = [])=>{
3586
3932
  const variantKeys = Object.keys(variants);
@@ -3774,6 +4120,22 @@ const dataTableSlotNames = [
3774
4120
  [
3775
4121
  "selectionCell",
3776
4122
  "seed-data-table__selectionCell"
4123
+ ],
4124
+ [
4125
+ "bulkBar",
4126
+ "seed-data-table__bulkBar"
4127
+ ],
4128
+ [
4129
+ "bulkBarCount",
4130
+ "seed-data-table__bulkBarCount"
4131
+ ],
4132
+ [
4133
+ "bulkBarClear",
4134
+ "seed-data-table__bulkBarClear"
4135
+ ],
4136
+ [
4137
+ "bulkBarActions",
4138
+ "seed-data-table__bulkBarActions"
3777
4139
  ]
3778
4140
  ];
3779
4141
  const defaultVariant$6 = {
@@ -3811,7 +4173,7 @@ Object.assign(dataTable, {
3811
4173
  // other file (`DataTable.tsx`, `column-menu.tsx`, `resize.tsx`, `column-calc.tsx`,
3812
4174
  // `toolbar.tsx`, `body-cells.tsx`, `editors.tsx`) imports its exports from here rather
3813
4175
  // than re-deriving its own.
3814
- const { ClassNamesProvider: ClassNamesProvider$1, withContext: withContext$2, useClassNames: useClassNames$1 } = createSlotRecipeContext(dataTable);
4176
+ const { ClassNamesProvider, withContext: withContext$1, useClassNames } = createSlotRecipeContext(dataTable);
3815
4177
 
3816
4178
  /**
3817
4179
  * The pure filter itself, split out of `useSelectSearch` below so it can be
@@ -4519,7 +4881,7 @@ function isCellTruncated(cell) {
4519
4881
  * actually reported against a pinned column. */ function isChipListEditable(column) {
4520
4882
  return !!column.editable && column.type === "chip-list";
4521
4883
  }
4522
- function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast }) {
4884
+ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast, cellContextMenu, onViewEditHistory, cellContextMenuItems }) {
4523
4885
  const effective = useEffectiveColumns(columns, {
4524
4886
  selectable,
4525
4887
  stickyFirst
@@ -4534,7 +4896,10 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4534
4896
  row: row,
4535
4897
  classNames: classNames,
4536
4898
  locale: locale,
4537
- sticky: sticky
4899
+ sticky: sticky,
4900
+ cellContextMenu: cellContextMenu,
4901
+ onViewEditHistory: onViewEditHistory,
4902
+ cellContextMenuItems: cellContextMenuItems
4538
4903
  }, column.key);
4539
4904
  })
4540
4905
  });
@@ -4546,7 +4911,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4546
4911
  * on every row's render. React keeps this instance stable across re-renders because
4547
4912
  * `ColumnBodyCells` keys it by `column.key` the same way it always keyed the bare
4548
4913
  * `<DataTableCell>` before this split.
4549
- */ function DataTableBodyCell({ column, row, classNames, locale, sticky }) {
4914
+ */ function DataTableBodyCell({ column, row, classNames, locale, sticky, cellContextMenu, onViewEditHistory, cellContextMenuItems }) {
4550
4915
  const { startEdit, cancelEdit, isEditing, isPending, commitEdit, isPeeking, openPeek, closePeek } = useDataTableContext();
4551
4916
  const cellRef = React__namespace.useRef(null);
4552
4917
  // Feature B rework (#8) -- the chip-list popover's own live position, in VIEWPORT
@@ -4562,6 +4927,16 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4562
4927
  // editable cell's existing double-click-to-edit (tranche 5, unchanged) must keep
4563
4928
  // rendering exactly as it always has, not suddenly gain overlay chrome.
4564
4929
  const [overlayOpen, setOverlayOpen] = React__namespace.useState(false);
4930
+ // Attio parity (attio-column-features-full.md gap #1) -- the right-click menu's own
4931
+ // point + open state. `contextMenuPoint` is frozen at the moment of the right-click
4932
+ // (never re-read live), same reasoning as `overlayOpen` above: the menu shouldn't
4933
+ // chase the cursor after it opens. `<CellContextMenu>` itself is only rendered below
4934
+ // while `contextMenuOpen` is true (not permanently mounted per cell) -- unlike
4935
+ // `ColumnMenu` (one per COLUMN, header row only), this component runs once per
4936
+ // row×column cell, and a floating-ui instance genuinely idle in every one of
4937
+ // thousands of cells would be real, avoidable overhead.
4938
+ const [contextMenuPoint, setContextMenuPoint] = React__namespace.useState(null);
4939
+ const [contextMenuOpen, setContextMenuOpen] = React__namespace.useState(false);
4565
4940
  const alwaysLive = isAlwaysLiveEditable(column);
4566
4941
  const chipListEditable = isChipListEditable(column);
4567
4942
  // A gated `editable` column (everything except the always-live widgets AND the
@@ -4613,6 +4988,19 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4613
4988
  setOverlayOpen(isCellTruncated(event.currentTarget));
4614
4989
  startEdit(row.id, column.key);
4615
4990
  }
4991
+ function handleContextMenu(event) {
4992
+ if (!cellContextMenu) return;
4993
+ // Replaces the browser's own native context menu (Attio's own gesture) -- and
4994
+ // must not also reach the row's own `onRowClick`, same reasoning as every other
4995
+ // cell-local click handler in this file.
4996
+ event.preventDefault();
4997
+ event.stopPropagation();
4998
+ setContextMenuPoint({
4999
+ x: event.clientX,
5000
+ y: event.clientY
5001
+ });
5002
+ setContextMenuOpen(true);
5003
+ }
4616
5004
  function handleClick(event) {
4617
5005
  if (chipListEditable) {
4618
5006
  // Attio's own chip-list gesture: click opens the editor regardless of
@@ -4716,7 +5104,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4716
5104
  const pending = isPending(row.id, column.key);
4717
5105
  return /*#__PURE__*/ jsxRuntime.jsxs(jsxRuntime.Fragment, {
4718
5106
  children: [
4719
- /*#__PURE__*/ jsxRuntime.jsx(DataTableCell, {
5107
+ /*#__PURE__*/ jsxRuntime.jsxs(DataTableCell, {
4720
5108
  ref: cellRef,
4721
5109
  "data-numeric": domUtils.dataAttr(column.type === "number" || column.type === "currency"),
4722
5110
  "data-editable": domUtils.dataAttr(!!column.editable),
@@ -4727,14 +5115,34 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4727
5115
  style: sticky.style,
4728
5116
  onClick: handleClick,
4729
5117
  onDoubleClick: handleDoubleClick,
4730
- children: /*#__PURE__*/ jsxRuntime.jsx(CellContent, {
4731
- column: column,
4732
- row: row,
4733
- rowId: row.id,
4734
- classNames: classNames,
4735
- locale: locale,
4736
- overlayActive: overlayActive
4737
- })
5118
+ onContextMenu: handleContextMenu,
5119
+ children: [
5120
+ /*#__PURE__*/ jsxRuntime.jsx(CellContent, {
5121
+ column: column,
5122
+ row: row,
5123
+ rowId: row.id,
5124
+ classNames: classNames,
5125
+ locale: locale,
5126
+ overlayActive: overlayActive
5127
+ }),
5128
+ cellContextMenu && contextMenuOpen && contextMenuPoint && // A `<td>` (unlike its own parent `<tr>`) can validly hold any flow
5129
+ // content, so `CellContextMenu`'s own 0x0 `MenuAnchor` -- itself never
5130
+ // portaled, only `MenuPositioner`'s actual dropdown content is (via
5131
+ // floating-ui's own `FloatingPortal`, straight to `document.body`) --
5132
+ // renders here, INSIDE this cell, rather than as a sibling of it.
5133
+ /*#__PURE__*/ jsxRuntime.jsx(CellContextMenu, {
5134
+ column: column,
5135
+ row: row,
5136
+ value: rawValue,
5137
+ locale: locale,
5138
+ point: contextMenuPoint,
5139
+ open: contextMenuOpen,
5140
+ onOpenChange: setContextMenuOpen,
5141
+ onCommit: (value)=>commitEdit(row.id, column.key, value),
5142
+ onViewEditHistory: onViewEditHistory,
5143
+ cellContextMenuItems: cellContextMenuItems
5144
+ })
5145
+ ]
4738
5146
  }),
4739
5147
  chipListEditorOpen && popoverStyle && /*#__PURE__*/ reactDom.createPortal(// biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- same shape as `CellExpandOverlay` (editors.tsx), which this box is a portaled variant of: it never responds to interaction on ITSELF, only stops propagation of clicks that already occurred on a real interactive descendant (the editor's input/remove buttons). No role fits it because it isn't a control.
4740
5148
  /*#__PURE__*/ jsxRuntime.jsx("div", {
@@ -4964,64 +5372,52 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
4964
5372
  }
4965
5373
  }
4966
5374
 
4967
- const { ClassNamesProvider, withContext: withContext$1, useClassNames } = createSlotRecipeContext(menu.menu);
4968
- const { PropsProvider: ItemPropsProvider, useProps: useItemProps, withContext: withItemContext, ClassNamesProvider: ItemClassNamesProvider } = createSlotRecipeContext(menuItem.menuItem);
4969
- const withStateProps = createWithStateProps([
4970
- reactMenu.useMenuContext
4971
- ]);
4972
- const withItemStateProps = createWithStateProps([
4973
- reactMenu.useMenuItemContext
4974
- ]);
4975
- const MenuRoot = (props)=>{
4976
- const [variantProps, otherProps] = menu.menu.splitVariantProps(props);
4977
- const classNames = menu.menu(variantProps);
4978
- return /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider, {
4979
- value: classNames,
4980
- children: /*#__PURE__*/ jsxRuntime.jsx(ItemPropsProvider, {
4981
- value: {
4982
- size: variantProps.size
4983
- },
4984
- children: /*#__PURE__*/ jsxRuntime.jsx(reactMenu.Menu.Root, {
4985
- ...otherProps
5375
+ const DataTableBulkBarRoot = withContext$1(Primitive.div, "bulkBar");
5376
+ const DataTableBulkBarCount = withContext$1(Primitive.span, "bulkBarCount");
5377
+ const DataTableBulkBarClear = withContext$1(Primitive.button, "bulkBarClear");
5378
+ /** Wraps whatever the CONSUMER passes as this bar's own action buttons -- the bar
5379
+ * itself only ever supplies chrome (count + clear); every actual action (Add to
5380
+ * list, Send email, …) is the consumer's own `<ActionButton>`/`<Menu>`, the same
5381
+ * "chrome vs. consumer content" split `toolbarActions`/`toolbarEnd` already draw for
5382
+ * the toolbar's own custom actions. */ const DataTableBulkBarActions = withContext$1(Primitive.div, "bulkBarActions");
5383
+ /**
5384
+ * The floating bulk-action pill (gap #4) -- renders nothing while no row is selected
5385
+ * (reads `selectedIds` off the table-wide context itself, so a consumer doesn't have
5386
+ * to gate this on `selectable`/`selectedIds` on their own). Composed as the LAST
5387
+ * child of the table's own root (`DataTable.tsx`), which is `position: relative` for
5388
+ * exactly this reason -- see `data-table.ts`'s own `bulkBar` slot comment for why its
5389
+ * geometry borrows `contextual-floating-button`'s already-measured vars rather than a
5390
+ * guessed literal.
5391
+ */ function DataTableBulkBar({ clearLabel = "Clear selection", children }) {
5392
+ const { selectedIds, clearSelection } = useDataTableContext();
5393
+ if (selectedIds.length === 0) return null;
5394
+ return /*#__PURE__*/ jsxRuntime.jsxs(DataTableBulkBarRoot, {
5395
+ role: "toolbar",
5396
+ "aria-label": "Bulk actions",
5397
+ "aria-orientation": "horizontal",
5398
+ children: [
5399
+ /*#__PURE__*/ jsxRuntime.jsxs(DataTableBulkBarCount, {
5400
+ "aria-live": "polite",
5401
+ children: [
5402
+ selectedIds.length,
5403
+ " selected"
5404
+ ]
5405
+ }),
5406
+ /*#__PURE__*/ jsxRuntime.jsx(DataTableBulkBarClear, {
5407
+ type: "button",
5408
+ "aria-label": clearLabel,
5409
+ onClick: clearSelection,
5410
+ children: /*#__PURE__*/ jsxRuntime.jsx(IconXmarkLine_default, {
5411
+ size: 12,
5412
+ "aria-hidden": "true"
5413
+ })
5414
+ }),
5415
+ children && /*#__PURE__*/ jsxRuntime.jsx(DataTableBulkBarActions, {
5416
+ children: children(selectedIds)
4986
5417
  })
4987
- })
4988
- });
4989
- };
4990
- reactMenu.Menu.Anchor;
4991
- const MenuTrigger = reactMenu.Menu.Trigger;
4992
- const MenuPositioner = /*#__PURE__*/ React__namespace.forwardRef(({ className, ...props }, ref)=>{
4993
- const classNames = useClassNames();
4994
- return /*#__PURE__*/ jsxRuntime.jsx(reactMenu.Menu.Positioner, {
4995
- ref: ref,
4996
- className: clsx(classNames.positioner, className),
4997
- ...props
4998
- });
4999
- });
5000
- MenuPositioner.displayName = "MenuPositioner";
5001
- const MenuContent = withContext$1(reactMenu.Menu.Content, "content");
5002
- const MenuScrollArea = withContext$1(withStateProps(Primitive.div), "scrollArea");
5003
- withContext$1(reactMenu.Menu.Group, "group");
5004
- withContext$1(reactMenu.Menu.GroupLabel, "groupLabel");
5005
- const MenuItem = /*#__PURE__*/ React__namespace.forwardRef(({ className: propClassName, ...props }, ref)=>{
5006
- const [variantProps, otherProps] = menuItem.menuItem.splitVariantProps(props);
5007
- const parentProps = useItemProps();
5008
- const classNames = menuItem.menuItem({
5009
- ...parentProps,
5010
- ...variantProps
5011
- });
5012
- return /*#__PURE__*/ jsxRuntime.jsx(ItemClassNamesProvider, {
5013
- value: classNames,
5014
- children: /*#__PURE__*/ jsxRuntime.jsx(reactMenu.Menu.Item, {
5015
- ref: ref,
5016
- className: clsx(classNames.root, propClassName),
5017
- ...otherProps
5018
- })
5418
+ ]
5019
5419
  });
5020
- });
5021
- MenuItem.displayName = "MenuItem";
5022
- withItemContext(withItemStateProps(Primitive.div), "body");
5023
- const MenuItemLabel = withItemContext(withItemStateProps(Primitive.span), "label");
5024
- withItemContext(withItemStateProps(Primitive.span), "description");
5420
+ }
5025
5421
 
5026
5422
  function ColumnCalcCells({ columns, rows, selectable, stickyFirst, stickyLast }) {
5027
5423
  const effective = useEffectiveColumns(columns, {
@@ -5048,10 +5444,11 @@ function ColumnCalcCells({ columns, rows, selectable, stickyFirst, stickyLast })
5048
5444
  })
5049
5445
  });
5050
5446
  }
5051
- function formatCalcValue(value) {
5052
- return value.toLocaleString(undefined, {
5447
+ function formatCalcValue(value, op) {
5448
+ const formatted = value.toLocaleString(undefined, {
5053
5449
  maximumFractionDigits: 2
5054
5450
  });
5451
+ return op === "percentEmpty" || op === "percentFilled" ? `${formatted}%` : formatted;
5055
5452
  }
5056
5453
  /**
5057
5454
  * One ColumnCalc footer cell -- a click-anywhere-in-the-cell trigger (crisp's own
@@ -5086,7 +5483,7 @@ function formatCalcValue(value) {
5086
5483
  children: [
5087
5484
  /*#__PURE__*/ jsxRuntime.jsx("span", {
5088
5485
  className: "seed-data-table-column-calc-value",
5089
- children: formatCalcValue(result)
5486
+ children: formatCalcValue(result, op)
5090
5487
  }),
5091
5488
  /*#__PURE__*/ jsxRuntime.jsx("span", {
5092
5489
  className: "seed-data-table-column-calc-op",
@@ -5107,21 +5504,13 @@ function formatCalcValue(value) {
5107
5504
  style: {
5108
5505
  minWidth: 160
5109
5506
  },
5110
- children: /*#__PURE__*/ jsxRuntime.jsxs(MenuScrollArea, {
5111
- children: [
5112
- ops.map((candidate)=>/*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
5113
- onClick: ()=>setColumnCalcOp(column.key, candidate),
5114
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
5115
- children: DATA_TABLE_CALC_LABEL[candidate]
5116
- })
5117
- }, candidate)),
5118
- op && /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
5119
- onClick: ()=>setColumnCalcOp(column.key, undefined),
5507
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuScrollArea, {
5508
+ children: ops.map((candidate)=>/*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
5509
+ onClick: ()=>setColumnCalcOp(column.key, candidate),
5120
5510
  children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
5121
- children: "Clear"
5511
+ children: DATA_TABLE_CALC_LABEL[candidate]
5122
5512
  })
5123
- })
5124
- ]
5513
+ }, candidate))
5125
5514
  })
5126
5515
  })
5127
5516
  })
@@ -5400,9 +5789,9 @@ function ColumnRenameInput({ column, initialLabel, onDone }) {
5400
5789
  const columnLabel = typeof label === "string" ? label : column.key;
5401
5790
  const pinned = isColumnPinned(column.key);
5402
5791
  // Attio/GRC's "Remove sort" (live-review fix D) -- the header's own click-to-sort
5403
- // (`SortIndicator`/`toggleSort`, above) only cycles asc/desc once a column has been
5404
- // clicked; this is the only affordance back to no sort at all, so it only makes
5405
- // sense to show on the column that IS currently sorted.
5792
+ // (`SortIndicator`/`toggleSort`, above) already cycles asc -> desc -> none on its
5793
+ // own third click, but this stays as a one-click shortcut to the same place, so it
5794
+ // only makes sense to show on the column that IS currently sorted.
5406
5795
  const isSorted = sort?.key === column.key;
5407
5796
  const stopPropagation = (event)=>event.stopPropagation();
5408
5797
  return(// Live-review fix (D): a `MenuItem` inside `MenuPositioner` renders through a
@@ -10386,6 +10775,18 @@ ContinuousDatePicker.displayName = "ContinuousDatePicker";
10386
10775
  // value list) down to `is`/`is not`/`empty`/`not empty`. `"isanyof"` is appended to any
10387
10776
  // of the four whenever the column supplies `filterValues` -- a faceted checkbox list,
10388
10777
  // independent of the column's own kind.
10778
+ //
10779
+ // Multi-select fix (DATATABLE-ATTIO-SPEC.md gap #12, live-measured 2026-09-30): a
10780
+ // `kind: "select"` column whose CELL is multi-valued (`type: "tags"`/`"chip-list"` --
10781
+ // a chip-list/tag column, not a single status pill) offers Attio's own
10782
+ // `contains`/`does not contain` instead of `is`/`is not` -- "is" reads as an exact-
10783
+ // set-equality check against an array value, which is not what Attio's own multi-
10784
+ // select filter means or what `applyToolbarFilters`' `is`/`isnot` branch (a plain
10785
+ // string-equality compare via `asText`) computes. `contains`/`doesnotcontain` are
10786
+ // already correct here unmodified: `applyToolbarFilters` already runs them against
10787
+ // `asText(raw)` (an array's own values joined), which is exactly "does any tag
10788
+ // match". Single-select (no `filterValues`-array cell type) keeps `is`/`is not` --
10789
+ // not re-measured against Attio, so left alone per the discovery doc's own hedge.
10389
10790
  ////////////////////////////////////////////////////////////////////////////////////
10390
10791
  const OP_ORDER = [
10391
10792
  "contains",
@@ -10419,9 +10820,19 @@ const SELECT_OP_ORDER = [
10419
10820
  "empty",
10420
10821
  "notempty"
10421
10822
  ];
10823
+ const MULTI_SELECT_OP_ORDER = [
10824
+ "contains",
10825
+ "doesnotcontain",
10826
+ "empty",
10827
+ "notempty"
10828
+ ];
10422
10829
  /** The operators a column of this `kind` offers, in display order -- `hasValues`
10423
- * (the column supplies `filterValues`) appends `"isanyof"` regardless of kind. */ function opsForKind(kind, hasValues) {
10424
- const base = kind === "number" ? NUMBER_OP_ORDER : kind === "date" ? DATE_OP_ORDER : kind === "select" ? SELECT_OP_ORDER : OP_ORDER;
10830
+ * (the column supplies `filterValues`) appends `"isanyof"` regardless of kind.
10831
+ * `multiValue` (the column's CELL type is a chip-list/tags column, not just its
10832
+ * filter `kind`) swaps `SELECT_OP_ORDER`'s `is`/`is not` for `contains`/`does not
10833
+ * contain`, matching Attio's own multi-select filter -- see the operator-sets
10834
+ * comment above. */ function opsForKind(kind, hasValues, multiValue = false) {
10835
+ const base = kind === "number" ? NUMBER_OP_ORDER : kind === "date" ? DATE_OP_ORDER : kind === "select" ? multiValue ? MULTI_SELECT_OP_ORDER : SELECT_OP_ORDER : OP_ORDER;
10425
10836
  return hasValues ? [
10426
10837
  ...base,
10427
10838
  "isanyof"
@@ -10789,66 +11200,58 @@ const formatFilterDate = (iso)=>{
10789
11200
  return `${OPERATOR_LABELS[filter.op]} ${value}`;
10790
11201
  }
10791
11202
  /**
10792
- * One filter condition, INSIDE the Filter popover -- a compact one-line summary row
10793
- * (field: operator value) with a trailing remove "x", expanding in place (accordion,
10794
- * not a second popover) to the field/operator/value editor when clicked. Presentation
10795
- * note (crisp/#593-style live re-check, 2026-09-28): Attio's own toolbar keeps active
10796
- * conditions as a compact "Filter · N" count on the trigger, not a row of always-on
10797
- * pill chips spread across the toolbar -- this replaces that first pass (which DID
10798
- * render one big chip per condition, directly in the toolbar row) with conditions
10799
- * living inside the ONE popover the trigger opens, the same list-of-rows shape
10800
- * `SortControl` above already uses for its own conditions.
10801
- */ function FilterConditionRow({ filter, columns, expanded, onToggle }) {
11203
+ * One filter condition, INSIDE the Filter popover -- Attio's own compact chip shape
11204
+ * (DATATABLE-ATTIO-SPEC.md gap #12, re-measured live 2026-09-30): `[attribute ▾]
11205
+ * [operator …] [value] [remove ×]`, ALL of it visible at once, never behind a click-
11206
+ * to-expand accordion. Supersedes this file's earlier 2026-09-28 note (which read
11207
+ * Attio as keeping conditions collapsed behind a bare "Filter · N" count and put them
11208
+ * in an accordion for that reason) -- the fresh read found the opposite: every active
11209
+ * condition shows attribute/operator/value simultaneously. This keeps the outer shell
11210
+ * (the "Filter"/"Filter · N" trigger opening ONE popover, `SortControl`'s own
11211
+ * list-of-rows shape) rather than moving chips into the toolbar row itself, so the
11212
+ * already-covered trigger/`Add filter` DOM (`DataTable.test.tsx`) stays put; only the
11213
+ * PER-CONDITION row tightens, from an accordion to an always-open card. The operator
11214
+ * set stays a row of small pill buttons (not a dropdown) so every existing test that
11215
+ * clicks an operator by its exact button name keeps working unchanged.
11216
+ */ function FilterConditionRow({ filter, columns }) {
10802
11217
  const { patchToolbarFilter, changeToolbarFilterField, removeToolbarFilter } = useDataTableContext();
10803
11218
  const column = columns.find((c)=>c.key === filter.key);
10804
11219
  const kind = column?.kind;
11220
+ const multiValue = column?.type === "tags" || column?.type === "chip-list";
10805
11221
  const values = column?.filterValues ?? [];
10806
- const ops = opsForKind(kind, values.length > 0);
10807
- return /*#__PURE__*/ jsxRuntime.jsxs("div", {
11222
+ const ops = opsForKind(kind, values.length > 0, multiValue);
11223
+ return /*#__PURE__*/ jsxRuntime.jsxs("fieldset", {
11224
+ "aria-label": `${column ? columnLabel(column) : filter.key} filter: ${conditionSummary(filter, column)}`,
10808
11225
  style: {
11226
+ margin: 0,
10809
11227
  display: "flex",
10810
11228
  flexDirection: "column",
10811
- gap: 6,
11229
+ gap: 8,
10812
11230
  borderRadius: 6,
10813
- background: expanded ? "var(--seed-color-bg-neutral-weak)" : "transparent",
10814
- padding: expanded ? 6 : 0
11231
+ border: "1px solid var(--seed-color-border-neutral)",
11232
+ background: "var(--seed-color-bg-neutral-weak)",
11233
+ padding: 6
10815
11234
  },
10816
11235
  children: [
10817
11236
  /*#__PURE__*/ jsxRuntime.jsxs("div", {
10818
11237
  style: {
10819
11238
  display: "flex",
10820
11239
  alignItems: "center",
10821
- gap: 2
11240
+ gap: 4
10822
11241
  },
10823
11242
  children: [
10824
- /*#__PURE__*/ jsxRuntime.jsxs("button", {
10825
- type: "button",
10826
- onClick: onToggle,
11243
+ /*#__PURE__*/ jsxRuntime.jsx(InlineSelect, {
11244
+ ariaLabel: "Filter by field",
11245
+ value: filter.key,
11246
+ onChange: (key)=>changeToolbarFilterField(filter.id, key),
11247
+ options: columns.map((c)=>({
11248
+ value: c.key,
11249
+ label: columnLabel(c)
11250
+ })),
10827
11251
  style: {
10828
11252
  flex: 1,
10829
- minWidth: 0,
10830
- textAlign: "left",
10831
- padding: "6px 8px",
10832
- border: "none",
10833
- borderRadius: 6,
10834
- background: "transparent",
10835
- cursor: "pointer",
10836
- fontSize: 13,
10837
- overflow: "hidden",
10838
- textOverflow: "ellipsis",
10839
- whiteSpace: "nowrap"
10840
- },
10841
- children: [
10842
- /*#__PURE__*/ jsxRuntime.jsx("strong", {
10843
- style: {
10844
- fontWeight: 500
10845
- },
10846
- children: column ? columnLabel(column) : filter.key
10847
- }),
10848
- ":",
10849
- " ",
10850
- conditionSummary(filter, column)
10851
- ]
11253
+ minWidth: 0
11254
+ }
10852
11255
  }),
10853
11256
  /*#__PURE__*/ jsxRuntime.jsx("button", {
10854
11257
  type: "button",
@@ -10862,136 +11265,103 @@ const formatFilterDate = (iso)=>{
10862
11265
  })
10863
11266
  ]
10864
11267
  }),
10865
- expanded && /*#__PURE__*/ jsxRuntime.jsxs("div", {
11268
+ /*#__PURE__*/ jsxRuntime.jsx("div", {
10866
11269
  style: {
10867
- padding: "0 8px 4px",
10868
11270
  display: "flex",
10869
- flexDirection: "column",
10870
- gap: 8
11271
+ flexWrap: "wrap",
11272
+ gap: 2
10871
11273
  },
10872
- children: [
10873
- /*#__PURE__*/ jsxRuntime.jsx("div", {
10874
- style: {
10875
- fontSize: 12,
10876
- color: "var(--seed-color-fg-neutral-subtle)"
10877
- },
10878
- children: "Field"
10879
- }),
10880
- /*#__PURE__*/ jsxRuntime.jsx(InlineSelect, {
10881
- ariaLabel: "Filter by field",
10882
- value: filter.key,
10883
- onChange: (key)=>changeToolbarFilterField(filter.id, key),
10884
- options: columns.map((c)=>({
10885
- value: c.key,
10886
- label: columnLabel(c)
10887
- }))
10888
- }),
10889
- /*#__PURE__*/ jsxRuntime.jsx("div", {
10890
- style: {
10891
- fontSize: 12,
10892
- color: "var(--seed-color-fg-neutral-subtle)"
10893
- },
10894
- children: "Condition"
10895
- }),
10896
- /*#__PURE__*/ jsxRuntime.jsx("div", {
11274
+ children: ops.map((op)=>/*#__PURE__*/ jsxRuntime.jsx("button", {
11275
+ type: "button",
11276
+ "data-active": filter.op === op ? "" : undefined,
11277
+ onClick: ()=>patchToolbarFilter(filter.id, {
11278
+ op
11279
+ }),
10897
11280
  style: {
10898
- display: "flex",
10899
- flexDirection: "column",
10900
- gap: 2
11281
+ textAlign: "left",
11282
+ padding: "4px 8px",
11283
+ borderRadius: 999,
11284
+ border: "none",
11285
+ cursor: "pointer",
11286
+ background: filter.op === op ? "var(--seed-color-bg-layer-default)" : "transparent",
11287
+ fontSize: 12
10901
11288
  },
10902
- children: ops.map((op)=>/*#__PURE__*/ jsxRuntime.jsx("button", {
10903
- type: "button",
10904
- "data-active": filter.op === op ? "" : undefined,
10905
- onClick: ()=>patchToolbarFilter(filter.id, {
10906
- op
10907
- }),
10908
- style: {
10909
- textAlign: "left",
10910
- padding: "6px 8px",
10911
- borderRadius: 6,
10912
- border: "none",
10913
- cursor: "pointer",
10914
- background: filter.op === op ? "var(--seed-color-bg-layer-default)" : "transparent",
10915
- fontSize: 13
10916
- },
10917
- children: OPERATOR_LABELS[op]
10918
- }, op))
10919
- }),
10920
- opNeedsValues(filter.op) && /*#__PURE__*/ jsxRuntime.jsx("div", {
11289
+ children: OPERATOR_LABELS[op]
11290
+ }, op))
11291
+ }),
11292
+ opNeedsValues(filter.op) && /*#__PURE__*/ jsxRuntime.jsx("div", {
11293
+ style: {
11294
+ display: "flex",
11295
+ flexDirection: "column",
11296
+ gap: 4,
11297
+ maxHeight: 160,
11298
+ overflow: "auto"
11299
+ },
11300
+ children: values.map((value)=>{
11301
+ const checked = (filter.values ?? []).includes(value);
11302
+ return /*#__PURE__*/ jsxRuntime.jsxs(CheckboxRoot, {
11303
+ checked: checked,
11304
+ onCheckedChange: ()=>patchToolbarFilter(filter.id, {
11305
+ values: checked ? (filter.values ?? []).filter((v)=>v !== value) : [
11306
+ ...filter.values ?? [],
11307
+ value
11308
+ ]
11309
+ }),
10921
11310
  style: {
10922
11311
  display: "flex",
10923
- flexDirection: "column",
10924
- gap: 4,
10925
- maxHeight: 160,
10926
- overflow: "auto"
11312
+ alignItems: "center",
11313
+ gap: 8,
11314
+ padding: "4px 0",
11315
+ cursor: "pointer"
10927
11316
  },
10928
- children: values.map((value)=>{
10929
- const checked = (filter.values ?? []).includes(value);
10930
- return /*#__PURE__*/ jsxRuntime.jsxs(CheckboxRoot, {
10931
- checked: checked,
10932
- onCheckedChange: ()=>patchToolbarFilter(filter.id, {
10933
- values: checked ? (filter.values ?? []).filter((v)=>v !== value) : [
10934
- ...filter.values ?? [],
10935
- value
10936
- ]
10937
- }),
10938
- style: {
10939
- display: "flex",
10940
- alignItems: "center",
10941
- gap: 8,
10942
- padding: "4px 0",
10943
- cursor: "pointer"
10944
- },
10945
- children: [
10946
- /*#__PURE__*/ jsxRuntime.jsx(CheckboxControl, {
10947
- children: /*#__PURE__*/ jsxRuntime.jsx(CheckboxIndicator, {
10948
- checked: /*#__PURE__*/ jsxRuntime.jsx(IconCheckmarkLine_default, {
10949
- size: 12
10950
- })
10951
- })
10952
- }),
10953
- /*#__PURE__*/ jsxRuntime.jsx(CheckboxLabel, {
10954
- children: value
10955
- }),
10956
- /*#__PURE__*/ jsxRuntime.jsx(CheckboxHiddenInput, {
10957
- "aria-label": value
11317
+ children: [
11318
+ /*#__PURE__*/ jsxRuntime.jsx(CheckboxControl, {
11319
+ children: /*#__PURE__*/ jsxRuntime.jsx(CheckboxIndicator, {
11320
+ checked: /*#__PURE__*/ jsxRuntime.jsx(IconCheckmarkLine_default, {
11321
+ size: 12
10958
11322
  })
10959
- ]
10960
- }, value);
10961
- })
10962
- }),
10963
- opNeedsValue(filter.op) && kind === "date" && /*#__PURE__*/ jsxRuntime.jsx(DatePicker, {
10964
- value: filter.value ? toDatePickerDate(isoToDate(filter.value) ?? new Date()) : undefined,
10965
- onValueChange: (d)=>d && patchToolbarFilter(filter.id, {
10966
- value: dateToISO(fromDatePickerDate(d))
10967
- })
10968
- }),
10969
- opNeedsValue(filter.op) && kind === "select" && /*#__PURE__*/ jsxRuntime.jsx(InlineSelect, {
10970
- ariaLabel: "Filter value",
10971
- placeholder: "Value…",
10972
- value: filter.value || undefined,
10973
- onChange: (value)=>patchToolbarFilter(filter.id, {
10974
- value
11323
+ })
10975
11324
  }),
10976
- options: values.map((value)=>({
10977
- value,
10978
- label: value
10979
- }))
10980
- }),
10981
- opNeedsValue(filter.op) && kind !== "date" && kind !== "select" && /*#__PURE__*/ jsxRuntime.jsx(TextFieldRoot, {
10982
- variant: "outline",
10983
- size: "medium",
10984
- children: /*#__PURE__*/ jsxRuntime.jsx(TextFieldInput, {
10985
- type: kind === "number" ? "number" : "text",
10986
- value: filter.value,
10987
- onChange: (e)=>patchToolbarFilter(filter.id, {
10988
- value: e.target.value
10989
- }),
10990
- placeholder: "Value…",
10991
- "aria-label": "Filter value"
10992
- })
11325
+ /*#__PURE__*/ jsxRuntime.jsx(CheckboxLabel, {
11326
+ children: value
11327
+ }),
11328
+ /*#__PURE__*/ jsxRuntime.jsx(CheckboxHiddenInput, {
11329
+ "aria-label": value
11330
+ })
11331
+ ]
11332
+ }, value);
11333
+ })
11334
+ }),
11335
+ opNeedsValue(filter.op) && kind === "date" && /*#__PURE__*/ jsxRuntime.jsx(DatePicker, {
11336
+ value: filter.value ? toDatePickerDate(isoToDate(filter.value) ?? new Date()) : undefined,
11337
+ onValueChange: (d)=>d && patchToolbarFilter(filter.id, {
11338
+ value: dateToISO(fromDatePickerDate(d))
10993
11339
  })
10994
- ]
11340
+ }),
11341
+ opNeedsValue(filter.op) && kind === "select" && /*#__PURE__*/ jsxRuntime.jsx(InlineSelect, {
11342
+ ariaLabel: "Filter value",
11343
+ placeholder: "Value…",
11344
+ value: filter.value || undefined,
11345
+ onChange: (value)=>patchToolbarFilter(filter.id, {
11346
+ value
11347
+ }),
11348
+ options: values.map((value)=>({
11349
+ value,
11350
+ label: value
11351
+ }))
11352
+ }),
11353
+ opNeedsValue(filter.op) && kind !== "date" && kind !== "select" && /*#__PURE__*/ jsxRuntime.jsx(TextFieldRoot, {
11354
+ variant: "outline",
11355
+ size: "medium",
11356
+ children: /*#__PURE__*/ jsxRuntime.jsx(TextFieldInput, {
11357
+ type: kind === "number" ? "number" : "text",
11358
+ value: filter.value,
11359
+ onChange: (e)=>patchToolbarFilter(filter.id, {
11360
+ value: e.target.value
11361
+ }),
11362
+ placeholder: "Value…",
11363
+ "aria-label": "Filter value"
11364
+ })
10995
11365
  })
10996
11366
  ]
10997
11367
  });
@@ -10999,15 +11369,14 @@ const formatFilterDate = (iso)=>{
10999
11369
  /**
11000
11370
  * The toolbar's Filter trigger + popover -- a compact `"Filter"` / `"Filter · N"`
11001
11371
  * button (matching `SortControl`'s own `"Sort"` / `"Sort · N"`), opening ONE popover
11002
- * that holds both the active conditions (compact rows, `FilterConditionRow` above,
11003
- * one expanded at a time) and, behind "+ Add filter", the field picker that appends a
11004
- * new one -- not a row of always-on pill chips spread across the toolbar (this
11005
- * replaces that first pass; see `FilterConditionRow`'s own doc comment).
11372
+ * that holds both the active conditions (compact chips, `FilterConditionRow` above,
11373
+ * ALL open at once -- no accordion) and, behind "+ Add filter", the field picker that
11374
+ * appends a new one. See `FilterConditionRow`'s own doc comment for why the chips
11375
+ * live inside this one popover rather than spread across the toolbar row itself.
11006
11376
  */ function FilterControl({ columns, label = "Filter" }) {
11007
11377
  const { toolbarFilters, addToolbarFilter } = useDataTableContext();
11008
11378
  const [open, setOpen] = React__namespace.useState(false);
11009
11379
  const [pickingField, setPickingField] = React__namespace.useState(false);
11010
- const [expandedId, setExpandedId] = React__namespace.useState(null);
11011
11380
  return /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
11012
11381
  open: open,
11013
11382
  onOpenChange: (next)=>{
@@ -11069,8 +11438,7 @@ const formatFilterDate = (iso)=>{
11069
11438
  columns.map((c)=>/*#__PURE__*/ jsxRuntime.jsx("button", {
11070
11439
  type: "button",
11071
11440
  onClick: ()=>{
11072
- const id = addToolbarFilter(c.key);
11073
- setExpandedId(id);
11441
+ addToolbarFilter(c.key);
11074
11442
  setPickingField(false);
11075
11443
  },
11076
11444
  style: {
@@ -11103,9 +11471,7 @@ const formatFilterDate = (iso)=>{
11103
11471
  }),
11104
11472
  toolbarFilters.map((f)=>/*#__PURE__*/ jsxRuntime.jsx(FilterConditionRow, {
11105
11473
  filter: f,
11106
- columns: columns,
11107
- expanded: expandedId === f.id,
11108
- onToggle: ()=>setExpandedId((id)=>id === f.id ? null : f.id)
11474
+ columns: columns
11109
11475
  }, f.id)),
11110
11476
  /*#__PURE__*/ jsxRuntime.jsxs("button", {
11111
11477
  type: "button",
@@ -11134,8 +11500,8 @@ const formatFilterDate = (iso)=>{
11134
11500
  });
11135
11501
  }
11136
11502
 
11137
- const DataTableToolbar = withContext$2(Primitive.div, "toolbar");
11138
- const DataTableCount = withContext$2(Primitive.span, "count");
11503
+ const DataTableToolbar = withContext$1(Primitive.div, "toolbar");
11504
+ const DataTableCount = withContext$1(Primitive.span, "count");
11139
11505
  /**
11140
11506
  * The toolbar's own right-hand group -- `count`'s `marginInlineStart: auto` moved
11141
11507
  * here (off the `<span>` itself) so a consumer's `toolbarActions` and the row count
@@ -11146,7 +11512,7 @@ const DataTableCount = withContext$2(Primitive.span, "count");
11146
11512
  * it. `DataTable.tsx` always renders this wrapper once either `toolbarActions` or
11147
11513
  * `showCount` is on, so it degrades to exactly the old single-count layout when no
11148
11514
  * `toolbarActions` is passed.
11149
- */ const DataTableToolbarEnd = withContext$2(Primitive.div, "toolbarEnd");
11515
+ */ const DataTableToolbarEnd = withContext$1(Primitive.div, "toolbarEnd");
11150
11516
  /**
11151
11517
  * A compact, borderless search box -- crisp's own measured `.crisp-datatable-search`
11152
11518
  * (32px, `bgSurface` background, a leading icon, a soft rest-state shadow, a 1px brand
@@ -11196,7 +11562,7 @@ function RowCount({ shown, total, countLabel }) {
11196
11562
  }
11197
11563
 
11198
11564
  function DataTableInner(props, ref) {
11199
- const { columns, data, surface, className, locale = "en-US", sort, defaultSort, onSortChange, sortIcon, selectable, selectedIds, defaultSelectedIds, onSelectionChange, selectAllLabel = "Select all rows", getRowSelectionLabel = ()=>"Select row", selectionCheckedIcon, selectionIndeterminateIcon, query, defaultQuery, onQueryChange, searchPlaceholder = "Search…", searchLabel = "Search table", toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, showCount = true, countLabel, toolbarActions, columnMenu = false, reorderable = true, resizable = true, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, onColumnsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, stickyFirst = false, stickyLast = false, onEdit, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", loading = false, loadingLabel = "Loading...", error, empty = "No results.", onLoadMore, hasMore, loadingMore, scrollProps, ...restProps } = props;
11565
+ const { columns, data, surface, className, locale = "en-US", sort, defaultSort, onSortChange, sortIcon, selectable, selectedIds, defaultSelectedIds, onSelectionChange, selectAllLabel = "Select all rows", getRowSelectionLabel = ()=>"Select row", selectionCheckedIcon, selectionIndeterminateIcon, bulkActions, clearSelectionLabel, query, defaultQuery, onQueryChange, searchPlaceholder = "Search…", searchLabel = "Search table", toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, showCount = true, countLabel, toolbarActions, columnMenu = false, reorderable = true, resizable = true, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, onColumnsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, stickyFirst = false, stickyLast = false, onEdit, cellContextMenu = true, onViewEditHistory, cellContextMenuItems, interactive = false, onRowClick, onAddRow, addRowLabel = "New record", loading = false, loadingLabel = "Loading...", error, empty = "No results.", onLoadMore, hasMore, loadingMore, scrollProps, ...restProps } = props;
11200
11566
  const classNames = dataTable({
11201
11567
  surface,
11202
11568
  interactive
@@ -11320,7 +11686,7 @@ function DataTableInner(props, ref) {
11320
11686
  // loading/error/empty body, infinite scroll's sentinel, the add-row footer) reads
11321
11687
  // this ONE value rather than each re-deriving it, so they can never drift apart.
11322
11688
  const totalCols = columns.length + (selectable ? 1 : 0);
11323
- return /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider$1, {
11689
+ return /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider, {
11324
11690
  value: classNames,
11325
11691
  children: /*#__PURE__*/ jsxRuntime.jsxs(DataTableRoot, {
11326
11692
  ref: ref,
@@ -11476,7 +11842,10 @@ function DataTableInner(props, ref) {
11476
11842
  locale: locale,
11477
11843
  selectable: !!selectable,
11478
11844
  stickyFirst: stickyFirst,
11479
- stickyLast: resolvedStickyLast
11845
+ stickyLast: resolvedStickyLast,
11846
+ cellContextMenu: cellContextMenu,
11847
+ onViewEditHistory: onViewEditHistory,
11848
+ cellContextMenuItems: cellContextMenuItems
11480
11849
  })
11481
11850
  ]
11482
11851
  }, row.id)),
@@ -11515,6 +11884,10 @@ function DataTableInner(props, ref) {
11515
11884
  })
11516
11885
  ]
11517
11886
  })
11887
+ }),
11888
+ selectable && /*#__PURE__*/ jsxRuntime.jsx(DataTableBulkBar, {
11889
+ clearLabel: clearSelectionLabel,
11890
+ children: bulkActions
11518
11891
  })
11519
11892
  ]
11520
11893
  })
@@ -11568,7 +11941,7 @@ DataTableInner.displayName = "DataTable";
11568
11941
  // is a SIBLING of the toolbar (both inside `DataTablePrimitiveRoot`'s card `<div>`)
11569
11942
  // rather than `DataTablePrimitiveRoot` itself: a `<table>` cannot host the toolbar as a
11570
11943
  // child without the HTML parser foster-parenting it back out.
11571
- const DataTableTable = withContext$2(DataTableTable$1, "table");
11944
+ const DataTableTable = withContext$1(DataTableTable$1, "table");
11572
11945
  /** One `<col>` per rendered column (plus the 40px selection column, if any) -- the
11573
11946
  * SINGLE place a column's width is ever set now. Every row's header/body/footer cell
11574
11947
  * reads its own width off this shared `<colgroup>` by construction (native table
@@ -11605,15 +11978,15 @@ const DataTableTable = withContext$2(DataTableTable$1, "table");
11605
11978
  ]
11606
11979
  });
11607
11980
  }
11608
- const DataTableHeader = withContext$2(DataTableHeader$1, "header");
11609
- const DataTableHeaderCell = withContext$2(DataTableHeaderCell$1, "headerCell");
11981
+ const DataTableHeader = withContext$1(DataTableHeader$1, "header");
11982
+ const DataTableHeaderCell = withContext$1(DataTableHeaderCell$1, "headerCell");
11610
11983
  // `.crisp-datatable-th-inner` -- the flex row INSIDE a `<th>` (label + filter trigger +
11611
11984
  // sort chevron + column menu). See qvism-preset's own `headerCellInner` slot comment
11612
11985
  // for why this lives one level below the cell itself rather than on it.
11613
- const DataTableHeaderCellInner = withContext$2(Primitive.div, "headerCellInner");
11986
+ const DataTableHeaderCellInner = withContext$1(Primitive.div, "headerCellInner");
11614
11987
  /** The label text specifically -- the one thing in `DataTableHeaderCellInner` that
11615
11988
  * truncates; the trailing filter/sort/menu icons stay fully visible regardless of how
11616
- * long a column's own header text is. */ const DataTableHeaderCellLabel = withContext$2(Primitive.span, "headerCellLabel");
11989
+ * long a column's own header text is. */ const DataTableHeaderCellLabel = withContext$1(Primitive.span, "headerCellLabel");
11617
11990
  /**
11618
11991
  * A selection `<th>`/`<td>`/footer `<td>` composes ITS base slot (`headerCell`/`cell`/
11619
11992
  * `footerCell`) together with the `selectionCell` MODIFIER, rather than replacing it --
@@ -11624,7 +11997,7 @@ const DataTableHeaderCellInner = withContext$2(Primitive.div, "headerCellInner")
11624
11997
  * a second recipe call.
11625
11998
  */ function withSelectionCell(Component, baseSlot) {
11626
11999
  const StyledComponent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
11627
- const classNames = useClassNames$1();
12000
+ const classNames = useClassNames();
11628
12001
  return /*#__PURE__*/ jsxRuntime.jsx(Component, {
11629
12002
  ref: ref,
11630
12003
  ...props,
@@ -11635,12 +12008,12 @@ const DataTableHeaderCellInner = withContext$2(Primitive.div, "headerCellInner")
11635
12008
  return StyledComponent;
11636
12009
  }
11637
12010
  const DataTableSelectionHeaderCell = withSelectionCell(DataTableHeaderCell$1, "headerCell");
11638
- const DataTableHeaderCellIcon = withContext$2(Primitive.span, "headerCellIcon");
12011
+ const DataTableHeaderCellIcon = withContext$1(Primitive.span, "headerCellIcon");
11639
12012
  // body has no CSS slot of its own in the rootage spec -- it is a plain structural
11640
12013
  // `<tbody>`, so it is not wrapped in `withContext`.
11641
12014
  const DataTableBody = DataTableBody$1;
11642
- const DataTableRow = withContext$2(DataTableRow$1, "row");
11643
- const DataTableCell = withContext$2(DataTableCell$1, "cell");
12015
+ const DataTableRow = withContext$1(DataTableRow$1, "row");
12016
+ const DataTableCell = withContext$1(DataTableCell$1, "cell");
11644
12017
  const DataTableSelectionCell = withSelectionCell(DataTableCell$1, "cell");
11645
12018
  // Tranche 4 -- column menu + reorder + resize + sticky columns. A scroll boundary
11646
12019
  // wrapping the whole `<table>`: `overflowX: auto` is what makes a wide table (more
@@ -11650,7 +12023,7 @@ const DataTableSelectionCell = withSelectionCell(DataTableCell$1, "cell");
11650
12023
  // stays out of this slot's own base css (nothing here has a bounded height of its own
11651
12024
  // to clip), so it never introduces a second vertical scrollbar next to whatever
11652
12025
  // ambient container the whole DataTable already sits in.
11653
- const DataTableScrollBodyBase = withContext$2(Primitive.div, "scrollArea");
12026
+ const DataTableScrollBodyBase = withContext$1(Primitive.div, "scrollArea");
11654
12027
  function DataTableScrollBody({ children, scrollProps }) {
11655
12028
  const { scrollAreaRef, scrolledStart, scrolledEnd, onScroll } = useDataTableContext();
11656
12029
  const { className: scrollClassName, style: scrollStyle, ...otherScrollProps } = scrollProps ?? {};
@@ -11665,7 +12038,7 @@ function DataTableScrollBody({ children, scrollProps }) {
11665
12038
  children: children
11666
12039
  });
11667
12040
  }
11668
- const DataTableFooterBase = withContext$2(Primitive.tfoot, "footer");
12041
+ const DataTableFooterBase = withContext$1(Primitive.tfoot, "footer");
11669
12042
  function DataTableFooter({ children, ...props }) {
11670
12043
  return /*#__PURE__*/ jsxRuntime.jsx(DataTableFooterBase, {
11671
12044
  ...props,
@@ -11674,7 +12047,7 @@ function DataTableFooter({ children, ...props }) {
11674
12047
  })
11675
12048
  });
11676
12049
  }
11677
- const DataTableFooterCell = withContext$2(Primitive.td, "footerCell");
12050
+ const DataTableFooterCell = withContext$1(Primitive.td, "footerCell");
11678
12051
  // The selection column's OWN sticky wrappers -- one each for header/body/footer,
11679
12052
  // reading the live pinned set via `useSelectionStickyFirst` (column-layout.ts) instead
11680
12053
  // of the `stickyFirst` PROP alone, so the checkbox column sticks the moment ANY column
@@ -15612,6 +15985,7 @@ function RecordPreviewPanel({ open: _open, record, onClose, className, animation
15612
15985
  RecordPreviewPanel.displayName = "RecordPreviewPanel";
15613
15986
 
15614
15987
  exports.DataTable = DataTable;
15988
+ exports.DataTableBulkBar = DataTableBulkBar;
15615
15989
  exports.DataTableViewsControl = DataTableViewsControl;
15616
15990
  exports.RecordPreview = RecordPreview;
15617
15991
  exports.ResizablePanel = ResizablePanel;