@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.cjs CHANGED
@@ -878,18 +878,33 @@ function useDataTableColumnCalc(props) {
878
878
  return next;
879
879
  }
880
880
  /**
881
- * Drag-to-reorder: removes `dragKey` from `order` and reinserts it immediately before
882
- * `dropKey`'s CURRENT position (i.e. its position after the removal, so dropping a
883
- * column just after its own old slot doesn't appear to do nothing). A no-op (an
884
- * equal-valued copy) when either key is missing or they're the same key.
881
+ * Drag-to-reorder: moves `dragKey` to `dropKey`'s own ORIGINAL index (the standard
882
+ * `arrayMove` two-splice shape -- remove `dragKey`, then reinsert it at `dropKey`'s
883
+ * pre-removal index of the now-shorter array), the same semantics dnd-kit's own
884
+ * `arrayMove` uses. A no-op (an equal-valued copy) when either key is missing or
885
+ * they're the same key.
886
+ *
887
+ * An EARLIER version of this function instead reinserted `dragKey` immediately before
888
+ * `dropKey`'s post-removal position. That reads the same for a multi-column jump, but
889
+ * is a silent no-op for the single most common drag gesture there is: dragging a
890
+ * column onto its own immediate RIGHT neighbor. Concretely, for `["a","b"]`, dragging
891
+ * "a" onto "b": removing "a" leaves `["b"]`, and "immediately before b" is index 0 --
892
+ * exactly where "a" already was, so nothing moved even though a real drag-and-drop
893
+ * gesture happened (caught by `DataTable.columns.test.tsx`'s own end-to-end drag test,
894
+ * which fires a real `dragover` DOM event and asserts the header order actually
895
+ * changed -- it didn't). This version's two-splice shape lands the dragged column at
896
+ * the target's own original slot instead, which swaps immediate neighbors correctly in
897
+ * both directions and still matches the old multi-jump behavior for a LEFTWARD drag
898
+ * (only a RIGHTWARD multi-column jump now lands one slot further, after the drop
899
+ * target instead of before it -- see this file's own test for both directions).
885
900
  */ function reorderColumnKey(order, dragKey, dropKey) {
886
901
  if (dragKey === dropKey || !order.includes(dragKey) || !order.includes(dropKey)) {
887
902
  return [
888
903
  ...order
889
904
  ];
890
905
  }
906
+ const dropIndex = order.indexOf(dropKey);
891
907
  const withoutDrag = order.filter((key)=>key !== dragKey);
892
- const dropIndex = withoutDrag.indexOf(dropKey);
893
908
  return [
894
909
  ...withoutDrag.slice(0, dropIndex),
895
910
  dragKey,
@@ -901,6 +916,24 @@ function useDataTableColumnCalc(props) {
901
916
  * nor balloon past a sane cap from one fast mouse move). */ function clampColumnWidth(width, min = 80, max = 640) {
902
917
  return Math.min(max, Math.max(min, width));
903
918
  }
919
+ /**
920
+ * Reconciles a persisted/controlled PINNED set against the table's current column
921
+ * KEYS -- the same drop-what's-gone rule `reconcileColumnOrder` already applies to
922
+ * `columnOrder`, but pins never need the "append what's new" half of that: an unpinned
923
+ * column simply isn't in the set, appending it here would silently pin every brand-new
924
+ * column by accident. A key no longer in `keys` (hidden isn't "gone" -- only removed
925
+ * from `columns` entirely) is dropped; everything else keeps its order.
926
+ */ function reconcilePinnedColumns(pinned, keys) {
927
+ const keySet = new Set(keys);
928
+ return pinned.filter((key)=>keySet.has(key));
929
+ }
930
+ /** Pin/unpin: adds `key` if absent, removes it if present -- column-menu.tsx's own
931
+ * "Pin column"/"Unpin column" toggle is one call of this, the same shape
932
+ * `toggleMembership` already gives row selection. Kept as its own named function
933
+ * (rather than reusing `toggleMembership` directly) so a pin-specific doc comment and
934
+ * call sites aren't reading a generically-named helper for a column-specific concern. */ function togglePinnedColumn(pinned, key) {
935
+ return toggleMembership(pinned, key);
936
+ }
904
937
  /**
905
938
  * Infinite scroll's own gate: fire `onLoadMore` only when the sentinel is ACTUALLY
906
939
  * intersecting (not e.g. an observer's initial `false` callback, which fires once on
@@ -914,19 +947,90 @@ function useDataTableColumnCalc(props) {
914
947
  */ function shouldTriggerLoadMore(isIntersecting, hasMore, loadingMore) {
915
948
  return hasMore && !loadingMore;
916
949
  }
950
+ function isPlainObject(value) {
951
+ return typeof value === "object" && value !== null && !Array.isArray(value);
952
+ }
953
+ /** Structural validation for ONE parsed view -- every field of `DataTableViewState` is
954
+ * optional, so this only rejects a field that's actually PRESENT but the wrong shape
955
+ * (a consumer's storage from a future/older version of this shape), not a field that's
956
+ * simply missing. */ function isValidDataTableView(value) {
957
+ if (!isPlainObject(value)) return false;
958
+ if (typeof value["id"] !== "string" || typeof value["name"] !== "string") return false;
959
+ if (!isPlainObject(value["state"])) return false;
960
+ const state = value["state"];
961
+ if (state["columnOrder"] !== undefined && !isStringArray(state["columnOrder"])) return false;
962
+ if (state["hiddenColumnKeys"] !== undefined && !isStringArray(state["hiddenColumnKeys"])) return false;
963
+ if (state["pinnedColumnKeys"] !== undefined && !isStringArray(state["pinnedColumnKeys"])) return false;
964
+ if (state["columnWidths"] !== undefined && !isPlainObject(state["columnWidths"])) return false;
965
+ if (state["toolbarSorts"] !== undefined && !Array.isArray(state["toolbarSorts"])) return false;
966
+ if (state["toolbarFilters"] !== undefined && !Array.isArray(state["toolbarFilters"])) return false;
967
+ return true;
968
+ }
969
+ function isStringArray(value) {
970
+ return Array.isArray(value) && value.every((entry)=>typeof entry === "string");
971
+ }
972
+ /** JSON-serializes a saved-views list for the default localStorage store -- pure so it
973
+ * can be unit- and mutation-tested without touching `window.localStorage` at all (the
974
+ * hook's own effect, `useDataTableViews.ts`, is the only thing that actually touches
975
+ * storage; this is just the byte-for-byte shape it writes/reads). */ function serializeDataTableViews(views) {
976
+ return JSON.stringify(views);
977
+ }
978
+ /**
979
+ * Parses a saved-views blob back into a `DataTableView[]`, defensively: malformed JSON,
980
+ * a non-array root, or any single malformed entry drops just that problem (an empty
981
+ * list, or that one entry) rather than throwing -- a consumer's localStorage can hold a
982
+ * PRIOR version's shape after this ships a new field, and one bad/stale entry shouldn't
983
+ * cost every other saved view.
984
+ */ function deserializeDataTableViews(raw) {
985
+ if (!raw) return [];
986
+ let parsed;
987
+ try {
988
+ parsed = JSON.parse(raw);
989
+ } catch {
990
+ return [];
991
+ }
992
+ if (!Array.isArray(parsed)) return [];
993
+ return parsed.filter(isValidDataTableView);
994
+ }
995
+ /** Inserts `view` (by `id`), or replaces the existing entry with the same `id` in
996
+ * place -- "Save" (new view) and "update the currently loaded view" are both one call
997
+ * of this, matching `moveColumnKey`/`reorderColumnKey`'s own "one pure op per user
998
+ * gesture" shape. */ function upsertDataTableView(views, view) {
999
+ const index = views.findIndex((existing)=>existing.id === view.id);
1000
+ if (index === -1) return [
1001
+ ...views,
1002
+ view
1003
+ ];
1004
+ const next = [
1005
+ ...views
1006
+ ];
1007
+ next[index] = view;
1008
+ return next;
1009
+ }
1010
+ /** Renames the view with this `id` -- a no-op (an equal-valued copy) if no view has
1011
+ * that id. */ function renameDataTableView(views, id, name) {
1012
+ return views.map((view)=>view.id === id ? {
1013
+ ...view,
1014
+ name
1015
+ } : view);
1016
+ }
1017
+ /** Removes the view with this `id`. */ function removeDataTableView(views, id) {
1018
+ return views.filter((view)=>view.id !== id);
1019
+ }
917
1020
  const EMPTY_STRINGS = [];
918
1021
  const EMPTY_RECORD = {};
919
1022
  /**
920
1023
  * Owns everything DATATABLE-ATTIO-SPEC.md's column menu + reorder + resize tranche
921
- * needs -- order, visibility, a per-column label override, and a per-column width --
922
- * the way useDataTableSelection owns the one selected-id SET rather than one hook per
923
- * row. All four are "one value per column key" maps/lists a consumer may want to
924
- * persist (a saved view), so they share this hook instead of splitting into four
925
- * separate ones; drag-to-reorder and drag-to-resize (below) are the two INTERACTIONS
926
- * built on top of the order/width state this hook already owns, not independent state
1024
+ * needs -- order, visibility, a per-column label override, a per-column width, and
1025
+ * (Attio's "Pin column") a per-column left-pinned flag -- the way useDataTableSelection
1026
+ * owns the one selected-id SET rather than one hook per row. All five are "one value
1027
+ * per column key" maps/lists/sets a consumer may want to persist (a saved view), so
1028
+ * they share this hook instead of splitting into five separate ones; drag-to-reorder
1029
+ * and drag-to-resize (below) are the two INTERACTIONS built on top of the order/width
1030
+ * state this hook already owns, not independent state
927
1031
  * of their own.
928
1032
  */ function useDataTableColumns(props) {
929
- 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;
1033
+ 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;
930
1034
  const [rawOrder, setRawOrder] = useControllableState$1({
931
1035
  prop: columnOrderProp,
932
1036
  defaultProp: defaultColumnOrder ?? columnKeys,
@@ -963,6 +1067,36 @@ const EMPTY_RECORD = {};
963
1067
  columnOrder,
964
1068
  hiddenSet
965
1069
  ]);
1070
+ const [rawPinnedColumnKeys, setRawPinnedColumnKeys] = useControllableState$1({
1071
+ prop: pinnedProp,
1072
+ defaultProp: defaultPinnedColumnKeys,
1073
+ onChange: onPinnedColumnKeysChange
1074
+ });
1075
+ // Reconciled against `columnKeys` every render, the same way `columnOrder` is above --
1076
+ // a controlled `pinnedColumnKeys` a consumer persisted (e.g. a saved view) before a
1077
+ // column was removed from `columns` must not keep pinning a ghost key.
1078
+ const pinnedColumnKeys = React.useMemo(()=>reconcilePinnedColumns(rawPinnedColumnKeys ?? EMPTY_STRINGS, columnKeys), [
1079
+ rawPinnedColumnKeys,
1080
+ columnKeys
1081
+ ]);
1082
+ const pinnedSet = React.useMemo(()=>new Set(pinnedColumnKeys), [
1083
+ pinnedColumnKeys
1084
+ ]);
1085
+ const isColumnPinned = React.useCallback((key)=>pinnedSet.has(key), [
1086
+ pinnedSet
1087
+ ]);
1088
+ const pinColumn = React.useCallback((key)=>setRawPinnedColumnKeys((current = EMPTY_STRINGS)=>current.includes(key) ? current : [
1089
+ ...current,
1090
+ key
1091
+ ]), [
1092
+ setRawPinnedColumnKeys
1093
+ ]);
1094
+ const unpinColumn = React.useCallback((key)=>setRawPinnedColumnKeys((current = EMPTY_STRINGS)=>current.filter((existing)=>existing !== key)), [
1095
+ setRawPinnedColumnKeys
1096
+ ]);
1097
+ const togglePinColumn = React.useCallback((key)=>setRawPinnedColumnKeys((current = EMPTY_STRINGS)=>togglePinnedColumn(current, key)), [
1098
+ setRawPinnedColumnKeys
1099
+ ]);
966
1100
  const isColumnHidden = React.useCallback((key)=>hiddenSet.has(key), [
967
1101
  hiddenSet
968
1102
  ]);
@@ -1007,22 +1141,30 @@ const EMPTY_RECORD = {};
1007
1141
  columnKeys
1008
1142
  ]);
1009
1143
  // -- drag-to-reorder ---------------------------------------------------------------
1010
- // Live state only (which column is currently being dragged) -- the styled layer
1011
- // drives it by calling `dragOverColumn` as the pointer crosses OTHER header cells
1012
- // (via `document.elementFromPoint`, since a captured pointer stops native
1013
- // `pointerenter` from firing on siblings), which reorders immediately rather than
1014
- // waiting for a drop. That is exactly what Attio's own header drag does (the column
1015
- // being passed visibly swaps live), and it means `endColumnDrag` never has to also
1016
- // commit a move -- there is nothing left to commit.
1144
+ // Live state only (which column is currently being dragged) -- the styled layer's
1145
+ // header cells are natively `draggable`, so `dragOverColumn` is driven straight off
1146
+ // each cell's own `onDragOver` (one real browser event per column the pointer
1147
+ // crosses), which reorders immediately rather than waiting for a drop. That is
1148
+ // exactly what Attio's own header drag does (the column being passed visibly swaps
1149
+ // live), and it means `endColumnDrag` never has to also commit a move -- there is
1150
+ // nothing left to commit.
1017
1151
  const [draggingColumnKey, setDraggingColumnKey] = React.useState(null);
1018
1152
  const beginColumnDrag = React.useCallback((key)=>setDraggingColumnKey(key), []);
1153
+ // Reads `draggingColumnKey` straight from this render's own closure rather than via
1154
+ // `setDraggingColumnKey`'s functional-updater form (an earlier version of this hook
1155
+ // nested the `reorderColumn` call inside that updater, to read a "live" value --
1156
+ // `draggingColumnKey` never actually CHANGES during a drag, so that state update was
1157
+ // pure ceremony, and nesting one hook's setState inside another's updater callback is
1158
+ // exactly what silently dropped the reorder in DataTable.columns.test.tsx's own
1159
+ // end-to-end drag test: a REAL `dragover` DOM event fired, `dragOverColumn` fired with
1160
+ // the right key, but `columnOrder` never actually moved). Each `onDragOver` is its own
1161
+ // discrete browser event (not a tight synchronous loop within one render), so a plain
1162
+ // closure read is exactly as live as the updater trick was trying to be.
1019
1163
  const dragOverColumn = React.useCallback((hoverKey)=>{
1020
- setDraggingColumnKey((current)=>{
1021
- if (!current || current === hoverKey) return current;
1022
- reorderColumn(current, hoverKey);
1023
- return current;
1024
- });
1164
+ if (!draggingColumnKey || draggingColumnKey === hoverKey) return;
1165
+ reorderColumn(draggingColumnKey, hoverKey);
1025
1166
  }, [
1167
+ draggingColumnKey,
1026
1168
  reorderColumn
1027
1169
  ]);
1028
1170
  const endColumnDrag = React.useCallback(()=>setDraggingColumnKey(null), []);
@@ -1058,10 +1200,15 @@ const EMPTY_RECORD = {};
1058
1200
  hiddenColumnKeys,
1059
1201
  columnWidths,
1060
1202
  columnLabels,
1203
+ pinnedColumnKeys,
1061
1204
  isColumnHidden,
1062
1205
  hideColumn,
1063
1206
  showColumn,
1064
1207
  toggleColumnVisibility,
1208
+ isColumnPinned,
1209
+ pinColumn,
1210
+ unpinColumn,
1211
+ togglePinColumn,
1065
1212
  getColumnLabel,
1066
1213
  renameColumn,
1067
1214
  getColumnWidth,
@@ -1710,7 +1857,7 @@ function useDataTableRowContext({ strict = true } = {}) {
1710
1857
  return context;
1711
1858
  }
1712
1859
  const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1713
- 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;
1860
+ 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;
1714
1861
  const api = useDataTable({
1715
1862
  sort,
1716
1863
  defaultSort,
@@ -1742,6 +1889,9 @@ const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1742
1889
  columnLabels,
1743
1890
  defaultColumnLabels,
1744
1891
  onColumnLabelsChange,
1892
+ pinnedColumnKeys,
1893
+ defaultPinnedColumnKeys,
1894
+ onPinnedColumnKeysChange,
1745
1895
  columnCalcOps,
1746
1896
  defaultColumnCalcOps,
1747
1897
  onColumnCalcOpsChange,
@@ -1829,6 +1979,132 @@ const DataTableCell$1 = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1829
1979
  });
1830
1980
  DataTableCell$1.displayName = "DataTableCell";
1831
1981
 
1982
+ const EMPTY_VIEWS = [];
1983
+ function readStoredViews(storageKey) {
1984
+ if (!storageKey || typeof window === "undefined") return EMPTY_VIEWS;
1985
+ try {
1986
+ return deserializeDataTableViews(window.localStorage.getItem(storageKey));
1987
+ } catch {
1988
+ // A private-browsing tab / blocked storage throws on access, not just on quota --
1989
+ // this hook degrades to in-memory-only rather than crash the table over it.
1990
+ return EMPTY_VIEWS;
1991
+ }
1992
+ }
1993
+ /** A reasonably unique id for a newly created view -- `crypto.randomUUID` where it
1994
+ * exists (every real browser + modern Node), a plain timestamp+random fallback
1995
+ * otherwise (older jsdom/test environments) so this hook never throws just because a
1996
+ * test runner's `crypto` is a stub. */ function generateViewId() {
1997
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1998
+ return crypto.randomUUID();
1999
+ }
2000
+ return `view-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2001
+ }
2002
+ /**
2003
+ * Owns the saved-views LIST (create/rename/delete/select) and its default persistence
2004
+ * (localStorage, pluggable away by controlling `views`/`onViewsChange` instead) -- the
2005
+ * same controllable shape every other DataTable state hook already uses
2006
+ * (`useDataTableColumns`, `useDataTableToolbarSort`, ...). Read `DataTableViewsControl`
2007
+ * (`views.tsx`, the styled layer) for how a consumer wires `captureState`/`applyState`
2008
+ * around this to actually save/restore column + sort + filter state.
2009
+ */ function useDataTableViews(props) {
2010
+ const { views: viewsProp, defaultViews = EMPTY_VIEWS, onViewsChange, activeViewId: activeViewIdProp, defaultActiveViewId = null, onActiveViewIdChange, storageKey } = props;
2011
+ // Lazy, ONE-TIME read of localStorage as this render's default -- re-reading it on
2012
+ // every render (rather than a lazy ref) would clobber whatever the user just did in
2013
+ // THIS tab with whatever another tab most recently wrote. Only fires at all when this
2014
+ // hook truly owns persistence: `views` is uncontrolled AND a `storageKey` was given.
2015
+ const initialViewsRef = React.useRef(null);
2016
+ if (initialViewsRef.current === null) {
2017
+ if (viewsProp !== undefined) {
2018
+ initialViewsRef.current = defaultViews;
2019
+ } else {
2020
+ const stored = readStoredViews(storageKey);
2021
+ // Nothing in storage yet (first run, or no `storageKey` at all) -- seed from
2022
+ // `defaultViews` instead of an empty list, matching every other hook's own
2023
+ // `defaultProp` semantics.
2024
+ initialViewsRef.current = stored.length > 0 ? stored : defaultViews;
2025
+ }
2026
+ }
2027
+ const [views = EMPTY_VIEWS, setViews] = useControllableState$1({
2028
+ prop: viewsProp,
2029
+ defaultProp: initialViewsRef.current,
2030
+ onChange: onViewsChange
2031
+ });
2032
+ // Persists every change back to `storageKey` -- same "only when this hook owns
2033
+ // persistence" gate as the lazy read above. Skips the FIRST render (the effect would
2034
+ // otherwise immediately re-write back the exact bytes it just read, or stomp an
2035
+ // empty default over a controlled consumer's storage before it ever mounts).
2036
+ const skipFirstPersistRef = React.useRef(true);
2037
+ React.useEffect(()=>{
2038
+ if (viewsProp !== undefined || !storageKey || typeof window === "undefined") return;
2039
+ if (skipFirstPersistRef.current) {
2040
+ skipFirstPersistRef.current = false;
2041
+ return;
2042
+ }
2043
+ try {
2044
+ window.localStorage.setItem(storageKey, serializeDataTableViews(views));
2045
+ } catch {
2046
+ // Storage full/blocked -- the view still exists in memory for this session.
2047
+ }
2048
+ }, [
2049
+ views,
2050
+ viewsProp,
2051
+ storageKey
2052
+ ]);
2053
+ const [activeViewId = null, setActiveViewId] = useControllableState$1({
2054
+ prop: activeViewIdProp,
2055
+ defaultProp: defaultActiveViewId,
2056
+ onChange: onActiveViewIdChange
2057
+ });
2058
+ const activeView = views.find((view)=>view.id === activeViewId) ?? null;
2059
+ const createView = React.useCallback((name, state)=>{
2060
+ const id = generateViewId();
2061
+ setViews((current = EMPTY_VIEWS)=>upsertDataTableView(current, {
2062
+ id,
2063
+ name,
2064
+ state
2065
+ }));
2066
+ setActiveViewId(id);
2067
+ return id;
2068
+ }, [
2069
+ setViews,
2070
+ setActiveViewId
2071
+ ]);
2072
+ const updateView = React.useCallback((id, state)=>{
2073
+ setViews((current = EMPTY_VIEWS)=>{
2074
+ const existing = current.find((view)=>view.id === id);
2075
+ return existing ? upsertDataTableView(current, {
2076
+ ...existing,
2077
+ state
2078
+ }) : current;
2079
+ });
2080
+ }, [
2081
+ setViews
2082
+ ]);
2083
+ const renameView = React.useCallback((id, name)=>setViews((current = EMPTY_VIEWS)=>renameDataTableView(current, id, name)), [
2084
+ setViews
2085
+ ]);
2086
+ const deleteView = React.useCallback((id)=>{
2087
+ setViews((current = EMPTY_VIEWS)=>removeDataTableView(current, id));
2088
+ setActiveViewId((current)=>current === id ? null : current);
2089
+ }, [
2090
+ setViews,
2091
+ setActiveViewId
2092
+ ]);
2093
+ const selectView = React.useCallback((id)=>setActiveViewId(id), [
2094
+ setActiveViewId
2095
+ ]);
2096
+ return {
2097
+ views,
2098
+ activeViewId,
2099
+ activeView,
2100
+ createView,
2101
+ updateView,
2102
+ renameView,
2103
+ deleteView,
2104
+ selectView
2105
+ };
2106
+ }
2107
+
1832
2108
  function r(e) {
1833
2109
  var t, f, n = "";
1834
2110
  if ("string" == typeof e || "number" == typeof e) n += e;
@@ -2354,76 +2630,121 @@ function RatingCell({ value, maxRating = 5 }) {
2354
2630
  });
2355
2631
  }
2356
2632
 
2357
- /** The subset of `useDataTableContext()` every "resolve which columns to actually
2358
- * render, in what order, under what label/width" helper below needs -- narrowed so
2359
- * each helper's own signature documents exactly what it reads, rather than the whole
2360
- * table API. */ function useEffectiveColumns(columns) {
2361
- const { visibleColumnOrder, getColumnLabel, getColumnWidth } = useDataTableContext();
2633
+ /** Fallback width (px) used to compute a pinned column's cumulative sticky offset when
2634
+ * it has no explicit `width` and hasn't been drag-resized -- pinning needs a STATIC
2635
+ * number to put in an inline `insetInlineStart`, and `table-layout: fixed` only
2636
+ * decides how an unset column's share of the leftover width actually renders at paint
2637
+ * time (something no amount of JS math here can know ahead of it). Matches
2638
+ * `clampColumnWidth`'s own floor (react-headless's `dom.ts`) -- a real pinned column
2639
+ * narrower than this only needs its offset "wide enough not to overlap", not exact. */ const DEFAULT_PINNED_COLUMN_WIDTH = 180;
2640
+ /**
2641
+ * Every column this table actually renders, in RENDER order: any pinned columns first
2642
+ * (their own relative order preserved from `visibleColumnOrder`, not a separate pin-
2643
+ * order of their own), then everything else in normal order -- resolved label/width,
2644
+ * and this file's own sticky-left bookkeeping (`pinned`/`pinOffset`/`pinnedEdge`). Read
2645
+ * by the header, the body, the ColumnCalc footer AND the `<colgroup>` alike -- all four
2646
+ * must agree on this exact order, since a `<colgroup>`'s `<col>`s are matched
2647
+ * POSITIONALLY to every row's cells; splitting "order" (here) from "which one is
2648
+ * pinned" into two independently-ordered helpers would let a `<colgroup>` built from
2649
+ * one and a header row built from the other drift out of sync under `table-layout:
2650
+ * fixed`'s own column-to-cell matching.
2651
+ */ function useEffectiveColumns(columns, options) {
2652
+ const { visibleColumnOrder, getColumnLabel, getColumnWidth, pinnedColumnKeys } = useDataTableContext();
2653
+ const { selectable, stickyFirst } = options;
2362
2654
  return React__namespace.useMemo(()=>{
2363
2655
  const byKey = new Map(columns.map((column)=>[
2364
2656
  column.key,
2365
2657
  column
2366
2658
  ]));
2367
- return visibleColumnOrder.map((key)=>byKey.get(key)).filter((column)=>Boolean(column)).map((column)=>({
2659
+ const visibleKeySet = new Set(visibleColumnOrder);
2660
+ const explicitPins = new Set((pinnedColumnKeys ?? []).filter((key)=>byKey.has(key) && visibleKeySet.has(key)));
2661
+ // Legacy fallback (DataTable.columns.test.tsx's own `stickyFirst` tests, predating
2662
+ // per-column pin): no EXPLICIT pin at all, `stickyFirst` pins exactly the first
2663
+ // visible column -- the single-column behavior this file now generalizes to a
2664
+ // whole pinned GROUP.
2665
+ const pinnedSet = explicitPins.size > 0 ? explicitPins : stickyFirst && visibleColumnOrder.length > 0 ? new Set([
2666
+ visibleColumnOrder[0]
2667
+ ]) : explicitPins;
2668
+ // Stable partition: pinned columns first (their relative order among THEMSELVES
2669
+ // preserved from `visibleColumnOrder`), then every other column, also in its
2670
+ // existing relative order. `Array.prototype.sort` is spec-stable (ES2019+).
2671
+ const ordered = [
2672
+ ...visibleColumnOrder
2673
+ ].sort((a, b)=>{
2674
+ const aPinned = pinnedSet.has(a) ? 0 : 1;
2675
+ const bPinned = pinnedSet.has(b) ? 0 : 1;
2676
+ return aPinned - bPinned;
2677
+ });
2678
+ let cursor = selectable ? 40 : 0;
2679
+ let pinnedCount = 0;
2680
+ const entries = ordered.map((key)=>byKey.get(key)).filter((column)=>Boolean(column)).map((column)=>{
2681
+ const width = getColumnWidth(column.key) ?? column.width;
2682
+ const pinned = pinnedSet.has(column.key);
2683
+ const pinOffset = pinned ? cursor : 0;
2684
+ if (pinned) {
2685
+ cursor += width ?? DEFAULT_PINNED_COLUMN_WIDTH;
2686
+ pinnedCount += 1;
2687
+ }
2688
+ return {
2368
2689
  column,
2369
2690
  label: getColumnLabel(column.key, column.header),
2370
- width: getColumnWidth(column.key) ?? column.width
2371
- }));
2691
+ width,
2692
+ pinned,
2693
+ pinOffset,
2694
+ pinnedEdge: false
2695
+ };
2696
+ });
2697
+ if (pinnedCount > 0) entries[pinnedCount - 1].pinnedEdge = true;
2698
+ return entries;
2372
2699
  }, [
2373
2700
  columns,
2374
2701
  visibleColumnOrder,
2375
2702
  getColumnLabel,
2376
- getColumnWidth
2377
- ]);
2378
- }
2379
- /** Sticky positioning for the leading/trailing visible cell of a row (header, body, or
2380
- * footer) -- `stickyFirst` pins the SELECTION cell when the table has one, plus the
2381
- * first data column; `stickyLast` always pins the last VISIBLE data column. Only the
2382
- * OUTERMOST sticky cell on each side gets `data-sticky-edge(-end)` -- that's the one
2383
- * whose shadow should reveal on scroll, not every sticky cell.
2384
- *
2385
- * When both a selection column and a sticky first data column are present, the edge
2386
- * is the data column (e.g. Company), not the 40px selection cell: the data column is
2387
- * ALSO `position: sticky`, sitting immediately to its right, so a shadow cast from the
2388
- * selection cell's trailing edge would render directly underneath that opaque sticky
2389
- * cell and never actually be visible. Putting `data-sticky-edge` on the trailing-most
2390
- * sticky-left cell is what makes the shadow render somewhere unobstructed -- past the
2391
- * whole pinned group, not between two members of it. */ function stickyProps(options) {
2392
- const { selectable, stickyFirst, stickyLast, isSelectionCell, isFirst, isLast } = options;
2393
- if (stickyFirst && isSelectionCell) {
2394
- // Still sticky either way. The edge marker only belongs here when there is no
2395
- // sticky first DATA column to hand it to instead (i.e. no columns at all) --
2396
- // otherwise that data column is the true trailing edge of the pinned group.
2397
- return selectable ? {
2398
- "data-sticky": "",
2399
- style: {
2400
- insetInlineStart: 0
2401
- }
2402
- } : {
2403
- "data-sticky": "",
2404
- "data-sticky-edge": "",
2405
- style: {
2406
- insetInlineStart: 0
2407
- }
2408
- };
2409
- }
2410
- if (stickyFirst && isFirst && !selectable) {
2411
- return {
2412
- "data-sticky": "",
2413
- "data-sticky-edge": "",
2414
- style: {
2415
- insetInlineStart: 0
2416
- }
2417
- };
2418
- }
2419
- if (stickyFirst && isFirst && selectable) {
2420
- // The trailing edge of the pinned group -- past the 40px selection cell, so this
2421
- // is where the scroll shadow actually renders unobstructed.
2703
+ getColumnWidth,
2704
+ pinnedColumnKeys,
2705
+ selectable,
2706
+ stickyFirst
2707
+ ]);
2708
+ }
2709
+ /** The 40px selection column's own sticky-left state -- `stickyFirst` (the table-wide
2710
+ * prop) alone undercounts "should the checkbox column stick" the moment ANY per-column
2711
+ * pin is live, controlled or not, since the live pinned SET only exists once
2712
+ * `DataTablePrimitiveRoot`'s provider has mounted (an uncontrolled consumer never
2713
+ * hands its pinned columns back up as a prop for `DataTable.tsx` to read directly).
2714
+ * Mirrors `DataTable.tsx`'s own `resolvedStickyLast = stickyLast ||
2715
+ * columns.some(actions)` widening, just resolved one level down (live STATE, not
2716
+ * static column config) -- read via context from a component under the provider, not
2717
+ * computed above it. */ function useSelectionStickyFirst(stickyFirst) {
2718
+ const { pinnedColumnKeys } = useDataTableContext();
2719
+ return stickyFirst || (pinnedColumnKeys?.length ?? 0) > 0;
2720
+ }
2721
+ /** Sticky positioning for the 40px SELECTION cell only (header/body/footer) -- data
2722
+ * columns use their own `pinned`/`pinOffset`/`pinnedEdge` (`useEffectiveColumns`,
2723
+ * above) instead, via `pinnedCellProps` below. The selection cell is always the
2724
+ * leading edge of the pinned-left group when it's sticky at all, so it never itself
2725
+ * carries `data-sticky-edge` -- the trailing-most pinned DATA column does (its own
2726
+ * `pinnedEdge`), the same split the original single-column `stickyFirst` already drew
2727
+ * between the selection cell and Company/Name. */ function selectionStickyProps(stickyFirst) {
2728
+ return stickyFirst ? {
2729
+ "data-sticky": "",
2730
+ style: {
2731
+ insetInlineStart: 0
2732
+ }
2733
+ } : {};
2734
+ }
2735
+ /** Sticky positioning for one DATA cell (header, body, or ColumnCalc footer) -- pinned-
2736
+ * left (this column's own `pinOffset`, `data-sticky-edge` only on the group's
2737
+ * trailing-most member) takes priority over sticky-right (`stickyLast`, always just
2738
+ * the last VISIBLE column, e.g. an `actions` column) since a column can only stick to
2739
+ * one edge. */ function pinnedCellProps(entry, stickyLast, isLast) {
2740
+ if (entry.pinned) {
2422
2741
  return {
2423
2742
  "data-sticky": "",
2424
- "data-sticky-edge": "",
2743
+ ...entry.pinnedEdge ? {
2744
+ "data-sticky-edge": ""
2745
+ } : {},
2425
2746
  style: {
2426
- insetInlineStart: 40
2747
+ insetInlineStart: entry.pinOffset
2427
2748
  }
2428
2749
  };
2429
2750
  }
@@ -2763,265 +3084,555 @@ const TextFieldTextarea = /*#__PURE__*/ React.forwardRef(({ className, autoresiz
2763
3084
  });
2764
3085
  TextFieldTextarea.displayName = "TextFieldTextarea";
2765
3086
 
2766
- /** The raw value a live editor reads, distinct from `DataTableColumn.value` (the
2767
- * accessor sort/search/filter/export share): `editValue` returns whatever TYPE the
2768
- * editor itself expects -- a real `boolean` for `editor: "switch"`/`type:
2769
- * "checkbox"`, a `number` for `type: "rating"`, the option's own string for `select`/
2770
- * `reference`/`actor` -- where `value`/`render` may format that same field for
2771
- * display. Falls back to the table's own accessor when the column supplies none. */ function resolveEditValue(column, row, fallback) {
2772
- return column.editValue ? column.editValue(row) : fallback;
2773
- }
2774
- function EditPendingIndicator() {
2775
- return /*#__PURE__*/ jsxRuntime.jsxs("svg", {
2776
- className: "seed-data-table-edit-pending",
2777
- width: 14,
2778
- height: 14,
2779
- viewBox: "0 0 16 16",
2780
- fill: "none",
2781
- role: "presentation",
2782
- "aria-hidden": "true",
2783
- children: [
2784
- /*#__PURE__*/ jsxRuntime.jsx("circle", {
2785
- cx: 8,
2786
- cy: 8,
2787
- r: 6.5,
2788
- stroke: "currentColor",
2789
- strokeOpacity: 0.25,
2790
- strokeWidth: 2
2791
- }),
2792
- /*#__PURE__*/ jsxRuntime.jsx("path", {
2793
- d: "M14.5 8a6.5 6.5 0 0 0-6.5-6.5",
2794
- stroke: "currentColor",
2795
- strokeWidth: 2,
2796
- strokeLinecap: "round"
2797
- })
2798
- ]
2799
- });
2800
- }
2801
- function EditError({ message }) {
2802
- return /*#__PURE__*/ jsxRuntime.jsx("span", {
2803
- className: "seed-data-table-edit-error",
2804
- children: message
2805
- });
2806
- }
2807
- ////////////////////////////////////////////////////////////////////////////////////
2808
- // -- plain editable: text/number (seed TextField) and date/datetime (a native input,
2809
- // styled the same way, per `cellType`) -- both double-click gated, Enter/blur
2810
- // commits, Escape cancels. -------------------------------------------------------
2811
- function TextEditor({ column, value, pending, onCommit, onCancel }) {
2812
- const [draft, setDraft] = React__namespace.useState(()=>value == null ? "" : String(value));
2813
- return(// `size="medium"` (seed's smallest fixed `TextField` size, 40px min-height,
2814
- // `text-input.ts`'s own token) -- the default `size="large"` overflowed a 36px
2815
- // table row (same class of fix as `toolbar.tsx`'s `SearchBox`, which made the
2816
- // same call for its own 32px-ish search box).
2817
- /*#__PURE__*/ jsxRuntime.jsx(TextFieldRoot, {
2818
- size: "medium",
2819
- style: {
2820
- width: "100%"
2821
- },
2822
- onClick: (event)=>event.stopPropagation(),
2823
- onKeyDown: (event)=>event.stopPropagation(),
2824
- children: /*#__PURE__*/ jsxRuntime.jsx(TextFieldInput, {
2825
- autoFocus: true,
2826
- type: column.cellType === "number" ? "number" : "text",
2827
- "aria-label": `Edit ${column.key}`,
2828
- value: draft,
2829
- disabled: pending,
2830
- onChange: (event)=>setDraft(event.target.value),
2831
- onBlur: ()=>onCommit(draft),
2832
- onKeyDown: (event)=>{
2833
- if (event.key === "Enter") onCommit(draft);
2834
- else if (event.key === "Escape") onCancel();
2835
- }
2836
- })
2837
- }));
3087
+ __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))}");
3088
+
3089
+ const createClassName = (className, variants, compoundVariants = [])=>{
3090
+ const variantKeys = Object.keys(variants);
3091
+ const variantValues = variantKeys.map((key)=>variants[key]);
3092
+ const variantClassName = variantKeys.map((key, index)=>`${className}--${key}_${variantValues[index]}`).join(" ");
3093
+ const compountVariantClassName = compoundVariants.filter((compoundVariant)=>Object.keys(compoundVariant).every((key)=>compoundVariant[key] === variants[key])).map((compoundVariant)=>`${className}--${Object.keys(compoundVariant).map((key)=>{
3094
+ return `${key}_${compoundVariant[key]}`;
3095
+ }).join("-")}`).join(" ");
3096
+ return [
3097
+ className,
3098
+ variantClassName,
3099
+ compountVariantClassName
3100
+ ].filter(Boolean).join(" ");
3101
+ };
3102
+ function mergeVariants(a, b) {
3103
+ const result = {
3104
+ ...a
3105
+ };
3106
+ for(const k in b){
3107
+ if (b[k] != null) {
3108
+ result[k] = b[k];
3109
+ }
3110
+ }
3111
+ return result;
2838
3112
  }
2839
- function DateEditor({ column, value, pending, onCommit, onCancel }) {
2840
- const isDateTime = column.cellType === "datetime";
2841
- const [draft, setDraft] = React__namespace.useState(()=>isDateTime ? toDateTimeInputValue(value) : toDateInputValue(value));
2842
- return(// See `TextEditor`'s own comment above -- same `size="medium"` fix.
2843
- /*#__PURE__*/ jsxRuntime.jsx(TextFieldRoot, {
2844
- size: "medium",
2845
- style: {
2846
- width: "100%"
2847
- },
2848
- onClick: (event)=>event.stopPropagation(),
2849
- onKeyDown: (event)=>event.stopPropagation(),
2850
- children: /*#__PURE__*/ jsxRuntime.jsx(TextFieldInput, {
2851
- autoFocus: true,
2852
- type: isDateTime ? "datetime-local" : "date",
2853
- "aria-label": `Edit ${column.key}`,
2854
- value: draft,
2855
- disabled: pending,
2856
- onChange: (event)=>setDraft(event.target.value),
2857
- onBlur: ()=>onCommit(draft),
2858
- onKeyDown: (event)=>{
2859
- if (event.key === "Enter") onCommit(draft);
2860
- else if (event.key === "Escape") onCancel();
2861
- }
2862
- })
2863
- }));
3113
+ function splitVariantProps(props, variantMap) {
3114
+ const variantProps = {};
3115
+ const otherProps = {};
3116
+ for(const key in props){
3117
+ if (variantMap[key] != null) {
3118
+ variantProps[key] = props[key];
3119
+ } else {
3120
+ otherProps[key] = props[key];
3121
+ }
3122
+ }
3123
+ return [
3124
+ variantProps,
3125
+ otherProps
3126
+ ];
2864
3127
  }
2865
- ////////////////////////////////////////////////////////////////////////////////////
2866
- // -- editor: "select" -- double-click opens seed's own Select, auto-opened (seed's
2867
- // `SelectRoot` takes `defaultOpen` directly -- unlike crisp's closed-API `Select`,
2868
- // which needs a synthetic click on its own trigger to start open). -------------
2869
- function SelectEditor({ column, value, pending, onCommit, onCancel }) {
2870
- const options = column.options ?? [];
2871
- const current = value == null ? "" : String(value);
2872
- return /*#__PURE__*/ jsxRuntime.jsxs(SelectRoot, {
2873
- // Controlled open, per docs/examples/react/select/controlled-open.tsx --
2874
- // this editor is only ever mounted while the cell IS editing, so it always
2875
- // wants to start (and stay) open; `onOpenChange(false)` is what tells us
2876
- // the popover wants to close for ANY reason (Escape, an outside click, OR
2877
- // the internal auto-close a single-select commit already triggers), so
2878
- // `onCancel` alone now covers the Escape gap the old raw `document`
2879
- // listener existed for, without a second, independent listener.
2880
- open: true,
2881
- onOpenChange: (next)=>{
2882
- if (!next) onCancel();
2883
- },
2884
- disabled: pending,
2885
- value: current ? [
2886
- current
2887
- ] : [],
2888
- onValueChange: (next)=>onCommit(next[0] ?? ""),
2889
- children: [
2890
- /*#__PURE__*/ jsxRuntime.jsxs(SelectTrigger, {
2891
- "aria-label": `Edit ${column.key}`,
2892
- children: [
2893
- /*#__PURE__*/ jsxRuntime.jsx(SelectValue, {}),
2894
- /*#__PURE__*/ jsxRuntime.jsx(SelectPlaceholder, {
2895
- children: "Select…"
2896
- }),
2897
- /*#__PURE__*/ jsxRuntime.jsx(SelectSuffixIcon, {
2898
- svg: /*#__PURE__*/ jsxRuntime.jsx(IconChevronDownSmallLine_default, {})
2899
- })
2900
- ]
2901
- }),
2902
- /*#__PURE__*/ jsxRuntime.jsx(SelectPositioner, {
2903
- children: /*#__PURE__*/ jsxRuntime.jsx(SelectContent, {
2904
- children: /*#__PURE__*/ jsxRuntime.jsx(SelectScrollArea, {
2905
- children: options.map((option)=>/*#__PURE__*/ jsxRuntime.jsxs(SelectItem, {
2906
- value: option.value,
2907
- label: option.label,
2908
- children: [
2909
- /*#__PURE__*/ jsxRuntime.jsx(SelectItemBody, {
2910
- children: /*#__PURE__*/ jsxRuntime.jsx(SelectItemLabel, {})
2911
- }),
2912
- /*#__PURE__*/ jsxRuntime.jsx(SelectItemIndicator, {
2913
- selected: /*#__PURE__*/ jsxRuntime.jsx(IconCheckmarkFatFill_default, {})
2914
- })
2915
- ]
2916
- }, option.value))
2917
- })
2918
- })
2919
- })
2920
- ]
2921
- });
3128
+
3129
+ const dataTableSlotNames = [
3130
+ [
3131
+ "root",
3132
+ "seed-data-table__root"
3133
+ ],
3134
+ [
3135
+ "table",
3136
+ "seed-data-table__table"
3137
+ ],
3138
+ [
3139
+ "header",
3140
+ "seed-data-table__header"
3141
+ ],
3142
+ [
3143
+ "headerCell",
3144
+ "seed-data-table__headerCell"
3145
+ ],
3146
+ [
3147
+ "headerCellInner",
3148
+ "seed-data-table__headerCellInner"
3149
+ ],
3150
+ [
3151
+ "headerCellLabel",
3152
+ "seed-data-table__headerCellLabel"
3153
+ ],
3154
+ [
3155
+ "headerCellIcon",
3156
+ "seed-data-table__headerCellIcon"
3157
+ ],
3158
+ [
3159
+ "row",
3160
+ "seed-data-table__row"
3161
+ ],
3162
+ [
3163
+ "cell",
3164
+ "seed-data-table__cell"
3165
+ ],
3166
+ [
3167
+ "entity",
3168
+ "seed-data-table__entity"
3169
+ ],
3170
+ [
3171
+ "entityName",
3172
+ "seed-data-table__entityName"
3173
+ ],
3174
+ [
3175
+ "tagList",
3176
+ "seed-data-table__tagList"
3177
+ ],
3178
+ [
3179
+ "link",
3180
+ "seed-data-table__link"
3181
+ ],
3182
+ [
3183
+ "toolbar",
3184
+ "seed-data-table__toolbar"
3185
+ ],
3186
+ [
3187
+ "search",
3188
+ "seed-data-table__search"
3189
+ ],
3190
+ [
3191
+ "count",
3192
+ "seed-data-table__count"
3193
+ ],
3194
+ [
3195
+ "toolbarEnd",
3196
+ "seed-data-table__toolbarEnd"
3197
+ ],
3198
+ [
3199
+ "scrollArea",
3200
+ "seed-data-table__scrollArea"
3201
+ ],
3202
+ [
3203
+ "footer",
3204
+ "seed-data-table__footer"
3205
+ ],
3206
+ [
3207
+ "footerCell",
3208
+ "seed-data-table__footerCell"
3209
+ ],
3210
+ [
3211
+ "emptyState",
3212
+ "seed-data-table__emptyState"
3213
+ ],
3214
+ [
3215
+ "loadingState",
3216
+ "seed-data-table__loadingState"
3217
+ ],
3218
+ [
3219
+ "addRow",
3220
+ "seed-data-table__addRow"
3221
+ ],
3222
+ [
3223
+ "addRowCell",
3224
+ "seed-data-table__addRowCell"
3225
+ ],
3226
+ [
3227
+ "selectionCell",
3228
+ "seed-data-table__selectionCell"
3229
+ ]
3230
+ ];
3231
+ const defaultVariant$4 = {
3232
+ "surface": "card",
3233
+ "interactive": false
3234
+ };
3235
+ const compoundVariants$4 = [];
3236
+ const dataTableVariantMap = {
3237
+ "surface": [
3238
+ "card",
3239
+ "bleed"
3240
+ ],
3241
+ "interactive": [
3242
+ true,
3243
+ false
3244
+ ]
3245
+ };
3246
+ function dataTable(props) {
3247
+ return Object.fromEntries(dataTableSlotNames.map(([slot, className])=>{
3248
+ return [
3249
+ slot,
3250
+ createClassName(className, mergeVariants(defaultVariant$4, props), compoundVariants$4)
3251
+ ];
3252
+ }));
2922
3253
  }
2923
- ////////////////////////////////////////////////////////////////////////////////////
2924
- // -- editor: "reference" / "actor" -- same picker, search-enabled, each option
2925
- // carrying a small avatar (a photo for a real one, a generic person glyph for
2926
- // none) -- `actor` is `reference` over people specifically (crisp's own #449/
2927
- // #432 `ReferenceCell` shape), so the two share one component; only the search
2928
- // placeholder differs. Seed's `Select` has no built-in search combobox (unlike
2929
- // crisp's own `Select`), so this filters `refOptions` itself against a small
2930
- // `TextField` rendered as the content's own first row. --------------------------
2931
- function ReferenceEditor({ column, value, pending, onCommit, onCancel }) {
3254
+ Object.assign(dataTable, {
3255
+ splitVariantProps: (props)=>splitVariantProps(props, dataTableVariantMap)
3256
+ }); // @recipe(seed): data-table
3257
+
3258
+ // The one `createSlotRecipeContext(dataTable)` call every sibling file in this
3259
+ // directory shares -- `ClassNamesProvider` and `withContext` must be the SAME
3260
+ // binding everywhere (a slot component built from a second, independent call would
3261
+ // read a DIFFERENT React context and never see the classNames `DataTableInner`
3262
+ // provides), so this is the one place that calls `createSlotRecipeContext`, and every
3263
+ // other file (`DataTable.tsx`, `column-menu.tsx`, `resize.tsx`, `column-calc.tsx`,
3264
+ // `toolbar.tsx`, `body-cells.tsx`, `editors.tsx`) imports its exports from here rather
3265
+ // than re-deriving its own.
3266
+ const { ClassNamesProvider: ClassNamesProvider$1, withContext: withContext$1, useClassNames: useClassNames$1 } = createSlotRecipeContext(dataTable);
3267
+
3268
+ /**
3269
+ * The pure filter itself, split out of `useSelectSearch` below so it can be
3270
+ * mutation-tested directly (the same "small pure function beside its own test" shape
3271
+ * `filtering.ts`'s `matchesSearch`/`getColumnValue` already use in this directory) --
3272
+ * case-insensitive substring match, on whitespace-trimmed input. An empty/whitespace-
3273
+ * only query returns `options` unfiltered rather than matching nothing, the same
3274
+ * "no query yet" case crisp's own `select.tsx` special-cases (`searchable && q`).
3275
+ */ function filterSelectOptions(options, query, getText) {
3276
+ const q = query.trim().toLowerCase();
3277
+ if (!q) return options;
3278
+ return options.filter((option)=>getText(option).toLowerCase().includes(q));
3279
+ }
3280
+ /**
3281
+ * Filters `options` by `getText(option)` against a live query string -- the one
3282
+ * concern every searchable Select popover shares (crisp's own `select.tsx` computes
3283
+ * this inline; this hook is `filterSelectOptions` above plus the query state it reads,
3284
+ * kept generic over `T` so it works for `DataTableEditOption` today and a future
3285
+ * filter-value picker's own option shape without a second copy of the same filter).
3286
+ */ function useSelectSearch(options, getText) {
2932
3287
  const [query, setQuery] = React__namespace.useState("");
2933
- const options = column.refOptions ?? [];
2934
- const filtered = query.trim() ? options.filter((option)=>option.label.toLowerCase().includes(query.trim().toLowerCase())) : options;
2935
- const current = value == null ? "" : String(value);
2936
- const isActor = column.editor === "actor";
2937
- return /*#__PURE__*/ jsxRuntime.jsxs(SelectRoot, {
2938
- // See `SelectEditor`'s own comment above -- same controlled-open wiring.
2939
- open: true,
2940
- onOpenChange: (next)=>{
2941
- if (!next) onCancel();
3288
+ const filtered = React__namespace.useMemo(()=>filterSelectOptions(options, query, getText), [
3289
+ options,
3290
+ query,
3291
+ getText
3292
+ ]);
3293
+ return {
3294
+ query,
3295
+ setQuery,
3296
+ filtered
3297
+ };
3298
+ }
3299
+ /**
3300
+ * The search box itself -- crisp's own top-of-listbox filter field
3301
+ * (`select.tsx`'s `crisp-select-search`), styled with the SAME compact chrome
3302
+ * `toolbar.tsx`'s `SearchBox` already standardized for the table's own toolbar search
3303
+ * (seed's real `text-input.ts` outline-variant tokens, via the `dataTable` recipe's
3304
+ * own `search` slot -- not seed's documented `TextField`, measured too heavy for a
3305
+ * ~32px inline control; see `SearchBox`'s own comment for that measurement). Reusing
3306
+ * the SAME recipe slot here, rather than standing up a second near-identical one,
3307
+ * keeps every "small bordered search input" in this package pixel-identical by
3308
+ * construction -- the slot's `max-width: 280px` (a toolbar-row layout constraint, not
3309
+ * chrome) is overridden inline since this field fills its popover instead.
3310
+ *
3311
+ * Swallows every keydown except Escape before it can reach seed's own listbox
3312
+ * (`SelectContent`'s typeahead-by-keyboard): a search field that ALSO drives the
3313
+ * listbox's letter-jump would fight the query it is supposed to be typing into. Escape
3314
+ * passes through untouched -- `SelectRoot`'s own `onOpenChange` (in every caller) is
3315
+ * what actually closes the editor on Escape, not a handler on this field.
3316
+ *
3317
+ * `autoFocus` is applied imperatively (an effect + ref), not the native `autoFocus`
3318
+ * attribute -- lint (`a11y/noAutofocus`) bars that on a raw `<input>`, and this is the
3319
+ * same ref-and-focus shape crisp's own `select.tsx` already uses for its search field
3320
+ * (`searchRef.current?.focus({ preventScroll: true })` on open), not a workaround
3321
+ * invented for the lint rule.
3322
+ */ function SelectSearchField({ value, onChange, placeholder, "aria-label": ariaLabel, autoFocus, onKeyDown }) {
3323
+ const classNames = dataTable();
3324
+ const inputRef = React__namespace.useRef(null);
3325
+ React__namespace.useEffect(()=>{
3326
+ if (autoFocus) inputRef.current?.focus({
3327
+ preventScroll: true
3328
+ });
3329
+ }, [
3330
+ autoFocus
3331
+ ]);
3332
+ return /*#__PURE__*/ jsxRuntime.jsxs("span", {
3333
+ className: classNames.search,
3334
+ style: {
3335
+ display: "flex",
3336
+ maxWidth: "none",
3337
+ margin: 4
2942
3338
  },
2943
- disabled: pending,
2944
- value: current ? [
2945
- current
2946
- ] : [],
2947
- onValueChange: (next)=>onCommit(next[0] ?? ""),
2948
3339
  children: [
2949
- /*#__PURE__*/ jsxRuntime.jsxs(SelectTrigger, {
2950
- "aria-label": `Edit ${column.key}`,
2951
- children: [
2952
- /*#__PURE__*/ jsxRuntime.jsx(SelectValue, {}),
2953
- /*#__PURE__*/ jsxRuntime.jsx(SelectPlaceholder, {
2954
- children: isActor ? "Assign…" : "Select…"
2955
- }),
2956
- /*#__PURE__*/ jsxRuntime.jsx(SelectSuffixIcon, {
2957
- svg: /*#__PURE__*/ jsxRuntime.jsx(IconChevronDownSmallLine_default, {})
2958
- })
2959
- ]
3340
+ /*#__PURE__*/ jsxRuntime.jsx(IconMagnifyingglassLine_default, {
3341
+ "aria-hidden": "true"
2960
3342
  }),
2961
- /*#__PURE__*/ jsxRuntime.jsx(SelectPositioner, {
2962
- children: /*#__PURE__*/ jsxRuntime.jsxs(SelectContent, {
2963
- children: [
2964
- /*#__PURE__*/ jsxRuntime.jsx(TextFieldRoot, {
2965
- size: "medium",
2966
- style: {
2967
- margin: 4
2968
- },
2969
- onKeyDown: (event)=>{
2970
- // The listbox's own typeahead/arrow handling lives on THIS field's
2971
- // sibling items, not on the field itself -- stop everything except
2972
- // Escape from also driving the listbox's own keyboard nav. Escape
2973
- // itself needs no special-casing to "reach" anything any more: it's
2974
- // SelectContent's own `DismissibleLayer` (Select.tsx) that closes the
2975
- // listbox, and that dismissal is a document-level listener outside
2976
- // this React subtree entirely -- a synthetic `stopPropagation` here
2977
- // was never able to block it either way, so the controlled `open`
2978
- // above (via `onOpenChange`) is what actually ends this cell's edit.
2979
- if (event.key !== "Escape") event.stopPropagation();
2980
- },
2981
- children: /*#__PURE__*/ jsxRuntime.jsx(TextFieldInput, {
2982
- autoFocus: true,
2983
- "aria-label": isActor ? "Search people" : `Search ${column.key}`,
2984
- placeholder: isActor ? "Search people…" : "Search…",
2985
- value: query,
2986
- onChange: (event)=>setQuery(event.target.value)
2987
- })
2988
- }),
2989
- /*#__PURE__*/ jsxRuntime.jsxs(SelectScrollArea, {
2990
- children: [
2991
- filtered.length === 0 && /*#__PURE__*/ jsxRuntime.jsx("div", {
2992
- style: {
2993
- padding: "6px 12px",
2994
- opacity: 0.63,
2995
- fontSize: 13
2996
- },
2997
- children: isActor ? "No users" : "No results"
2998
- }),
2999
- filtered.map((option)=>/*#__PURE__*/ jsxRuntime.jsxs(SelectItem, {
3000
- value: option.value,
3001
- label: option.label,
3002
- children: [
3003
- /*#__PURE__*/ jsxRuntime.jsxs(SelectItemBody, {
3004
- children: [
3005
- /*#__PURE__*/ jsxRuntime.jsx(ReferenceAvatar, {
3006
- avatar: option.avatar
3007
- }),
3008
- /*#__PURE__*/ jsxRuntime.jsx(SelectItemLabel, {})
3009
- ]
3010
- }),
3011
- /*#__PURE__*/ jsxRuntime.jsx(SelectItemIndicator, {
3012
- selected: /*#__PURE__*/ jsxRuntime.jsx(IconCheckmarkFatFill_default, {})
3013
- })
3014
- ]
3015
- }, option.value))
3016
- ]
3017
- })
3018
- ]
3019
- })
3343
+ /*#__PURE__*/ jsxRuntime.jsx("input", {
3344
+ ref: inputRef,
3345
+ type: "text",
3346
+ value: value,
3347
+ onChange: (event)=>onChange(event.target.value),
3348
+ placeholder: placeholder,
3349
+ "aria-label": ariaLabel ?? placeholder,
3350
+ onClick: (event)=>event.stopPropagation(),
3351
+ onKeyDown: (event)=>{
3352
+ if (event.key !== "Escape") event.stopPropagation();
3353
+ onKeyDown?.(event);
3354
+ }
3020
3355
  })
3021
3356
  ]
3022
3357
  });
3023
3358
  }
3024
- function ReferenceAvatar({ avatar }) {
3359
+ /** The empty state shown when a search matches nothing -- crisp's own `searchable`
3360
+ * `emptyLabel` (`crisp-select-empty`), reproduced at the same inset/opacity/size the
3361
+ * ad-hoc `ReferenceEditor` "No users"/"No results" row already used before this file
3362
+ * existed. */ function SelectSearchEmpty({ children }) {
3363
+ return /*#__PURE__*/ jsxRuntime.jsx("div", {
3364
+ style: {
3365
+ padding: "6px 12px",
3366
+ opacity: 0.63,
3367
+ fontSize: 13
3368
+ },
3369
+ children: children
3370
+ });
3371
+ }
3372
+
3373
+ /** The raw value a live editor reads, distinct from `DataTableColumn.value` (the
3374
+ * accessor sort/search/filter/export share): `editValue` returns whatever TYPE the
3375
+ * editor itself expects -- a real `boolean` for `editor: "switch"`/`type:
3376
+ * "checkbox"`, a `number` for `type: "rating"`, the option's own string for `select`/
3377
+ * `reference`/`actor` -- where `value`/`render` may format that same field for
3378
+ * display. Falls back to the table's own accessor when the column supplies none. */ function resolveEditValue(column, row, fallback) {
3379
+ return column.editValue ? column.editValue(row) : fallback;
3380
+ }
3381
+ function EditPendingIndicator() {
3382
+ return /*#__PURE__*/ jsxRuntime.jsxs("svg", {
3383
+ className: "seed-data-table-edit-pending",
3384
+ width: 14,
3385
+ height: 14,
3386
+ viewBox: "0 0 16 16",
3387
+ fill: "none",
3388
+ role: "presentation",
3389
+ "aria-hidden": "true",
3390
+ children: [
3391
+ /*#__PURE__*/ jsxRuntime.jsx("circle", {
3392
+ cx: 8,
3393
+ cy: 8,
3394
+ r: 6.5,
3395
+ stroke: "currentColor",
3396
+ strokeOpacity: 0.25,
3397
+ strokeWidth: 2
3398
+ }),
3399
+ /*#__PURE__*/ jsxRuntime.jsx("path", {
3400
+ d: "M14.5 8a6.5 6.5 0 0 0-6.5-6.5",
3401
+ stroke: "currentColor",
3402
+ strokeWidth: 2,
3403
+ strokeLinecap: "round"
3404
+ })
3405
+ ]
3406
+ });
3407
+ }
3408
+ function EditError({ message }) {
3409
+ return /*#__PURE__*/ jsxRuntime.jsx("span", {
3410
+ className: "seed-data-table-edit-error",
3411
+ children: message
3412
+ });
3413
+ }
3414
+ ////////////////////////////////////////////////////////////////////////////////////
3415
+ // -- plain editable: text/number (seed TextField) and date/datetime (a native input,
3416
+ // styled the same way, per `cellType`) -- both double-click gated, Enter/blur
3417
+ // commits, Escape cancels. -------------------------------------------------------
3418
+ function TextEditor({ column, value, pending, onCommit, onCancel }) {
3419
+ const [draft, setDraft] = React__namespace.useState(()=>value == null ? "" : String(value));
3420
+ return(// `size="medium"` (seed's smallest fixed `TextField` size, 40px min-height,
3421
+ // `text-input.ts`'s own token) -- the default `size="large"` overflowed a 36px
3422
+ // table row (same class of fix as `toolbar.tsx`'s `SearchBox`, which made the
3423
+ // same call for its own 32px-ish search box).
3424
+ /*#__PURE__*/ jsxRuntime.jsx(TextFieldRoot, {
3425
+ size: "medium",
3426
+ style: {
3427
+ width: "100%"
3428
+ },
3429
+ onClick: (event)=>event.stopPropagation(),
3430
+ onKeyDown: (event)=>event.stopPropagation(),
3431
+ children: /*#__PURE__*/ jsxRuntime.jsx(TextFieldInput, {
3432
+ autoFocus: true,
3433
+ type: column.cellType === "number" ? "number" : "text",
3434
+ "aria-label": `Edit ${column.key}`,
3435
+ value: draft,
3436
+ disabled: pending,
3437
+ onChange: (event)=>setDraft(event.target.value),
3438
+ onBlur: ()=>onCommit(draft),
3439
+ onKeyDown: (event)=>{
3440
+ if (event.key === "Enter") onCommit(draft);
3441
+ else if (event.key === "Escape") onCancel();
3442
+ }
3443
+ })
3444
+ }));
3445
+ }
3446
+ function DateEditor({ column, value, pending, onCommit, onCancel }) {
3447
+ const isDateTime = column.cellType === "datetime";
3448
+ const [draft, setDraft] = React__namespace.useState(()=>isDateTime ? toDateTimeInputValue(value) : toDateInputValue(value));
3449
+ return(// See `TextEditor`'s own comment above -- same `size="medium"` fix.
3450
+ /*#__PURE__*/ jsxRuntime.jsx(TextFieldRoot, {
3451
+ size: "medium",
3452
+ style: {
3453
+ width: "100%"
3454
+ },
3455
+ onClick: (event)=>event.stopPropagation(),
3456
+ onKeyDown: (event)=>event.stopPropagation(),
3457
+ children: /*#__PURE__*/ jsxRuntime.jsx(TextFieldInput, {
3458
+ autoFocus: true,
3459
+ type: isDateTime ? "datetime-local" : "date",
3460
+ "aria-label": `Edit ${column.key}`,
3461
+ value: draft,
3462
+ disabled: pending,
3463
+ onChange: (event)=>setDraft(event.target.value),
3464
+ onBlur: ()=>onCommit(draft),
3465
+ onKeyDown: (event)=>{
3466
+ if (event.key === "Enter") onCommit(draft);
3467
+ else if (event.key === "Escape") onCancel();
3468
+ }
3469
+ })
3470
+ }));
3471
+ }
3472
+ ////////////////////////////////////////////////////////////////////////////////////
3473
+ // -- editor: "select" -- double-click opens seed's own Select, auto-opened (seed's
3474
+ // `SelectRoot` takes `defaultOpen` directly -- unlike crisp's closed-API `Select`,
3475
+ // which needs a synthetic click on its own trigger to start open). A long option
3476
+ // list gets crisp's own search-on-top (`select-search.tsx`) for free, past
3477
+ // `SELECT_EDITOR_SEARCH_THRESHOLD` -- fewer options than that and a filter field
3478
+ // is pure noise above a list you can already scan (crisp's own `searchable` is
3479
+ // opt-in for the identical reason); `column.editorSearchable` overrides either way.
3480
+ /** Above this many options, `SelectEditor` turns its search field on by default --
3481
+ * below it, a filter box adds more chrome than it saves (nothing here claims this
3482
+ * exact number is measured; it is a judgment call, which is why `editorSearchable`
3483
+ * exists to override it either direction). */ const SELECT_EDITOR_SEARCH_THRESHOLD = 8;
3484
+ function SelectEditor({ column, value, pending, onCommit, onCancel }) {
3485
+ const options = column.options ?? [];
3486
+ const current = value == null ? "" : String(value);
3487
+ const searchable = column.editorSearchable ?? options.length > SELECT_EDITOR_SEARCH_THRESHOLD;
3488
+ const { query, setQuery, filtered } = useSelectSearch(options, (option)=>option.label);
3489
+ const visible = searchable ? filtered : options;
3490
+ return /*#__PURE__*/ jsxRuntime.jsxs(SelectRoot, {
3491
+ // Controlled open, per docs/examples/react/select/controlled-open.tsx --
3492
+ // this editor is only ever mounted while the cell IS editing, so it always
3493
+ // wants to start (and stay) open; `onOpenChange(false)` is what tells us
3494
+ // the popover wants to close for ANY reason (Escape, an outside click, OR
3495
+ // the internal auto-close a single-select commit already triggers), so
3496
+ // `onCancel` alone now covers the Escape gap the old raw `document`
3497
+ // listener existed for, without a second, independent listener.
3498
+ open: true,
3499
+ onOpenChange: (next)=>{
3500
+ if (!next) onCancel();
3501
+ },
3502
+ disabled: pending,
3503
+ value: current ? [
3504
+ current
3505
+ ] : [],
3506
+ onValueChange: (next)=>onCommit(next[0] ?? ""),
3507
+ children: [
3508
+ /*#__PURE__*/ jsxRuntime.jsxs(SelectTrigger, {
3509
+ "aria-label": `Edit ${column.key}`,
3510
+ children: [
3511
+ /*#__PURE__*/ jsxRuntime.jsx(SelectValue, {}),
3512
+ /*#__PURE__*/ jsxRuntime.jsx(SelectPlaceholder, {
3513
+ children: "Select…"
3514
+ }),
3515
+ /*#__PURE__*/ jsxRuntime.jsx(SelectSuffixIcon, {
3516
+ svg: /*#__PURE__*/ jsxRuntime.jsx(IconChevronDownSmallLine_default, {})
3517
+ })
3518
+ ]
3519
+ }),
3520
+ /*#__PURE__*/ jsxRuntime.jsx(SelectPositioner, {
3521
+ children: /*#__PURE__*/ jsxRuntime.jsxs(SelectContent, {
3522
+ children: [
3523
+ searchable && /*#__PURE__*/ jsxRuntime.jsx(SelectSearchField, {
3524
+ autoFocus: true,
3525
+ value: query,
3526
+ onChange: setQuery,
3527
+ placeholder: column.editorSearchPlaceholder ?? "Search…",
3528
+ "aria-label": `Search ${column.key}`
3529
+ }),
3530
+ /*#__PURE__*/ jsxRuntime.jsxs(SelectScrollArea, {
3531
+ children: [
3532
+ searchable && visible.length === 0 && /*#__PURE__*/ jsxRuntime.jsx(SelectSearchEmpty, {
3533
+ children: column.editorEmptyLabel ?? "No results"
3534
+ }),
3535
+ visible.map((option)=>/*#__PURE__*/ jsxRuntime.jsxs(SelectItem, {
3536
+ value: option.value,
3537
+ label: option.label,
3538
+ children: [
3539
+ /*#__PURE__*/ jsxRuntime.jsx(SelectItemBody, {
3540
+ children: /*#__PURE__*/ jsxRuntime.jsx(SelectItemLabel, {})
3541
+ }),
3542
+ /*#__PURE__*/ jsxRuntime.jsx(SelectItemIndicator, {
3543
+ selected: /*#__PURE__*/ jsxRuntime.jsx(IconCheckmarkFatFill_default, {})
3544
+ })
3545
+ ]
3546
+ }, option.value))
3547
+ ]
3548
+ })
3549
+ ]
3550
+ })
3551
+ })
3552
+ ]
3553
+ });
3554
+ }
3555
+ ////////////////////////////////////////////////////////////////////////////////////
3556
+ // -- editor: "reference" / "actor" -- same picker, ALWAYS search-enabled (unlike
3557
+ // `SelectEditor`'s threshold -- a reference/actor list is people/records, search
3558
+ // is the point regardless of how few there are today), each option carrying a
3559
+ // small avatar (a photo for a real one, a generic person glyph for none) --
3560
+ // `actor` is `reference` over people specifically (crisp's own #449/#432
3561
+ // `ReferenceCell` shape), so the two share one component; only the search
3562
+ // placeholder/empty copy differs. Seed's `Select` has no built-in search combobox
3563
+ // (unlike crisp's own `Select`), so this uses the shared `select-search.tsx`
3564
+ // field/hook -- the same one `SelectEditor` above uses -- rather than its own
3565
+ // copy of the query/filter/empty triangle. --------------------------------------
3566
+ function ReferenceEditor({ column, value, pending, onCommit, onCancel }) {
3567
+ const options = column.refOptions ?? [];
3568
+ const { query, setQuery, filtered } = useSelectSearch(options, (option)=>option.label);
3569
+ const current = value == null ? "" : String(value);
3570
+ const isActor = column.editor === "actor";
3571
+ return /*#__PURE__*/ jsxRuntime.jsxs(SelectRoot, {
3572
+ // See `SelectEditor`'s own comment above -- same controlled-open wiring.
3573
+ open: true,
3574
+ onOpenChange: (next)=>{
3575
+ if (!next) onCancel();
3576
+ },
3577
+ disabled: pending,
3578
+ value: current ? [
3579
+ current
3580
+ ] : [],
3581
+ onValueChange: (next)=>onCommit(next[0] ?? ""),
3582
+ children: [
3583
+ /*#__PURE__*/ jsxRuntime.jsxs(SelectTrigger, {
3584
+ "aria-label": `Edit ${column.key}`,
3585
+ children: [
3586
+ /*#__PURE__*/ jsxRuntime.jsx(SelectValue, {}),
3587
+ /*#__PURE__*/ jsxRuntime.jsx(SelectPlaceholder, {
3588
+ children: isActor ? "Assign…" : "Select…"
3589
+ }),
3590
+ /*#__PURE__*/ jsxRuntime.jsx(SelectSuffixIcon, {
3591
+ svg: /*#__PURE__*/ jsxRuntime.jsx(IconChevronDownSmallLine_default, {})
3592
+ })
3593
+ ]
3594
+ }),
3595
+ /*#__PURE__*/ jsxRuntime.jsx(SelectPositioner, {
3596
+ children: /*#__PURE__*/ jsxRuntime.jsxs(SelectContent, {
3597
+ children: [
3598
+ /*#__PURE__*/ jsxRuntime.jsx(SelectSearchField, {
3599
+ autoFocus: true,
3600
+ value: query,
3601
+ onChange: setQuery,
3602
+ placeholder: column.refSearchPlaceholder ?? (isActor ? "Search people…" : "Search…"),
3603
+ "aria-label": isActor ? "Search people" : `Search ${column.key}`
3604
+ }),
3605
+ /*#__PURE__*/ jsxRuntime.jsxs(SelectScrollArea, {
3606
+ children: [
3607
+ filtered.length === 0 && /*#__PURE__*/ jsxRuntime.jsx(SelectSearchEmpty, {
3608
+ children: column.refEmptyLabel ?? (isActor ? "No users" : "No results")
3609
+ }),
3610
+ filtered.map((option)=>/*#__PURE__*/ jsxRuntime.jsxs(SelectItem, {
3611
+ value: option.value,
3612
+ label: option.label,
3613
+ children: [
3614
+ /*#__PURE__*/ jsxRuntime.jsxs(SelectItemBody, {
3615
+ children: [
3616
+ /*#__PURE__*/ jsxRuntime.jsx(ReferenceAvatar, {
3617
+ avatar: option.avatar
3618
+ }),
3619
+ /*#__PURE__*/ jsxRuntime.jsx(SelectItemLabel, {})
3620
+ ]
3621
+ }),
3622
+ /*#__PURE__*/ jsxRuntime.jsx(SelectItemIndicator, {
3623
+ selected: /*#__PURE__*/ jsxRuntime.jsx(IconCheckmarkFatFill_default, {})
3624
+ })
3625
+ ]
3626
+ }, option.value))
3627
+ ]
3628
+ })
3629
+ ]
3630
+ })
3631
+ })
3632
+ ]
3633
+ });
3634
+ }
3635
+ function ReferenceAvatar({ avatar }) {
3025
3636
  return /*#__PURE__*/ jsxRuntime.jsxs(AvatarRoot, {
3026
3637
  size: "20",
3027
3638
  children: [
@@ -3181,20 +3792,16 @@ function RatingEditor({ column, value, pending, onCommit }) {
3181
3792
  return column.editor === "switch" || column.type === "rating" || column.type === "checkbox";
3182
3793
  }
3183
3794
  function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast }) {
3184
- const effective = useEffectiveColumns(columns);
3795
+ const effective = useEffectiveColumns(columns, {
3796
+ selectable,
3797
+ stickyFirst
3798
+ });
3185
3799
  const { startEdit } = useDataTableContext();
3186
3800
  return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
3187
- children: effective.map(({ column }, index)=>{
3188
- const isFirst = index === 0;
3801
+ children: effective.map((entry, index)=>{
3802
+ const { column } = entry;
3189
3803
  const isLast = index === effective.length - 1;
3190
- const sticky = stickyProps({
3191
- selectable,
3192
- stickyFirst,
3193
- stickyLast,
3194
- isSelectionCell: false,
3195
- isFirst,
3196
- isLast
3197
- });
3804
+ const sticky = pinnedCellProps(entry, stickyLast, isLast);
3198
3805
  // A gated `editable` column (everything except the always-live widgets)
3199
3806
  // opens on double-click -- Attio's own measured gesture (crisp's #411),
3200
3807
  // matched here rather than a single click, which stays "select this cell"
@@ -3370,7 +3977,7 @@ function DisplayCellContent({ column, row, value, classNames, locale }) {
3370
3977
  }
3371
3978
  }
3372
3979
 
3373
- const { ClassNamesProvider: ClassNamesProvider$1, withContext: withContext$1, useClassNames: useClassNames$1 } = createSlotRecipeContext(menu.menu);
3980
+ const { ClassNamesProvider, withContext, useClassNames } = createSlotRecipeContext(menu.menu);
3374
3981
  const { PropsProvider: ItemPropsProvider, useProps: useItemProps, withContext: withItemContext, ClassNamesProvider: ItemClassNamesProvider } = createSlotRecipeContext(menuItem.menuItem);
3375
3982
  const withStateProps = createWithStateProps([
3376
3983
  reactMenu.useMenuContext
@@ -3381,7 +3988,7 @@ const withItemStateProps = createWithStateProps([
3381
3988
  const MenuRoot = (props)=>{
3382
3989
  const [variantProps, otherProps] = menu.menu.splitVariantProps(props);
3383
3990
  const classNames = menu.menu(variantProps);
3384
- return /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider$1, {
3991
+ return /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider, {
3385
3992
  value: classNames,
3386
3993
  children: /*#__PURE__*/ jsxRuntime.jsx(ItemPropsProvider, {
3387
3994
  value: {
@@ -3396,7 +4003,7 @@ const MenuRoot = (props)=>{
3396
4003
  reactMenu.Menu.Anchor;
3397
4004
  const MenuTrigger = reactMenu.Menu.Trigger;
3398
4005
  const MenuPositioner = /*#__PURE__*/ React__namespace.forwardRef(({ className, ...props }, ref)=>{
3399
- const classNames = useClassNames$1();
4006
+ const classNames = useClassNames();
3400
4007
  return /*#__PURE__*/ jsxRuntime.jsx(reactMenu.Menu.Positioner, {
3401
4008
  ref: ref,
3402
4009
  className: clsx(classNames.positioner, className),
@@ -3404,10 +4011,10 @@ const MenuPositioner = /*#__PURE__*/ React__namespace.forwardRef(({ className, .
3404
4011
  });
3405
4012
  });
3406
4013
  MenuPositioner.displayName = "MenuPositioner";
3407
- const MenuContent = withContext$1(reactMenu.Menu.Content, "content");
3408
- const MenuScrollArea = withContext$1(withStateProps(Primitive.div), "scrollArea");
3409
- withContext$1(reactMenu.Menu.Group, "group");
3410
- withContext$1(reactMenu.Menu.GroupLabel, "groupLabel");
4014
+ const MenuContent = withContext(reactMenu.Menu.Content, "content");
4015
+ const MenuScrollArea = withContext(withStateProps(Primitive.div), "scrollArea");
4016
+ withContext(reactMenu.Menu.Group, "group");
4017
+ withContext(reactMenu.Menu.GroupLabel, "groupLabel");
3411
4018
  const MenuItem = /*#__PURE__*/ React__namespace.forwardRef(({ className: propClassName, ...props }, ref)=>{
3412
4019
  const [variantProps, otherProps] = menuItem.menuItem.splitVariantProps(props);
3413
4020
  const parentProps = useItemProps();
@@ -3430,19 +4037,15 @@ const MenuItemLabel = withItemContext(withItemStateProps(Primitive.span), "label
3430
4037
  withItemContext(withItemStateProps(Primitive.span), "description");
3431
4038
 
3432
4039
  function ColumnCalcCells({ columns, rows, selectable, stickyFirst, stickyLast }) {
3433
- const effective = useEffectiveColumns(columns);
4040
+ const effective = useEffectiveColumns(columns, {
4041
+ selectable,
4042
+ stickyFirst
4043
+ });
3434
4044
  return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
3435
- children: effective.map(({ column }, index)=>{
3436
- const isFirst = index === 0;
4045
+ children: effective.map((entry, index)=>{
4046
+ const { column } = entry;
3437
4047
  const isLast = index === effective.length - 1;
3438
- const sticky = stickyProps({
3439
- selectable,
3440
- stickyFirst,
3441
- stickyLast,
3442
- isSelectionCell: false,
3443
- isFirst,
3444
- isLast
3445
- });
4048
+ const sticky = pinnedCellProps(entry, stickyLast, isLast);
3446
4049
  return /*#__PURE__*/ jsxRuntime.jsx(DataTableFooterCell, {
3447
4050
  "data-sticky": sticky["data-sticky"],
3448
4051
  "data-sticky-edge": sticky["data-sticky-edge"],
@@ -3626,20 +4229,16 @@ const DEFAULT_SORT_ICON = {
3626
4229
  * `visibleColumnOrder`/labels/widths change, without also re-deriving `sortedData`'s
3627
4230
  * render above it. */ function ColumnHeaderCells({ columns, columnMenu, reorderable, resizable, sortIcon, selectable, stickyFirst, stickyLast }) {
3628
4231
  const { draggingColumnKey, beginColumnDrag, dragOverColumn, endColumnDrag } = useDataTableContext();
3629
- const effective = useEffectiveColumns(columns);
4232
+ const effective = useEffectiveColumns(columns, {
4233
+ selectable,
4234
+ stickyFirst
4235
+ });
3630
4236
  const [renamingKey, setRenamingKey] = React__namespace.useState(null);
3631
4237
  return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
3632
- children: effective.map(({ column, label, width }, index)=>{
3633
- const isFirst = index === 0;
4238
+ children: effective.map((entry, index)=>{
4239
+ const { column, label, width } = entry;
3634
4240
  const isLast = index === effective.length - 1;
3635
- const sticky = stickyProps({
3636
- selectable,
3637
- stickyFirst,
3638
- stickyLast,
3639
- isSelectionCell: false,
3640
- isFirst,
3641
- isLast
3642
- });
4241
+ const sticky = pinnedCellProps(entry, stickyLast, isLast);
3643
4242
  return /*#__PURE__*/ jsxRuntime.jsxs(DataTableHeaderCell, {
3644
4243
  columnKey: column.key,
3645
4244
  sortable: column.sortable,
@@ -3708,304 +4307,130 @@ function ColumnRenameInput({ column, initialLabel, onDone }) {
3708
4307
  // The menu's close restores focus to its trigger one frame after this input
3709
4308
  // mounts, stealing focus from `autoFocus`. Re-grab it in the SAME frame (after
3710
4309
  // the restore) and select the text, so the user can type/replace immediately.
3711
- const frame = requestAnimationFrame(()=>{
3712
- readyRef.current = true;
3713
- inputRef.current?.focus();
3714
- inputRef.current?.select();
3715
- });
3716
- return ()=>cancelAnimationFrame(frame);
3717
- }, []);
3718
- const commit = ()=>{
3719
- if (value.trim()) renameColumn(column.key, value.trim());
3720
- onDone();
3721
- };
3722
- return(// A bare, minimal input -- NOT seed's `TextField` (its labeled root measures
3723
- // ~52px tall, per DATATABLE-ATTIO-SPEC.md's own note; this has to fit a 40px
3724
- // header cell with no label/description chrome of its own). Seed has no lighter
3725
- // input primitive at this level, so this follows crisp's own measured
3726
- // `.crisp-datatable-th-rename` (`datatable.recipe.ts`) instead: full-width,
3727
- // header-row height, a small inset, no border (an inset brand-solid box-shadow
3728
- // stands in for a focus ring), the header's own font weight/size inherited
3729
- // rather than a new size introduced for one input.
3730
- /*#__PURE__*/ jsxRuntime.jsx("input", {
3731
- ref: inputRef,
3732
- // 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.
3733
- autoFocus: true,
3734
- "aria-label": `Rename ${initialLabel}`,
3735
- value: value,
3736
- onClick: (event)=>event.stopPropagation(),
3737
- onChange: (event)=>setValue(event.target.value),
3738
- onBlur: ()=>{
3739
- if (readyRef.current) commit();
3740
- },
3741
- onKeyDown: (event)=>{
3742
- event.stopPropagation();
3743
- if (event.key === "Enter") commit();
3744
- if (event.key === "Escape") onDone();
3745
- },
3746
- // Bare inline input: no border/ring/background/radius -- it reads as the
3747
- // header label made editable in place, not a boxed field.
3748
- style: {
3749
- width: "100%",
3750
- minWidth: 0,
3751
- height: "100%",
3752
- margin: 0,
3753
- padding: 0,
3754
- border: "none",
3755
- borderRadius: 0,
3756
- background: "transparent",
3757
- boxShadow: "none",
3758
- font: "inherit",
3759
- fontWeight: "var(--seed-font-weight-medium)",
3760
- color: "var(--seed-color-fg-neutral)",
3761
- outline: "none"
3762
- }
3763
- }));
3764
- }
3765
- /** The per-column header menu (rename / hide / move left / move right) -- seed
3766
- * `Menu`, opened from the same `headerCellIcon` slot the filter trigger and sort
3767
- * chevron already share. Hover/focus-reveal (`data-column-menu`'s own CSS in
3768
- * `data-table.ts`) -- crisp's own header shows the trigger only on hover, not
3769
- * permanently, so this one carries its own marker attribute distinct from that
3770
- * slot's OTHER occupants (the sort chevron, the filter trigger), which stay visible
3771
- * under their own separate rules regardless of hover. */ function ColumnMenu({ column, onRename }) {
3772
- const { moveColumn, hideColumn } = useDataTableContext();
3773
- const columnLabel = typeof column.header === "string" ? column.header : column.key;
3774
- const stopPropagation = (event)=>event.stopPropagation();
3775
- return /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
3776
- children: [
3777
- /*#__PURE__*/ jsxRuntime.jsx(DataTableHeaderCellIcon, {
3778
- asChild: true,
3779
- "data-column-menu": "",
3780
- onClick: stopPropagation,
3781
- onKeyDown: stopPropagation,
3782
- draggable: false,
3783
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuTrigger, {
3784
- "aria-label": `${columnLabel} column menu`,
3785
- children: /*#__PURE__*/ jsxRuntime.jsx(IconDot3HorizontalLine_default, {
3786
- size: 16
3787
- })
3788
- })
3789
- }),
3790
- /*#__PURE__*/ jsxRuntime.jsx(MenuPositioner, {
3791
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuContent, {
3792
- style: {
3793
- minWidth: 160
3794
- },
3795
- children: /*#__PURE__*/ jsxRuntime.jsxs(MenuScrollArea, {
3796
- children: [
3797
- /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
3798
- onClick: onRename,
3799
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
3800
- children: "Rename"
3801
- })
3802
- }),
3803
- /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
3804
- onClick: ()=>moveColumn(column.key, "left"),
3805
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
3806
- children: "Move left"
3807
- })
3808
- }),
3809
- /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
3810
- onClick: ()=>moveColumn(column.key, "right"),
3811
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
3812
- children: "Move right"
3813
- })
3814
- }),
3815
- /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
3816
- onClick: ()=>hideColumn(column.key),
3817
- children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
3818
- children: "Hide column"
3819
- })
3820
- })
3821
- ]
3822
- })
3823
- })
3824
- })
3825
- ]
3826
- });
3827
- }
3828
-
3829
- __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))}");
3830
-
3831
- const createClassName = (className, variants, compoundVariants = [])=>{
3832
- const variantKeys = Object.keys(variants);
3833
- const variantValues = variantKeys.map((key)=>variants[key]);
3834
- const variantClassName = variantKeys.map((key, index)=>`${className}--${key}_${variantValues[index]}`).join(" ");
3835
- const compountVariantClassName = compoundVariants.filter((compoundVariant)=>Object.keys(compoundVariant).every((key)=>compoundVariant[key] === variants[key])).map((compoundVariant)=>`${className}--${Object.keys(compoundVariant).map((key)=>{
3836
- return `${key}_${compoundVariant[key]}`;
3837
- }).join("-")}`).join(" ");
3838
- return [
3839
- className,
3840
- variantClassName,
3841
- compountVariantClassName
3842
- ].filter(Boolean).join(" ");
3843
- };
3844
- function mergeVariants(a, b) {
3845
- const result = {
3846
- ...a
3847
- };
3848
- for(const k in b){
3849
- if (b[k] != null) {
3850
- result[k] = b[k];
3851
- }
3852
- }
3853
- return result;
3854
- }
3855
- function splitVariantProps(props, variantMap) {
3856
- const variantProps = {};
3857
- const otherProps = {};
3858
- for(const key in props){
3859
- if (variantMap[key] != null) {
3860
- variantProps[key] = props[key];
3861
- } else {
3862
- otherProps[key] = props[key];
3863
- }
3864
- }
3865
- return [
3866
- variantProps,
3867
- otherProps
3868
- ];
3869
- }
3870
-
3871
- const dataTableSlotNames = [
3872
- [
3873
- "root",
3874
- "seed-data-table__root"
3875
- ],
3876
- [
3877
- "table",
3878
- "seed-data-table__table"
3879
- ],
3880
- [
3881
- "header",
3882
- "seed-data-table__header"
3883
- ],
3884
- [
3885
- "headerCell",
3886
- "seed-data-table__headerCell"
3887
- ],
3888
- [
3889
- "headerCellInner",
3890
- "seed-data-table__headerCellInner"
3891
- ],
3892
- [
3893
- "headerCellLabel",
3894
- "seed-data-table__headerCellLabel"
3895
- ],
3896
- [
3897
- "headerCellIcon",
3898
- "seed-data-table__headerCellIcon"
3899
- ],
3900
- [
3901
- "row",
3902
- "seed-data-table__row"
3903
- ],
3904
- [
3905
- "cell",
3906
- "seed-data-table__cell"
3907
- ],
3908
- [
3909
- "entity",
3910
- "seed-data-table__entity"
3911
- ],
3912
- [
3913
- "entityName",
3914
- "seed-data-table__entityName"
3915
- ],
3916
- [
3917
- "tagList",
3918
- "seed-data-table__tagList"
3919
- ],
3920
- [
3921
- "link",
3922
- "seed-data-table__link"
3923
- ],
3924
- [
3925
- "toolbar",
3926
- "seed-data-table__toolbar"
3927
- ],
3928
- [
3929
- "search",
3930
- "seed-data-table__search"
3931
- ],
3932
- [
3933
- "count",
3934
- "seed-data-table__count"
3935
- ],
3936
- [
3937
- "toolbarEnd",
3938
- "seed-data-table__toolbarEnd"
3939
- ],
3940
- [
3941
- "scrollArea",
3942
- "seed-data-table__scrollArea"
3943
- ],
3944
- [
3945
- "footer",
3946
- "seed-data-table__footer"
3947
- ],
3948
- [
3949
- "footerCell",
3950
- "seed-data-table__footerCell"
3951
- ],
3952
- [
3953
- "emptyState",
3954
- "seed-data-table__emptyState"
3955
- ],
3956
- [
3957
- "loadingState",
3958
- "seed-data-table__loadingState"
3959
- ],
3960
- [
3961
- "addRow",
3962
- "seed-data-table__addRow"
3963
- ],
3964
- [
3965
- "addRowCell",
3966
- "seed-data-table__addRowCell"
3967
- ],
3968
- [
3969
- "selectionCell",
3970
- "seed-data-table__selectionCell"
3971
- ]
3972
- ];
3973
- const defaultVariant$4 = {
3974
- "surface": "card",
3975
- "interactive": false
3976
- };
3977
- const compoundVariants$4 = [];
3978
- const dataTableVariantMap = {
3979
- "surface": [
3980
- "card",
3981
- "bleed"
3982
- ],
3983
- "interactive": [
3984
- true,
3985
- false
3986
- ]
3987
- };
3988
- function dataTable(props) {
3989
- return Object.fromEntries(dataTableSlotNames.map(([slot, className])=>{
3990
- return [
3991
- slot,
3992
- createClassName(className, mergeVariants(defaultVariant$4, props), compoundVariants$4)
3993
- ];
4310
+ const frame = requestAnimationFrame(()=>{
4311
+ readyRef.current = true;
4312
+ inputRef.current?.focus();
4313
+ inputRef.current?.select();
4314
+ });
4315
+ return ()=>cancelAnimationFrame(frame);
4316
+ }, []);
4317
+ const commit = ()=>{
4318
+ if (value.trim()) renameColumn(column.key, value.trim());
4319
+ onDone();
4320
+ };
4321
+ return(// A bare, minimal input -- NOT seed's `TextField` (its labeled root measures
4322
+ // ~52px tall, per DATATABLE-ATTIO-SPEC.md's own note; this has to fit a 40px
4323
+ // header cell with no label/description chrome of its own). Seed has no lighter
4324
+ // input primitive at this level, so this follows crisp's own measured
4325
+ // `.crisp-datatable-th-rename` (`datatable.recipe.ts`) instead: full-width,
4326
+ // header-row height, a small inset, no border (an inset brand-solid box-shadow
4327
+ // stands in for a focus ring), the header's own font weight/size inherited
4328
+ // rather than a new size introduced for one input.
4329
+ /*#__PURE__*/ jsxRuntime.jsx("input", {
4330
+ ref: inputRef,
4331
+ // 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.
4332
+ autoFocus: true,
4333
+ "aria-label": `Rename ${initialLabel}`,
4334
+ value: value,
4335
+ onClick: (event)=>event.stopPropagation(),
4336
+ onChange: (event)=>setValue(event.target.value),
4337
+ onBlur: ()=>{
4338
+ if (readyRef.current) commit();
4339
+ },
4340
+ onKeyDown: (event)=>{
4341
+ event.stopPropagation();
4342
+ if (event.key === "Enter") commit();
4343
+ if (event.key === "Escape") onDone();
4344
+ },
4345
+ // Bare inline input: no border/ring/background/radius -- it reads as the
4346
+ // header label made editable in place, not a boxed field.
4347
+ style: {
4348
+ width: "100%",
4349
+ minWidth: 0,
4350
+ height: "100%",
4351
+ margin: 0,
4352
+ padding: 0,
4353
+ border: "none",
4354
+ borderRadius: 0,
4355
+ background: "transparent",
4356
+ boxShadow: "none",
4357
+ font: "inherit",
4358
+ fontWeight: "var(--seed-font-weight-medium)",
4359
+ color: "var(--seed-color-fg-neutral)",
4360
+ outline: "none"
4361
+ }
3994
4362
  }));
3995
4363
  }
3996
- Object.assign(dataTable, {
3997
- splitVariantProps: (props)=>splitVariantProps(props, dataTableVariantMap)
3998
- }); // @recipe(seed): data-table
3999
-
4000
- // The one `createSlotRecipeContext(dataTable)` call every sibling file in this
4001
- // directory shares -- `ClassNamesProvider` and `withContext` must be the SAME
4002
- // binding everywhere (a slot component built from a second, independent call would
4003
- // read a DIFFERENT React context and never see the classNames `DataTableInner`
4004
- // provides), so this is the one place that calls `createSlotRecipeContext`, and every
4005
- // other file (`DataTable.tsx`, `column-menu.tsx`, `resize.tsx`, `column-calc.tsx`,
4006
- // `toolbar.tsx`, `body-cells.tsx`, `editors.tsx`) imports its exports from here rather
4007
- // than re-deriving its own.
4008
- const { ClassNamesProvider, withContext, useClassNames } = createSlotRecipeContext(dataTable);
4364
+ /** The per-column header menu (rename / hide / move left / move right) -- seed
4365
+ * `Menu`, opened from the same `headerCellIcon` slot the filter trigger and sort
4366
+ * chevron already share. Hover/focus-reveal (`data-column-menu`'s own CSS in
4367
+ * `data-table.ts`) -- crisp's own header shows the trigger only on hover, not
4368
+ * permanently, so this one carries its own marker attribute distinct from that
4369
+ * slot's OTHER occupants (the sort chevron, the filter trigger), which stay visible
4370
+ * under their own separate rules regardless of hover. */ function ColumnMenu({ column, onRename }) {
4371
+ const { moveColumn, hideColumn, isColumnPinned, togglePinColumn } = useDataTableContext();
4372
+ const columnLabel = typeof column.header === "string" ? column.header : column.key;
4373
+ const pinned = isColumnPinned(column.key);
4374
+ const stopPropagation = (event)=>event.stopPropagation();
4375
+ return /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
4376
+ children: [
4377
+ /*#__PURE__*/ jsxRuntime.jsx(DataTableHeaderCellIcon, {
4378
+ asChild: true,
4379
+ "data-column-menu": "",
4380
+ onClick: stopPropagation,
4381
+ onKeyDown: stopPropagation,
4382
+ draggable: false,
4383
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuTrigger, {
4384
+ "aria-label": `${columnLabel} column menu`,
4385
+ children: /*#__PURE__*/ jsxRuntime.jsx(IconDot3HorizontalLine_default, {
4386
+ size: 16
4387
+ })
4388
+ })
4389
+ }),
4390
+ /*#__PURE__*/ jsxRuntime.jsx(MenuPositioner, {
4391
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuContent, {
4392
+ style: {
4393
+ minWidth: 160
4394
+ },
4395
+ children: /*#__PURE__*/ jsxRuntime.jsxs(MenuScrollArea, {
4396
+ children: [
4397
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4398
+ onClick: onRename,
4399
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4400
+ children: "Rename"
4401
+ })
4402
+ }),
4403
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4404
+ onClick: ()=>moveColumn(column.key, "left"),
4405
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4406
+ children: "Move left"
4407
+ })
4408
+ }),
4409
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4410
+ onClick: ()=>moveColumn(column.key, "right"),
4411
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4412
+ children: "Move right"
4413
+ })
4414
+ }),
4415
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4416
+ onClick: ()=>togglePinColumn(column.key),
4417
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4418
+ children: pinned ? "Unpin column" : "Pin column"
4419
+ })
4420
+ }),
4421
+ /*#__PURE__*/ jsxRuntime.jsx(MenuItem, {
4422
+ onClick: ()=>hideColumn(column.key),
4423
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuItemLabel, {
4424
+ children: "Hide column"
4425
+ })
4426
+ })
4427
+ ]
4428
+ })
4429
+ })
4430
+ })
4431
+ ]
4432
+ });
4433
+ }
4009
4434
 
4010
4435
  /**
4011
4436
  * A small inline spinner, not seed's own `ProgressCircle` -- that component's
@@ -9213,7 +9638,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9213
9638
  "aria-label": "Move sort up",
9214
9639
  disabled: index === 0,
9215
9640
  onClick: ()=>moveToolbarSort(index, index - 1),
9216
- style: iconButtonStyle,
9641
+ style: iconButtonStyle$1,
9217
9642
  children: /*#__PURE__*/ jsxRuntime.jsx(IconArrowUpLine_default, {
9218
9643
  size: 13,
9219
9644
  strokeWidth: 1.75
@@ -9224,7 +9649,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9224
9649
  "aria-label": "Move sort down",
9225
9650
  disabled: index === toolbarSorts.length - 1,
9226
9651
  onClick: ()=>moveToolbarSort(index, index + 1),
9227
- style: iconButtonStyle,
9652
+ style: iconButtonStyle$1,
9228
9653
  children: /*#__PURE__*/ jsxRuntime.jsx(IconArrowDownLine_default, {
9229
9654
  size: 13,
9230
9655
  strokeWidth: 1.75
@@ -9234,7 +9659,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9234
9659
  type: "button",
9235
9660
  "aria-label": "Remove sort",
9236
9661
  onClick: ()=>removeToolbarSort(index),
9237
- style: iconButtonStyle,
9662
+ style: iconButtonStyle$1,
9238
9663
  children: /*#__PURE__*/ jsxRuntime.jsx(IconXmarkLine_default, {
9239
9664
  size: 13,
9240
9665
  strokeWidth: 1.75
@@ -9249,7 +9674,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9249
9674
  disabled: availableColumns.length === 0,
9250
9675
  onClick: ()=>availableColumns[0] && addToolbarSort(availableColumns[0].key),
9251
9676
  style: {
9252
- ...iconButtonStyle,
9677
+ ...iconButtonStyle$1,
9253
9678
  width: "auto",
9254
9679
  gap: 6,
9255
9680
  padding: "6px 8px",
@@ -9271,7 +9696,7 @@ const fromDatePickerDate = (d)=>new Date(d.year, d.month - 1, d.day);
9271
9696
  ]
9272
9697
  });
9273
9698
  }
9274
- const iconButtonStyle = {
9699
+ const iconButtonStyle$1 = {
9275
9700
  display: "inline-flex",
9276
9701
  alignItems: "center",
9277
9702
  justifyContent: "center",
@@ -9369,7 +9794,7 @@ const formatFilterDate = (iso)=>{
9369
9794
  type: "button",
9370
9795
  "aria-label": `Remove ${column ? columnLabel(column) : filter.key} filter`,
9371
9796
  onClick: ()=>removeToolbarFilter(filter.id),
9372
- style: iconButtonStyle,
9797
+ style: iconButtonStyle$1,
9373
9798
  children: /*#__PURE__*/ jsxRuntime.jsx(IconXmarkLine_default, {
9374
9799
  size: 12,
9375
9800
  strokeWidth: 1.75
@@ -9626,7 +10051,7 @@ const formatFilterDate = (iso)=>{
9626
10051
  type: "button",
9627
10052
  onClick: ()=>setPickingField(true),
9628
10053
  style: {
9629
- ...iconButtonStyle,
10054
+ ...iconButtonStyle$1,
9630
10055
  width: "auto",
9631
10056
  gap: 6,
9632
10057
  padding: "6px 8px",
@@ -9649,8 +10074,8 @@ const formatFilterDate = (iso)=>{
9649
10074
  });
9650
10075
  }
9651
10076
 
9652
- const DataTableToolbar = withContext(Primitive.div, "toolbar");
9653
- const DataTableCount = withContext(Primitive.span, "count");
10077
+ const DataTableToolbar = withContext$1(Primitive.div, "toolbar");
10078
+ const DataTableCount = withContext$1(Primitive.span, "count");
9654
10079
  /**
9655
10080
  * The toolbar's own right-hand group -- `count`'s `marginInlineStart: auto` moved
9656
10081
  * here (off the `<span>` itself) so a consumer's `toolbarActions` and the row count
@@ -9661,7 +10086,7 @@ const DataTableCount = withContext(Primitive.span, "count");
9661
10086
  * it. `DataTable.tsx` always renders this wrapper once either `toolbarActions` or
9662
10087
  * `showCount` is on, so it degrades to exactly the old single-count layout when no
9663
10088
  * `toolbarActions` is passed.
9664
- */ const DataTableToolbarEnd = withContext(Primitive.div, "toolbarEnd");
10089
+ */ const DataTableToolbarEnd = withContext$1(Primitive.div, "toolbarEnd");
9665
10090
  /**
9666
10091
  * A compact, borderless search box -- crisp's own measured `.crisp-datatable-search`
9667
10092
  * (32px, `bgSurface` background, a leading icon, a soft rest-state shadow, a 1px brand
@@ -9711,7 +10136,7 @@ function RowCount({ shown, total, countLabel }) {
9711
10136
  }
9712
10137
 
9713
10138
  function DataTableInner(props, ref) {
9714
- 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;
10139
+ 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;
9715
10140
  const classNames = dataTable({
9716
10141
  surface,
9717
10142
  interactive
@@ -9816,7 +10241,7 @@ function DataTableInner(props, ref) {
9816
10241
  // loading/error/empty body, infinite scroll's sentinel, the add-row footer) reads
9817
10242
  // this ONE value rather than each re-deriving it, so they can never drift apart.
9818
10243
  const totalCols = columns.length + (selectable ? 1 : 0);
9819
- return /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider, {
10244
+ return /*#__PURE__*/ jsxRuntime.jsx(ClassNamesProvider$1, {
9820
10245
  value: classNames,
9821
10246
  children: /*#__PURE__*/ jsxRuntime.jsxs(DataTableRoot, {
9822
10247
  ref: ref,
@@ -9847,6 +10272,9 @@ function DataTableInner(props, ref) {
9847
10272
  columnLabels: columnLabels,
9848
10273
  defaultColumnLabels: defaultColumnLabels,
9849
10274
  onColumnLabelsChange: onColumnLabelsChange,
10275
+ pinnedColumnKeys: pinnedColumnKeys,
10276
+ defaultPinnedColumnKeys: defaultPinnedColumnKeys,
10277
+ onPinnedColumnKeysChange: onPinnedColumnKeysChange,
9850
10278
  columnCalcOps: columnCalcOps,
9851
10279
  defaultColumnCalcOps: defaultColumnCalcOps,
9852
10280
  onColumnCalcOpsChange: onColumnCalcOpsChange,
@@ -9890,19 +10318,13 @@ function DataTableInner(props, ref) {
9890
10318
  children: [
9891
10319
  /*#__PURE__*/ jsxRuntime.jsx(DataTableColGroup, {
9892
10320
  columns: columns,
9893
- selectable: !!selectable
10321
+ selectable: !!selectable,
10322
+ stickyFirst: stickyFirst
9894
10323
  }),
9895
10324
  /*#__PURE__*/ jsxRuntime.jsxs(DataTableHeader, {
9896
10325
  children: [
9897
- selectable && /*#__PURE__*/ jsxRuntime.jsx(DataTableSelectionHeaderCell, {
9898
- ...stickyProps({
9899
- selectable,
9900
- stickyFirst,
9901
- stickyLast: resolvedStickyLast,
9902
- isSelectionCell: true,
9903
- isFirst: false,
9904
- isLast: false
9905
- }),
10326
+ selectable && /*#__PURE__*/ jsxRuntime.jsx(SelectionHeaderCell, {
10327
+ stickyFirst: stickyFirst,
9906
10328
  children: /*#__PURE__*/ jsxRuntime.jsx(SelectAllCheckbox, {
9907
10329
  label: selectAllLabel,
9908
10330
  checkedIcon: selectionCheckedIcon ?? DEFAULT_CHECKED_ICON,
@@ -9953,7 +10375,7 @@ function DataTableInner(props, ref) {
9953
10375
  "data-clickable": onRowClick ? "" : undefined,
9954
10376
  onClick: onRowClick ? ()=>onRowClick(row) : undefined,
9955
10377
  children: [
9956
- selectable && /*#__PURE__*/ jsxRuntime.jsx(DataTableSelectionCell, {
10378
+ selectable && /*#__PURE__*/ jsxRuntime.jsx(SelectionCell, {
9957
10379
  // The row itself carries `onClick={onRowClick}` above, and a
9958
10380
  // native click on this cell's checkbox bubbles up through it
9959
10381
  // (React attaches DataTableRow's handler on the <tr>, which
@@ -9961,14 +10383,7 @@ function DataTableInner(props, ref) {
9961
10383
  // it from at -- so this has to live here, not in the consumer).
9962
10384
  // Without this, checking a row's box also fires `onRowClick`.
9963
10385
  onClick: (event)=>event.stopPropagation(),
9964
- ...stickyProps({
9965
- selectable,
9966
- stickyFirst,
9967
- stickyLast: resolvedStickyLast,
9968
- isSelectionCell: true,
9969
- isFirst: false,
9970
- isLast: false
9971
- }),
10386
+ stickyFirst: stickyFirst,
9972
10387
  children: /*#__PURE__*/ jsxRuntime.jsx(RowCheckbox, {
9973
10388
  label: getRowSelectionLabel(row),
9974
10389
  checkedIcon: selectionCheckedIcon ?? DEFAULT_CHECKED_ICON
@@ -10000,15 +10415,8 @@ function DataTableInner(props, ref) {
10000
10415
  // is what makes every row's selection column line up now, not a matching
10001
10416
  // per-slot width formula, so there is no more reason for this cell to
10002
10417
  // borrow a different slot than its footer siblings.
10003
- /*#__PURE__*/ jsxRuntime.jsx(DataTableFooterCell, {
10004
- ...stickyProps({
10005
- selectable,
10006
- stickyFirst,
10007
- stickyLast: resolvedStickyLast,
10008
- isSelectionCell: true,
10009
- isFirst: false,
10010
- isLast: false
10011
- })
10418
+ /*#__PURE__*/ jsxRuntime.jsx(SelectionFooterCell, {
10419
+ stickyFirst: stickyFirst
10012
10420
  }),
10013
10421
  /*#__PURE__*/ jsxRuntime.jsx(ColumnCalcCells, {
10014
10422
  columns: columns,
@@ -10044,7 +10452,16 @@ DataTableInner.displayName = "DataTable";
10044
10452
  * `DataTableInner`) purely so it can call `useDataTableContext()` -- that context is
10045
10453
  * provided by the `DataTablePrimitiveRoot` this renders as a SIBLING of, one level
10046
10454
  * up from where `DataTableInner` itself sits. */ function ColumnsChangeEffect({ columns, onColumnsChange }) {
10047
- const effective = useEffectiveColumns(columns);
10455
+ // `selectable`/`stickyFirst` are both `false` here on purpose: this convenience blob
10456
+ // never renders a row of its own, so it needs neither the +40px selection offset nor
10457
+ // `stickyFirst`'s legacy "no explicit pin -> pin the first visible column" fallback.
10458
+ // An EXPLICIT per-column pin (`pinnedColumnKeys`) still reorders this list the same
10459
+ // way it reorders every rendered row -- this callback reports "what's showing now",
10460
+ // and a pinned column showing first IS what's showing now.
10461
+ const effective = useEffectiveColumns(columns, {
10462
+ selectable: false,
10463
+ stickyFirst: false
10464
+ });
10048
10465
  // A NEW array every time `effective` itself is a new array (its own `useMemo`
10049
10466
  // recomputes only when order/hidden/label/width actually change) -- `header`
10050
10467
  // resolves to the menu's rename override (falling back to the column's own static
@@ -10071,7 +10488,7 @@ DataTableInner.displayName = "DataTable";
10071
10488
  // is a SIBLING of the toolbar (both inside `DataTablePrimitiveRoot`'s card `<div>`)
10072
10489
  // rather than `DataTablePrimitiveRoot` itself: a `<table>` cannot host the toolbar as a
10073
10490
  // child without the HTML parser foster-parenting it back out.
10074
- const DataTableTable = withContext(DataTableTable$1, "table");
10491
+ const DataTableTable = withContext$1(DataTableTable$1, "table");
10075
10492
  /** One `<col>` per rendered column (plus the 40px selection column, if any) -- the
10076
10493
  * SINGLE place a column's width is ever set now. Every row's header/body/footer cell
10077
10494
  * reads its own width off this shared `<colgroup>` by construction (native table
@@ -10079,8 +10496,16 @@ const DataTableTable = withContext(DataTableTable$1, "table");
10079
10496
  * out of sync between the three. A column with no explicit width gets a bare `<col>`
10080
10497
  * (no `style`): `table-layout: fixed`'s own algorithm gives every such column an equal
10081
10498
  * share of whatever space is left, matching the old flex grid's `flex: 1 1 0%`
10082
- * default with no extra code needed here. */ function DataTableColGroup({ columns, selectable }) {
10083
- const effective = useEffectiveColumns(columns);
10499
+ * default with no extra code needed here. */ function DataTableColGroup({ columns, selectable, stickyFirst }) {
10500
+ // MUST use the same pinned-first ordering every OTHER row (header/body/footer)
10501
+ // renders in (`useEffectiveColumns`'s own doc comment) -- a `<colgroup>`'s `<col>`s
10502
+ // are matched positionally to each row's cells, so if a pinned column visually moves
10503
+ // to the front of the header/body but not here, every column's width applies to the
10504
+ // WRONG cell.
10505
+ const effective = useEffectiveColumns(columns, {
10506
+ selectable,
10507
+ stickyFirst
10508
+ });
10084
10509
  return /*#__PURE__*/ jsxRuntime.jsxs("colgroup", {
10085
10510
  children: [
10086
10511
  selectable && /*#__PURE__*/ jsxRuntime.jsx("col", {
@@ -10096,15 +10521,15 @@ const DataTableTable = withContext(DataTableTable$1, "table");
10096
10521
  ]
10097
10522
  });
10098
10523
  }
10099
- const DataTableHeader = withContext(DataTableHeader$1, "header");
10100
- const DataTableHeaderCell = withContext(DataTableHeaderCell$1, "headerCell");
10524
+ const DataTableHeader = withContext$1(DataTableHeader$1, "header");
10525
+ const DataTableHeaderCell = withContext$1(DataTableHeaderCell$1, "headerCell");
10101
10526
  // `.crisp-datatable-th-inner` -- the flex row INSIDE a `<th>` (label + filter trigger +
10102
10527
  // sort chevron + column menu). See qvism-preset's own `headerCellInner` slot comment
10103
10528
  // for why this lives one level below the cell itself rather than on it.
10104
- const DataTableHeaderCellInner = withContext(Primitive.div, "headerCellInner");
10529
+ const DataTableHeaderCellInner = withContext$1(Primitive.div, "headerCellInner");
10105
10530
  /** The label text specifically -- the one thing in `DataTableHeaderCellInner` that
10106
10531
  * truncates; the trailing filter/sort/menu icons stay fully visible regardless of how
10107
- * long a column's own header text is. */ const DataTableHeaderCellLabel = withContext(Primitive.span, "headerCellLabel");
10532
+ * long a column's own header text is. */ const DataTableHeaderCellLabel = withContext$1(Primitive.span, "headerCellLabel");
10108
10533
  /**
10109
10534
  * A selection `<th>`/`<td>`/footer `<td>` composes ITS base slot (`headerCell`/`cell`/
10110
10535
  * `footerCell`) together with the `selectionCell` MODIFIER, rather than replacing it --
@@ -10115,7 +10540,7 @@ const DataTableHeaderCellInner = withContext(Primitive.div, "headerCellInner");
10115
10540
  * a second recipe call.
10116
10541
  */ function withSelectionCell(Component, baseSlot) {
10117
10542
  const StyledComponent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
10118
- const classNames = useClassNames();
10543
+ const classNames = useClassNames$1();
10119
10544
  return /*#__PURE__*/ jsxRuntime.jsx(Component, {
10120
10545
  ref: ref,
10121
10546
  ...props,
@@ -10126,12 +10551,12 @@ const DataTableHeaderCellInner = withContext(Primitive.div, "headerCellInner");
10126
10551
  return StyledComponent;
10127
10552
  }
10128
10553
  const DataTableSelectionHeaderCell = withSelectionCell(DataTableHeaderCell$1, "headerCell");
10129
- const DataTableHeaderCellIcon = withContext(Primitive.span, "headerCellIcon");
10554
+ const DataTableHeaderCellIcon = withContext$1(Primitive.span, "headerCellIcon");
10130
10555
  // body has no CSS slot of its own in the rootage spec -- it is a plain structural
10131
10556
  // `<tbody>`, so it is not wrapped in `withContext`.
10132
10557
  const DataTableBody = DataTableBody$1;
10133
- const DataTableRow = withContext(DataTableRow$1, "row");
10134
- const DataTableCell = withContext(DataTableCell$1, "cell");
10558
+ const DataTableRow = withContext$1(DataTableRow$1, "row");
10559
+ const DataTableCell = withContext$1(DataTableCell$1, "cell");
10135
10560
  const DataTableSelectionCell = withSelectionCell(DataTableCell$1, "cell");
10136
10561
  // Tranche 4 -- column menu + reorder + resize + sticky columns. A scroll boundary
10137
10562
  // wrapping the whole `<table>`: `overflowX: auto` is what makes a wide table (more
@@ -10141,7 +10566,7 @@ const DataTableSelectionCell = withSelectionCell(DataTableCell$1, "cell");
10141
10566
  // stays out of this slot's own base css (nothing here has a bounded height of its own
10142
10567
  // to clip), so it never introduces a second vertical scrollbar next to whatever
10143
10568
  // ambient container the whole DataTable already sits in.
10144
- const DataTableScrollBodyBase = withContext(Primitive.div, "scrollArea");
10569
+ const DataTableScrollBodyBase = withContext$1(Primitive.div, "scrollArea");
10145
10570
  function DataTableScrollBody({ children, scrollProps }) {
10146
10571
  const { scrollAreaRef, scrolledStart, scrolledEnd, onScroll } = useDataTableContext();
10147
10572
  const { className: scrollClassName, style: scrollStyle, ...otherScrollProps } = scrollProps ?? {};
@@ -10156,7 +10581,7 @@ function DataTableScrollBody({ children, scrollProps }) {
10156
10581
  children: children
10157
10582
  });
10158
10583
  }
10159
- const DataTableFooterBase = withContext(Primitive.tfoot, "footer");
10584
+ const DataTableFooterBase = withContext$1(Primitive.tfoot, "footer");
10160
10585
  function DataTableFooter({ children, ...props }) {
10161
10586
  return /*#__PURE__*/ jsxRuntime.jsx(DataTableFooterBase, {
10162
10587
  ...props,
@@ -10165,7 +10590,33 @@ function DataTableFooter({ children, ...props }) {
10165
10590
  })
10166
10591
  });
10167
10592
  }
10168
- const DataTableFooterCell = withContext(Primitive.td, "footerCell");
10593
+ const DataTableFooterCell = withContext$1(Primitive.td, "footerCell");
10594
+ // The selection column's OWN sticky wrappers -- one each for header/body/footer,
10595
+ // reading the live pinned set via `useSelectionStickyFirst` (column-layout.ts) instead
10596
+ // of the `stickyFirst` PROP alone, so the checkbox column sticks the moment ANY column
10597
+ // is pinned, uncontrolled or not (see that hook's own doc comment for why this can't
10598
+ // just be computed once, above, in `DataTableInner`).
10599
+ function SelectionHeaderCell({ stickyFirst, children }) {
10600
+ const resolvedStickyFirst = useSelectionStickyFirst(stickyFirst);
10601
+ return /*#__PURE__*/ jsxRuntime.jsx(DataTableSelectionHeaderCell, {
10602
+ ...selectionStickyProps(resolvedStickyFirst),
10603
+ children: children
10604
+ });
10605
+ }
10606
+ function SelectionCell({ stickyFirst, onClick, children }) {
10607
+ const resolvedStickyFirst = useSelectionStickyFirst(stickyFirst);
10608
+ return /*#__PURE__*/ jsxRuntime.jsx(DataTableSelectionCell, {
10609
+ onClick: onClick,
10610
+ ...selectionStickyProps(resolvedStickyFirst),
10611
+ children: children
10612
+ });
10613
+ }
10614
+ function SelectionFooterCell({ stickyFirst }) {
10615
+ const resolvedStickyFirst = useSelectionStickyFirst(stickyFirst);
10616
+ return /*#__PURE__*/ jsxRuntime.jsx(DataTableFooterCell, {
10617
+ ...selectionStickyProps(resolvedStickyFirst)
10618
+ });
10619
+ }
10169
10620
  ////////////////////////////////////////////////////////////////////////////////////
10170
10621
  // Fix (bug report): karrot icons default to a 24px intrinsic SVG attribute size
10171
10622
  // -- see column-menu.tsx's own `SORT_ICON_SIZE` comment for the full mechanism.
@@ -10221,4 +10672,275 @@ function SelectAllCheckbox({ label, checkedIcon, indeterminateIcon }) {
10221
10672
  });
10222
10673
  }
10223
10674
 
10675
+ const rowStyle = {
10676
+ display: "flex",
10677
+ alignItems: "center",
10678
+ gap: 4
10679
+ };
10680
+ const rowButtonStyle = {
10681
+ flex: 1,
10682
+ minWidth: 0,
10683
+ textAlign: "left",
10684
+ display: "flex",
10685
+ alignItems: "center",
10686
+ gap: 6,
10687
+ padding: "6px 8px",
10688
+ border: "none",
10689
+ borderRadius: 6,
10690
+ background: "transparent",
10691
+ color: "var(--seed-color-fg-neutral)",
10692
+ fontSize: 13,
10693
+ cursor: "pointer",
10694
+ overflow: "hidden",
10695
+ textOverflow: "ellipsis",
10696
+ whiteSpace: "nowrap"
10697
+ };
10698
+ const smallButtonStyle = {
10699
+ flexShrink: 0,
10700
+ border: "none",
10701
+ borderRadius: 6,
10702
+ background: "transparent",
10703
+ color: "var(--seed-color-fg-neutral-subtle)",
10704
+ fontSize: 12,
10705
+ padding: "4px 6px",
10706
+ cursor: "pointer"
10707
+ };
10708
+ const iconButtonStyle = {
10709
+ display: "inline-flex",
10710
+ alignItems: "center",
10711
+ justifyContent: "center",
10712
+ width: 22,
10713
+ height: 22,
10714
+ flexShrink: 0,
10715
+ border: "none",
10716
+ borderRadius: 6,
10717
+ background: "transparent",
10718
+ color: "var(--seed-color-fg-neutral-subtle)",
10719
+ cursor: "pointer"
10720
+ };
10721
+ /**
10722
+ * A compact saved-views switcher -- current view name (or `label`) as the trigger,
10723
+ * a popover listing every saved view (click to switch, inline Rename/Delete), a
10724
+ * trailing "Save as new view", and (once a view is active) "Update view" to overwrite
10725
+ * the loaded view with the table's current state.
10726
+ */ 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 }) {
10727
+ const viewsApi = useDataTableViews(viewsProps);
10728
+ const { activeViewId, activeView } = viewsApi;
10729
+ const triggerLabel = activeView ? activeView.name : label;
10730
+ return /*#__PURE__*/ jsxRuntime.jsxs(MenuRoot, {
10731
+ children: [
10732
+ /*#__PURE__*/ jsxRuntime.jsx(MenuTrigger, {
10733
+ asChild: true,
10734
+ children: /*#__PURE__*/ jsxRuntime.jsxs(ActionButton, {
10735
+ variant: "neutralOutline",
10736
+ size: "xsmall",
10737
+ "data-active": activeViewId ? "" : undefined,
10738
+ children: [
10739
+ /*#__PURE__*/ jsxRuntime.jsx(IconArrowUpArrowDownLine_default, {
10740
+ size: 14,
10741
+ strokeWidth: 1.75
10742
+ }),
10743
+ triggerLabel,
10744
+ /*#__PURE__*/ jsxRuntime.jsx(IconChevronDownSmallLine_default, {
10745
+ size: 14,
10746
+ strokeWidth: 1.75
10747
+ })
10748
+ ]
10749
+ })
10750
+ }),
10751
+ /*#__PURE__*/ jsxRuntime.jsx(MenuPositioner, {
10752
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuContent, {
10753
+ style: {
10754
+ minWidth: 240
10755
+ },
10756
+ children: /*#__PURE__*/ jsxRuntime.jsx(MenuScrollArea, {
10757
+ children: /*#__PURE__*/ jsxRuntime.jsx(ViewsMenuBody, {
10758
+ viewsApi: viewsApi,
10759
+ captureState: captureState,
10760
+ applyState: applyState,
10761
+ label: label,
10762
+ showClearRow: showClearRow,
10763
+ clearLabel: clearLabel,
10764
+ newViewLabel: newViewLabel,
10765
+ updateViewLabel: updateViewLabel,
10766
+ renameLabel: renameLabel,
10767
+ deleteLabel: deleteLabel,
10768
+ renameInputLabel: renameInputLabel
10769
+ })
10770
+ })
10771
+ })
10772
+ })
10773
+ ]
10774
+ });
10775
+ }
10776
+ DataTableViewsControl.displayName = "DataTableViewsControl";
10777
+ /**
10778
+ * The popover's own body -- split out from `DataTableViewsControl` purely so it can
10779
+ * call `useMenuContext()` (`@seed-design/react-menu`), which only resolves for a
10780
+ * descendant of `MenuRoot`, not the component that RENDERS `MenuRoot` itself (the same
10781
+ * reason `column-layout.ts`'s `useSelectionStickyFirst` is its own component-level
10782
+ * hook rather than something computed in `DataTable.tsx` above its provider). Plain
10783
+ * `<button>` rows here (not seed `MenuItem`s -- `toolbar-conditions.tsx`'s own Sort/
10784
+ * Filter popovers make the same call) don't auto-close the menu on click the way a real
10785
+ * `MenuItem` does, so switching/creating/updating a view calls `setOpen(false)` here
10786
+ * explicitly; renaming and deleting deliberately do NOT close it, so a consumer can
10787
+ * clean up more than one view without reopening the popover each time.
10788
+ */ function ViewsMenuBody({ viewsApi, captureState, applyState, label, showClearRow, clearLabel, newViewLabel, updateViewLabel, renameLabel, deleteLabel, renameInputLabel }) {
10789
+ const { views, activeViewId, createView, updateView, renameView, deleteView, selectView } = viewsApi;
10790
+ const { setOpen } = reactMenu.useMenuContext();
10791
+ const [renamingId, setRenamingId] = React__namespace.useState(null);
10792
+ const [renameValue, setRenameValue] = React__namespace.useState("");
10793
+ // Same race `column-menu.tsx`'s own inline rename input already guards against:
10794
+ // starting a rename swaps the clicked "Rename" button for an input in the SAME
10795
+ // MenuContent this popover's own floating-ui focus management watches, and its
10796
+ // focus-guard spans can steal focus right back (to the content, or the trigger)
10797
+ // one frame after mount -- BEFORE this ref flips true -- so `onBlur` ignores that
10798
+ // one spurious blur and only commits a REAL blur (an actual click/tab away) after.
10799
+ const renameReadyRef = React__namespace.useRef(false);
10800
+ const renameInputRef = React__namespace.useRef(null);
10801
+ const handleSelect = (id)=>{
10802
+ selectView(id);
10803
+ if (id !== null) {
10804
+ const view = views.find((v)=>v.id === id);
10805
+ if (view) applyState(view.state);
10806
+ }
10807
+ setOpen(false);
10808
+ };
10809
+ const handleCreate = ()=>{
10810
+ createView(`View ${views.length + 1}`, captureState());
10811
+ setOpen(false);
10812
+ };
10813
+ const handleUpdate = ()=>{
10814
+ if (activeViewId) updateView(activeViewId, captureState());
10815
+ setOpen(false);
10816
+ };
10817
+ const startRename = (view)=>{
10818
+ renameReadyRef.current = false;
10819
+ setRenamingId(view.id);
10820
+ setRenameValue(view.name);
10821
+ };
10822
+ const commitRename = ()=>{
10823
+ if (renamingId && renameValue.trim()) renameView(renamingId, renameValue.trim());
10824
+ setRenamingId(null);
10825
+ };
10826
+ React__namespace.useEffect(()=>{
10827
+ if (!renamingId) return;
10828
+ // Re-grab focus (and select the text) one frame after mount, AFTER whatever
10829
+ // floating-ui's own focus-guard restore was going to do -- see this component's
10830
+ // own `renameReadyRef` doc comment above.
10831
+ const frame = requestAnimationFrame(()=>{
10832
+ renameReadyRef.current = true;
10833
+ renameInputRef.current?.focus();
10834
+ renameInputRef.current?.select();
10835
+ });
10836
+ return ()=>cancelAnimationFrame(frame);
10837
+ }, [
10838
+ renamingId
10839
+ ]);
10840
+ return /*#__PURE__*/ jsxRuntime.jsxs("div", {
10841
+ style: {
10842
+ display: "flex",
10843
+ flexDirection: "column",
10844
+ gap: 2,
10845
+ padding: "4px 8px"
10846
+ },
10847
+ children: [
10848
+ showClearRow && /*#__PURE__*/ jsxRuntime.jsx("button", {
10849
+ type: "button",
10850
+ onClick: ()=>handleSelect(null),
10851
+ "data-active": !activeViewId ? "" : undefined,
10852
+ style: rowButtonStyle,
10853
+ children: clearLabel ?? label
10854
+ }),
10855
+ views.length === 0 && /*#__PURE__*/ jsxRuntime.jsx("div", {
10856
+ style: {
10857
+ fontSize: 13,
10858
+ color: "var(--seed-color-fg-neutral-subtle)",
10859
+ padding: "4px 8px"
10860
+ },
10861
+ children: "No saved views yet."
10862
+ }),
10863
+ views.map((view)=>renamingId === view.id ? /*#__PURE__*/ jsxRuntime.jsx("div", {
10864
+ style: rowStyle,
10865
+ children: /*#__PURE__*/ jsxRuntime.jsx("input", {
10866
+ ref: renameInputRef,
10867
+ // 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).
10868
+ autoFocus: true,
10869
+ "aria-label": renameInputLabel(view.name),
10870
+ value: renameValue,
10871
+ onChange: (event)=>setRenameValue(event.target.value),
10872
+ onBlur: ()=>{
10873
+ if (renameReadyRef.current) commitRename();
10874
+ },
10875
+ onKeyDown: (event)=>{
10876
+ event.stopPropagation();
10877
+ if (event.key === "Enter") commitRename();
10878
+ if (event.key === "Escape") setRenamingId(null);
10879
+ },
10880
+ style: {
10881
+ flex: 1,
10882
+ minWidth: 0,
10883
+ font: "inherit",
10884
+ fontSize: 13,
10885
+ padding: "6px 8px",
10886
+ border: "1px solid var(--seed-color-border-neutral)",
10887
+ borderRadius: 6,
10888
+ background: "var(--seed-color-bg-surface)",
10889
+ color: "var(--seed-color-fg-neutral)"
10890
+ }
10891
+ })
10892
+ }, view.id) : /*#__PURE__*/ jsxRuntime.jsxs("div", {
10893
+ style: rowStyle,
10894
+ children: [
10895
+ /*#__PURE__*/ jsxRuntime.jsx("button", {
10896
+ type: "button",
10897
+ onClick: ()=>handleSelect(view.id),
10898
+ "data-active": view.id === activeViewId ? "" : undefined,
10899
+ style: rowButtonStyle,
10900
+ children: view.name
10901
+ }),
10902
+ /*#__PURE__*/ jsxRuntime.jsx("button", {
10903
+ type: "button",
10904
+ onClick: ()=>startRename(view),
10905
+ style: smallButtonStyle,
10906
+ children: renameLabel
10907
+ }),
10908
+ /*#__PURE__*/ jsxRuntime.jsx("button", {
10909
+ type: "button",
10910
+ "aria-label": `${deleteLabel} ${view.name}`,
10911
+ onClick: ()=>deleteView(view.id),
10912
+ style: iconButtonStyle,
10913
+ children: /*#__PURE__*/ jsxRuntime.jsx(IconXmarkLine_default, {
10914
+ size: 13,
10915
+ strokeWidth: 1.75
10916
+ })
10917
+ })
10918
+ ]
10919
+ }, view.id)),
10920
+ activeViewId && /*#__PURE__*/ jsxRuntime.jsx("button", {
10921
+ type: "button",
10922
+ onClick: handleUpdate,
10923
+ style: {
10924
+ ...rowButtonStyle,
10925
+ color: "var(--seed-color-fg-neutral-subtle)"
10926
+ },
10927
+ children: updateViewLabel
10928
+ }),
10929
+ /*#__PURE__*/ jsxRuntime.jsxs("button", {
10930
+ type: "button",
10931
+ onClick: handleCreate,
10932
+ style: rowButtonStyle,
10933
+ children: [
10934
+ /*#__PURE__*/ jsxRuntime.jsx(IconPlusLine_default, {
10935
+ size: 13,
10936
+ strokeWidth: 1.75
10937
+ }),
10938
+ newViewLabel
10939
+ ]
10940
+ })
10941
+ ]
10942
+ });
10943
+ }
10944
+
10224
10945
  exports.DataTable = DataTable;
10946
+ exports.DataTableViewsControl = DataTableViewsControl;