@crispy-seed/data-table 0.3.1 → 0.5.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.js CHANGED
@@ -859,18 +859,33 @@ function useDataTableColumnCalc(props) {
859
859
  return next;
860
860
  }
861
861
  /**
862
- * Drag-to-reorder: removes `dragKey` from `order` and reinserts it immediately before
863
- * `dropKey`'s CURRENT position (i.e. its position after the removal, so dropping a
864
- * column just after its own old slot doesn't appear to do nothing). A no-op (an
865
- * equal-valued copy) when either key is missing or they're the same key.
862
+ * Drag-to-reorder: moves `dragKey` to `dropKey`'s own ORIGINAL index (the standard
863
+ * `arrayMove` two-splice shape -- remove `dragKey`, then reinsert it at `dropKey`'s
864
+ * pre-removal index of the now-shorter array), the same semantics dnd-kit's own
865
+ * `arrayMove` uses. A no-op (an equal-valued copy) when either key is missing or
866
+ * they're the same key.
867
+ *
868
+ * An EARLIER version of this function instead reinserted `dragKey` immediately before
869
+ * `dropKey`'s post-removal position. That reads the same for a multi-column jump, but
870
+ * is a silent no-op for the single most common drag gesture there is: dragging a
871
+ * column onto its own immediate RIGHT neighbor. Concretely, for `["a","b"]`, dragging
872
+ * "a" onto "b": removing "a" leaves `["b"]`, and "immediately before b" is index 0 --
873
+ * exactly where "a" already was, so nothing moved even though a real drag-and-drop
874
+ * gesture happened (caught by `DataTable.columns.test.tsx`'s own end-to-end drag test,
875
+ * which fires a real `dragover` DOM event and asserts the header order actually
876
+ * changed -- it didn't). This version's two-splice shape lands the dragged column at
877
+ * the target's own original slot instead, which swaps immediate neighbors correctly in
878
+ * both directions and still matches the old multi-jump behavior for a LEFTWARD drag
879
+ * (only a RIGHTWARD multi-column jump now lands one slot further, after the drop
880
+ * target instead of before it -- see this file's own test for both directions).
866
881
  */ function reorderColumnKey(order, dragKey, dropKey) {
867
882
  if (dragKey === dropKey || !order.includes(dragKey) || !order.includes(dropKey)) {
868
883
  return [
869
884
  ...order
870
885
  ];
871
886
  }
887
+ const dropIndex = order.indexOf(dropKey);
872
888
  const withoutDrag = order.filter((key)=>key !== dragKey);
873
- const dropIndex = withoutDrag.indexOf(dropKey);
874
889
  return [
875
890
  ...withoutDrag.slice(0, dropIndex),
876
891
  dragKey,
@@ -882,6 +897,24 @@ function useDataTableColumnCalc(props) {
882
897
  * nor balloon past a sane cap from one fast mouse move). */ function clampColumnWidth(width, min = 80, max = 640) {
883
898
  return Math.min(max, Math.max(min, width));
884
899
  }
900
+ /**
901
+ * Reconciles a persisted/controlled PINNED set against the table's current column
902
+ * KEYS -- the same drop-what's-gone rule `reconcileColumnOrder` already applies to
903
+ * `columnOrder`, but pins never need the "append what's new" half of that: an unpinned
904
+ * column simply isn't in the set, appending it here would silently pin every brand-new
905
+ * column by accident. A key no longer in `keys` (hidden isn't "gone" -- only removed
906
+ * from `columns` entirely) is dropped; everything else keeps its order.
907
+ */ function reconcilePinnedColumns(pinned, keys) {
908
+ const keySet = new Set(keys);
909
+ return pinned.filter((key)=>keySet.has(key));
910
+ }
911
+ /** Pin/unpin: adds `key` if absent, removes it if present -- column-menu.tsx's own
912
+ * "Pin column"/"Unpin column" toggle is one call of this, the same shape
913
+ * `toggleMembership` already gives row selection. Kept as its own named function
914
+ * (rather than reusing `toggleMembership` directly) so a pin-specific doc comment and
915
+ * call sites aren't reading a generically-named helper for a column-specific concern. */ function togglePinnedColumn(pinned, key) {
916
+ return toggleMembership(pinned, key);
917
+ }
885
918
  /**
886
919
  * Infinite scroll's own gate: fire `onLoadMore` only when the sentinel is ACTUALLY
887
920
  * intersecting (not e.g. an observer's initial `false` callback, which fires once on
@@ -895,19 +928,90 @@ function useDataTableColumnCalc(props) {
895
928
  */ function shouldTriggerLoadMore(isIntersecting, hasMore, loadingMore) {
896
929
  return hasMore && !loadingMore;
897
930
  }
931
+ function isPlainObject(value) {
932
+ return typeof value === "object" && value !== null && !Array.isArray(value);
933
+ }
934
+ /** Structural validation for ONE parsed view -- every field of `DataTableViewState` is
935
+ * optional, so this only rejects a field that's actually PRESENT but the wrong shape
936
+ * (a consumer's storage from a future/older version of this shape), not a field that's
937
+ * simply missing. */ function isValidDataTableView(value) {
938
+ if (!isPlainObject(value)) return false;
939
+ if (typeof value["id"] !== "string" || typeof value["name"] !== "string") return false;
940
+ if (!isPlainObject(value["state"])) return false;
941
+ const state = value["state"];
942
+ if (state["columnOrder"] !== undefined && !isStringArray(state["columnOrder"])) return false;
943
+ if (state["hiddenColumnKeys"] !== undefined && !isStringArray(state["hiddenColumnKeys"])) return false;
944
+ if (state["pinnedColumnKeys"] !== undefined && !isStringArray(state["pinnedColumnKeys"])) return false;
945
+ if (state["columnWidths"] !== undefined && !isPlainObject(state["columnWidths"])) return false;
946
+ if (state["toolbarSorts"] !== undefined && !Array.isArray(state["toolbarSorts"])) return false;
947
+ if (state["toolbarFilters"] !== undefined && !Array.isArray(state["toolbarFilters"])) return false;
948
+ return true;
949
+ }
950
+ function isStringArray(value) {
951
+ return Array.isArray(value) && value.every((entry)=>typeof entry === "string");
952
+ }
953
+ /** JSON-serializes a saved-views list for the default localStorage store -- pure so it
954
+ * can be unit- and mutation-tested without touching `window.localStorage` at all (the
955
+ * hook's own effect, `useDataTableViews.ts`, is the only thing that actually touches
956
+ * storage; this is just the byte-for-byte shape it writes/reads). */ function serializeDataTableViews(views) {
957
+ return JSON.stringify(views);
958
+ }
959
+ /**
960
+ * Parses a saved-views blob back into a `DataTableView[]`, defensively: malformed JSON,
961
+ * a non-array root, or any single malformed entry drops just that problem (an empty
962
+ * list, or that one entry) rather than throwing -- a consumer's localStorage can hold a
963
+ * PRIOR version's shape after this ships a new field, and one bad/stale entry shouldn't
964
+ * cost every other saved view.
965
+ */ function deserializeDataTableViews(raw) {
966
+ if (!raw) return [];
967
+ let parsed;
968
+ try {
969
+ parsed = JSON.parse(raw);
970
+ } catch {
971
+ return [];
972
+ }
973
+ if (!Array.isArray(parsed)) return [];
974
+ return parsed.filter(isValidDataTableView);
975
+ }
976
+ /** Inserts `view` (by `id`), or replaces the existing entry with the same `id` in
977
+ * place -- "Save" (new view) and "update the currently loaded view" are both one call
978
+ * of this, matching `moveColumnKey`/`reorderColumnKey`'s own "one pure op per user
979
+ * gesture" shape. */ function upsertDataTableView(views, view) {
980
+ const index = views.findIndex((existing)=>existing.id === view.id);
981
+ if (index === -1) return [
982
+ ...views,
983
+ view
984
+ ];
985
+ const next = [
986
+ ...views
987
+ ];
988
+ next[index] = view;
989
+ return next;
990
+ }
991
+ /** Renames the view with this `id` -- a no-op (an equal-valued copy) if no view has
992
+ * that id. */ function renameDataTableView(views, id, name) {
993
+ return views.map((view)=>view.id === id ? {
994
+ ...view,
995
+ name
996
+ } : view);
997
+ }
998
+ /** Removes the view with this `id`. */ function removeDataTableView(views, id) {
999
+ return views.filter((view)=>view.id !== id);
1000
+ }
898
1001
  const EMPTY_STRINGS = [];
899
1002
  const EMPTY_RECORD = {};
900
1003
  /**
901
1004
  * Owns everything DATATABLE-ATTIO-SPEC.md's column menu + reorder + resize tranche
902
- * needs -- order, visibility, a per-column label override, and a per-column width --
903
- * the way useDataTableSelection owns the one selected-id SET rather than one hook per
904
- * row. All four are "one value per column key" maps/lists a consumer may want to
905
- * persist (a saved view), so they share this hook instead of splitting into four
906
- * separate ones; drag-to-reorder and drag-to-resize (below) are the two INTERACTIONS
907
- * built on top of the order/width state this hook already owns, not independent state
1005
+ * needs -- order, visibility, a per-column label override, a per-column width, and
1006
+ * (Attio's "Pin column") a per-column left-pinned flag -- the way useDataTableSelection
1007
+ * owns the one selected-id SET rather than one hook per row. All five are "one value
1008
+ * per column key" maps/lists/sets a consumer may want to persist (a saved view), so
1009
+ * they share this hook instead of splitting into five separate ones; drag-to-reorder
1010
+ * and drag-to-resize (below) are the two INTERACTIONS built on top of the order/width
1011
+ * state this hook already owns, not independent state
908
1012
  * of their own.
909
1013
  */ function useDataTableColumns(props) {
910
- const { columnKeys = EMPTY_STRINGS, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange } = props;
1014
+ const { columnKeys = EMPTY_STRINGS, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
911
1015
  const [rawOrder, setRawOrder] = useControllableState$1({
912
1016
  prop: columnOrderProp,
913
1017
  defaultProp: defaultColumnOrder ?? columnKeys,
@@ -944,6 +1048,36 @@ const EMPTY_RECORD = {};
944
1048
  columnOrder,
945
1049
  hiddenSet
946
1050
  ]);
1051
+ const [rawPinnedColumnKeys, setRawPinnedColumnKeys] = useControllableState$1({
1052
+ prop: pinnedProp,
1053
+ defaultProp: defaultPinnedColumnKeys,
1054
+ onChange: onPinnedColumnKeysChange
1055
+ });
1056
+ // Reconciled against `columnKeys` every render, the same way `columnOrder` is above --
1057
+ // a controlled `pinnedColumnKeys` a consumer persisted (e.g. a saved view) before a
1058
+ // column was removed from `columns` must not keep pinning a ghost key.
1059
+ const pinnedColumnKeys = useMemo(()=>reconcilePinnedColumns(rawPinnedColumnKeys ?? EMPTY_STRINGS, columnKeys), [
1060
+ rawPinnedColumnKeys,
1061
+ columnKeys
1062
+ ]);
1063
+ const pinnedSet = useMemo(()=>new Set(pinnedColumnKeys), [
1064
+ pinnedColumnKeys
1065
+ ]);
1066
+ const isColumnPinned = useCallback((key)=>pinnedSet.has(key), [
1067
+ pinnedSet
1068
+ ]);
1069
+ const pinColumn = useCallback((key)=>setRawPinnedColumnKeys((current = EMPTY_STRINGS)=>current.includes(key) ? current : [
1070
+ ...current,
1071
+ key
1072
+ ]), [
1073
+ setRawPinnedColumnKeys
1074
+ ]);
1075
+ const unpinColumn = useCallback((key)=>setRawPinnedColumnKeys((current = EMPTY_STRINGS)=>current.filter((existing)=>existing !== key)), [
1076
+ setRawPinnedColumnKeys
1077
+ ]);
1078
+ const togglePinColumn = useCallback((key)=>setRawPinnedColumnKeys((current = EMPTY_STRINGS)=>togglePinnedColumn(current, key)), [
1079
+ setRawPinnedColumnKeys
1080
+ ]);
947
1081
  const isColumnHidden = useCallback((key)=>hiddenSet.has(key), [
948
1082
  hiddenSet
949
1083
  ]);
@@ -988,22 +1122,30 @@ const EMPTY_RECORD = {};
988
1122
  columnKeys
989
1123
  ]);
990
1124
  // -- drag-to-reorder ---------------------------------------------------------------
991
- // Live state only (which column is currently being dragged) -- the styled layer
992
- // drives it by calling `dragOverColumn` as the pointer crosses OTHER header cells
993
- // (via `document.elementFromPoint`, since a captured pointer stops native
994
- // `pointerenter` from firing on siblings), which reorders immediately rather than
995
- // waiting for a drop. That is exactly what Attio's own header drag does (the column
996
- // being passed visibly swaps live), and it means `endColumnDrag` never has to also
997
- // commit a move -- there is nothing left to commit.
1125
+ // Live state only (which column is currently being dragged) -- the styled layer's
1126
+ // header cells are natively `draggable`, so `dragOverColumn` is driven straight off
1127
+ // each cell's own `onDragOver` (one real browser event per column the pointer
1128
+ // crosses), which reorders immediately rather than waiting for a drop. That is
1129
+ // exactly what Attio's own header drag does (the column being passed visibly swaps
1130
+ // live), and it means `endColumnDrag` never has to also commit a move -- there is
1131
+ // nothing left to commit.
998
1132
  const [draggingColumnKey, setDraggingColumnKey] = useState(null);
999
1133
  const beginColumnDrag = useCallback((key)=>setDraggingColumnKey(key), []);
1134
+ // Reads `draggingColumnKey` straight from this render's own closure rather than via
1135
+ // `setDraggingColumnKey`'s functional-updater form (an earlier version of this hook
1136
+ // nested the `reorderColumn` call inside that updater, to read a "live" value --
1137
+ // `draggingColumnKey` never actually CHANGES during a drag, so that state update was
1138
+ // pure ceremony, and nesting one hook's setState inside another's updater callback is
1139
+ // exactly what silently dropped the reorder in DataTable.columns.test.tsx's own
1140
+ // end-to-end drag test: a REAL `dragover` DOM event fired, `dragOverColumn` fired with
1141
+ // the right key, but `columnOrder` never actually moved). Each `onDragOver` is its own
1142
+ // discrete browser event (not a tight synchronous loop within one render), so a plain
1143
+ // closure read is exactly as live as the updater trick was trying to be.
1000
1144
  const dragOverColumn = useCallback((hoverKey)=>{
1001
- setDraggingColumnKey((current)=>{
1002
- if (!current || current === hoverKey) return current;
1003
- reorderColumn(current, hoverKey);
1004
- return current;
1005
- });
1145
+ if (!draggingColumnKey || draggingColumnKey === hoverKey) return;
1146
+ reorderColumn(draggingColumnKey, hoverKey);
1006
1147
  }, [
1148
+ draggingColumnKey,
1007
1149
  reorderColumn
1008
1150
  ]);
1009
1151
  const endColumnDrag = useCallback(()=>setDraggingColumnKey(null), []);
@@ -1039,10 +1181,15 @@ const EMPTY_RECORD = {};
1039
1181
  hiddenColumnKeys,
1040
1182
  columnWidths,
1041
1183
  columnLabels,
1184
+ pinnedColumnKeys,
1042
1185
  isColumnHidden,
1043
1186
  hideColumn,
1044
1187
  showColumn,
1045
1188
  toggleColumnVisibility,
1189
+ isColumnPinned,
1190
+ pinColumn,
1191
+ unpinColumn,
1192
+ togglePinColumn,
1046
1193
  getColumnLabel,
1047
1194
  renameColumn,
1048
1195
  getColumnWidth,
@@ -1691,7 +1838,7 @@ function useDataTableRowContext({ strict = true } = {}) {
1691
1838
  return context;
1692
1839
  }
1693
1840
  const DataTableRoot = /*#__PURE__*/ forwardRef((props, ref)=>{
1694
- const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
1841
+ const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
1695
1842
  const api = useDataTable({
1696
1843
  sort,
1697
1844
  defaultSort,
@@ -1723,6 +1870,9 @@ const DataTableRoot = /*#__PURE__*/ forwardRef((props, ref)=>{
1723
1870
  columnLabels,
1724
1871
  defaultColumnLabels,
1725
1872
  onColumnLabelsChange,
1873
+ pinnedColumnKeys,
1874
+ defaultPinnedColumnKeys,
1875
+ onPinnedColumnKeysChange,
1726
1876
  columnCalcOps,
1727
1877
  defaultColumnCalcOps,
1728
1878
  onColumnCalcOpsChange,
@@ -1810,6 +1960,132 @@ const DataTableCell$1 = /*#__PURE__*/ forwardRef((props, ref)=>{
1810
1960
  });
1811
1961
  DataTableCell$1.displayName = "DataTableCell";
1812
1962
 
1963
+ const EMPTY_VIEWS = [];
1964
+ function readStoredViews(storageKey) {
1965
+ if (!storageKey || typeof window === "undefined") return EMPTY_VIEWS;
1966
+ try {
1967
+ return deserializeDataTableViews(window.localStorage.getItem(storageKey));
1968
+ } catch {
1969
+ // A private-browsing tab / blocked storage throws on access, not just on quota --
1970
+ // this hook degrades to in-memory-only rather than crash the table over it.
1971
+ return EMPTY_VIEWS;
1972
+ }
1973
+ }
1974
+ /** A reasonably unique id for a newly created view -- `crypto.randomUUID` where it
1975
+ * exists (every real browser + modern Node), a plain timestamp+random fallback
1976
+ * otherwise (older jsdom/test environments) so this hook never throws just because a
1977
+ * test runner's `crypto` is a stub. */ function generateViewId() {
1978
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1979
+ return crypto.randomUUID();
1980
+ }
1981
+ return `view-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1982
+ }
1983
+ /**
1984
+ * Owns the saved-views LIST (create/rename/delete/select) and its default persistence
1985
+ * (localStorage, pluggable away by controlling `views`/`onViewsChange` instead) -- the
1986
+ * same controllable shape every other DataTable state hook already uses
1987
+ * (`useDataTableColumns`, `useDataTableToolbarSort`, ...). Read `DataTableViewsControl`
1988
+ * (`views.tsx`, the styled layer) for how a consumer wires `captureState`/`applyState`
1989
+ * around this to actually save/restore column + sort + filter state.
1990
+ */ function useDataTableViews(props) {
1991
+ const { views: viewsProp, defaultViews = EMPTY_VIEWS, onViewsChange, activeViewId: activeViewIdProp, defaultActiveViewId = null, onActiveViewIdChange, storageKey } = props;
1992
+ // Lazy, ONE-TIME read of localStorage as this render's default -- re-reading it on
1993
+ // every render (rather than a lazy ref) would clobber whatever the user just did in
1994
+ // THIS tab with whatever another tab most recently wrote. Only fires at all when this
1995
+ // hook truly owns persistence: `views` is uncontrolled AND a `storageKey` was given.
1996
+ const initialViewsRef = useRef(null);
1997
+ if (initialViewsRef.current === null) {
1998
+ if (viewsProp !== undefined) {
1999
+ initialViewsRef.current = defaultViews;
2000
+ } else {
2001
+ const stored = readStoredViews(storageKey);
2002
+ // Nothing in storage yet (first run, or no `storageKey` at all) -- seed from
2003
+ // `defaultViews` instead of an empty list, matching every other hook's own
2004
+ // `defaultProp` semantics.
2005
+ initialViewsRef.current = stored.length > 0 ? stored : defaultViews;
2006
+ }
2007
+ }
2008
+ const [views = EMPTY_VIEWS, setViews] = useControllableState$1({
2009
+ prop: viewsProp,
2010
+ defaultProp: initialViewsRef.current,
2011
+ onChange: onViewsChange
2012
+ });
2013
+ // Persists every change back to `storageKey` -- same "only when this hook owns
2014
+ // persistence" gate as the lazy read above. Skips the FIRST render (the effect would
2015
+ // otherwise immediately re-write back the exact bytes it just read, or stomp an
2016
+ // empty default over a controlled consumer's storage before it ever mounts).
2017
+ const skipFirstPersistRef = useRef(true);
2018
+ useEffect(()=>{
2019
+ if (viewsProp !== undefined || !storageKey || typeof window === "undefined") return;
2020
+ if (skipFirstPersistRef.current) {
2021
+ skipFirstPersistRef.current = false;
2022
+ return;
2023
+ }
2024
+ try {
2025
+ window.localStorage.setItem(storageKey, serializeDataTableViews(views));
2026
+ } catch {
2027
+ // Storage full/blocked -- the view still exists in memory for this session.
2028
+ }
2029
+ }, [
2030
+ views,
2031
+ viewsProp,
2032
+ storageKey
2033
+ ]);
2034
+ const [activeViewId = null, setActiveViewId] = useControllableState$1({
2035
+ prop: activeViewIdProp,
2036
+ defaultProp: defaultActiveViewId,
2037
+ onChange: onActiveViewIdChange
2038
+ });
2039
+ const activeView = views.find((view)=>view.id === activeViewId) ?? null;
2040
+ const createView = useCallback((name, state)=>{
2041
+ const id = generateViewId();
2042
+ setViews((current = EMPTY_VIEWS)=>upsertDataTableView(current, {
2043
+ id,
2044
+ name,
2045
+ state
2046
+ }));
2047
+ setActiveViewId(id);
2048
+ return id;
2049
+ }, [
2050
+ setViews,
2051
+ setActiveViewId
2052
+ ]);
2053
+ const updateView = useCallback((id, state)=>{
2054
+ setViews((current = EMPTY_VIEWS)=>{
2055
+ const existing = current.find((view)=>view.id === id);
2056
+ return existing ? upsertDataTableView(current, {
2057
+ ...existing,
2058
+ state
2059
+ }) : current;
2060
+ });
2061
+ }, [
2062
+ setViews
2063
+ ]);
2064
+ const renameView = useCallback((id, name)=>setViews((current = EMPTY_VIEWS)=>renameDataTableView(current, id, name)), [
2065
+ setViews
2066
+ ]);
2067
+ const deleteView = useCallback((id)=>{
2068
+ setViews((current = EMPTY_VIEWS)=>removeDataTableView(current, id));
2069
+ setActiveViewId((current)=>current === id ? null : current);
2070
+ }, [
2071
+ setViews,
2072
+ setActiveViewId
2073
+ ]);
2074
+ const selectView = useCallback((id)=>setActiveViewId(id), [
2075
+ setActiveViewId
2076
+ ]);
2077
+ return {
2078
+ views,
2079
+ activeViewId,
2080
+ activeView,
2081
+ createView,
2082
+ updateView,
2083
+ renameView,
2084
+ deleteView,
2085
+ selectView
2086
+ };
2087
+ }
2088
+
1813
2089
  function r(e) {
1814
2090
  var t, f, n = "";
1815
2091
  if ("string" == typeof e || "number" == typeof e) n += e;
@@ -2335,76 +2611,121 @@ function RatingCell({ value, maxRating = 5 }) {
2335
2611
  });
2336
2612
  }
2337
2613
 
2338
- /** The subset of `useDataTableContext()` every "resolve which columns to actually
2339
- * render, in what order, under what label/width" helper below needs -- narrowed so
2340
- * each helper's own signature documents exactly what it reads, rather than the whole
2341
- * table API. */ function useEffectiveColumns(columns) {
2342
- const { visibleColumnOrder, getColumnLabel, getColumnWidth } = useDataTableContext();
2614
+ /** Fallback width (px) used to compute a pinned column's cumulative sticky offset when
2615
+ * it has no explicit `width` and hasn't been drag-resized -- pinning needs a STATIC
2616
+ * number to put in an inline `insetInlineStart`, and `table-layout: fixed` only
2617
+ * decides how an unset column's share of the leftover width actually renders at paint
2618
+ * time (something no amount of JS math here can know ahead of it). Matches
2619
+ * `clampColumnWidth`'s own floor (react-headless's `dom.ts`) -- a real pinned column
2620
+ * narrower than this only needs its offset "wide enough not to overlap", not exact. */ const DEFAULT_PINNED_COLUMN_WIDTH = 180;
2621
+ /**
2622
+ * Every column this table actually renders, in RENDER order: any pinned columns first
2623
+ * (their own relative order preserved from `visibleColumnOrder`, not a separate pin-
2624
+ * order of their own), then everything else in normal order -- resolved label/width,
2625
+ * and this file's own sticky-left bookkeeping (`pinned`/`pinOffset`/`pinnedEdge`). Read
2626
+ * by the header, the body, the ColumnCalc footer AND the `<colgroup>` alike -- all four
2627
+ * must agree on this exact order, since a `<colgroup>`'s `<col>`s are matched
2628
+ * POSITIONALLY to every row's cells; splitting "order" (here) from "which one is
2629
+ * pinned" into two independently-ordered helpers would let a `<colgroup>` built from
2630
+ * one and a header row built from the other drift out of sync under `table-layout:
2631
+ * fixed`'s own column-to-cell matching.
2632
+ */ function useEffectiveColumns(columns, options) {
2633
+ const { visibleColumnOrder, getColumnLabel, getColumnWidth, pinnedColumnKeys } = useDataTableContext();
2634
+ const { selectable, stickyFirst } = options;
2343
2635
  return React.useMemo(()=>{
2344
2636
  const byKey = new Map(columns.map((column)=>[
2345
2637
  column.key,
2346
2638
  column
2347
2639
  ]));
2348
- return visibleColumnOrder.map((key)=>byKey.get(key)).filter((column)=>Boolean(column)).map((column)=>({
2640
+ const visibleKeySet = new Set(visibleColumnOrder);
2641
+ const explicitPins = new Set((pinnedColumnKeys ?? []).filter((key)=>byKey.has(key) && visibleKeySet.has(key)));
2642
+ // Legacy fallback (DataTable.columns.test.tsx's own `stickyFirst` tests, predating
2643
+ // per-column pin): no EXPLICIT pin at all, `stickyFirst` pins exactly the first
2644
+ // visible column -- the single-column behavior this file now generalizes to a
2645
+ // whole pinned GROUP.
2646
+ const pinnedSet = explicitPins.size > 0 ? explicitPins : stickyFirst && visibleColumnOrder.length > 0 ? new Set([
2647
+ visibleColumnOrder[0]
2648
+ ]) : explicitPins;
2649
+ // Stable partition: pinned columns first (their relative order among THEMSELVES
2650
+ // preserved from `visibleColumnOrder`), then every other column, also in its
2651
+ // existing relative order. `Array.prototype.sort` is spec-stable (ES2019+).
2652
+ const ordered = [
2653
+ ...visibleColumnOrder
2654
+ ].sort((a, b)=>{
2655
+ const aPinned = pinnedSet.has(a) ? 0 : 1;
2656
+ const bPinned = pinnedSet.has(b) ? 0 : 1;
2657
+ return aPinned - bPinned;
2658
+ });
2659
+ let cursor = selectable ? 40 : 0;
2660
+ let pinnedCount = 0;
2661
+ const entries = ordered.map((key)=>byKey.get(key)).filter((column)=>Boolean(column)).map((column)=>{
2662
+ const width = getColumnWidth(column.key) ?? column.width;
2663
+ const pinned = pinnedSet.has(column.key);
2664
+ const pinOffset = pinned ? cursor : 0;
2665
+ if (pinned) {
2666
+ cursor += width ?? DEFAULT_PINNED_COLUMN_WIDTH;
2667
+ pinnedCount += 1;
2668
+ }
2669
+ return {
2349
2670
  column,
2350
2671
  label: getColumnLabel(column.key, column.header),
2351
- width: getColumnWidth(column.key) ?? column.width
2352
- }));
2672
+ width,
2673
+ pinned,
2674
+ pinOffset,
2675
+ pinnedEdge: false
2676
+ };
2677
+ });
2678
+ if (pinnedCount > 0) entries[pinnedCount - 1].pinnedEdge = true;
2679
+ return entries;
2353
2680
  }, [
2354
2681
  columns,
2355
2682
  visibleColumnOrder,
2356
2683
  getColumnLabel,
2357
- getColumnWidth
2358
- ]);
2359
- }
2360
- /** Sticky positioning for the leading/trailing visible cell of a row (header, body, or
2361
- * footer) -- `stickyFirst` pins the SELECTION cell when the table has one, plus the
2362
- * first data column; `stickyLast` always pins the last VISIBLE data column. Only the
2363
- * OUTERMOST sticky cell on each side gets `data-sticky-edge(-end)` -- that's the one
2364
- * whose shadow should reveal on scroll, not every sticky cell.
2365
- *
2366
- * When both a selection column and a sticky first data column are present, the edge
2367
- * is the data column (e.g. Company), not the 40px selection cell: the data column is
2368
- * ALSO `position: sticky`, sitting immediately to its right, so a shadow cast from the
2369
- * selection cell's trailing edge would render directly underneath that opaque sticky
2370
- * cell and never actually be visible. Putting `data-sticky-edge` on the trailing-most
2371
- * sticky-left cell is what makes the shadow render somewhere unobstructed -- past the
2372
- * whole pinned group, not between two members of it. */ function stickyProps(options) {
2373
- const { selectable, stickyFirst, stickyLast, isSelectionCell, isFirst, isLast } = options;
2374
- if (stickyFirst && isSelectionCell) {
2375
- // Still sticky either way. The edge marker only belongs here when there is no
2376
- // sticky first DATA column to hand it to instead (i.e. no columns at all) --
2377
- // otherwise that data column is the true trailing edge of the pinned group.
2378
- return selectable ? {
2379
- "data-sticky": "",
2380
- style: {
2381
- insetInlineStart: 0
2382
- }
2383
- } : {
2384
- "data-sticky": "",
2385
- "data-sticky-edge": "",
2386
- style: {
2387
- insetInlineStart: 0
2388
- }
2389
- };
2390
- }
2391
- if (stickyFirst && isFirst && !selectable) {
2392
- return {
2393
- "data-sticky": "",
2394
- "data-sticky-edge": "",
2395
- style: {
2396
- insetInlineStart: 0
2397
- }
2398
- };
2399
- }
2400
- if (stickyFirst && isFirst && selectable) {
2401
- // The trailing edge of the pinned group -- past the 40px selection cell, so this
2402
- // is where the scroll shadow actually renders unobstructed.
2684
+ getColumnWidth,
2685
+ pinnedColumnKeys,
2686
+ selectable,
2687
+ stickyFirst
2688
+ ]);
2689
+ }
2690
+ /** The 40px selection column's own sticky-left state -- `stickyFirst` (the table-wide
2691
+ * prop) alone undercounts "should the checkbox column stick" the moment ANY per-column
2692
+ * pin is live, controlled or not, since the live pinned SET only exists once
2693
+ * `DataTablePrimitiveRoot`'s provider has mounted (an uncontrolled consumer never
2694
+ * hands its pinned columns back up as a prop for `DataTable.tsx` to read directly).
2695
+ * Mirrors `DataTable.tsx`'s own `resolvedStickyLast = stickyLast ||
2696
+ * columns.some(actions)` widening, just resolved one level down (live STATE, not
2697
+ * static column config) -- read via context from a component under the provider, not
2698
+ * computed above it. */ function useSelectionStickyFirst(stickyFirst) {
2699
+ const { pinnedColumnKeys } = useDataTableContext();
2700
+ return stickyFirst || (pinnedColumnKeys?.length ?? 0) > 0;
2701
+ }
2702
+ /** Sticky positioning for the 40px SELECTION cell only (header/body/footer) -- data
2703
+ * columns use their own `pinned`/`pinOffset`/`pinnedEdge` (`useEffectiveColumns`,
2704
+ * above) instead, via `pinnedCellProps` below. The selection cell is always the
2705
+ * leading edge of the pinned-left group when it's sticky at all, so it never itself
2706
+ * carries `data-sticky-edge` -- the trailing-most pinned DATA column does (its own
2707
+ * `pinnedEdge`), the same split the original single-column `stickyFirst` already drew
2708
+ * between the selection cell and Company/Name. */ function selectionStickyProps(stickyFirst) {
2709
+ return stickyFirst ? {
2710
+ "data-sticky": "",
2711
+ style: {
2712
+ insetInlineStart: 0
2713
+ }
2714
+ } : {};
2715
+ }
2716
+ /** Sticky positioning for one DATA cell (header, body, or ColumnCalc footer) -- pinned-
2717
+ * left (this column's own `pinOffset`, `data-sticky-edge` only on the group's
2718
+ * trailing-most member) takes priority over sticky-right (`stickyLast`, always just
2719
+ * the last VISIBLE column, e.g. an `actions` column) since a column can only stick to
2720
+ * one edge. */ function pinnedCellProps(entry, stickyLast, isLast) {
2721
+ if (entry.pinned) {
2403
2722
  return {
2404
2723
  "data-sticky": "",
2405
- "data-sticky-edge": "",
2724
+ ...entry.pinnedEdge ? {
2725
+ "data-sticky-edge": ""
2726
+ } : {},
2406
2727
  style: {
2407
- insetInlineStart: 40
2728
+ insetInlineStart: entry.pinOffset
2408
2729
  }
2409
2730
  };
2410
2731
  }
@@ -2744,265 +3065,555 @@ const TextFieldTextarea = /*#__PURE__*/ forwardRef(({ className, autoresize = tr
2744
3065
  });
2745
3066
  TextFieldTextarea.displayName = "TextFieldTextarea";
2746
3067
 
2747
- /** The raw value a live editor reads, distinct from `DataTableColumn.value` (the
2748
- * accessor sort/search/filter/export share): `editValue` returns whatever TYPE the
2749
- * editor itself expects -- a real `boolean` for `editor: "switch"`/`type:
2750
- * "checkbox"`, a `number` for `type: "rating"`, the option's own string for `select`/
2751
- * `reference`/`actor` -- where `value`/`render` may format that same field for
2752
- * display. Falls back to the table's own accessor when the column supplies none. */ function resolveEditValue(column, row, fallback) {
2753
- return column.editValue ? column.editValue(row) : fallback;
2754
- }
2755
- function EditPendingIndicator() {
2756
- return /*#__PURE__*/ jsxs("svg", {
2757
- className: "seed-data-table-edit-pending",
2758
- width: 14,
2759
- height: 14,
2760
- viewBox: "0 0 16 16",
2761
- fill: "none",
2762
- role: "presentation",
2763
- "aria-hidden": "true",
2764
- children: [
2765
- /*#__PURE__*/ jsx("circle", {
2766
- cx: 8,
2767
- cy: 8,
2768
- r: 6.5,
2769
- stroke: "currentColor",
2770
- strokeOpacity: 0.25,
2771
- strokeWidth: 2
2772
- }),
2773
- /*#__PURE__*/ jsx("path", {
2774
- d: "M14.5 8a6.5 6.5 0 0 0-6.5-6.5",
2775
- stroke: "currentColor",
2776
- strokeWidth: 2,
2777
- strokeLinecap: "round"
2778
- })
2779
- ]
2780
- });
2781
- }
2782
- function EditError({ message }) {
2783
- return /*#__PURE__*/ jsx("span", {
2784
- className: "seed-data-table-edit-error",
2785
- children: message
2786
- });
2787
- }
2788
- ////////////////////////////////////////////////////////////////////////////////////
2789
- // -- plain editable: text/number (seed TextField) and date/datetime (a native input,
2790
- // styled the same way, per `cellType`) -- both double-click gated, Enter/blur
2791
- // commits, Escape cancels. -------------------------------------------------------
2792
- function TextEditor({ column, value, pending, onCommit, onCancel }) {
2793
- const [draft, setDraft] = React.useState(()=>value == null ? "" : String(value));
2794
- return(// `size="medium"` (seed's smallest fixed `TextField` size, 40px min-height,
2795
- // `text-input.ts`'s own token) -- the default `size="large"` overflowed a 36px
2796
- // table row (same class of fix as `toolbar.tsx`'s `SearchBox`, which made the
2797
- // same call for its own 32px-ish search box).
2798
- /*#__PURE__*/ jsx(TextFieldRoot, {
2799
- size: "medium",
2800
- style: {
2801
- width: "100%"
2802
- },
2803
- onClick: (event)=>event.stopPropagation(),
2804
- onKeyDown: (event)=>event.stopPropagation(),
2805
- children: /*#__PURE__*/ jsx(TextFieldInput, {
2806
- autoFocus: true,
2807
- type: column.cellType === "number" ? "number" : "text",
2808
- "aria-label": `Edit ${column.key}`,
2809
- value: draft,
2810
- disabled: pending,
2811
- onChange: (event)=>setDraft(event.target.value),
2812
- onBlur: ()=>onCommit(draft),
2813
- onKeyDown: (event)=>{
2814
- if (event.key === "Enter") onCommit(draft);
2815
- else if (event.key === "Escape") onCancel();
2816
- }
2817
- })
2818
- }));
3068
+ __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: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-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__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__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))}");
3069
+
3070
+ const createClassName = (className, variants, compoundVariants = [])=>{
3071
+ const variantKeys = Object.keys(variants);
3072
+ const variantValues = variantKeys.map((key)=>variants[key]);
3073
+ const variantClassName = variantKeys.map((key, index)=>`${className}--${key}_${variantValues[index]}`).join(" ");
3074
+ const compountVariantClassName = compoundVariants.filter((compoundVariant)=>Object.keys(compoundVariant).every((key)=>compoundVariant[key] === variants[key])).map((compoundVariant)=>`${className}--${Object.keys(compoundVariant).map((key)=>{
3075
+ return `${key}_${compoundVariant[key]}`;
3076
+ }).join("-")}`).join(" ");
3077
+ return [
3078
+ className,
3079
+ variantClassName,
3080
+ compountVariantClassName
3081
+ ].filter(Boolean).join(" ");
3082
+ };
3083
+ function mergeVariants(a, b) {
3084
+ const result = {
3085
+ ...a
3086
+ };
3087
+ for(const k in b){
3088
+ if (b[k] != null) {
3089
+ result[k] = b[k];
3090
+ }
3091
+ }
3092
+ return result;
2819
3093
  }
2820
- function DateEditor({ column, value, pending, onCommit, onCancel }) {
2821
- const isDateTime = column.cellType === "datetime";
2822
- const [draft, setDraft] = React.useState(()=>isDateTime ? toDateTimeInputValue(value) : toDateInputValue(value));
2823
- return(// See `TextEditor`'s own comment above -- same `size="medium"` fix.
2824
- /*#__PURE__*/ jsx(TextFieldRoot, {
2825
- size: "medium",
2826
- style: {
2827
- width: "100%"
2828
- },
2829
- onClick: (event)=>event.stopPropagation(),
2830
- onKeyDown: (event)=>event.stopPropagation(),
2831
- children: /*#__PURE__*/ jsx(TextFieldInput, {
2832
- autoFocus: true,
2833
- type: isDateTime ? "datetime-local" : "date",
2834
- "aria-label": `Edit ${column.key}`,
2835
- value: draft,
2836
- disabled: pending,
2837
- onChange: (event)=>setDraft(event.target.value),
2838
- onBlur: ()=>onCommit(draft),
2839
- onKeyDown: (event)=>{
2840
- if (event.key === "Enter") onCommit(draft);
2841
- else if (event.key === "Escape") onCancel();
2842
- }
2843
- })
2844
- }));
3094
+ function splitVariantProps(props, variantMap) {
3095
+ const variantProps = {};
3096
+ const otherProps = {};
3097
+ for(const key in props){
3098
+ if (variantMap[key] != null) {
3099
+ variantProps[key] = props[key];
3100
+ } else {
3101
+ otherProps[key] = props[key];
3102
+ }
3103
+ }
3104
+ return [
3105
+ variantProps,
3106
+ otherProps
3107
+ ];
2845
3108
  }
2846
- ////////////////////////////////////////////////////////////////////////////////////
2847
- // -- editor: "select" -- double-click opens seed's own Select, auto-opened (seed's
2848
- // `SelectRoot` takes `defaultOpen` directly -- unlike crisp's closed-API `Select`,
2849
- // which needs a synthetic click on its own trigger to start open). -------------
2850
- function SelectEditor({ column, value, pending, onCommit, onCancel }) {
2851
- const options = column.options ?? [];
2852
- const current = value == null ? "" : String(value);
2853
- return /*#__PURE__*/ jsxs(SelectRoot, {
2854
- // Controlled open, per docs/examples/react/select/controlled-open.tsx --
2855
- // this editor is only ever mounted while the cell IS editing, so it always
2856
- // wants to start (and stay) open; `onOpenChange(false)` is what tells us
2857
- // the popover wants to close for ANY reason (Escape, an outside click, OR
2858
- // the internal auto-close a single-select commit already triggers), so
2859
- // `onCancel` alone now covers the Escape gap the old raw `document`
2860
- // listener existed for, without a second, independent listener.
2861
- open: true,
2862
- onOpenChange: (next)=>{
2863
- if (!next) onCancel();
2864
- },
2865
- disabled: pending,
2866
- value: current ? [
2867
- current
2868
- ] : [],
2869
- onValueChange: (next)=>onCommit(next[0] ?? ""),
2870
- children: [
2871
- /*#__PURE__*/ jsxs(SelectTrigger, {
2872
- "aria-label": `Edit ${column.key}`,
2873
- children: [
2874
- /*#__PURE__*/ jsx(SelectValue, {}),
2875
- /*#__PURE__*/ jsx(SelectPlaceholder, {
2876
- children: "Select…"
2877
- }),
2878
- /*#__PURE__*/ jsx(SelectSuffixIcon, {
2879
- svg: /*#__PURE__*/ jsx(IconChevronDownSmallLine_default, {})
2880
- })
2881
- ]
2882
- }),
2883
- /*#__PURE__*/ jsx(SelectPositioner, {
2884
- children: /*#__PURE__*/ jsx(SelectContent, {
2885
- children: /*#__PURE__*/ jsx(SelectScrollArea, {
2886
- children: options.map((option)=>/*#__PURE__*/ jsxs(SelectItem, {
2887
- value: option.value,
2888
- label: option.label,
2889
- children: [
2890
- /*#__PURE__*/ jsx(SelectItemBody, {
2891
- children: /*#__PURE__*/ jsx(SelectItemLabel, {})
2892
- }),
2893
- /*#__PURE__*/ jsx(SelectItemIndicator, {
2894
- selected: /*#__PURE__*/ jsx(IconCheckmarkFatFill_default, {})
2895
- })
2896
- ]
2897
- }, option.value))
2898
- })
2899
- })
2900
- })
2901
- ]
2902
- });
3109
+
3110
+ const dataTableSlotNames = [
3111
+ [
3112
+ "root",
3113
+ "seed-data-table__root"
3114
+ ],
3115
+ [
3116
+ "table",
3117
+ "seed-data-table__table"
3118
+ ],
3119
+ [
3120
+ "header",
3121
+ "seed-data-table__header"
3122
+ ],
3123
+ [
3124
+ "headerCell",
3125
+ "seed-data-table__headerCell"
3126
+ ],
3127
+ [
3128
+ "headerCellInner",
3129
+ "seed-data-table__headerCellInner"
3130
+ ],
3131
+ [
3132
+ "headerCellLabel",
3133
+ "seed-data-table__headerCellLabel"
3134
+ ],
3135
+ [
3136
+ "headerCellIcon",
3137
+ "seed-data-table__headerCellIcon"
3138
+ ],
3139
+ [
3140
+ "row",
3141
+ "seed-data-table__row"
3142
+ ],
3143
+ [
3144
+ "cell",
3145
+ "seed-data-table__cell"
3146
+ ],
3147
+ [
3148
+ "entity",
3149
+ "seed-data-table__entity"
3150
+ ],
3151
+ [
3152
+ "entityName",
3153
+ "seed-data-table__entityName"
3154
+ ],
3155
+ [
3156
+ "tagList",
3157
+ "seed-data-table__tagList"
3158
+ ],
3159
+ [
3160
+ "link",
3161
+ "seed-data-table__link"
3162
+ ],
3163
+ [
3164
+ "toolbar",
3165
+ "seed-data-table__toolbar"
3166
+ ],
3167
+ [
3168
+ "search",
3169
+ "seed-data-table__search"
3170
+ ],
3171
+ [
3172
+ "count",
3173
+ "seed-data-table__count"
3174
+ ],
3175
+ [
3176
+ "toolbarEnd",
3177
+ "seed-data-table__toolbarEnd"
3178
+ ],
3179
+ [
3180
+ "scrollArea",
3181
+ "seed-data-table__scrollArea"
3182
+ ],
3183
+ [
3184
+ "footer",
3185
+ "seed-data-table__footer"
3186
+ ],
3187
+ [
3188
+ "footerCell",
3189
+ "seed-data-table__footerCell"
3190
+ ],
3191
+ [
3192
+ "emptyState",
3193
+ "seed-data-table__emptyState"
3194
+ ],
3195
+ [
3196
+ "loadingState",
3197
+ "seed-data-table__loadingState"
3198
+ ],
3199
+ [
3200
+ "addRow",
3201
+ "seed-data-table__addRow"
3202
+ ],
3203
+ [
3204
+ "addRowCell",
3205
+ "seed-data-table__addRowCell"
3206
+ ],
3207
+ [
3208
+ "selectionCell",
3209
+ "seed-data-table__selectionCell"
3210
+ ]
3211
+ ];
3212
+ const defaultVariant$4 = {
3213
+ "surface": "card",
3214
+ "interactive": false
3215
+ };
3216
+ const compoundVariants$4 = [];
3217
+ const dataTableVariantMap = {
3218
+ "surface": [
3219
+ "card",
3220
+ "bleed"
3221
+ ],
3222
+ "interactive": [
3223
+ true,
3224
+ false
3225
+ ]
3226
+ };
3227
+ function dataTable(props) {
3228
+ return Object.fromEntries(dataTableSlotNames.map(([slot, className])=>{
3229
+ return [
3230
+ slot,
3231
+ createClassName(className, mergeVariants(defaultVariant$4, props), compoundVariants$4)
3232
+ ];
3233
+ }));
2903
3234
  }
2904
- ////////////////////////////////////////////////////////////////////////////////////
2905
- // -- editor: "reference" / "actor" -- same picker, search-enabled, each option
2906
- // carrying a small avatar (a photo for a real one, a generic person glyph for
2907
- // none) -- `actor` is `reference` over people specifically (crisp's own #449/
2908
- // #432 `ReferenceCell` shape), so the two share one component; only the search
2909
- // placeholder differs. Seed's `Select` has no built-in search combobox (unlike
2910
- // crisp's own `Select`), so this filters `refOptions` itself against a small
2911
- // `TextField` rendered as the content's own first row. --------------------------
2912
- function ReferenceEditor({ column, value, pending, onCommit, onCancel }) {
3235
+ Object.assign(dataTable, {
3236
+ splitVariantProps: (props)=>splitVariantProps(props, dataTableVariantMap)
3237
+ }); // @recipe(seed): data-table
3238
+
3239
+ // The one `createSlotRecipeContext(dataTable)` call every sibling file in this
3240
+ // directory shares -- `ClassNamesProvider` and `withContext` must be the SAME
3241
+ // binding everywhere (a slot component built from a second, independent call would
3242
+ // read a DIFFERENT React context and never see the classNames `DataTableInner`
3243
+ // provides), so this is the one place that calls `createSlotRecipeContext`, and every
3244
+ // other file (`DataTable.tsx`, `column-menu.tsx`, `resize.tsx`, `column-calc.tsx`,
3245
+ // `toolbar.tsx`, `body-cells.tsx`, `editors.tsx`) imports its exports from here rather
3246
+ // than re-deriving its own.
3247
+ const { ClassNamesProvider: ClassNamesProvider$1, withContext: withContext$1, useClassNames: useClassNames$1 } = createSlotRecipeContext(dataTable);
3248
+
3249
+ /**
3250
+ * The pure filter itself, split out of `useSelectSearch` below so it can be
3251
+ * mutation-tested directly (the same "small pure function beside its own test" shape
3252
+ * `filtering.ts`'s `matchesSearch`/`getColumnValue` already use in this directory) --
3253
+ * case-insensitive substring match, on whitespace-trimmed input. An empty/whitespace-
3254
+ * only query returns `options` unfiltered rather than matching nothing, the same
3255
+ * "no query yet" case crisp's own `select.tsx` special-cases (`searchable && q`).
3256
+ */ function filterSelectOptions(options, query, getText) {
3257
+ const q = query.trim().toLowerCase();
3258
+ if (!q) return options;
3259
+ return options.filter((option)=>getText(option).toLowerCase().includes(q));
3260
+ }
3261
+ /**
3262
+ * Filters `options` by `getText(option)` against a live query string -- the one
3263
+ * concern every searchable Select popover shares (crisp's own `select.tsx` computes
3264
+ * this inline; this hook is `filterSelectOptions` above plus the query state it reads,
3265
+ * kept generic over `T` so it works for `DataTableEditOption` today and a future
3266
+ * filter-value picker's own option shape without a second copy of the same filter).
3267
+ */ function useSelectSearch(options, getText) {
2913
3268
  const [query, setQuery] = React.useState("");
2914
- const options = column.refOptions ?? [];
2915
- const filtered = query.trim() ? options.filter((option)=>option.label.toLowerCase().includes(query.trim().toLowerCase())) : options;
2916
- const current = value == null ? "" : String(value);
2917
- const isActor = column.editor === "actor";
2918
- return /*#__PURE__*/ jsxs(SelectRoot, {
2919
- // See `SelectEditor`'s own comment above -- same controlled-open wiring.
2920
- open: true,
2921
- onOpenChange: (next)=>{
2922
- if (!next) onCancel();
3269
+ const filtered = React.useMemo(()=>filterSelectOptions(options, query, getText), [
3270
+ options,
3271
+ query,
3272
+ getText
3273
+ ]);
3274
+ return {
3275
+ query,
3276
+ setQuery,
3277
+ filtered
3278
+ };
3279
+ }
3280
+ /**
3281
+ * The search box itself -- crisp's own top-of-listbox filter field
3282
+ * (`select.tsx`'s `crisp-select-search`), styled with the SAME compact chrome
3283
+ * `toolbar.tsx`'s `SearchBox` already standardized for the table's own toolbar search
3284
+ * (seed's real `text-input.ts` outline-variant tokens, via the `dataTable` recipe's
3285
+ * own `search` slot -- not seed's documented `TextField`, measured too heavy for a
3286
+ * ~32px inline control; see `SearchBox`'s own comment for that measurement). Reusing
3287
+ * the SAME recipe slot here, rather than standing up a second near-identical one,
3288
+ * keeps every "small bordered search input" in this package pixel-identical by
3289
+ * construction -- the slot's `max-width: 280px` (a toolbar-row layout constraint, not
3290
+ * chrome) is overridden inline since this field fills its popover instead.
3291
+ *
3292
+ * Swallows every keydown except Escape before it can reach seed's own listbox
3293
+ * (`SelectContent`'s typeahead-by-keyboard): a search field that ALSO drives the
3294
+ * listbox's letter-jump would fight the query it is supposed to be typing into. Escape
3295
+ * passes through untouched -- `SelectRoot`'s own `onOpenChange` (in every caller) is
3296
+ * what actually closes the editor on Escape, not a handler on this field.
3297
+ *
3298
+ * `autoFocus` is applied imperatively (an effect + ref), not the native `autoFocus`
3299
+ * attribute -- lint (`a11y/noAutofocus`) bars that on a raw `<input>`, and this is the
3300
+ * same ref-and-focus shape crisp's own `select.tsx` already uses for its search field
3301
+ * (`searchRef.current?.focus({ preventScroll: true })` on open), not a workaround
3302
+ * invented for the lint rule.
3303
+ */ function SelectSearchField({ value, onChange, placeholder, "aria-label": ariaLabel, autoFocus, onKeyDown }) {
3304
+ const classNames = dataTable();
3305
+ const inputRef = React.useRef(null);
3306
+ React.useEffect(()=>{
3307
+ if (autoFocus) inputRef.current?.focus({
3308
+ preventScroll: true
3309
+ });
3310
+ }, [
3311
+ autoFocus
3312
+ ]);
3313
+ return /*#__PURE__*/ jsxs("span", {
3314
+ className: classNames.search,
3315
+ style: {
3316
+ display: "flex",
3317
+ maxWidth: "none",
3318
+ margin: 4
2923
3319
  },
2924
- disabled: pending,
2925
- value: current ? [
2926
- current
2927
- ] : [],
2928
- onValueChange: (next)=>onCommit(next[0] ?? ""),
2929
3320
  children: [
2930
- /*#__PURE__*/ jsxs(SelectTrigger, {
2931
- "aria-label": `Edit ${column.key}`,
2932
- children: [
2933
- /*#__PURE__*/ jsx(SelectValue, {}),
2934
- /*#__PURE__*/ jsx(SelectPlaceholder, {
2935
- children: isActor ? "Assign…" : "Select…"
2936
- }),
2937
- /*#__PURE__*/ jsx(SelectSuffixIcon, {
2938
- svg: /*#__PURE__*/ jsx(IconChevronDownSmallLine_default, {})
2939
- })
2940
- ]
3321
+ /*#__PURE__*/ jsx(IconMagnifyingglassLine_default, {
3322
+ "aria-hidden": "true"
2941
3323
  }),
2942
- /*#__PURE__*/ jsx(SelectPositioner, {
2943
- children: /*#__PURE__*/ jsxs(SelectContent, {
2944
- children: [
2945
- /*#__PURE__*/ jsx(TextFieldRoot, {
2946
- size: "medium",
2947
- style: {
2948
- margin: 4
2949
- },
2950
- onKeyDown: (event)=>{
2951
- // The listbox's own typeahead/arrow handling lives on THIS field's
2952
- // sibling items, not on the field itself -- stop everything except
2953
- // Escape from also driving the listbox's own keyboard nav. Escape
2954
- // itself needs no special-casing to "reach" anything any more: it's
2955
- // SelectContent's own `DismissibleLayer` (Select.tsx) that closes the
2956
- // listbox, and that dismissal is a document-level listener outside
2957
- // this React subtree entirely -- a synthetic `stopPropagation` here
2958
- // was never able to block it either way, so the controlled `open`
2959
- // above (via `onOpenChange`) is what actually ends this cell's edit.
2960
- if (event.key !== "Escape") event.stopPropagation();
2961
- },
2962
- children: /*#__PURE__*/ jsx(TextFieldInput, {
2963
- autoFocus: true,
2964
- "aria-label": isActor ? "Search people" : `Search ${column.key}`,
2965
- placeholder: isActor ? "Search people…" : "Search…",
2966
- value: query,
2967
- onChange: (event)=>setQuery(event.target.value)
2968
- })
2969
- }),
2970
- /*#__PURE__*/ jsxs(SelectScrollArea, {
2971
- children: [
2972
- filtered.length === 0 && /*#__PURE__*/ jsx("div", {
2973
- style: {
2974
- padding: "6px 12px",
2975
- opacity: 0.63,
2976
- fontSize: 13
2977
- },
2978
- children: isActor ? "No users" : "No results"
2979
- }),
2980
- filtered.map((option)=>/*#__PURE__*/ jsxs(SelectItem, {
2981
- value: option.value,
2982
- label: option.label,
2983
- children: [
2984
- /*#__PURE__*/ jsxs(SelectItemBody, {
2985
- children: [
2986
- /*#__PURE__*/ jsx(ReferenceAvatar, {
2987
- avatar: option.avatar
2988
- }),
2989
- /*#__PURE__*/ jsx(SelectItemLabel, {})
2990
- ]
2991
- }),
2992
- /*#__PURE__*/ jsx(SelectItemIndicator, {
2993
- selected: /*#__PURE__*/ jsx(IconCheckmarkFatFill_default, {})
2994
- })
2995
- ]
2996
- }, option.value))
2997
- ]
2998
- })
2999
- ]
3000
- })
3324
+ /*#__PURE__*/ jsx("input", {
3325
+ ref: inputRef,
3326
+ type: "text",
3327
+ value: value,
3328
+ onChange: (event)=>onChange(event.target.value),
3329
+ placeholder: placeholder,
3330
+ "aria-label": ariaLabel ?? placeholder,
3331
+ onClick: (event)=>event.stopPropagation(),
3332
+ onKeyDown: (event)=>{
3333
+ if (event.key !== "Escape") event.stopPropagation();
3334
+ onKeyDown?.(event);
3335
+ }
3001
3336
  })
3002
3337
  ]
3003
3338
  });
3004
3339
  }
3005
- function ReferenceAvatar({ avatar }) {
3340
+ /** The empty state shown when a search matches nothing -- crisp's own `searchable`
3341
+ * `emptyLabel` (`crisp-select-empty`), reproduced at the same inset/opacity/size the
3342
+ * ad-hoc `ReferenceEditor` "No users"/"No results" row already used before this file
3343
+ * existed. */ function SelectSearchEmpty({ children }) {
3344
+ return /*#__PURE__*/ jsx("div", {
3345
+ style: {
3346
+ padding: "6px 12px",
3347
+ opacity: 0.63,
3348
+ fontSize: 13
3349
+ },
3350
+ children: children
3351
+ });
3352
+ }
3353
+
3354
+ /** The raw value a live editor reads, distinct from `DataTableColumn.value` (the
3355
+ * accessor sort/search/filter/export share): `editValue` returns whatever TYPE the
3356
+ * editor itself expects -- a real `boolean` for `editor: "switch"`/`type:
3357
+ * "checkbox"`, a `number` for `type: "rating"`, the option's own string for `select`/
3358
+ * `reference`/`actor` -- where `value`/`render` may format that same field for
3359
+ * display. Falls back to the table's own accessor when the column supplies none. */ function resolveEditValue(column, row, fallback) {
3360
+ return column.editValue ? column.editValue(row) : fallback;
3361
+ }
3362
+ function EditPendingIndicator() {
3363
+ return /*#__PURE__*/ jsxs("svg", {
3364
+ className: "seed-data-table-edit-pending",
3365
+ width: 14,
3366
+ height: 14,
3367
+ viewBox: "0 0 16 16",
3368
+ fill: "none",
3369
+ role: "presentation",
3370
+ "aria-hidden": "true",
3371
+ children: [
3372
+ /*#__PURE__*/ jsx("circle", {
3373
+ cx: 8,
3374
+ cy: 8,
3375
+ r: 6.5,
3376
+ stroke: "currentColor",
3377
+ strokeOpacity: 0.25,
3378
+ strokeWidth: 2
3379
+ }),
3380
+ /*#__PURE__*/ jsx("path", {
3381
+ d: "M14.5 8a6.5 6.5 0 0 0-6.5-6.5",
3382
+ stroke: "currentColor",
3383
+ strokeWidth: 2,
3384
+ strokeLinecap: "round"
3385
+ })
3386
+ ]
3387
+ });
3388
+ }
3389
+ function EditError({ message }) {
3390
+ return /*#__PURE__*/ jsx("span", {
3391
+ className: "seed-data-table-edit-error",
3392
+ children: message
3393
+ });
3394
+ }
3395
+ ////////////////////////////////////////////////////////////////////////////////////
3396
+ // -- plain editable: text/number (seed TextField) and date/datetime (a native input,
3397
+ // styled the same way, per `cellType`) -- both double-click gated, Enter/blur
3398
+ // commits, Escape cancels. -------------------------------------------------------
3399
+ function TextEditor({ column, value, pending, onCommit, onCancel }) {
3400
+ const [draft, setDraft] = React.useState(()=>value == null ? "" : String(value));
3401
+ return(// `size="medium"` (seed's smallest fixed `TextField` size, 40px min-height,
3402
+ // `text-input.ts`'s own token) -- the default `size="large"` overflowed a 36px
3403
+ // table row (same class of fix as `toolbar.tsx`'s `SearchBox`, which made the
3404
+ // same call for its own 32px-ish search box).
3405
+ /*#__PURE__*/ jsx(TextFieldRoot, {
3406
+ size: "medium",
3407
+ style: {
3408
+ width: "100%"
3409
+ },
3410
+ onClick: (event)=>event.stopPropagation(),
3411
+ onKeyDown: (event)=>event.stopPropagation(),
3412
+ children: /*#__PURE__*/ jsx(TextFieldInput, {
3413
+ autoFocus: true,
3414
+ type: column.cellType === "number" ? "number" : "text",
3415
+ "aria-label": `Edit ${column.key}`,
3416
+ value: draft,
3417
+ disabled: pending,
3418
+ onChange: (event)=>setDraft(event.target.value),
3419
+ onBlur: ()=>onCommit(draft),
3420
+ onKeyDown: (event)=>{
3421
+ if (event.key === "Enter") onCommit(draft);
3422
+ else if (event.key === "Escape") onCancel();
3423
+ }
3424
+ })
3425
+ }));
3426
+ }
3427
+ function DateEditor({ column, value, pending, onCommit, onCancel }) {
3428
+ const isDateTime = column.cellType === "datetime";
3429
+ const [draft, setDraft] = React.useState(()=>isDateTime ? toDateTimeInputValue(value) : toDateInputValue(value));
3430
+ return(// See `TextEditor`'s own comment above -- same `size="medium"` fix.
3431
+ /*#__PURE__*/ jsx(TextFieldRoot, {
3432
+ size: "medium",
3433
+ style: {
3434
+ width: "100%"
3435
+ },
3436
+ onClick: (event)=>event.stopPropagation(),
3437
+ onKeyDown: (event)=>event.stopPropagation(),
3438
+ children: /*#__PURE__*/ jsx(TextFieldInput, {
3439
+ autoFocus: true,
3440
+ type: isDateTime ? "datetime-local" : "date",
3441
+ "aria-label": `Edit ${column.key}`,
3442
+ value: draft,
3443
+ disabled: pending,
3444
+ onChange: (event)=>setDraft(event.target.value),
3445
+ onBlur: ()=>onCommit(draft),
3446
+ onKeyDown: (event)=>{
3447
+ if (event.key === "Enter") onCommit(draft);
3448
+ else if (event.key === "Escape") onCancel();
3449
+ }
3450
+ })
3451
+ }));
3452
+ }
3453
+ ////////////////////////////////////////////////////////////////////////////////////
3454
+ // -- editor: "select" -- double-click opens seed's own Select, auto-opened (seed's
3455
+ // `SelectRoot` takes `defaultOpen` directly -- unlike crisp's closed-API `Select`,
3456
+ // which needs a synthetic click on its own trigger to start open). A long option
3457
+ // list gets crisp's own search-on-top (`select-search.tsx`) for free, past
3458
+ // `SELECT_EDITOR_SEARCH_THRESHOLD` -- fewer options than that and a filter field
3459
+ // is pure noise above a list you can already scan (crisp's own `searchable` is
3460
+ // opt-in for the identical reason); `column.editorSearchable` overrides either way.
3461
+ /** Above this many options, `SelectEditor` turns its search field on by default --
3462
+ * below it, a filter box adds more chrome than it saves (nothing here claims this
3463
+ * exact number is measured; it is a judgment call, which is why `editorSearchable`
3464
+ * exists to override it either direction). */ const SELECT_EDITOR_SEARCH_THRESHOLD = 8;
3465
+ function SelectEditor({ column, value, pending, onCommit, onCancel }) {
3466
+ const options = column.options ?? [];
3467
+ const current = value == null ? "" : String(value);
3468
+ const searchable = column.editorSearchable ?? options.length > SELECT_EDITOR_SEARCH_THRESHOLD;
3469
+ const { query, setQuery, filtered } = useSelectSearch(options, (option)=>option.label);
3470
+ const visible = searchable ? filtered : options;
3471
+ return /*#__PURE__*/ jsxs(SelectRoot, {
3472
+ // Controlled open, per docs/examples/react/select/controlled-open.tsx --
3473
+ // this editor is only ever mounted while the cell IS editing, so it always
3474
+ // wants to start (and stay) open; `onOpenChange(false)` is what tells us
3475
+ // the popover wants to close for ANY reason (Escape, an outside click, OR
3476
+ // the internal auto-close a single-select commit already triggers), so
3477
+ // `onCancel` alone now covers the Escape gap the old raw `document`
3478
+ // listener existed for, without a second, independent listener.
3479
+ open: true,
3480
+ onOpenChange: (next)=>{
3481
+ if (!next) onCancel();
3482
+ },
3483
+ disabled: pending,
3484
+ value: current ? [
3485
+ current
3486
+ ] : [],
3487
+ onValueChange: (next)=>onCommit(next[0] ?? ""),
3488
+ children: [
3489
+ /*#__PURE__*/ jsxs(SelectTrigger, {
3490
+ "aria-label": `Edit ${column.key}`,
3491
+ children: [
3492
+ /*#__PURE__*/ jsx(SelectValue, {}),
3493
+ /*#__PURE__*/ jsx(SelectPlaceholder, {
3494
+ children: "Select…"
3495
+ }),
3496
+ /*#__PURE__*/ jsx(SelectSuffixIcon, {
3497
+ svg: /*#__PURE__*/ jsx(IconChevronDownSmallLine_default, {})
3498
+ })
3499
+ ]
3500
+ }),
3501
+ /*#__PURE__*/ jsx(SelectPositioner, {
3502
+ children: /*#__PURE__*/ jsxs(SelectContent, {
3503
+ children: [
3504
+ searchable && /*#__PURE__*/ jsx(SelectSearchField, {
3505
+ autoFocus: true,
3506
+ value: query,
3507
+ onChange: setQuery,
3508
+ placeholder: column.editorSearchPlaceholder ?? "Search…",
3509
+ "aria-label": `Search ${column.key}`
3510
+ }),
3511
+ /*#__PURE__*/ jsxs(SelectScrollArea, {
3512
+ children: [
3513
+ searchable && visible.length === 0 && /*#__PURE__*/ jsx(SelectSearchEmpty, {
3514
+ children: column.editorEmptyLabel ?? "No results"
3515
+ }),
3516
+ visible.map((option)=>/*#__PURE__*/ jsxs(SelectItem, {
3517
+ value: option.value,
3518
+ label: option.label,
3519
+ children: [
3520
+ /*#__PURE__*/ jsx(SelectItemBody, {
3521
+ children: /*#__PURE__*/ jsx(SelectItemLabel, {})
3522
+ }),
3523
+ /*#__PURE__*/ jsx(SelectItemIndicator, {
3524
+ selected: /*#__PURE__*/ jsx(IconCheckmarkFatFill_default, {})
3525
+ })
3526
+ ]
3527
+ }, option.value))
3528
+ ]
3529
+ })
3530
+ ]
3531
+ })
3532
+ })
3533
+ ]
3534
+ });
3535
+ }
3536
+ ////////////////////////////////////////////////////////////////////////////////////
3537
+ // -- editor: "reference" / "actor" -- same picker, ALWAYS search-enabled (unlike
3538
+ // `SelectEditor`'s threshold -- a reference/actor list is people/records, search
3539
+ // is the point regardless of how few there are today), each option carrying a
3540
+ // small avatar (a photo for a real one, a generic person glyph for none) --
3541
+ // `actor` is `reference` over people specifically (crisp's own #449/#432
3542
+ // `ReferenceCell` shape), so the two share one component; only the search
3543
+ // placeholder/empty copy differs. Seed's `Select` has no built-in search combobox
3544
+ // (unlike crisp's own `Select`), so this uses the shared `select-search.tsx`
3545
+ // field/hook -- the same one `SelectEditor` above uses -- rather than its own
3546
+ // copy of the query/filter/empty triangle. --------------------------------------
3547
+ function ReferenceEditor({ column, value, pending, onCommit, onCancel }) {
3548
+ const options = column.refOptions ?? [];
3549
+ const { query, setQuery, filtered } = useSelectSearch(options, (option)=>option.label);
3550
+ const current = value == null ? "" : String(value);
3551
+ const isActor = column.editor === "actor";
3552
+ return /*#__PURE__*/ jsxs(SelectRoot, {
3553
+ // See `SelectEditor`'s own comment above -- same controlled-open wiring.
3554
+ open: true,
3555
+ onOpenChange: (next)=>{
3556
+ if (!next) onCancel();
3557
+ },
3558
+ disabled: pending,
3559
+ value: current ? [
3560
+ current
3561
+ ] : [],
3562
+ onValueChange: (next)=>onCommit(next[0] ?? ""),
3563
+ children: [
3564
+ /*#__PURE__*/ jsxs(SelectTrigger, {
3565
+ "aria-label": `Edit ${column.key}`,
3566
+ children: [
3567
+ /*#__PURE__*/ jsx(SelectValue, {}),
3568
+ /*#__PURE__*/ jsx(SelectPlaceholder, {
3569
+ children: isActor ? "Assign…" : "Select…"
3570
+ }),
3571
+ /*#__PURE__*/ jsx(SelectSuffixIcon, {
3572
+ svg: /*#__PURE__*/ jsx(IconChevronDownSmallLine_default, {})
3573
+ })
3574
+ ]
3575
+ }),
3576
+ /*#__PURE__*/ jsx(SelectPositioner, {
3577
+ children: /*#__PURE__*/ jsxs(SelectContent, {
3578
+ children: [
3579
+ /*#__PURE__*/ jsx(SelectSearchField, {
3580
+ autoFocus: true,
3581
+ value: query,
3582
+ onChange: setQuery,
3583
+ placeholder: column.refSearchPlaceholder ?? (isActor ? "Search people…" : "Search…"),
3584
+ "aria-label": isActor ? "Search people" : `Search ${column.key}`
3585
+ }),
3586
+ /*#__PURE__*/ jsxs(SelectScrollArea, {
3587
+ children: [
3588
+ filtered.length === 0 && /*#__PURE__*/ jsx(SelectSearchEmpty, {
3589
+ children: column.refEmptyLabel ?? (isActor ? "No users" : "No results")
3590
+ }),
3591
+ filtered.map((option)=>/*#__PURE__*/ jsxs(SelectItem, {
3592
+ value: option.value,
3593
+ label: option.label,
3594
+ children: [
3595
+ /*#__PURE__*/ jsxs(SelectItemBody, {
3596
+ children: [
3597
+ /*#__PURE__*/ jsx(ReferenceAvatar, {
3598
+ avatar: option.avatar
3599
+ }),
3600
+ /*#__PURE__*/ jsx(SelectItemLabel, {})
3601
+ ]
3602
+ }),
3603
+ /*#__PURE__*/ jsx(SelectItemIndicator, {
3604
+ selected: /*#__PURE__*/ jsx(IconCheckmarkFatFill_default, {})
3605
+ })
3606
+ ]
3607
+ }, option.value))
3608
+ ]
3609
+ })
3610
+ ]
3611
+ })
3612
+ })
3613
+ ]
3614
+ });
3615
+ }
3616
+ function ReferenceAvatar({ avatar }) {
3006
3617
  return /*#__PURE__*/ jsxs(AvatarRoot, {
3007
3618
  size: "20",
3008
3619
  children: [
@@ -3162,20 +3773,16 @@ function RatingEditor({ column, value, pending, onCommit }) {
3162
3773
  return column.editor === "switch" || column.type === "rating" || column.type === "checkbox";
3163
3774
  }
3164
3775
  function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast }) {
3165
- const effective = useEffectiveColumns(columns);
3776
+ const effective = useEffectiveColumns(columns, {
3777
+ selectable,
3778
+ stickyFirst
3779
+ });
3166
3780
  const { startEdit } = useDataTableContext();
3167
3781
  return /*#__PURE__*/ jsx(Fragment, {
3168
- children: effective.map(({ column }, index)=>{
3169
- const isFirst = index === 0;
3782
+ children: effective.map((entry, index)=>{
3783
+ const { column } = entry;
3170
3784
  const isLast = index === effective.length - 1;
3171
- const sticky = stickyProps({
3172
- selectable,
3173
- stickyFirst,
3174
- stickyLast,
3175
- isSelectionCell: false,
3176
- isFirst,
3177
- isLast
3178
- });
3785
+ const sticky = pinnedCellProps(entry, stickyLast, isLast);
3179
3786
  // A gated `editable` column (everything except the always-live widgets)
3180
3787
  // opens on double-click -- Attio's own measured gesture (crisp's #411),
3181
3788
  // matched here rather than a single click, which stays "select this cell"
@@ -3351,7 +3958,7 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
3351
3958
  }
3352
3959
  }
3353
3960
 
3354
- const { ClassNamesProvider: ClassNamesProvider$1, withContext: withContext$1, useClassNames: useClassNames$1 } = createSlotRecipeContext(menu);
3961
+ const { ClassNamesProvider, withContext, useClassNames } = createSlotRecipeContext(menu);
3355
3962
  const { PropsProvider: ItemPropsProvider, useProps: useItemProps, withContext: withItemContext, ClassNamesProvider: ItemClassNamesProvider } = createSlotRecipeContext(menuItem);
3356
3963
  const withStateProps = createWithStateProps([
3357
3964
  useMenuContext
@@ -3362,7 +3969,7 @@ const withItemStateProps = createWithStateProps([
3362
3969
  const MenuRoot = (props)=>{
3363
3970
  const [variantProps, otherProps] = menu.splitVariantProps(props);
3364
3971
  const classNames = menu(variantProps);
3365
- return /*#__PURE__*/ jsx(ClassNamesProvider$1, {
3972
+ return /*#__PURE__*/ jsx(ClassNamesProvider, {
3366
3973
  value: classNames,
3367
3974
  children: /*#__PURE__*/ jsx(ItemPropsProvider, {
3368
3975
  value: {
@@ -3377,7 +3984,7 @@ const MenuRoot = (props)=>{
3377
3984
  Menu.Anchor;
3378
3985
  const MenuTrigger = Menu.Trigger;
3379
3986
  const MenuPositioner = /*#__PURE__*/ React.forwardRef(({ className, ...props }, ref)=>{
3380
- const classNames = useClassNames$1();
3987
+ const classNames = useClassNames();
3381
3988
  return /*#__PURE__*/ jsx(Menu.Positioner, {
3382
3989
  ref: ref,
3383
3990
  className: clsx(classNames.positioner, className),
@@ -3385,10 +3992,10 @@ const MenuPositioner = /*#__PURE__*/ React.forwardRef(({ className, ...props },
3385
3992
  });
3386
3993
  });
3387
3994
  MenuPositioner.displayName = "MenuPositioner";
3388
- const MenuContent = withContext$1(Menu.Content, "content");
3389
- const MenuScrollArea = withContext$1(withStateProps(Primitive.div), "scrollArea");
3390
- withContext$1(Menu.Group, "group");
3391
- withContext$1(Menu.GroupLabel, "groupLabel");
3995
+ const MenuContent = withContext(Menu.Content, "content");
3996
+ const MenuScrollArea = withContext(withStateProps(Primitive.div), "scrollArea");
3997
+ withContext(Menu.Group, "group");
3998
+ withContext(Menu.GroupLabel, "groupLabel");
3392
3999
  const MenuItem = /*#__PURE__*/ React.forwardRef(({ className: propClassName, ...props }, ref)=>{
3393
4000
  const [variantProps, otherProps] = menuItem.splitVariantProps(props);
3394
4001
  const parentProps = useItemProps();
@@ -3411,19 +4018,15 @@ const MenuItemLabel = withItemContext(withItemStateProps(Primitive.span), "label
3411
4018
  withItemContext(withItemStateProps(Primitive.span), "description");
3412
4019
 
3413
4020
  function ColumnCalcCells({ columns, rows, selectable, stickyFirst, stickyLast }) {
3414
- const effective = useEffectiveColumns(columns);
4021
+ const effective = useEffectiveColumns(columns, {
4022
+ selectable,
4023
+ stickyFirst
4024
+ });
3415
4025
  return /*#__PURE__*/ jsx(Fragment, {
3416
- children: effective.map(({ column }, index)=>{
3417
- const isFirst = index === 0;
4026
+ children: effective.map((entry, index)=>{
4027
+ const { column } = entry;
3418
4028
  const isLast = index === effective.length - 1;
3419
- const sticky = stickyProps({
3420
- selectable,
3421
- stickyFirst,
3422
- stickyLast,
3423
- isSelectionCell: false,
3424
- isFirst,
3425
- isLast
3426
- });
4029
+ const sticky = pinnedCellProps(entry, stickyLast, isLast);
3427
4030
  return /*#__PURE__*/ jsx(DataTableFooterCell, {
3428
4031
  "data-sticky": sticky["data-sticky"],
3429
4032
  "data-sticky-edge": sticky["data-sticky-edge"],
@@ -3607,20 +4210,16 @@ const DEFAULT_SORT_ICON = {
3607
4210
  * `visibleColumnOrder`/labels/widths change, without also re-deriving `sortedData`'s
3608
4211
  * render above it. */ function ColumnHeaderCells({ columns, columnMenu, reorderable, resizable, sortIcon, selectable, stickyFirst, stickyLast }) {
3609
4212
  const { draggingColumnKey, beginColumnDrag, dragOverColumn, endColumnDrag } = useDataTableContext();
3610
- const effective = useEffectiveColumns(columns);
4213
+ const effective = useEffectiveColumns(columns, {
4214
+ selectable,
4215
+ stickyFirst
4216
+ });
3611
4217
  const [renamingKey, setRenamingKey] = React.useState(null);
3612
4218
  return /*#__PURE__*/ jsx(Fragment, {
3613
- children: effective.map(({ column, label, width }, index)=>{
3614
- const isFirst = index === 0;
4219
+ children: effective.map((entry, index)=>{
4220
+ const { column, label, width } = entry;
3615
4221
  const isLast = index === effective.length - 1;
3616
- const sticky = stickyProps({
3617
- selectable,
3618
- stickyFirst,
3619
- stickyLast,
3620
- isSelectionCell: false,
3621
- isFirst,
3622
- isLast
3623
- });
4222
+ const sticky = pinnedCellProps(entry, stickyLast, isLast);
3624
4223
  return /*#__PURE__*/ jsxs(DataTableHeaderCell, {
3625
4224
  columnKey: column.key,
3626
4225
  sortable: column.sortable,
@@ -3689,304 +4288,130 @@ function ColumnRenameInput({ column, initialLabel, onDone }) {
3689
4288
  // The menu's close restores focus to its trigger one frame after this input
3690
4289
  // mounts, stealing focus from `autoFocus`. Re-grab it in the SAME frame (after
3691
4290
  // the restore) and select the text, so the user can type/replace immediately.
3692
- const frame = requestAnimationFrame(()=>{
3693
- readyRef.current = true;
3694
- inputRef.current?.focus();
3695
- inputRef.current?.select();
3696
- });
3697
- return ()=>cancelAnimationFrame(frame);
3698
- }, []);
3699
- const commit = ()=>{
3700
- if (value.trim()) renameColumn(column.key, value.trim());
3701
- onDone();
3702
- };
3703
- return(// A bare, minimal input -- NOT seed's `TextField` (its labeled root measures
3704
- // ~52px tall, per DATATABLE-ATTIO-SPEC.md's own note; this has to fit a 40px
3705
- // header cell with no label/description chrome of its own). Seed has no lighter
3706
- // input primitive at this level, so this follows crisp's own measured
3707
- // `.crisp-datatable-th-rename` (`datatable.recipe.ts`) instead: full-width,
3708
- // header-row height, a small inset, no border (an inset brand-solid box-shadow
3709
- // stands in for a focus ring), the header's own font weight/size inherited
3710
- // rather than a new size introduced for one input.
3711
- /*#__PURE__*/ jsx("input", {
3712
- ref: inputRef,
3713
- // biome-ignore lint/a11y/noAutofocus: opening this input IS the user's own click a moment earlier (crisp's `.crisp-datatable-th-rename` does the same), not an unannounced page-load autofocus.
3714
- autoFocus: true,
3715
- "aria-label": `Rename ${initialLabel}`,
3716
- value: value,
3717
- onClick: (event)=>event.stopPropagation(),
3718
- onChange: (event)=>setValue(event.target.value),
3719
- onBlur: ()=>{
3720
- if (readyRef.current) commit();
3721
- },
3722
- onKeyDown: (event)=>{
3723
- event.stopPropagation();
3724
- if (event.key === "Enter") commit();
3725
- if (event.key === "Escape") onDone();
3726
- },
3727
- // Bare inline input: no border/ring/background/radius -- it reads as the
3728
- // header label made editable in place, not a boxed field.
3729
- style: {
3730
- width: "100%",
3731
- minWidth: 0,
3732
- height: "100%",
3733
- margin: 0,
3734
- padding: 0,
3735
- border: "none",
3736
- borderRadius: 0,
3737
- background: "transparent",
3738
- boxShadow: "none",
3739
- font: "inherit",
3740
- fontWeight: "var(--seed-font-weight-medium)",
3741
- color: "var(--seed-color-fg-neutral)",
3742
- outline: "none"
3743
- }
3744
- }));
3745
- }
3746
- /** The per-column header menu (rename / hide / move left / move right) -- seed
3747
- * `Menu`, opened from the same `headerCellIcon` slot the filter trigger and sort
3748
- * chevron already share. Hover/focus-reveal (`data-column-menu`'s own CSS in
3749
- * `data-table.ts`) -- crisp's own header shows the trigger only on hover, not
3750
- * permanently, so this one carries its own marker attribute distinct from that
3751
- * slot's OTHER occupants (the sort chevron, the filter trigger), which stay visible
3752
- * under their own separate rules regardless of hover. */ function ColumnMenu({ column, onRename }) {
3753
- const { moveColumn, hideColumn } = useDataTableContext();
3754
- const columnLabel = typeof column.header === "string" ? column.header : column.key;
3755
- const stopPropagation = (event)=>event.stopPropagation();
3756
- return /*#__PURE__*/ jsxs(MenuRoot, {
3757
- children: [
3758
- /*#__PURE__*/ jsx(DataTableHeaderCellIcon, {
3759
- asChild: true,
3760
- "data-column-menu": "",
3761
- onClick: stopPropagation,
3762
- onKeyDown: stopPropagation,
3763
- draggable: false,
3764
- children: /*#__PURE__*/ jsx(MenuTrigger, {
3765
- "aria-label": `${columnLabel} column menu`,
3766
- children: /*#__PURE__*/ jsx(IconDot3HorizontalLine_default, {
3767
- size: 16
3768
- })
3769
- })
3770
- }),
3771
- /*#__PURE__*/ jsx(MenuPositioner, {
3772
- children: /*#__PURE__*/ jsx(MenuContent, {
3773
- style: {
3774
- minWidth: 160
3775
- },
3776
- children: /*#__PURE__*/ jsxs(MenuScrollArea, {
3777
- children: [
3778
- /*#__PURE__*/ jsx(MenuItem, {
3779
- onClick: onRename,
3780
- children: /*#__PURE__*/ jsx(MenuItemLabel, {
3781
- children: "Rename"
3782
- })
3783
- }),
3784
- /*#__PURE__*/ jsx(MenuItem, {
3785
- onClick: ()=>moveColumn(column.key, "left"),
3786
- children: /*#__PURE__*/ jsx(MenuItemLabel, {
3787
- children: "Move left"
3788
- })
3789
- }),
3790
- /*#__PURE__*/ jsx(MenuItem, {
3791
- onClick: ()=>moveColumn(column.key, "right"),
3792
- children: /*#__PURE__*/ jsx(MenuItemLabel, {
3793
- children: "Move right"
3794
- })
3795
- }),
3796
- /*#__PURE__*/ jsx(MenuItem, {
3797
- onClick: ()=>hideColumn(column.key),
3798
- children: /*#__PURE__*/ jsx(MenuItemLabel, {
3799
- children: "Hide column"
3800
- })
3801
- })
3802
- ]
3803
- })
3804
- })
3805
- })
3806
- ]
3807
- });
3808
- }
3809
-
3810
- __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: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-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__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__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))}");
3811
-
3812
- const createClassName = (className, variants, compoundVariants = [])=>{
3813
- const variantKeys = Object.keys(variants);
3814
- const variantValues = variantKeys.map((key)=>variants[key]);
3815
- const variantClassName = variantKeys.map((key, index)=>`${className}--${key}_${variantValues[index]}`).join(" ");
3816
- const compountVariantClassName = compoundVariants.filter((compoundVariant)=>Object.keys(compoundVariant).every((key)=>compoundVariant[key] === variants[key])).map((compoundVariant)=>`${className}--${Object.keys(compoundVariant).map((key)=>{
3817
- return `${key}_${compoundVariant[key]}`;
3818
- }).join("-")}`).join(" ");
3819
- return [
3820
- className,
3821
- variantClassName,
3822
- compountVariantClassName
3823
- ].filter(Boolean).join(" ");
3824
- };
3825
- function mergeVariants(a, b) {
3826
- const result = {
3827
- ...a
3828
- };
3829
- for(const k in b){
3830
- if (b[k] != null) {
3831
- result[k] = b[k];
3832
- }
3833
- }
3834
- return result;
3835
- }
3836
- function splitVariantProps(props, variantMap) {
3837
- const variantProps = {};
3838
- const otherProps = {};
3839
- for(const key in props){
3840
- if (variantMap[key] != null) {
3841
- variantProps[key] = props[key];
3842
- } else {
3843
- otherProps[key] = props[key];
3844
- }
3845
- }
3846
- return [
3847
- variantProps,
3848
- otherProps
3849
- ];
3850
- }
3851
-
3852
- const dataTableSlotNames = [
3853
- [
3854
- "root",
3855
- "seed-data-table__root"
3856
- ],
3857
- [
3858
- "table",
3859
- "seed-data-table__table"
3860
- ],
3861
- [
3862
- "header",
3863
- "seed-data-table__header"
3864
- ],
3865
- [
3866
- "headerCell",
3867
- "seed-data-table__headerCell"
3868
- ],
3869
- [
3870
- "headerCellInner",
3871
- "seed-data-table__headerCellInner"
3872
- ],
3873
- [
3874
- "headerCellLabel",
3875
- "seed-data-table__headerCellLabel"
3876
- ],
3877
- [
3878
- "headerCellIcon",
3879
- "seed-data-table__headerCellIcon"
3880
- ],
3881
- [
3882
- "row",
3883
- "seed-data-table__row"
3884
- ],
3885
- [
3886
- "cell",
3887
- "seed-data-table__cell"
3888
- ],
3889
- [
3890
- "entity",
3891
- "seed-data-table__entity"
3892
- ],
3893
- [
3894
- "entityName",
3895
- "seed-data-table__entityName"
3896
- ],
3897
- [
3898
- "tagList",
3899
- "seed-data-table__tagList"
3900
- ],
3901
- [
3902
- "link",
3903
- "seed-data-table__link"
3904
- ],
3905
- [
3906
- "toolbar",
3907
- "seed-data-table__toolbar"
3908
- ],
3909
- [
3910
- "search",
3911
- "seed-data-table__search"
3912
- ],
3913
- [
3914
- "count",
3915
- "seed-data-table__count"
3916
- ],
3917
- [
3918
- "toolbarEnd",
3919
- "seed-data-table__toolbarEnd"
3920
- ],
3921
- [
3922
- "scrollArea",
3923
- "seed-data-table__scrollArea"
3924
- ],
3925
- [
3926
- "footer",
3927
- "seed-data-table__footer"
3928
- ],
3929
- [
3930
- "footerCell",
3931
- "seed-data-table__footerCell"
3932
- ],
3933
- [
3934
- "emptyState",
3935
- "seed-data-table__emptyState"
3936
- ],
3937
- [
3938
- "loadingState",
3939
- "seed-data-table__loadingState"
3940
- ],
3941
- [
3942
- "addRow",
3943
- "seed-data-table__addRow"
3944
- ],
3945
- [
3946
- "addRowCell",
3947
- "seed-data-table__addRowCell"
3948
- ],
3949
- [
3950
- "selectionCell",
3951
- "seed-data-table__selectionCell"
3952
- ]
3953
- ];
3954
- const defaultVariant$4 = {
3955
- "surface": "card",
3956
- "interactive": false
3957
- };
3958
- const compoundVariants$4 = [];
3959
- const dataTableVariantMap = {
3960
- "surface": [
3961
- "card",
3962
- "bleed"
3963
- ],
3964
- "interactive": [
3965
- true,
3966
- false
3967
- ]
3968
- };
3969
- function dataTable(props) {
3970
- return Object.fromEntries(dataTableSlotNames.map(([slot, className])=>{
3971
- return [
3972
- slot,
3973
- createClassName(className, mergeVariants(defaultVariant$4, props), compoundVariants$4)
3974
- ];
4291
+ const frame = requestAnimationFrame(()=>{
4292
+ readyRef.current = true;
4293
+ inputRef.current?.focus();
4294
+ inputRef.current?.select();
4295
+ });
4296
+ return ()=>cancelAnimationFrame(frame);
4297
+ }, []);
4298
+ const commit = ()=>{
4299
+ if (value.trim()) renameColumn(column.key, value.trim());
4300
+ onDone();
4301
+ };
4302
+ return(// A bare, minimal input -- NOT seed's `TextField` (its labeled root measures
4303
+ // ~52px tall, per DATATABLE-ATTIO-SPEC.md's own note; this has to fit a 40px
4304
+ // header cell with no label/description chrome of its own). Seed has no lighter
4305
+ // input primitive at this level, so this follows crisp's own measured
4306
+ // `.crisp-datatable-th-rename` (`datatable.recipe.ts`) instead: full-width,
4307
+ // header-row height, a small inset, no border (an inset brand-solid box-shadow
4308
+ // stands in for a focus ring), the header's own font weight/size inherited
4309
+ // rather than a new size introduced for one input.
4310
+ /*#__PURE__*/ jsx("input", {
4311
+ ref: inputRef,
4312
+ // biome-ignore lint/a11y/noAutofocus: opening this input IS the user's own click a moment earlier (crisp's `.crisp-datatable-th-rename` does the same), not an unannounced page-load autofocus.
4313
+ autoFocus: true,
4314
+ "aria-label": `Rename ${initialLabel}`,
4315
+ value: value,
4316
+ onClick: (event)=>event.stopPropagation(),
4317
+ onChange: (event)=>setValue(event.target.value),
4318
+ onBlur: ()=>{
4319
+ if (readyRef.current) commit();
4320
+ },
4321
+ onKeyDown: (event)=>{
4322
+ event.stopPropagation();
4323
+ if (event.key === "Enter") commit();
4324
+ if (event.key === "Escape") onDone();
4325
+ },
4326
+ // Bare inline input: no border/ring/background/radius -- it reads as the
4327
+ // header label made editable in place, not a boxed field.
4328
+ style: {
4329
+ width: "100%",
4330
+ minWidth: 0,
4331
+ height: "100%",
4332
+ margin: 0,
4333
+ padding: 0,
4334
+ border: "none",
4335
+ borderRadius: 0,
4336
+ background: "transparent",
4337
+ boxShadow: "none",
4338
+ font: "inherit",
4339
+ fontWeight: "var(--seed-font-weight-medium)",
4340
+ color: "var(--seed-color-fg-neutral)",
4341
+ outline: "none"
4342
+ }
3975
4343
  }));
3976
4344
  }
3977
- Object.assign(dataTable, {
3978
- splitVariantProps: (props)=>splitVariantProps(props, dataTableVariantMap)
3979
- }); // @recipe(seed): data-table
3980
-
3981
- // The one `createSlotRecipeContext(dataTable)` call every sibling file in this
3982
- // directory shares -- `ClassNamesProvider` and `withContext` must be the SAME
3983
- // binding everywhere (a slot component built from a second, independent call would
3984
- // read a DIFFERENT React context and never see the classNames `DataTableInner`
3985
- // provides), so this is the one place that calls `createSlotRecipeContext`, and every
3986
- // other file (`DataTable.tsx`, `column-menu.tsx`, `resize.tsx`, `column-calc.tsx`,
3987
- // `toolbar.tsx`, `body-cells.tsx`, `editors.tsx`) imports its exports from here rather
3988
- // than re-deriving its own.
3989
- const { ClassNamesProvider, withContext, useClassNames } = createSlotRecipeContext(dataTable);
4345
+ /** The per-column header menu (rename / hide / move left / move right) -- seed
4346
+ * `Menu`, opened from the same `headerCellIcon` slot the filter trigger and sort
4347
+ * chevron already share. Hover/focus-reveal (`data-column-menu`'s own CSS in
4348
+ * `data-table.ts`) -- crisp's own header shows the trigger only on hover, not
4349
+ * permanently, so this one carries its own marker attribute distinct from that
4350
+ * slot's OTHER occupants (the sort chevron, the filter trigger), which stay visible
4351
+ * under their own separate rules regardless of hover. */ function ColumnMenu({ column, onRename }) {
4352
+ const { moveColumn, hideColumn, isColumnPinned, togglePinColumn } = useDataTableContext();
4353
+ const columnLabel = typeof column.header === "string" ? column.header : column.key;
4354
+ const pinned = isColumnPinned(column.key);
4355
+ const stopPropagation = (event)=>event.stopPropagation();
4356
+ return /*#__PURE__*/ jsxs(MenuRoot, {
4357
+ children: [
4358
+ /*#__PURE__*/ jsx(DataTableHeaderCellIcon, {
4359
+ asChild: true,
4360
+ "data-column-menu": "",
4361
+ onClick: stopPropagation,
4362
+ onKeyDown: stopPropagation,
4363
+ draggable: false,
4364
+ children: /*#__PURE__*/ jsx(MenuTrigger, {
4365
+ "aria-label": `${columnLabel} column menu`,
4366
+ children: /*#__PURE__*/ jsx(IconDot3HorizontalLine_default, {
4367
+ size: 16
4368
+ })
4369
+ })
4370
+ }),
4371
+ /*#__PURE__*/ jsx(MenuPositioner, {
4372
+ children: /*#__PURE__*/ jsx(MenuContent, {
4373
+ style: {
4374
+ minWidth: 160
4375
+ },
4376
+ children: /*#__PURE__*/ jsxs(MenuScrollArea, {
4377
+ children: [
4378
+ /*#__PURE__*/ jsx(MenuItem, {
4379
+ onClick: onRename,
4380
+ children: /*#__PURE__*/ jsx(MenuItemLabel, {
4381
+ children: "Rename"
4382
+ })
4383
+ }),
4384
+ /*#__PURE__*/ jsx(MenuItem, {
4385
+ onClick: ()=>moveColumn(column.key, "left"),
4386
+ children: /*#__PURE__*/ jsx(MenuItemLabel, {
4387
+ children: "Move left"
4388
+ })
4389
+ }),
4390
+ /*#__PURE__*/ jsx(MenuItem, {
4391
+ onClick: ()=>moveColumn(column.key, "right"),
4392
+ children: /*#__PURE__*/ jsx(MenuItemLabel, {
4393
+ children: "Move right"
4394
+ })
4395
+ }),
4396
+ /*#__PURE__*/ jsx(MenuItem, {
4397
+ onClick: ()=>togglePinColumn(column.key),
4398
+ children: /*#__PURE__*/ jsx(MenuItemLabel, {
4399
+ children: pinned ? "Unpin column" : "Pin column"
4400
+ })
4401
+ }),
4402
+ /*#__PURE__*/ jsx(MenuItem, {
4403
+ onClick: ()=>hideColumn(column.key),
4404
+ children: /*#__PURE__*/ jsx(MenuItemLabel, {
4405
+ children: "Hide column"
4406
+ })
4407
+ })
4408
+ ]
4409
+ })
4410
+ })
4411
+ })
4412
+ ]
4413
+ });
4414
+ }
3990
4415
 
3991
4416
  /**
3992
4417
  * A small inline spinner, not seed's own `ProgressCircle` -- that component's
@@ -9194,7 +9619,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9194
9619
  "aria-label": "Move sort up",
9195
9620
  disabled: index === 0,
9196
9621
  onClick: ()=>moveToolbarSort(index, index - 1),
9197
- style: iconButtonStyle,
9622
+ style: iconButtonStyle$1,
9198
9623
  children: /*#__PURE__*/ jsx(IconArrowUpLine_default, {
9199
9624
  size: 13,
9200
9625
  strokeWidth: 1.75
@@ -9205,7 +9630,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9205
9630
  "aria-label": "Move sort down",
9206
9631
  disabled: index === toolbarSorts.length - 1,
9207
9632
  onClick: ()=>moveToolbarSort(index, index + 1),
9208
- style: iconButtonStyle,
9633
+ style: iconButtonStyle$1,
9209
9634
  children: /*#__PURE__*/ jsx(IconArrowDownLine_default, {
9210
9635
  size: 13,
9211
9636
  strokeWidth: 1.75
@@ -9215,7 +9640,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9215
9640
  type: "button",
9216
9641
  "aria-label": "Remove sort",
9217
9642
  onClick: ()=>removeToolbarSort(index),
9218
- style: iconButtonStyle,
9643
+ style: iconButtonStyle$1,
9219
9644
  children: /*#__PURE__*/ jsx(IconXmarkLine_default, {
9220
9645
  size: 13,
9221
9646
  strokeWidth: 1.75
@@ -9230,7 +9655,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9230
9655
  disabled: availableColumns.length === 0,
9231
9656
  onClick: ()=>availableColumns[0] && addToolbarSort(availableColumns[0].key),
9232
9657
  style: {
9233
- ...iconButtonStyle,
9658
+ ...iconButtonStyle$1,
9234
9659
  width: "auto",
9235
9660
  gap: 6,
9236
9661
  padding: "6px 8px",
@@ -9252,7 +9677,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9252
9677
  ]
9253
9678
  });
9254
9679
  }
9255
- const iconButtonStyle = {
9680
+ const iconButtonStyle$1 = {
9256
9681
  display: "inline-flex",
9257
9682
  alignItems: "center",
9258
9683
  justifyContent: "center",
@@ -9350,7 +9775,7 @@ const formatFilterDate = (iso)=>{
9350
9775
  type: "button",
9351
9776
  "aria-label": `Remove ${column ? columnLabel(column) : filter.key} filter`,
9352
9777
  onClick: ()=>removeToolbarFilter(filter.id),
9353
- style: iconButtonStyle,
9778
+ style: iconButtonStyle$1,
9354
9779
  children: /*#__PURE__*/ jsx(IconXmarkLine_default, {
9355
9780
  size: 12,
9356
9781
  strokeWidth: 1.75
@@ -9607,7 +10032,7 @@ const formatFilterDate = (iso)=>{
9607
10032
  type: "button",
9608
10033
  onClick: ()=>setPickingField(true),
9609
10034
  style: {
9610
- ...iconButtonStyle,
10035
+ ...iconButtonStyle$1,
9611
10036
  width: "auto",
9612
10037
  gap: 6,
9613
10038
  padding: "6px 8px",
@@ -9630,8 +10055,8 @@ const formatFilterDate = (iso)=>{
9630
10055
  });
9631
10056
  }
9632
10057
 
9633
- const DataTableToolbar = withContext(Primitive.div, "toolbar");
9634
- const DataTableCount = withContext(Primitive.span, "count");
10058
+ const DataTableToolbar = withContext$1(Primitive.div, "toolbar");
10059
+ const DataTableCount = withContext$1(Primitive.span, "count");
9635
10060
  /**
9636
10061
  * The toolbar's own right-hand group -- `count`'s `marginInlineStart: auto` moved
9637
10062
  * here (off the `<span>` itself) so a consumer's `toolbarActions` and the row count
@@ -9642,7 +10067,7 @@ const DataTableCount = withContext(Primitive.span, "count");
9642
10067
  * it. `DataTable.tsx` always renders this wrapper once either `toolbarActions` or
9643
10068
  * `showCount` is on, so it degrades to exactly the old single-count layout when no
9644
10069
  * `toolbarActions` is passed.
9645
- */ const DataTableToolbarEnd = withContext(Primitive.div, "toolbarEnd");
10070
+ */ const DataTableToolbarEnd = withContext$1(Primitive.div, "toolbarEnd");
9646
10071
  /**
9647
10072
  * A compact, borderless search box -- crisp's own measured `.crisp-datatable-search`
9648
10073
  * (32px, `bgSurface` background, a leading icon, a soft rest-state shadow, a 1px brand
@@ -9692,7 +10117,7 @@ function RowCount({ shown, total, countLabel }) {
9692
10117
  }
9693
10118
 
9694
10119
  function DataTableInner(props, ref) {
9695
- 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, 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;
10120
+ 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;
9696
10121
  const classNames = dataTable({
9697
10122
  surface,
9698
10123
  interactive
@@ -9797,7 +10222,7 @@ function DataTableInner(props, ref) {
9797
10222
  // loading/error/empty body, infinite scroll's sentinel, the add-row footer) reads
9798
10223
  // this ONE value rather than each re-deriving it, so they can never drift apart.
9799
10224
  const totalCols = columns.length + (selectable ? 1 : 0);
9800
- return /*#__PURE__*/ jsx(ClassNamesProvider, {
10225
+ return /*#__PURE__*/ jsx(ClassNamesProvider$1, {
9801
10226
  value: classNames,
9802
10227
  children: /*#__PURE__*/ jsxs(DataTableRoot, {
9803
10228
  ref: ref,
@@ -9828,6 +10253,9 @@ function DataTableInner(props, ref) {
9828
10253
  columnLabels: columnLabels,
9829
10254
  defaultColumnLabels: defaultColumnLabels,
9830
10255
  onColumnLabelsChange: onColumnLabelsChange,
10256
+ pinnedColumnKeys: pinnedColumnKeys,
10257
+ defaultPinnedColumnKeys: defaultPinnedColumnKeys,
10258
+ onPinnedColumnKeysChange: onPinnedColumnKeysChange,
9831
10259
  columnCalcOps: columnCalcOps,
9832
10260
  defaultColumnCalcOps: defaultColumnCalcOps,
9833
10261
  onColumnCalcOpsChange: onColumnCalcOpsChange,
@@ -9871,19 +10299,13 @@ function DataTableInner(props, ref) {
9871
10299
  children: [
9872
10300
  /*#__PURE__*/ jsx(DataTableColGroup, {
9873
10301
  columns: columns,
9874
- selectable: !!selectable
10302
+ selectable: !!selectable,
10303
+ stickyFirst: stickyFirst
9875
10304
  }),
9876
10305
  /*#__PURE__*/ jsxs(DataTableHeader, {
9877
10306
  children: [
9878
- selectable && /*#__PURE__*/ jsx(DataTableSelectionHeaderCell, {
9879
- ...stickyProps({
9880
- selectable,
9881
- stickyFirst,
9882
- stickyLast: resolvedStickyLast,
9883
- isSelectionCell: true,
9884
- isFirst: false,
9885
- isLast: false
9886
- }),
10307
+ selectable && /*#__PURE__*/ jsx(SelectionHeaderCell, {
10308
+ stickyFirst: stickyFirst,
9887
10309
  children: /*#__PURE__*/ jsx(SelectAllCheckbox, {
9888
10310
  label: selectAllLabel,
9889
10311
  checkedIcon: selectionCheckedIcon ?? DEFAULT_CHECKED_ICON,
@@ -9934,7 +10356,7 @@ function DataTableInner(props, ref) {
9934
10356
  "data-clickable": onRowClick ? "" : undefined,
9935
10357
  onClick: onRowClick ? ()=>onRowClick(row) : undefined,
9936
10358
  children: [
9937
- selectable && /*#__PURE__*/ jsx(DataTableSelectionCell, {
10359
+ selectable && /*#__PURE__*/ jsx(SelectionCell, {
9938
10360
  // The row itself carries `onClick={onRowClick}` above, and a
9939
10361
  // native click on this cell's checkbox bubbles up through it
9940
10362
  // (React attaches DataTableRow's handler on the <tr>, which
@@ -9942,14 +10364,7 @@ function DataTableInner(props, ref) {
9942
10364
  // it from at -- so this has to live here, not in the consumer).
9943
10365
  // Without this, checking a row's box also fires `onRowClick`.
9944
10366
  onClick: (event)=>event.stopPropagation(),
9945
- ...stickyProps({
9946
- selectable,
9947
- stickyFirst,
9948
- stickyLast: resolvedStickyLast,
9949
- isSelectionCell: true,
9950
- isFirst: false,
9951
- isLast: false
9952
- }),
10367
+ stickyFirst: stickyFirst,
9953
10368
  children: /*#__PURE__*/ jsx(RowCheckbox, {
9954
10369
  label: getRowSelectionLabel(row),
9955
10370
  checkedIcon: selectionCheckedIcon ?? DEFAULT_CHECKED_ICON
@@ -9981,15 +10396,8 @@ function DataTableInner(props, ref) {
9981
10396
  // is what makes every row's selection column line up now, not a matching
9982
10397
  // per-slot width formula, so there is no more reason for this cell to
9983
10398
  // borrow a different slot than its footer siblings.
9984
- /*#__PURE__*/ jsx(DataTableFooterCell, {
9985
- ...stickyProps({
9986
- selectable,
9987
- stickyFirst,
9988
- stickyLast: resolvedStickyLast,
9989
- isSelectionCell: true,
9990
- isFirst: false,
9991
- isLast: false
9992
- })
10399
+ /*#__PURE__*/ jsx(SelectionFooterCell, {
10400
+ stickyFirst: stickyFirst
9993
10401
  }),
9994
10402
  /*#__PURE__*/ jsx(ColumnCalcCells, {
9995
10403
  columns: columns,
@@ -10025,7 +10433,16 @@ DataTableInner.displayName = "DataTable";
10025
10433
  * `DataTableInner`) purely so it can call `useDataTableContext()` -- that context is
10026
10434
  * provided by the `DataTablePrimitiveRoot` this renders as a SIBLING of, one level
10027
10435
  * up from where `DataTableInner` itself sits. */ function ColumnsChangeEffect({ columns, onColumnsChange }) {
10028
- const effective = useEffectiveColumns(columns);
10436
+ // `selectable`/`stickyFirst` are both `false` here on purpose: this convenience blob
10437
+ // never renders a row of its own, so it needs neither the +40px selection offset nor
10438
+ // `stickyFirst`'s legacy "no explicit pin -> pin the first visible column" fallback.
10439
+ // An EXPLICIT per-column pin (`pinnedColumnKeys`) still reorders this list the same
10440
+ // way it reorders every rendered row -- this callback reports "what's showing now",
10441
+ // and a pinned column showing first IS what's showing now.
10442
+ const effective = useEffectiveColumns(columns, {
10443
+ selectable: false,
10444
+ stickyFirst: false
10445
+ });
10029
10446
  // A NEW array every time `effective` itself is a new array (its own `useMemo`
10030
10447
  // recomputes only when order/hidden/label/width actually change) -- `header`
10031
10448
  // resolves to the menu's rename override (falling back to the column's own static
@@ -10052,7 +10469,7 @@ DataTableInner.displayName = "DataTable";
10052
10469
  // is a SIBLING of the toolbar (both inside `DataTablePrimitiveRoot`'s card `<div>`)
10053
10470
  // rather than `DataTablePrimitiveRoot` itself: a `<table>` cannot host the toolbar as a
10054
10471
  // child without the HTML parser foster-parenting it back out.
10055
- const DataTableTable = withContext(DataTableTable$1, "table");
10472
+ const DataTableTable = withContext$1(DataTableTable$1, "table");
10056
10473
  /** One `<col>` per rendered column (plus the 40px selection column, if any) -- the
10057
10474
  * SINGLE place a column's width is ever set now. Every row's header/body/footer cell
10058
10475
  * reads its own width off this shared `<colgroup>` by construction (native table
@@ -10060,8 +10477,16 @@ const DataTableTable = withContext(DataTableTable$1, "table");
10060
10477
  * out of sync between the three. A column with no explicit width gets a bare `<col>`
10061
10478
  * (no `style`): `table-layout: fixed`'s own algorithm gives every such column an equal
10062
10479
  * share of whatever space is left, matching the old flex grid's `flex: 1 1 0%`
10063
- * default with no extra code needed here. */ function DataTableColGroup({ columns, selectable }) {
10064
- const effective = useEffectiveColumns(columns);
10480
+ * default with no extra code needed here. */ function DataTableColGroup({ columns, selectable, stickyFirst }) {
10481
+ // MUST use the same pinned-first ordering every OTHER row (header/body/footer)
10482
+ // renders in (`useEffectiveColumns`'s own doc comment) -- a `<colgroup>`'s `<col>`s
10483
+ // are matched positionally to each row's cells, so if a pinned column visually moves
10484
+ // to the front of the header/body but not here, every column's width applies to the
10485
+ // WRONG cell.
10486
+ const effective = useEffectiveColumns(columns, {
10487
+ selectable,
10488
+ stickyFirst
10489
+ });
10065
10490
  return /*#__PURE__*/ jsxs("colgroup", {
10066
10491
  children: [
10067
10492
  selectable && /*#__PURE__*/ jsx("col", {
@@ -10077,15 +10502,15 @@ const DataTableTable = withContext(DataTableTable$1, "table");
10077
10502
  ]
10078
10503
  });
10079
10504
  }
10080
- const DataTableHeader = withContext(DataTableHeader$1, "header");
10081
- const DataTableHeaderCell = withContext(DataTableHeaderCell$1, "headerCell");
10505
+ const DataTableHeader = withContext$1(DataTableHeader$1, "header");
10506
+ const DataTableHeaderCell = withContext$1(DataTableHeaderCell$1, "headerCell");
10082
10507
  // `.crisp-datatable-th-inner` -- the flex row INSIDE a `<th>` (label + filter trigger +
10083
10508
  // sort chevron + column menu). See qvism-preset's own `headerCellInner` slot comment
10084
10509
  // for why this lives one level below the cell itself rather than on it.
10085
- const DataTableHeaderCellInner = withContext(Primitive.div, "headerCellInner");
10510
+ const DataTableHeaderCellInner = withContext$1(Primitive.div, "headerCellInner");
10086
10511
  /** The label text specifically -- the one thing in `DataTableHeaderCellInner` that
10087
10512
  * truncates; the trailing filter/sort/menu icons stay fully visible regardless of how
10088
- * long a column's own header text is. */ const DataTableHeaderCellLabel = withContext(Primitive.span, "headerCellLabel");
10513
+ * long a column's own header text is. */ const DataTableHeaderCellLabel = withContext$1(Primitive.span, "headerCellLabel");
10089
10514
  /**
10090
10515
  * A selection `<th>`/`<td>`/footer `<td>` composes ITS base slot (`headerCell`/`cell`/
10091
10516
  * `footerCell`) together with the `selectionCell` MODIFIER, rather than replacing it --
@@ -10096,7 +10521,7 @@ const DataTableHeaderCellInner = withContext(Primitive.div, "headerCellInner");
10096
10521
  * a second recipe call.
10097
10522
  */ function withSelectionCell(Component, baseSlot) {
10098
10523
  const StyledComponent = /*#__PURE__*/ forwardRef((props, ref)=>{
10099
- const classNames = useClassNames();
10524
+ const classNames = useClassNames$1();
10100
10525
  return /*#__PURE__*/ jsx(Component, {
10101
10526
  ref: ref,
10102
10527
  ...props,
@@ -10107,12 +10532,12 @@ const DataTableHeaderCellInner = withContext(Primitive.div, "headerCellInner");
10107
10532
  return StyledComponent;
10108
10533
  }
10109
10534
  const DataTableSelectionHeaderCell = withSelectionCell(DataTableHeaderCell$1, "headerCell");
10110
- const DataTableHeaderCellIcon = withContext(Primitive.span, "headerCellIcon");
10535
+ const DataTableHeaderCellIcon = withContext$1(Primitive.span, "headerCellIcon");
10111
10536
  // body has no CSS slot of its own in the rootage spec -- it is a plain structural
10112
10537
  // `<tbody>`, so it is not wrapped in `withContext`.
10113
10538
  const DataTableBody = DataTableBody$1;
10114
- const DataTableRow = withContext(DataTableRow$1, "row");
10115
- const DataTableCell = withContext(DataTableCell$1, "cell");
10539
+ const DataTableRow = withContext$1(DataTableRow$1, "row");
10540
+ const DataTableCell = withContext$1(DataTableCell$1, "cell");
10116
10541
  const DataTableSelectionCell = withSelectionCell(DataTableCell$1, "cell");
10117
10542
  // Tranche 4 -- column menu + reorder + resize + sticky columns. A scroll boundary
10118
10543
  // wrapping the whole `<table>`: `overflowX: auto` is what makes a wide table (more
@@ -10122,7 +10547,7 @@ const DataTableSelectionCell = withSelectionCell(DataTableCell$1, "cell");
10122
10547
  // stays out of this slot's own base css (nothing here has a bounded height of its own
10123
10548
  // to clip), so it never introduces a second vertical scrollbar next to whatever
10124
10549
  // ambient container the whole DataTable already sits in.
10125
- const DataTableScrollBodyBase = withContext(Primitive.div, "scrollArea");
10550
+ const DataTableScrollBodyBase = withContext$1(Primitive.div, "scrollArea");
10126
10551
  function DataTableScrollBody({ children, scrollProps }) {
10127
10552
  const { scrollAreaRef, scrolledStart, scrolledEnd, onScroll } = useDataTableContext();
10128
10553
  const { className: scrollClassName, style: scrollStyle, ...otherScrollProps } = scrollProps ?? {};
@@ -10137,7 +10562,7 @@ function DataTableScrollBody({ children, scrollProps }) {
10137
10562
  children: children
10138
10563
  });
10139
10564
  }
10140
- const DataTableFooterBase = withContext(Primitive.tfoot, "footer");
10565
+ const DataTableFooterBase = withContext$1(Primitive.tfoot, "footer");
10141
10566
  function DataTableFooter({ children, ...props }) {
10142
10567
  return /*#__PURE__*/ jsx(DataTableFooterBase, {
10143
10568
  ...props,
@@ -10146,7 +10571,33 @@ function DataTableFooter({ children, ...props }) {
10146
10571
  })
10147
10572
  });
10148
10573
  }
10149
- const DataTableFooterCell = withContext(Primitive.td, "footerCell");
10574
+ const DataTableFooterCell = withContext$1(Primitive.td, "footerCell");
10575
+ // The selection column's OWN sticky wrappers -- one each for header/body/footer,
10576
+ // reading the live pinned set via `useSelectionStickyFirst` (column-layout.ts) instead
10577
+ // of the `stickyFirst` PROP alone, so the checkbox column sticks the moment ANY column
10578
+ // is pinned, uncontrolled or not (see that hook's own doc comment for why this can't
10579
+ // just be computed once, above, in `DataTableInner`).
10580
+ function SelectionHeaderCell({ stickyFirst, children }) {
10581
+ const resolvedStickyFirst = useSelectionStickyFirst(stickyFirst);
10582
+ return /*#__PURE__*/ jsx(DataTableSelectionHeaderCell, {
10583
+ ...selectionStickyProps(resolvedStickyFirst),
10584
+ children: children
10585
+ });
10586
+ }
10587
+ function SelectionCell({ stickyFirst, onClick, children }) {
10588
+ const resolvedStickyFirst = useSelectionStickyFirst(stickyFirst);
10589
+ return /*#__PURE__*/ jsx(DataTableSelectionCell, {
10590
+ onClick: onClick,
10591
+ ...selectionStickyProps(resolvedStickyFirst),
10592
+ children: children
10593
+ });
10594
+ }
10595
+ function SelectionFooterCell({ stickyFirst }) {
10596
+ const resolvedStickyFirst = useSelectionStickyFirst(stickyFirst);
10597
+ return /*#__PURE__*/ jsx(DataTableFooterCell, {
10598
+ ...selectionStickyProps(resolvedStickyFirst)
10599
+ });
10600
+ }
10150
10601
  ////////////////////////////////////////////////////////////////////////////////////
10151
10602
  // Fix (bug report): karrot icons default to a 24px intrinsic SVG attribute size
10152
10603
  // -- see column-menu.tsx's own `SORT_ICON_SIZE` comment for the full mechanism.
@@ -10202,4 +10653,274 @@ function SelectAllCheckbox({ label, checkedIcon, indeterminateIcon }) {
10202
10653
  });
10203
10654
  }
10204
10655
 
10205
- export { DataTable };
10656
+ const rowStyle = {
10657
+ display: "flex",
10658
+ alignItems: "center",
10659
+ gap: 4
10660
+ };
10661
+ const rowButtonStyle = {
10662
+ flex: 1,
10663
+ minWidth: 0,
10664
+ textAlign: "left",
10665
+ display: "flex",
10666
+ alignItems: "center",
10667
+ gap: 6,
10668
+ padding: "6px 8px",
10669
+ border: "none",
10670
+ borderRadius: 6,
10671
+ background: "transparent",
10672
+ color: "var(--seed-color-fg-neutral)",
10673
+ fontSize: 13,
10674
+ cursor: "pointer",
10675
+ overflow: "hidden",
10676
+ textOverflow: "ellipsis",
10677
+ whiteSpace: "nowrap"
10678
+ };
10679
+ const smallButtonStyle = {
10680
+ flexShrink: 0,
10681
+ border: "none",
10682
+ borderRadius: 6,
10683
+ background: "transparent",
10684
+ color: "var(--seed-color-fg-neutral-subtle)",
10685
+ fontSize: 12,
10686
+ padding: "4px 6px",
10687
+ cursor: "pointer"
10688
+ };
10689
+ const iconButtonStyle = {
10690
+ display: "inline-flex",
10691
+ alignItems: "center",
10692
+ justifyContent: "center",
10693
+ width: 22,
10694
+ height: 22,
10695
+ flexShrink: 0,
10696
+ border: "none",
10697
+ borderRadius: 6,
10698
+ background: "transparent",
10699
+ color: "var(--seed-color-fg-neutral-subtle)",
10700
+ cursor: "pointer"
10701
+ };
10702
+ /**
10703
+ * A compact saved-views switcher -- current view name (or `label`) as the trigger,
10704
+ * a popover listing every saved view (click to switch, inline Rename/Delete), a
10705
+ * trailing "Save as new view", and (once a view is active) "Update view" to overwrite
10706
+ * the loaded view with the table's current state.
10707
+ */ function DataTableViewsControl({ label = "Views", showClearRow = false, clearLabel, captureState, applyState, newViewLabel = "Save as new view", updateViewLabel = "Update view", renameLabel = "Rename", deleteLabel = "Delete", renameInputLabel = (name)=>`Rename ${name}`, ...viewsProps }) {
10708
+ const viewsApi = useDataTableViews(viewsProps);
10709
+ const { activeViewId, activeView } = viewsApi;
10710
+ const triggerLabel = activeView ? activeView.name : label;
10711
+ return /*#__PURE__*/ jsxs(MenuRoot, {
10712
+ children: [
10713
+ /*#__PURE__*/ jsx(MenuTrigger, {
10714
+ asChild: true,
10715
+ children: /*#__PURE__*/ jsxs(ActionButton, {
10716
+ variant: "neutralOutline",
10717
+ size: "xsmall",
10718
+ "data-active": activeViewId ? "" : undefined,
10719
+ children: [
10720
+ /*#__PURE__*/ jsx(IconArrowUpArrowDownLine_default, {
10721
+ size: 14,
10722
+ strokeWidth: 1.75
10723
+ }),
10724
+ triggerLabel,
10725
+ /*#__PURE__*/ jsx(IconChevronDownSmallLine_default, {
10726
+ size: 14,
10727
+ strokeWidth: 1.75
10728
+ })
10729
+ ]
10730
+ })
10731
+ }),
10732
+ /*#__PURE__*/ jsx(MenuPositioner, {
10733
+ children: /*#__PURE__*/ jsx(MenuContent, {
10734
+ style: {
10735
+ minWidth: 240
10736
+ },
10737
+ children: /*#__PURE__*/ jsx(MenuScrollArea, {
10738
+ children: /*#__PURE__*/ jsx(ViewsMenuBody, {
10739
+ viewsApi: viewsApi,
10740
+ captureState: captureState,
10741
+ applyState: applyState,
10742
+ label: label,
10743
+ showClearRow: showClearRow,
10744
+ clearLabel: clearLabel,
10745
+ newViewLabel: newViewLabel,
10746
+ updateViewLabel: updateViewLabel,
10747
+ renameLabel: renameLabel,
10748
+ deleteLabel: deleteLabel,
10749
+ renameInputLabel: renameInputLabel
10750
+ })
10751
+ })
10752
+ })
10753
+ })
10754
+ ]
10755
+ });
10756
+ }
10757
+ DataTableViewsControl.displayName = "DataTableViewsControl";
10758
+ /**
10759
+ * The popover's own body -- split out from `DataTableViewsControl` purely so it can
10760
+ * call `useMenuContext()` (`@seed-design/react-menu`), which only resolves for a
10761
+ * descendant of `MenuRoot`, not the component that RENDERS `MenuRoot` itself (the same
10762
+ * reason `column-layout.ts`'s `useSelectionStickyFirst` is its own component-level
10763
+ * hook rather than something computed in `DataTable.tsx` above its provider). Plain
10764
+ * `<button>` rows here (not seed `MenuItem`s -- `toolbar-conditions.tsx`'s own Sort/
10765
+ * Filter popovers make the same call) don't auto-close the menu on click the way a real
10766
+ * `MenuItem` does, so switching/creating/updating a view calls `setOpen(false)` here
10767
+ * explicitly; renaming and deleting deliberately do NOT close it, so a consumer can
10768
+ * clean up more than one view without reopening the popover each time.
10769
+ */ function ViewsMenuBody({ viewsApi, captureState, applyState, label, showClearRow, clearLabel, newViewLabel, updateViewLabel, renameLabel, deleteLabel, renameInputLabel }) {
10770
+ const { views, activeViewId, createView, updateView, renameView, deleteView, selectView } = viewsApi;
10771
+ const { setOpen } = useMenuContext();
10772
+ const [renamingId, setRenamingId] = React.useState(null);
10773
+ const [renameValue, setRenameValue] = React.useState("");
10774
+ // Same race `column-menu.tsx`'s own inline rename input already guards against:
10775
+ // starting a rename swaps the clicked "Rename" button for an input in the SAME
10776
+ // MenuContent this popover's own floating-ui focus management watches, and its
10777
+ // focus-guard spans can steal focus right back (to the content, or the trigger)
10778
+ // one frame after mount -- BEFORE this ref flips true -- so `onBlur` ignores that
10779
+ // one spurious blur and only commits a REAL blur (an actual click/tab away) after.
10780
+ const renameReadyRef = React.useRef(false);
10781
+ const renameInputRef = React.useRef(null);
10782
+ const handleSelect = (id)=>{
10783
+ selectView(id);
10784
+ if (id !== null) {
10785
+ const view = views.find((v)=>v.id === id);
10786
+ if (view) applyState(view.state);
10787
+ }
10788
+ setOpen(false);
10789
+ };
10790
+ const handleCreate = ()=>{
10791
+ createView(`View ${views.length + 1}`, captureState());
10792
+ setOpen(false);
10793
+ };
10794
+ const handleUpdate = ()=>{
10795
+ if (activeViewId) updateView(activeViewId, captureState());
10796
+ setOpen(false);
10797
+ };
10798
+ const startRename = (view)=>{
10799
+ renameReadyRef.current = false;
10800
+ setRenamingId(view.id);
10801
+ setRenameValue(view.name);
10802
+ };
10803
+ const commitRename = ()=>{
10804
+ if (renamingId && renameValue.trim()) renameView(renamingId, renameValue.trim());
10805
+ setRenamingId(null);
10806
+ };
10807
+ React.useEffect(()=>{
10808
+ if (!renamingId) return;
10809
+ // Re-grab focus (and select the text) one frame after mount, AFTER whatever
10810
+ // floating-ui's own focus-guard restore was going to do -- see this component's
10811
+ // own `renameReadyRef` doc comment above.
10812
+ const frame = requestAnimationFrame(()=>{
10813
+ renameReadyRef.current = true;
10814
+ renameInputRef.current?.focus();
10815
+ renameInputRef.current?.select();
10816
+ });
10817
+ return ()=>cancelAnimationFrame(frame);
10818
+ }, [
10819
+ renamingId
10820
+ ]);
10821
+ return /*#__PURE__*/ jsxs("div", {
10822
+ style: {
10823
+ display: "flex",
10824
+ flexDirection: "column",
10825
+ gap: 2,
10826
+ padding: "4px 8px"
10827
+ },
10828
+ children: [
10829
+ showClearRow && /*#__PURE__*/ jsx("button", {
10830
+ type: "button",
10831
+ onClick: ()=>handleSelect(null),
10832
+ "data-active": !activeViewId ? "" : undefined,
10833
+ style: rowButtonStyle,
10834
+ children: clearLabel ?? label
10835
+ }),
10836
+ views.length === 0 && /*#__PURE__*/ jsx("div", {
10837
+ style: {
10838
+ fontSize: 13,
10839
+ color: "var(--seed-color-fg-neutral-subtle)",
10840
+ padding: "4px 8px"
10841
+ },
10842
+ children: "No saved views yet."
10843
+ }),
10844
+ views.map((view)=>renamingId === view.id ? /*#__PURE__*/ jsx("div", {
10845
+ style: rowStyle,
10846
+ children: /*#__PURE__*/ jsx("input", {
10847
+ ref: renameInputRef,
10848
+ // biome-ignore lint/a11y/noAutofocus: opening this input IS the user's own "Rename" click a moment earlier, not an unannounced page-load autofocus (same call column-menu.tsx's own inline rename input makes).
10849
+ autoFocus: true,
10850
+ "aria-label": renameInputLabel(view.name),
10851
+ value: renameValue,
10852
+ onChange: (event)=>setRenameValue(event.target.value),
10853
+ onBlur: ()=>{
10854
+ if (renameReadyRef.current) commitRename();
10855
+ },
10856
+ onKeyDown: (event)=>{
10857
+ event.stopPropagation();
10858
+ if (event.key === "Enter") commitRename();
10859
+ if (event.key === "Escape") setRenamingId(null);
10860
+ },
10861
+ style: {
10862
+ flex: 1,
10863
+ minWidth: 0,
10864
+ font: "inherit",
10865
+ fontSize: 13,
10866
+ padding: "6px 8px",
10867
+ border: "1px solid var(--seed-color-border-neutral)",
10868
+ borderRadius: 6,
10869
+ background: "var(--seed-color-bg-surface)",
10870
+ color: "var(--seed-color-fg-neutral)"
10871
+ }
10872
+ })
10873
+ }, view.id) : /*#__PURE__*/ jsxs("div", {
10874
+ style: rowStyle,
10875
+ children: [
10876
+ /*#__PURE__*/ jsx("button", {
10877
+ type: "button",
10878
+ onClick: ()=>handleSelect(view.id),
10879
+ "data-active": view.id === activeViewId ? "" : undefined,
10880
+ style: rowButtonStyle,
10881
+ children: view.name
10882
+ }),
10883
+ /*#__PURE__*/ jsx("button", {
10884
+ type: "button",
10885
+ onClick: ()=>startRename(view),
10886
+ style: smallButtonStyle,
10887
+ children: renameLabel
10888
+ }),
10889
+ /*#__PURE__*/ jsx("button", {
10890
+ type: "button",
10891
+ "aria-label": `${deleteLabel} ${view.name}`,
10892
+ onClick: ()=>deleteView(view.id),
10893
+ style: iconButtonStyle,
10894
+ children: /*#__PURE__*/ jsx(IconXmarkLine_default, {
10895
+ size: 13,
10896
+ strokeWidth: 1.75
10897
+ })
10898
+ })
10899
+ ]
10900
+ }, view.id)),
10901
+ activeViewId && /*#__PURE__*/ jsx("button", {
10902
+ type: "button",
10903
+ onClick: handleUpdate,
10904
+ style: {
10905
+ ...rowButtonStyle,
10906
+ color: "var(--seed-color-fg-neutral-subtle)"
10907
+ },
10908
+ children: updateViewLabel
10909
+ }),
10910
+ /*#__PURE__*/ jsxs("button", {
10911
+ type: "button",
10912
+ onClick: handleCreate,
10913
+ style: rowButtonStyle,
10914
+ children: [
10915
+ /*#__PURE__*/ jsx(IconPlusLine_default, {
10916
+ size: 13,
10917
+ strokeWidth: 1.75
10918
+ }),
10919
+ newViewLabel
10920
+ ]
10921
+ })
10922
+ ]
10923
+ });
10924
+ }
10925
+
10926
+ export { DataTable, DataTableViewsControl };