@stackframe/dashboard-ui-components 2.8.89 → 2.8.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/components/chart-legend.d.ts +1 -1
  2. package/dist/components/data-grid/data-grid-sizing.d.ts +6 -5
  3. package/dist/components/data-grid/data-grid-sizing.d.ts.map +1 -1
  4. package/dist/components/data-grid/data-grid-sizing.js +9 -28
  5. package/dist/components/data-grid/data-grid-sizing.js.map +1 -1
  6. package/dist/components/data-grid/data-grid.d.ts +17 -237
  7. package/dist/components/data-grid/data-grid.d.ts.map +1 -1
  8. package/dist/components/data-grid/data-grid.js +377 -523
  9. package/dist/components/data-grid/data-grid.js.map +1 -1
  10. package/dist/components/data-grid/data-grid.test.js +82 -0
  11. package/dist/components/data-grid/data-grid.test.js.map +1 -1
  12. package/dist/components/data-grid/index.d.ts +4 -3
  13. package/dist/components/data-grid/index.js +17 -58
  14. package/dist/components/data-grid/state.d.ts +4 -61
  15. package/dist/components/data-grid/state.d.ts.map +1 -1
  16. package/dist/components/data-grid/state.js +13 -160
  17. package/dist/components/data-grid/state.js.map +1 -1
  18. package/dist/components/data-grid/types.d.ts +9 -4
  19. package/dist/components/data-grid/types.d.ts.map +1 -1
  20. package/dist/components/data-grid/use-url-state.d.ts +38 -0
  21. package/dist/components/data-grid/use-url-state.d.ts.map +1 -0
  22. package/dist/components/data-grid/use-url-state.js +214 -0
  23. package/dist/components/data-grid/use-url-state.js.map +1 -0
  24. package/dist/components/data-grid/use-url-state.test.d.ts +1 -0
  25. package/dist/components/data-grid/use-url-state.test.js +91 -0
  26. package/dist/components/data-grid/use-url-state.test.js.map +1 -0
  27. package/dist/components/dialog.d.ts +67 -0
  28. package/dist/components/dialog.d.ts.map +1 -0
  29. package/dist/components/dialog.js +100 -0
  30. package/dist/components/dialog.js.map +1 -0
  31. package/dist/dashboard-ui-components.global.js +10651 -6388
  32. package/dist/dashboard-ui-components.global.js.map +4 -4
  33. package/dist/esm/components/chart-legend.d.ts +1 -1
  34. package/dist/esm/components/data-grid/data-grid-sizing.d.ts +6 -5
  35. package/dist/esm/components/data-grid/data-grid-sizing.d.ts.map +1 -1
  36. package/dist/esm/components/data-grid/data-grid-sizing.js +7 -26
  37. package/dist/esm/components/data-grid/data-grid-sizing.js.map +1 -1
  38. package/dist/esm/components/data-grid/data-grid.d.ts +17 -237
  39. package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
  40. package/dist/esm/components/data-grid/data-grid.js +380 -526
  41. package/dist/esm/components/data-grid/data-grid.js.map +1 -1
  42. package/dist/esm/components/data-grid/data-grid.test.js +82 -0
  43. package/dist/esm/components/data-grid/data-grid.test.js.map +1 -1
  44. package/dist/esm/components/data-grid/index.d.ts +4 -3
  45. package/dist/esm/components/data-grid/index.js +4 -3
  46. package/dist/esm/components/data-grid/state.d.ts +4 -61
  47. package/dist/esm/components/data-grid/state.d.ts.map +1 -1
  48. package/dist/esm/components/data-grid/state.js +15 -150
  49. package/dist/esm/components/data-grid/state.js.map +1 -1
  50. package/dist/esm/components/data-grid/types.d.ts +9 -4
  51. package/dist/esm/components/data-grid/types.d.ts.map +1 -1
  52. package/dist/esm/components/data-grid/use-url-state.d.ts +38 -0
  53. package/dist/esm/components/data-grid/use-url-state.d.ts.map +1 -0
  54. package/dist/esm/components/data-grid/use-url-state.js +212 -0
  55. package/dist/esm/components/data-grid/use-url-state.js.map +1 -0
  56. package/dist/esm/components/data-grid/use-url-state.test.d.ts +1 -0
  57. package/dist/esm/components/data-grid/use-url-state.test.js +91 -0
  58. package/dist/esm/components/data-grid/use-url-state.test.js.map +1 -0
  59. package/dist/esm/components/dialog.d.ts +67 -0
  60. package/dist/esm/components/dialog.d.ts.map +1 -0
  61. package/dist/esm/components/dialog.js +92 -0
  62. package/dist/esm/components/dialog.js.map +1 -0
  63. package/dist/esm/index.d.ts +2 -1
  64. package/dist/esm/index.js +2 -1
  65. package/dist/index.d.ts +5 -3
  66. package/dist/index.js +37 -0
  67. package/package.json +4 -3
@@ -10,79 +10,13 @@ react = require_chunk.__toESM(react);
10
10
  let __state_js = require("./state.js");
11
11
  let __strings_js = require("./strings.js");
12
12
  let _stackframe_stack_shared_dist_utils_errors = require("@stackframe/stack-shared/dist/utils/errors");
13
+ let _tanstack_react_table = require("@tanstack/react-table");
13
14
  let _tanstack_react_virtual = require("@tanstack/react-virtual");
14
15
  let ___skeleton_js = require("../skeleton.js");
15
16
  let __data_grid_sizing_js = require("./data-grid-sizing.js");
16
17
  let __data_grid_toolbar_js = require("./data-grid-toolbar.js");
17
18
 
18
19
  //#region src/components/data-grid/data-grid.tsx
19
- function ResizeHandle({ onResize, onResizeEnd }) {
20
- const startXRef = (0, react.useRef)(0);
21
- const rafRef = (0, react.useRef)(0);
22
- const latestDeltaRef = (0, react.useRef)(0);
23
- const callbacksRef = (0, react.useRef)({
24
- onResize,
25
- onResizeEnd
26
- });
27
- callbacksRef.current = {
28
- onResize,
29
- onResizeEnd
30
- };
31
- const onPointerDown = (0, react.useCallback)((e) => {
32
- e.preventDefault();
33
- e.stopPropagation();
34
- startXRef.current = e.clientX;
35
- latestDeltaRef.current = 0;
36
- const el = e.currentTarget;
37
- el.setPointerCapture(e.pointerId);
38
- let finished = false;
39
- const onMove = (ev) => {
40
- latestDeltaRef.current = ev.clientX - startXRef.current;
41
- if (rafRef.current !== 0) return;
42
- rafRef.current = requestAnimationFrame(() => {
43
- rafRef.current = 0;
44
- callbacksRef.current.onResize(latestDeltaRef.current);
45
- });
46
- };
47
- const finish = () => {
48
- if (finished) return;
49
- finished = true;
50
- if (rafRef.current !== 0) {
51
- cancelAnimationFrame(rafRef.current);
52
- rafRef.current = 0;
53
- callbacksRef.current.onResize(latestDeltaRef.current);
54
- }
55
- el.removeEventListener("pointermove", onMove);
56
- el.removeEventListener("pointerup", finish);
57
- el.removeEventListener("pointercancel", finish);
58
- el.removeEventListener("lostpointercapture", finish);
59
- if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
60
- callbacksRef.current.onResizeEnd();
61
- };
62
- el.addEventListener("pointermove", onMove);
63
- el.addEventListener("pointerup", finish);
64
- el.addEventListener("pointercancel", finish);
65
- el.addEventListener("lostpointercapture", finish);
66
- }, []);
67
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
68
- className: (0, _stackframe_stack_ui.cn)("absolute right-0 top-0 bottom-0 z-10 w-[5px] cursor-col-resize touch-none", "group-hover/header:bg-foreground/[0.06] hover:!bg-blue-500/30", "transition-colors duration-100"),
69
- onClick: (e) => {
70
- e.preventDefault();
71
- e.stopPropagation();
72
- },
73
- onPointerDown
74
- });
75
- }
76
- function getNearestVerticalScrollElement(element) {
77
- let current = element?.parentElement ?? null;
78
- while (current) {
79
- const style = window.getComputedStyle(current);
80
- const overflowY = style.overflowY === "visible" ? style.overflow : style.overflowY;
81
- if ((overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && current.scrollHeight > current.clientHeight + 1) return current;
82
- current = current.parentElement;
83
- }
84
- return window;
85
- }
86
20
  function getEventTargetElement(target) {
87
21
  if (target instanceof Element) return target;
88
22
  if (target instanceof Node) return target.parentElement;
@@ -104,41 +38,122 @@ function isDataGridInteractiveRowClickTarget(target) {
104
38
  function shouldIgnoreRowClick(event) {
105
39
  return event.defaultPrevented || isDataGridInteractiveRowClickTarget(event.target);
106
40
  }
107
- function HeaderCell({ col, isSorted, sortIndex, resizable, onSort, onResize, onResizeEnd }) {
41
+ function toTanstackSorting(sorting) {
42
+ return sorting.map((s) => ({
43
+ id: s.columnId,
44
+ desc: s.direction === "desc"
45
+ }));
46
+ }
47
+ function fromTanstackSorting(sorting) {
48
+ return sorting.map((s) => ({
49
+ columnId: s.id,
50
+ direction: s.desc ? "desc" : "asc"
51
+ }));
52
+ }
53
+ function toTanstackRowSelection(ids) {
54
+ const out = {};
55
+ for (const id of ids) out[id] = true;
56
+ return out;
57
+ }
58
+ function resolveUpdater(updater, current) {
59
+ return typeof updater === "function" ? updater(current) : updater;
60
+ }
61
+ function distributeFlexWidths(sizes, visibleColumns, available) {
62
+ const flexCols = visibleColumns.filter((c) => c.flex != null && c.flex > 0);
63
+ if (flexCols.length === 0 || available <= 0) return;
64
+ const totalFlex = flexCols.reduce((acc, c) => acc + (c.flex ?? 0), 0);
65
+ let remaining = available;
66
+ flexCols.forEach((col, i) => {
67
+ const share = i === flexCols.length - 1 ? remaining : Math.floor(available * ((col.flex ?? 0) / totalFlex));
68
+ const max = col.maxWidth ?? Infinity;
69
+ const add = Math.max(0, Math.min(share, max - sizes[col.id]));
70
+ sizes[col.id] += add;
71
+ remaining -= add;
72
+ });
73
+ }
74
+ function selectSingle(current, rowId) {
75
+ const isSelected = current.selectedIds.has(rowId);
76
+ return {
77
+ selectedIds: isSelected ? /* @__PURE__ */ new Set() : new Set([rowId]),
78
+ anchorId: isSelected ? null : rowId
79
+ };
80
+ }
81
+ function selectRange(current, rowId, allRowIds, additive) {
82
+ if (current.anchorId == null) return null;
83
+ const anchorIdx = allRowIds.indexOf(current.anchorId);
84
+ const currentIdx = allRowIds.indexOf(rowId);
85
+ if (anchorIdx < 0 || currentIdx < 0) return null;
86
+ const start = Math.min(anchorIdx, currentIdx);
87
+ const end = Math.max(anchorIdx, currentIdx);
88
+ const next = additive ? new Set(current.selectedIds) : /* @__PURE__ */ new Set();
89
+ for (let i = start; i <= end; i++) next.add(allRowIds[i]);
90
+ return {
91
+ selectedIds: next,
92
+ anchorId: current.anchorId
93
+ };
94
+ }
95
+ function selectToggle(current, rowId) {
96
+ const next = new Set(current.selectedIds);
97
+ if (next.has(rowId)) next.delete(rowId);
98
+ else next.add(rowId);
99
+ return {
100
+ selectedIds: next,
101
+ anchorId: rowId
102
+ };
103
+ }
104
+ function nextSelection(input) {
105
+ const { current, rowId, mode, modifiers, allRowIds } = input;
106
+ if (mode === "single") return selectSingle(current, rowId);
107
+ if (modifiers.shift) {
108
+ const range = selectRange(current, rowId, allRowIds, modifiers.ctrl);
109
+ if (range != null) return range;
110
+ }
111
+ if (modifiers.ctrl) return selectToggle(current, rowId);
112
+ return {
113
+ selectedIds: new Set([rowId]),
114
+ anchorId: rowId
115
+ };
116
+ }
117
+ function HeaderCell({ header, col, resizable }) {
118
+ const sorted = header.column.getIsSorted();
119
+ const sortIndex = header.column.getSortIndex();
120
+ const totalSorts = header.column.getCanMultiSort() ? header.getContext().table.getState().sorting.length : 0;
108
121
  const ctx = {
109
122
  columnId: col.id,
110
123
  columnDef: col,
111
- isSorted,
112
- sortIndex
124
+ isSorted: sorted === false ? false : sorted,
125
+ sortIndex: totalSorts > 1 && sortIndex >= 0 ? sortIndex + 1 : null
113
126
  };
114
127
  const label = typeof col.header === "function" ? col.header(ctx) : col.header;
115
- const sortable = col.sortable !== false;
128
+ const sortable = header.column.getCanSort();
129
+ const canResize = resizable && header.column.getCanResize();
130
+ const isResizing = header.column.getIsResizing();
116
131
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
117
132
  className: (0, _stackframe_stack_ui.cn)("group/header relative flex items-center gap-1.5 px-3 select-none bg-transparent overflow-hidden", "border-r border-black/[0.04] dark:border-white/[0.04] last:border-r-0", sortable && "cursor-pointer"),
118
- style: (0, __data_grid_sizing_js.getColumnSizingStyle)(col),
133
+ style: { width: `calc(var(--col-${col.id}-size) * 1px)` },
119
134
  "data-col-id": col.id,
120
- onClick: (e) => sortable && onSort(col.id, e.metaKey || e.ctrlKey),
135
+ onClick: sortable ? header.column.getToggleSortingHandler() : void 0,
121
136
  role: "columnheader",
122
- "aria-sort": isSorted === "asc" ? "ascending" : isSorted === "desc" ? "descending" : "none",
137
+ "aria-sort": sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none",
123
138
  children: [
124
139
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
125
140
  className: (0, _stackframe_stack_ui.cn)("flex-1 min-w-0 truncate text-xs font-semibold uppercase tracking-wider text-muted-foreground", col.align === "center" && "text-center", col.align === "right" && "text-right"),
126
141
  children: label
127
142
  }),
128
- isSorted && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
143
+ sorted && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
129
144
  className: "flex items-center gap-0.5 text-foreground/60",
130
- children: [isSorted === "asc" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowUp, {
145
+ children: [sorted === "asc" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowUp, {
131
146
  className: "h-3 w-3",
132
147
  weight: "bold"
133
148
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.ArrowDown, {
134
149
  className: "h-3 w-3",
135
150
  weight: "bold"
136
- }), sortIndex != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
151
+ }), ctx.sortIndex != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
137
152
  className: "text-[10px] font-medium tabular-nums",
138
- children: sortIndex
153
+ children: ctx.sortIndex
139
154
  })]
140
155
  }),
141
- !isSorted && sortable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
156
+ !sorted && sortable && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
142
157
  className: "hidden group-hover/header:flex items-center text-foreground/20",
143
158
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CaretUp, {
144
159
  className: "h-2.5 w-2.5 -mb-[1px]",
@@ -148,9 +163,14 @@ function HeaderCell({ col, isSorted, sortIndex, resizable, onSort, onResize, onR
148
163
  weight: "bold"
149
164
  })]
150
165
  }),
151
- resizable && col.resizable !== false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResizeHandle, {
152
- onResize: (delta) => onResize(col.id, delta),
153
- onResizeEnd
166
+ canResize && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
167
+ onMouseDown: header.getResizeHandler(),
168
+ onTouchStart: header.getResizeHandler(),
169
+ onClick: (e) => {
170
+ e.preventDefault();
171
+ e.stopPropagation();
172
+ },
173
+ className: (0, _stackframe_stack_ui.cn)("absolute right-0 top-0 bottom-0 z-10 w-[5px] cursor-col-resize touch-none", "group-hover/header:bg-foreground/[0.06] hover:!bg-blue-500/30", "transition-colors duration-100", isResizing && "bg-blue-500/40")
154
174
  })
155
175
  ]
156
176
  });
@@ -175,7 +195,7 @@ function DataCell({ col, row, rowId, rowIndex, isSelected, dateDisplay }) {
175
195
  const isWrap = col.cellOverflow === "wrap";
176
196
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
177
197
  className: (0, _stackframe_stack_ui.cn)("flex px-3 bg-transparent overflow-hidden", "border-r border-black/[0.04] dark:border-white/[0.04] last:border-r-0", "text-sm text-foreground", isWrap ? "items-start py-2" : "items-center", col.align === "center" && "justify-center", col.align === "right" && "justify-end", hasCellClick && "cursor-pointer"),
178
- style: (0, __data_grid_sizing_js.getColumnSizingStyle)(col),
198
+ style: { width: `calc(var(--col-${col.id}-size) * 1px)` },
179
199
  "data-col-id": col.id,
180
200
  role: "gridcell",
181
201
  onClick: col.onCellClick ? (e) => {
@@ -210,10 +230,6 @@ function formatCellValue(value) {
210
230
  children: String(value)
211
231
  });
212
232
  }
213
- /** Built-in date cell — mirrors what `formatGridDate` returns but wraps
214
- * the display in a `<span>` with a `title` tooltip showing the absolute
215
- * datetime. Only used when the column has `type: "date" | "dateTime"`
216
- * and no custom `renderCell`. */
217
233
  function renderDateCell(value, dateDisplay, col) {
218
234
  const { display, tooltip } = (0, __state_js.formatGridDate)(value, dateDisplay, {
219
235
  parseValue: col.parseValue,
@@ -245,7 +261,7 @@ function SkeletonRow({ columns, height, showCheckbox }) {
245
261
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(___skeleton_js.DesignSkeleton, { className: "h-4 w-4 rounded" })
246
262
  }), columns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
247
263
  className: "flex items-center px-3 border-r border-black/[0.04] dark:border-white/[0.04] last:border-r-0",
248
- style: (0, __data_grid_sizing_js.getColumnSizingStyle)(col),
264
+ style: { width: `calc(var(--col-${col.id}-size) * 1px)` },
249
265
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(___skeleton_js.DesignSkeleton, {
250
266
  className: "h-3.5 rounded-md",
251
267
  style: { width: `${40 + hashStringToInt(col.id) % 40}%` }
@@ -253,7 +269,7 @@ function SkeletonRow({ columns, height, showCheckbox }) {
253
269
  }, col.id))]
254
270
  });
255
271
  }
256
- function SelectionCheckbox({ checked, indeterminate, onChange, ariaLabel }) {
272
+ function SelectionCheckbox({ checked, indeterminate, onChange, ariaLabel, title }) {
257
273
  const Icon = indeterminate ? _phosphor_icons_react.MinusSquare : checked ? _phosphor_icons_react.CheckSquare : _phosphor_icons_react.Square;
258
274
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
259
275
  className: (0, _stackframe_stack_ui.cn)("flex items-center justify-center w-full h-full", "hover:bg-foreground/[0.04] transition-colors duration-75", checked || indeterminate ? "text-blue-600 dark:text-blue-400" : "text-muted-foreground/40 hover:text-muted-foreground/60"),
@@ -262,6 +278,7 @@ function SelectionCheckbox({ checked, indeterminate, onChange, ariaLabel }) {
262
278
  onChange(e);
263
279
  },
264
280
  "aria-label": ariaLabel,
281
+ title: title ?? ariaLabel,
265
282
  role: "checkbox",
266
283
  "aria-checked": indeterminate ? "mixed" : checked,
267
284
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
@@ -376,396 +393,292 @@ function DefaultFooter({ ctx, pagination, onChange }) {
376
393
  });
377
394
  }
378
395
  /**
379
- * Interactive table with sorting, quick search, pagination, selection,
380
- * and virtualization. Handles 10k+ rows smoothly. Pair with
381
- * `useDataSource` for client-side data; use an async `dataSource`
382
- * generator for server or infinite-scroll modes.
396
+ * Interactive table built on TanStack Table v8. Sorting, column sizing,
397
+ * visibility, ordering, and pinning are owned by the table instance; we
398
+ * layer virtualization, sticky toolbar/header/footer, infinite scroll,
399
+ * quick search, CSV export, and date-format toggling on top.
383
400
  *
384
- * ## Mental model (read this first everything else depends on it)
385
- *
386
- * DataGrid is a **display** component. It does NOT sort, search, or
387
- * paginate your data directly — you own that, but `useDataSource` does
388
- * it for you. The `rows` prop is always the already-processed slice to
389
- * show. The grid tracks user intent in `state` (sort model, quick
390
- * search text, page index). You feed that state into `useDataSource`,
391
- * and its output goes back in as `rows`.
392
- *
393
- * `useDataSource` IS the processor. Given your full dataset and the
394
- * grid's state, it returns the searched + sorted + paginated rows
395
- * ready to pass to DataGrid. This is the ONLY correct pattern for
396
- * client-side data — do NOT pass a raw array to `rows`.
397
- *
398
- * ## Search (client vs async)
399
- *
400
- * - **Client mode** (`useDataSource` with `data`): a case-insensitive
401
- * substring match across every column is applied automatically.
402
- * Override the matcher with `matchRow` for fuzzy / weighted search,
403
- * or disable by passing `matchRow: () => true`.
404
- * - **Async mode** (`useDataSource` with `dataSource`): `state.quickSearch`
405
- * is forwarded to the generator as `params.quickSearch`. Same
406
- * mechanism as `params.sorting` — a change triggers a refetch, and
407
- * the generator is the "matching logic" (typically a WHERE / ILIKE
408
- * clause in the backend query). The grid does NO client-side
409
- * filtering in async mode.
410
- *
411
- * ## The canonical pattern
401
+ * The grid is display-only it does not fetch or page data itself. Pair
402
+ * with `useDataSource` for client- or server-side data and pass the
403
+ * already-processed slice through `rows`.
412
404
  *
413
405
  * ```tsx
414
- * // 1. Columns — define OUTSIDE the component or inside a useMemo. Must be stable.
415
- * const columns = React.useMemo(() => [
416
- * { id: "name", header: "Name", accessor: "name", width: 180, type: "string" },
417
- * { id: "email", header: "Email", accessor: "email", width: 240, type: "string" },
418
- * { id: "role", header: "Role", accessor: "role", width: 120, type: "singleSelect",
419
- * valueOptions: [{ value: "admin", label: "Admin" }, { value: "member", label: "Member" }] },
420
- * { id: "signUps", header: "Sign-ups", accessor: "signUps", width: 120, type: "number", align: "right",
421
- * renderCell: ({ value }) => <span className="tabular-nums">{Number(value).toLocaleString()}</span> },
422
- * ], []);
423
- *
424
- * // 2. Grid state — one hook, initialized from the columns. NEVER build the state object by hand.
425
- * const [gridState, setGridState] = React.useState(() => createDefaultDataGridState(columns));
426
- *
427
- * // 3. Data source — wires your raw array through the grid state. ALWAYS call this
428
- * // hook unconditionally at the top level (no if/return before it).
406
+ * const columns = useMemo(() => [...], []);
407
+ * const [gridState, setGridState] = useState(() => createDefaultDataGridState(columns));
429
408
  * const gridData = useDataSource({
430
- * data: users, // your raw array (can be [] while loading)
431
- * columns,
432
- * getRowId: (row) => row.id,
409
+ * data: users, columns, getRowId: (r) => r.id,
433
410
  * sorting: gridState.sorting,
434
411
  * quickSearch: gridState.quickSearch,
435
412
  * pagination: gridState.pagination,
436
- * paginationMode: "client", // "client" | "server" | "infinite"
413
+ * paginationMode: "client",
437
414
  * });
438
415
  *
439
- * // 4. Render — `rows` comes from gridData.rows, NOT from your raw array.
440
416
  * <DataGrid
441
417
  * columns={columns}
442
418
  * rows={gridData.rows}
443
- * getRowId={(row) => row.id}
419
+ * getRowId={(r) => r.id}
444
420
  * totalRowCount={gridData.totalRowCount}
445
421
  * isLoading={gridData.isLoading}
446
422
  * state={gridState}
447
423
  * onChange={setGridState}
448
- * selectionMode="none" // "none" | "single" | "multiple"
449
- * maxHeight={480}
450
424
  * />
451
425
  * ```
452
426
  *
453
- * ## Iron rules (violating any of these breaks the grid)
454
- *
455
- * 1. The prop is `rows`, NOT `data`. There is no `data` prop on DataGrid.
456
- * `data` belongs on `useDataSource`.
457
- * 2. `rows` is ALWAYS `gridData.rows`. Never pass your raw array to
458
- * `rows` — the grid won't search, sort, or paginate it.
459
- * 3. Columns must be stable across renders. Define them outside the
460
- * component or wrap in `React.useMemo`. A fresh columns array every
461
- * render will reset sorting state.
462
- * 4. Initialize grid state with `createDefaultDataGridState(columns)`.
463
- * Do NOT spell out the state object manually — you will miss fields
464
- * and crash.
465
- * 5. `onChange` takes a `SetStateAction` (the setter you got from
466
- * `useState`). Pass `setGridState` directly. Do NOT wrap it unless
467
- * you know exactly what you're doing.
468
- * 6. Call `useDataSource` ONCE per grid, at the top level, before any
469
- * early return. It contains hooks.
470
- * 7. `renderCell` is a PURE function of its context. NEVER call React
471
- * hooks inside it (no `useState`, `useMemo`, `useEffect`, nothing).
472
- * If you need derived data per row, compute it BEFORE the render —
473
- * e.g. build a `Map<rowId, sparklineData>` in a `useMemo` and look
474
- * it up in `renderCell`.
475
- * 8. `toolbar` accepts `false` (hide it) or a render function
476
- * `(ctx) => ReactNode`. Anything else — `true`, `undefined`, a state
477
- * variable — will either show the default toolbar or crash. If you
478
- * just want the default toolbar, omit the prop entirely.
479
- * 9. The toolbar's search input writes to `state.quickSearch`. That
480
- * value is consumed by `useDataSource` — client mode filters
481
- * client-side, async mode forwards to the generator. Do NOT wire
482
- * a separate "controlled" search prop, everything flows through
483
- * grid state.
484
- *
485
- * ## renderCell — what you can and cannot do inside it
486
- *
487
- * ```tsx
488
- * // OK — pure rendering from ctx:
489
- * renderCell: ({ value }) => <span className="tabular-nums">{Number(value).toLocaleString()}</span>
490
- * renderCell: ({ row }) => <Badge variant={row.active ? "default" : "outline"}>{row.status}</Badge>
491
- *
492
- * // OK — looking up pre-computed data by row id:
493
- * // BEFORE the return, in the component body:
494
- * const sparklinesById = React.useMemo(() => {
495
- * const m = new Map();
496
- * for (const u of users) {
497
- * m.set(u.id, u.recentActivity.map((n, i) => ({ ts: i, values: { primary: n } })));
498
- * }
499
- * return m;
500
- * }, [users]);
501
- * // Then inside the column def:
502
- * renderCell: ({ rowId }) => <MiniSparkline data={sparklinesById.get(rowId) ?? []} />
503
- *
504
- * // NOT OK — hooks inside renderCell:
505
- * renderCell: ({ row }) => {
506
- * const [hovered, setHovered] = React.useState(false); // ← crashes the grid
507
- * const data = React.useMemo(() => ..., []); // ← crashes the grid
508
- * return ...;
509
- * }
510
- *
511
- * // NOT OK — embedding AnalyticsChart (or any other controlled, stateful chart) per row:
512
- * // AnalyticsChart owns its own state, tooltips, zoom, and virtualized data
513
- * // pipeline. Instantiating one per row is expensive and fights the grid's
514
- * // virtualizer. Don't do it.
515
- * ```
516
- *
517
- * ## Sparklines and mini-charts in cells — use raw Recharts
518
- *
519
- * If you want a tiny chart (sparkline, micro bar chart, trend line) inside
520
- * a cell, drop down to raw `Recharts.*` components — they are lightweight
521
- * and stateless, so they render cleanly per row without owning any state.
522
- * Read pre-computed points off the row (or off a `Map<rowId, points>` you
523
- * built in a `useMemo` above) and pass them directly to the Recharts
524
- * primitive. Do NOT wrap them in `DesignChartContainer` or
525
- * `DesignChartCard` inside a cell — those add chrome meant for full-size
526
- * charts.
527
- *
528
- * ```tsx
529
- * // OK — raw Recharts sparkline per row:
530
- * renderCell: ({ rowId }) => {
531
- * const points = sparklinesById.get(rowId) ?? [];
532
- * return (
533
- * <Recharts.ResponsiveContainer width="100%" height={28}>
534
- * <Recharts.LineChart data={points} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
535
- * <Recharts.Line type="monotone" dataKey="v" stroke="currentColor" strokeWidth={1.5} dot={false} isAnimationActive={false} />
536
- * </Recharts.LineChart>
537
- * </Recharts.ResponsiveContainer>
538
- * );
539
- * }
540
- * ```
541
- *
542
- * Keep in-cell Recharts configs minimal: no axes, no tooltips, no animation
543
- * (`isAnimationActive={false}`), tight margins, fixed height. The goal is a
544
- * visual summary, not an interactive chart.
545
- *
546
- * ## State shape (from `createDefaultDataGridState`)
547
- *
548
- * ```ts
549
- * {
550
- * sorting: [], // { columnId, direction: "asc" | "desc" }[]
551
- * quickSearch: "", // search input text
552
- * dateDisplay: "relative", // "relative" | "absolute"
553
- * columnVisibility: {}, columnWidths: {...},
554
- * columnPinning: { left: [], right: [] }, columnOrder: [...],
555
- * pagination: { pageIndex: 0, pageSize: 50 },
556
- * selection: { selectedIds: new Set(), anchorId: null },
557
- * }
558
- * ```
559
- *
560
- * Everything is updated through `setGridState` — the toolbar, header,
561
- * and footer all call it for you. You do not need to wire any of this
562
- * manually.
563
- *
564
- * ## Cell overflow and dynamic row heights
565
- *
566
- * By default every cell truncates its content with an ellipsis
567
- * (`cellOverflow: "truncate"`). For columns whose content should wrap
568
- * — badge lists, multi-line text, permission chips — set
569
- * `cellOverflow: "wrap"` on the column definition.
570
- *
571
- * To let rows grow to fit their tallest cell, set `rowHeight="auto"`
572
- * on the grid. The virtualizer will measure each row after render and
573
- * adjust scroll positions accordingly. Pair with `estimatedRowHeight`
574
- * (default 44) for better scroll-position estimates before measurement.
575
- *
576
- * ```tsx
577
- * // Columns: UUIDs truncate, auth-method badges wrap
578
- * const columns = [
579
- * { id: "userId", header: "User ID", width: 130 }, // default truncate
580
- * { id: "auth", header: "Auth methods", width: 150, cellOverflow: "wrap",
581
- * renderCell: ({ row }) => (
582
- * <div className="flex flex-wrap gap-1">
583
- * {row.authTypes.map((t) => <Badge key={t}>{t}</Badge>)}
584
- * </div>
585
- * ),
586
- * },
587
- * ];
588
- *
589
- * <DataGrid columns={columns} rowHeight="auto" estimatedRowHeight={48} ... />
590
- * ```
591
- *
592
- * With a fixed numeric `rowHeight` (the default), `cellOverflow: "wrap"`
593
- * still lets content wrap within the row, but anything exceeding the
594
- * fixed height is clipped. This is useful when you want controlled
595
- * wrapping without variable row heights.
596
- *
597
- * ## Height and scrolling
598
- *
599
- * DataGrid is NOT a card. It has no border, rounded corners, or shadow of
600
- * its own. Wrap it in whatever chrome you want — a `DesignCard`, a section,
601
- * or just raw layout. The grid itself fills its parent's height via
602
- * `h-full`.
603
- *
604
- * How the grid gets its height (pick ONE):
605
- * 1. Bounded parent — put the grid inside a flex/grid container with a
606
- * definite height (e.g. `flex-1 min-h-0` inside a page-filling flex
607
- * column). The grid stretches to that height and scrolls its body.
608
- * 2. `maxHeight` prop — pass a number (pixels) or CSS string
609
- * (`"480px"`, `"60vh"`, `"100%"`). The grid caps at that size and
610
- * scrolls its body.
611
- * 3. Unbounded — omit `maxHeight` and let the parent grow freely. The
612
- * grid renders at its full content height and the page scrolls. Fine
613
- * for small lists; bad UX for thousands of rows.
614
- *
615
- * The toolbar, header, and footer are always `shrink-0`; only the body
616
- * scrolls. You do NOT need to subtract toolbar/footer heights from
617
- * `maxHeight` — the grid's internal flex layout handles that.
618
- *
619
- * ## When to use what
620
- *
621
- * - Simple static list, < 20 rows, no interaction → use a plain table component instead.
622
- * - Interactive table, sortable + searchable, any size → `DataGrid` +
623
- * `useDataSource` with `paginationMode: "client"`.
624
- * - Infinite scroll over a huge dataset you fetch in pages → `dataSource` async
625
- * generator + `paginationMode: "infinite"`. Only reach for this if you actually
626
- * need pagination over a remote source. For anything that fits in memory,
627
- * `"client"` is simpler and faster.
628
- *
629
- * ## Features you get for free
630
- *
631
- * Quick search, sortable columns (shift-click for multi-sort), column
632
- * visibility toggle, column resize, CSV export, virtualized rendering
633
- * for 10k+ rows, keyboard navigation, and a relative/absolute date
634
- * toggle for `date` / `dateTime` columns.
427
+ * Iron rules:
428
+ * - `rows` is always `gridData.rows`, never your raw array.
429
+ * - Columns must be stable (define outside the component or wrap in `useMemo`).
430
+ * - Initialize state with `createDefaultDataGridState(columns)`.
431
+ * - `renderCell` must be a pure function no React hooks inside.
635
432
  */
636
433
  function DataGrid(props) {
637
- const { columns: allColumns, rows, getRowId, totalRowCount, isLoading = false, isRefetching = false, hasMore = false, isLoadingMore = false, onLoadMore, state, onChange, paginationMode = "paginated", selectionMode = "none", resizable = true, rowHeight: rowHeightProp = 44, estimatedRowHeight: estimatedRowHeightProp, headerHeight = 44, overscan = 5, maxHeight, fillHeight = true, stickyTop, toolbar, toolbarExtra, emptyState, loadingState, footer, footerExtra, exportFilename = "export", strings: stringsOverride, className, onRowClick, onRowDoubleClick, onSelectionChange, onSortChange } = props;
434
+ const { columns: allColumns, rows, getRowId, totalRowCount, isLoading = false, isRefetching = false, hasMore = false, isLoadingMore = false, onLoadMore, state, onChange, paginationMode = "paginated", selectionMode = "none", resizable = true, rowHeight: rowHeightProp = 44, estimatedRowHeight: estimatedRowHeightProp, headerHeight = 44, overscan = 5, maxHeight, fillHeight = true, stickyTop, toolbar, toolbarExtra, emptyState, loadingState, footer, footerExtra, exportFilename = "export", strings: stringsOverride, className, onRowClick, onRowDoubleClick, onSelectionChange, onSortChange, onColumnResize, onColumnVisibilityChange } = props;
638
435
  const isDynamicRowHeight = rowHeightProp === "auto";
639
436
  const fixedRowHeight = isDynamicRowHeight ? void 0 : rowHeightProp;
640
437
  const estimatedRowHeight = estimatedRowHeightProp ?? fixedRowHeight ?? 44;
641
438
  const strings = (0, react.useMemo)(() => (0, __strings_js.resolveDataGridStrings)(stringsOverride), [stringsOverride]);
642
- const visibleColumns = (0, react.useMemo)(() => (state.columnOrder.length > 0 ? state.columnOrder.map((id) => allColumns.find((c) => c.id === id)).filter(Boolean) : allColumns).filter((col) => (0, __state_js.isColumnVisible)(col.id, state.columnVisibility)), [
643
- allColumns,
644
- state.columnOrder,
645
- state.columnVisibility
439
+ const tableColumns = (0, react.useMemo)(() => allColumns.map((col) => ({
440
+ id: col.id,
441
+ accessorFn: (row) => (0, __state_js.resolveColumnValue)(col, row),
442
+ header: typeof col.header === "string" ? col.header : col.id,
443
+ size: col.width ?? __data_grid_sizing_js.DEFAULT_COL_WIDTH,
444
+ minSize: (0, __data_grid_sizing_js.getEffectiveMinWidth)(col),
445
+ maxSize: (0, __data_grid_sizing_js.getEffectiveMaxWidth)(col),
446
+ enableSorting: col.sortable !== false,
447
+ enableHiding: col.hideable !== false,
448
+ enableResizing: col.resizable !== false,
449
+ enableMultiSort: true
450
+ })), [allColumns]);
451
+ const tanstackSorting = (0, react.useMemo)(() => toTanstackSorting(state.sorting), [state.sorting]);
452
+ const tanstackRowSelection = (0, react.useMemo)(() => toTanstackRowSelection(state.selection.selectedIds), [state.selection.selectedIds]);
453
+ const tanstackColumnPinning = (0, react.useMemo)(() => ({
454
+ left: [...state.columnPinning.left],
455
+ right: [...state.columnPinning.right]
456
+ }), [state.columnPinning]);
457
+ const tanstackColumnOrder = (0, react.useMemo)(() => [...state.columnOrder], [state.columnOrder]);
458
+ const allColumnsRef = (0, react.useRef)(allColumns);
459
+ allColumnsRef.current = allColumns;
460
+ const handleSortingChange = (0, react.useCallback)((updater) => {
461
+ const ours = fromTanstackSorting(resolveUpdater(updater, toTanstackSorting(state.sorting))).map((s) => ({ ...s }));
462
+ onChange((s) => ({
463
+ ...s,
464
+ sorting: ours,
465
+ pagination: {
466
+ ...s.pagination,
467
+ pageIndex: 0
468
+ }
469
+ }));
470
+ onSortChange?.(ours);
471
+ }, [
472
+ onChange,
473
+ onSortChange,
474
+ state.sorting
646
475
  ]);
647
- const rowIds = (0, react.useMemo)(() => rows.map(getRowId), [rows, getRowId]);
648
- const visibleColumnMetrics = (0, react.useMemo)(() => {
649
- const widths = /* @__PURE__ */ new Map();
650
- let totalWidth = selectionMode !== "none" ? 44 : 0;
651
- for (const col of visibleColumns) {
652
- const width = (0, __state_js.resolveColumnWidth)(col, state.columnWidths[col.id]);
653
- widths.set(col.id, width);
654
- totalWidth += width;
476
+ const handleColumnSizingChange = (0, react.useCallback)((updater) => {
477
+ const next = resolveUpdater(updater, state.columnWidths);
478
+ const clamped = {};
479
+ for (const [id, w] of Object.entries(next)) {
480
+ const col = allColumnsRef.current.find((c) => c.id === id);
481
+ clamped[id] = col ? (0, __data_grid_sizing_js.clampColumnWidth)(col, w) : w;
482
+ }
483
+ onChange((s) => ({
484
+ ...s,
485
+ columnWidths: clamped
486
+ }));
487
+ if (onColumnResize) {
488
+ for (const [id, w] of Object.entries(clamped)) if (state.columnWidths[id] !== w) onColumnResize(id, w);
655
489
  }
656
- return {
657
- widths,
658
- totalWidth
659
- };
660
490
  }, [
661
- selectionMode,
662
- state.columnWidths,
663
- visibleColumns
491
+ onChange,
492
+ onColumnResize,
493
+ state.columnWidths
664
494
  ]);
665
- const gridSizingStyle = (0, react.useMemo)(() => (0, __data_grid_sizing_js.createGridSizingStyle)(visibleColumnMetrics.widths, visibleColumnMetrics.totalWidth), [visibleColumnMetrics]);
666
- const resizeRef = (0, react.useRef)(null);
667
- const gridRef = (0, react.useRef)(null);
668
- const handleSort = (0, react.useCallback)((columnId, multi) => {
669
- const next = (0, __state_js.toggleSort)(state.sorting, columnId, multi);
495
+ const handleVisibilityChange = (0, react.useCallback)((updater) => {
496
+ const next = resolveUpdater(updater, state.columnVisibility);
670
497
  onChange((s) => ({
671
498
  ...s,
672
- sorting: next
499
+ columnVisibility: next
673
500
  }));
674
- onSortChange?.(next);
501
+ onColumnVisibilityChange?.(next);
675
502
  }, [
676
503
  onChange,
677
- onSortChange,
678
- state.sorting
504
+ onColumnVisibilityChange,
505
+ state.columnVisibility
679
506
  ]);
680
- const handleResize = (0, react.useCallback)((columnId, delta) => {
681
- const col = allColumns.find((c) => c.id === columnId);
682
- if (!col) return;
683
- if (!resizeRef.current || resizeRef.current.columnId !== columnId) {
684
- const baseWidth = visibleColumnMetrics.widths.get(columnId) ?? (0, __state_js.resolveColumnWidth)(col, state.columnWidths[columnId]);
685
- resizeRef.current = {
686
- columnId,
687
- baseWidth,
688
- baseTotalWidth: visibleColumnMetrics.totalWidth,
689
- latestWidth: baseWidth
690
- };
691
- }
692
- const newWidth = (0, __data_grid_sizing_js.clampColumnWidth)(col, resizeRef.current.baseWidth + delta);
693
- resizeRef.current.latestWidth = newWidth;
694
- if (gridRef.current) (0, __data_grid_sizing_js.applyDraggedColumnWidth)(gridRef.current, columnId, newWidth, resizeRef.current.baseTotalWidth + (newWidth - resizeRef.current.baseWidth));
507
+ const handleColumnOrderChange = (0, react.useCallback)((updater) => {
508
+ const next = resolveUpdater(updater, [...state.columnOrder]);
509
+ onChange((s) => ({
510
+ ...s,
511
+ columnOrder: next
512
+ }));
513
+ }, [onChange, state.columnOrder]);
514
+ const handleColumnPinningChange = (0, react.useCallback)((updater) => {
515
+ const next = resolveUpdater(updater, {
516
+ left: [...state.columnPinning.left],
517
+ right: [...state.columnPinning.right]
518
+ });
519
+ onChange((s) => ({
520
+ ...s,
521
+ columnPinning: {
522
+ left: next.left ?? [],
523
+ right: next.right ?? []
524
+ }
525
+ }));
526
+ }, [onChange, state.columnPinning]);
527
+ const table = (0, _tanstack_react_table.useReactTable)({
528
+ data: rows,
529
+ columns: tableColumns,
530
+ getRowId: (row) => getRowId(row),
531
+ getCoreRowModel: (0, _tanstack_react_table.getCoreRowModel)(),
532
+ state: {
533
+ sorting: tanstackSorting,
534
+ columnVisibility: state.columnVisibility,
535
+ columnSizing: state.columnWidths,
536
+ columnOrder: tanstackColumnOrder,
537
+ columnPinning: tanstackColumnPinning,
538
+ rowSelection: tanstackRowSelection
539
+ },
540
+ onSortingChange: handleSortingChange,
541
+ onColumnSizingChange: handleColumnSizingChange,
542
+ onColumnVisibilityChange: handleVisibilityChange,
543
+ onColumnOrderChange: handleColumnOrderChange,
544
+ onColumnPinningChange: handleColumnPinningChange,
545
+ columnResizeMode: "onEnd",
546
+ enableRowSelection: selectionMode !== "none",
547
+ enableMultiRowSelection: selectionMode === "multiple",
548
+ enableColumnResizing: resizable,
549
+ manualSorting: true,
550
+ manualPagination: true,
551
+ manualFiltering: true
552
+ });
553
+ const visibleColumns = (0, react.useMemo)(() => {
554
+ const colMap = new Map(allColumns.map((c) => [c.id, c]));
555
+ return table.getVisibleLeafColumns().map((c) => colMap.get(c.id)).filter(Boolean);
695
556
  }, [
696
557
  allColumns,
697
- state.columnWidths,
698
- visibleColumnMetrics
558
+ table,
559
+ state.columnOrder,
560
+ state.columnVisibility
699
561
  ]);
562
+ const rowIds = (0, react.useMemo)(() => rows.map(getRowId), [rows, getRowId]);
563
+ const [containerWidth, setContainerWidth] = (0, react.useState)(0);
700
564
  (0, react.useLayoutEffect)(() => {
701
- const r = resizeRef.current;
702
- if (r && gridRef.current) (0, __data_grid_sizing_js.applyDraggedColumnWidth)(gridRef.current, r.columnId, r.latestWidth, r.baseTotalWidth + (r.latestWidth - r.baseWidth));
703
- }, [gridSizingStyle]);
704
- const handleResizeEnd = (0, react.useCallback)(() => {
705
- const r = resizeRef.current;
706
- resizeRef.current = null;
707
- if (!r || r.latestWidth === r.baseWidth) return;
565
+ const grid = gridRef.current;
566
+ const scroller = scrollContainerRef.current;
567
+ if (!grid) return;
568
+ const update = () => {
569
+ const w = scroller?.clientWidth ?? grid.clientWidth;
570
+ if (w > 0) setContainerWidth(w);
571
+ };
572
+ update();
573
+ const observer = new ResizeObserver(update);
574
+ observer.observe(grid);
575
+ if (scroller) observer.observe(scroller);
576
+ return () => observer.disconnect();
577
+ }, []);
578
+ const columnSizingInfo = table.getState().columnSizingInfo;
579
+ const columnSizes = (0, react.useMemo)(() => {
580
+ const sizes = {};
581
+ let baseTotal = selectionMode !== "none" ? 44 : 0;
582
+ const resizingId = columnSizingInfo.isResizingColumn || null;
583
+ const deltaOffset = columnSizingInfo.deltaOffset ?? 0;
584
+ for (const col of visibleColumns) {
585
+ const baseSize = table.getColumn(col.id)?.getSize() ?? col.width ?? __data_grid_sizing_js.DEFAULT_COL_WIDTH;
586
+ const liveSize = resizingId === col.id ? (0, __data_grid_sizing_js.clampColumnWidth)(col, baseSize + deltaOffset) : baseSize;
587
+ sizes[col.id] = liveSize;
588
+ baseTotal += liveSize;
589
+ }
590
+ distributeFlexWidths(sizes, visibleColumns, containerWidth - baseTotal);
591
+ return sizes;
592
+ }, [
593
+ visibleColumns,
594
+ table,
595
+ columnSizingInfo,
596
+ state.columnWidths,
597
+ containerWidth,
598
+ selectionMode
599
+ ]);
600
+ const totalContentWidth = (0, react.useMemo)(() => {
601
+ let total = selectionMode !== "none" ? 44 : 0;
602
+ for (const col of visibleColumns) total += columnSizes[col.id] ?? 0;
603
+ return total;
604
+ }, [
605
+ visibleColumns,
606
+ columnSizes,
607
+ selectionMode
608
+ ]);
609
+ const cssVars = (0, react.useMemo)(() => {
610
+ const vars = { "--grid-total-w": `${totalContentWidth}px` };
611
+ for (const col of visibleColumns) vars[`--col-${col.id}-size`] = columnSizes[col.id];
612
+ return vars;
613
+ }, [
614
+ visibleColumns,
615
+ columnSizes,
616
+ totalContentWidth
617
+ ]);
618
+ const fireSelection = (0, react.useCallback)((next) => {
708
619
  onChange((s) => ({
709
620
  ...s,
710
- columnWidths: {
711
- ...s.columnWidths,
712
- [r.columnId]: r.latestWidth
713
- }
621
+ selection: next
714
622
  }));
715
- }, [onChange]);
716
- const handleRowClick = (0, react.useCallback)((row, rowId, event) => {
717
- if (selectionMode !== "none") {
718
- const next = (0, __state_js.toggleRowSelection)(state.selection, rowId, selectionMode, event.shiftKey, event.metaKey || event.ctrlKey, rowIds);
719
- onChange((s) => ({
720
- ...s,
721
- selection: next
722
- }));
723
- if (onSelectionChange) {
724
- const selectedRows = rows.filter((r) => next.selectedIds.has(getRowId(r)));
725
- onSelectionChange(next.selectedIds, selectedRows);
726
- }
623
+ if (onSelectionChange) {
624
+ const idSet = next.selectedIds;
625
+ onSelectionChange(idSet, rows.filter((r) => idSet.has(getRowId(r))));
727
626
  }
728
- onRowClick?.(row, rowId, event);
729
627
  }, [
730
- selectionMode,
731
628
  onChange,
732
- onRowClick,
733
629
  onSelectionChange,
734
- rowIds,
735
630
  rows,
736
- getRowId,
737
- state.selection
631
+ getRowId
738
632
  ]);
739
- const handleRowSelectionCheckboxClick = (0, react.useCallback)((row, rowId, event) => {
740
- handleRowClick(row, rowId, event);
741
- }, [handleRowClick]);
742
- const handleSelectAll = (0, react.useCallback)(() => {
743
- const allSelectedNow = rowIds.every((id) => state.selection.selectedIds.has(id));
744
- const next = allSelectedNow ? (0, __state_js.clearSelection)() : (0, __state_js.selectAll)(rowIds);
745
- const selectedRows = allSelectedNow ? [] : rows;
746
- onChange((s) => ({
747
- ...s,
748
- selection: next
633
+ const handleRowClick = (0, react.useCallback)((row, rowId, event) => {
634
+ if (selectionMode !== "none") fireSelection(nextSelection({
635
+ current: state.selection,
636
+ rowId,
637
+ mode: selectionMode,
638
+ modifiers: {
639
+ shift: event.shiftKey,
640
+ ctrl: event.metaKey || event.ctrlKey
641
+ },
642
+ allRowIds: rowIds
749
643
  }));
750
- if (onSelectionChange) onSelectionChange(next.selectedIds, [...selectedRows]);
644
+ onRowClick?.(row, rowId, event);
751
645
  }, [
752
- onChange,
646
+ selectionMode,
647
+ state.selection,
753
648
  rowIds,
754
- rows,
755
- onSelectionChange,
756
- state.selection
649
+ fireSelection,
650
+ onRowClick
651
+ ]);
652
+ const handleSelectAll = (0, react.useCallback)(() => {
653
+ fireSelection(rowIds.length > 0 && rowIds.every((id) => state.selection.selectedIds.has(id)) ? {
654
+ selectedIds: /* @__PURE__ */ new Set(),
655
+ anchorId: null
656
+ } : {
657
+ selectedIds: new Set(rowIds),
658
+ anchorId: null
659
+ });
660
+ }, [
661
+ rowIds,
662
+ state.selection.selectedIds,
663
+ fireSelection
757
664
  ]);
758
665
  const handleExportCsv = (0, react.useCallback)(() => {
666
+ if (typeof window !== "undefined" && rows.length > 0) {
667
+ const totalSuffix = totalRowCount != null && totalRowCount > rows.length ? ` of ${totalRowCount} total — load more rows first to include them` : "";
668
+ if (!window.confirm(`Export ${rows.length.toLocaleString()} loaded row${rows.length === 1 ? "" : "s"}${totalSuffix}?`)) return;
669
+ }
759
670
  (0, __state_js.exportToCsv)(rows, visibleColumns, exportFilename);
760
671
  }, [
761
672
  rows,
762
673
  visibleColumns,
763
- exportFilename
674
+ exportFilename,
675
+ totalRowCount
764
676
  ]);
765
677
  const scrollContainerRef = (0, react.useRef)(null);
766
678
  const headerScrollRef = (0, react.useRef)(null);
767
679
  const stickyChromeRef = (0, react.useRef)(null);
768
680
  const rowsClipRef = (0, react.useRef)(null);
681
+ const gridRef = (0, react.useRef)(null);
769
682
  const measureElementFn = (0, react.useCallback)((el) => el.getBoundingClientRect().height, []);
770
683
  const rowVirtualizer = (0, _tanstack_react_virtual.useVirtualizer)({
771
684
  count: rows.length,
@@ -778,104 +691,33 @@ function DataGrid(props) {
778
691
  },
779
692
  ...isDynamicRowHeight ? { measureElement: measureElementFn } : {}
780
693
  });
781
- (0, react.useLayoutEffect)(() => {
782
- const grid = gridRef.current;
783
- const stickyEl = stickyChromeRef.current;
784
- if (!grid || !stickyEl) return;
785
- const parseRgba = (raw) => {
786
- const rgbaMatch = raw.match(/rgba?\(\s*([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?\s*\)/);
787
- if (!rgbaMatch) return null;
788
- const alphaRaw = rgbaMatch[4];
789
- return [
790
- Number(rgbaMatch[1]),
791
- Number(rgbaMatch[2]),
792
- Number(rgbaMatch[3]),
793
- alphaRaw === void 0 ? 1 : Number(alphaRaw)
794
- ];
795
- };
796
- const blendOver = (base, top) => {
797
- const [tr, tg, tb, ta] = top;
798
- const [br, bg, bb, ba] = base;
799
- const outA = ta + ba * (1 - ta);
800
- if (outA === 0) return [
801
- 0,
802
- 0,
803
- 0,
804
- 0
805
- ];
806
- return [
807
- (tr * ta + br * ba * (1 - ta)) / outA,
808
- (tg * ta + bg * ba * (1 - ta)) / outA,
809
- (tb * ta + bb * ba * (1 - ta)) / outA,
810
- outA
811
- ];
812
- };
813
- const detect = () => {
814
- const layers = [];
815
- let ancestor = grid.parentElement;
816
- while (ancestor) {
817
- const parsed = parseRgba(getComputedStyle(ancestor).backgroundColor);
818
- if (parsed && parsed[3] > 0) {
819
- layers.push(parsed);
820
- if (parsed[3] >= 1) break;
821
- }
822
- ancestor = ancestor.parentElement;
823
- }
824
- if (layers.length === 0) {
825
- stickyEl.style.backgroundColor = "";
826
- return;
827
- }
828
- let result = layers[layers.length - 1];
829
- for (let i = layers.length - 2; i >= 0; i--) result = blendOver(result, layers[i]);
830
- const [r, g, b] = result;
831
- stickyEl.style.backgroundColor = `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
832
- };
833
- detect();
834
- const observer = new MutationObserver(detect);
835
- observer.observe(document.documentElement, {
836
- attributes: true,
837
- attributeFilter: ["class"]
838
- });
839
- return () => observer.disconnect();
840
- }, []);
841
694
  (0, react.useLayoutEffect)(() => {
842
695
  const gridEl = gridRef.current;
843
696
  const stickyEl = stickyChromeRef.current;
844
697
  const bodyEl = scrollContainerRef.current;
845
698
  const clipEl = rowsClipRef.current;
846
699
  if (!gridEl || !stickyEl || !bodyEl || !clipEl) return;
847
- const verticalScrollEl = fillHeight ? bodyEl : getNearestVerticalScrollElement(gridEl);
848
- let extraObservedScrollEl = null;
849
- if (verticalScrollEl instanceof HTMLElement && verticalScrollEl !== bodyEl) extraObservedScrollEl = verticalScrollEl;
850
700
  const updateClip = () => {
851
701
  const stickyRect = stickyEl.getBoundingClientRect();
852
702
  const clipRect = clipEl.getBoundingClientRect();
853
703
  const overlap = Math.max(0, stickyRect.bottom - clipRect.top);
854
- const clipValue = overlap > 0 ? `inset(${overlap}px 0 0 0)` : "";
855
- const maskValue = overlap > 0 ? `linear-gradient(to bottom, transparent 0px, transparent ${overlap}px, black ${overlap}px, black 100%)` : "";
856
- clipEl.style.clipPath = clipValue;
857
- clipEl.style.setProperty("-webkit-clip-path", clipValue);
858
- clipEl.style.maskImage = maskValue;
859
- clipEl.style.setProperty("-webkit-mask-image", maskValue);
704
+ clipEl.style.setProperty("--data-grid-sticky-overlap", `${overlap}px`);
860
705
  };
861
706
  updateClip();
862
707
  bodyEl.addEventListener("scroll", updateClip);
863
- if (verticalScrollEl === window) window.addEventListener("scroll", updateClip, true);
864
- else if (extraObservedScrollEl) extraObservedScrollEl.addEventListener("scroll", updateClip);
708
+ window.addEventListener("scroll", updateClip, true);
865
709
  window.addEventListener("resize", updateClip);
866
- const ro = new ResizeObserver(updateClip);
867
- ro.observe(gridEl);
868
- ro.observe(stickyEl);
869
- ro.observe(bodyEl);
870
- if (extraObservedScrollEl) ro.observe(extraObservedScrollEl);
710
+ const observer = new ResizeObserver(updateClip);
711
+ observer.observe(gridEl);
712
+ observer.observe(stickyEl);
713
+ observer.observe(bodyEl);
871
714
  return () => {
872
715
  bodyEl.removeEventListener("scroll", updateClip);
873
- if (verticalScrollEl === window) window.removeEventListener("scroll", updateClip, true);
874
- else if (extraObservedScrollEl) extraObservedScrollEl.removeEventListener("scroll", updateClip);
716
+ window.removeEventListener("scroll", updateClip, true);
875
717
  window.removeEventListener("resize", updateClip);
876
- ro.disconnect();
718
+ observer.disconnect();
877
719
  };
878
- }, [fillHeight]);
720
+ }, []);
879
721
  const handleBodyScroll = (0, react.useCallback)(() => {
880
722
  const body = scrollContainerRef.current;
881
723
  const header = headerScrollRef.current;
@@ -916,21 +758,28 @@ function DataGrid(props) {
916
758
  const allSelected = rowIds.length > 0 && rowIds.every((id) => state.selection.selectedIds.has(id));
917
759
  const someSelected = !allSelected && rowIds.some((id) => state.selection.selectedIds.has(id));
918
760
  const infiniteScrollRootRef = paginationMode === "infinite" && (fillHeight || maxHeight != null) ? scrollContainerRef : void 0;
761
+ const headers = (0, react.useMemo)(() => table.getHeaderGroups()[0]?.headers.filter((h) => visibleColumns.some((c) => c.id === h.column.id)) ?? [], [table, visibleColumns]);
762
+ const headerByColId = (0, react.useMemo)(() => {
763
+ const m = /* @__PURE__ */ new Map();
764
+ for (const h of headers) m.set(h.column.id, h);
765
+ return m;
766
+ }, [headers]);
767
+ const isBounded = fillHeight || maxHeight != null;
919
768
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
920
769
  ref: gridRef,
921
- className: (0, _stackframe_stack_ui.cn)("flex w-full min-w-0 max-w-full flex-col bg-transparent rounded-[calc(var(--radius)*2)]", fillHeight ? "min-h-0 h-full" : "min-h-0 h-auto", className),
770
+ className: (0, _stackframe_stack_ui.cn)("isolate flex w-full min-w-0 max-w-full flex-col bg-transparent rounded-[calc(var(--radius)*2)]", fillHeight ? "min-h-0 h-full" : "min-h-0 h-auto", isBounded && "overflow-hidden", className),
922
771
  style: maxHeight != null ? {
923
- ...gridSizingStyle,
772
+ ...cssVars,
924
773
  maxHeight
925
- } : gridSizingStyle,
774
+ } : cssVars,
926
775
  role: "grid",
927
776
  "aria-rowcount": totalRowCount ?? rows.length,
928
777
  "aria-colcount": visibleColumns.length,
929
778
  children: [
930
779
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
931
780
  ref: stickyChromeRef,
932
- className: "sticky z-20 w-full min-w-0 shrink-0 rounded-t-[calc(var(--radius)*2)] bg-background",
933
- style: { top: stickyTop ?? "var(--data-grid-sticky-top, 0px)" },
781
+ className: "sticky z-30 w-full min-w-0 shrink-0 overflow-visible rounded-t-[calc(var(--radius)*2)] bg-white/90 dark:bg-background/60 backdrop-blur-xl",
782
+ style: { top: stickyTop ?? (maxHeight != null ? 0 : "var(--data-grid-sticky-top, 0px)") },
934
783
  children: [toolbar !== false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
935
784
  className: "relative bg-transparent",
936
785
  children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__data_grid_toolbar_js.DataGridToolbar, {
@@ -949,7 +798,7 @@ function DataGrid(props) {
949
798
  className: "flex",
950
799
  style: {
951
800
  height: headerHeight,
952
- minWidth: visibleColumnMetrics.totalWidth
801
+ minWidth: totalContentWidth
953
802
  },
954
803
  role: "row",
955
804
  children: [selectionMode !== "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -959,30 +808,37 @@ function DataGrid(props) {
959
808
  checked: allSelected,
960
809
  indeterminate: someSelected,
961
810
  onChange: handleSelectAll,
962
- ariaLabel: "Select all rows"
811
+ ariaLabel: "Select all rows on this page",
812
+ title: "Select all rows on this page"
963
813
  })
964
- }), visibleColumns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeaderCell, {
965
- col,
966
- isSorted: (0, __state_js.getSortDirection)(state.sorting, col.id),
967
- sortIndex: (0, __state_js.getSortIndex)(state.sorting, col.id),
968
- resizable,
969
- onSort: handleSort,
970
- onResize: handleResize,
971
- onResizeEnd: handleResizeEnd
972
- }, col.id))]
814
+ }), visibleColumns.map((col) => {
815
+ const header = headerByColId.get(col.id);
816
+ if (!header) return null;
817
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HeaderCell, {
818
+ header,
819
+ col,
820
+ resizable
821
+ }, col.id);
822
+ })]
973
823
  })
974
824
  })]
975
825
  })]
976
826
  }),
977
827
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
978
828
  ref: scrollContainerRef,
979
- className: (0, _stackframe_stack_ui.cn)("w-full min-w-0 overflow-auto bg-transparent", fillHeight ? "min-h-0 flex-1" : "flex-none", "[&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar]:h-1.5", "[&::-webkit-scrollbar-track]:bg-transparent", "[&::-webkit-scrollbar-thumb]:bg-foreground/[0.08] [&::-webkit-scrollbar-thumb]:rounded-full", "[&::-webkit-scrollbar-thumb]:hover:bg-foreground/[0.15]"),
829
+ className: (0, _stackframe_stack_ui.cn)("relative z-0 w-full min-w-0 overflow-auto bg-transparent", isBounded ? "min-h-0 flex-1" : "flex-none", "[&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar]:h-1.5", "[&::-webkit-scrollbar-track]:bg-transparent", "[&::-webkit-scrollbar-thumb]:bg-foreground/[0.08] [&::-webkit-scrollbar-thumb]:rounded-full", "[&::-webkit-scrollbar-thumb]:hover:bg-foreground/[0.15]"),
980
830
  onScroll: handleBodyScroll,
981
831
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
982
832
  ref: rowsClipRef,
833
+ "data-data-grid-rows-clip": "",
834
+ className: "relative z-0",
835
+ style: {
836
+ minWidth: totalContentWidth,
837
+ clipPath: "inset(var(--data-grid-sticky-overlap, 0px) 0 0 0)"
838
+ },
983
839
  children: [
984
840
  isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
985
- style: { minWidth: visibleColumnMetrics.totalWidth },
841
+ style: { minWidth: totalContentWidth },
986
842
  children: loadingState ?? Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkeletonRow, {
987
843
  columns: visibleColumns,
988
844
  height: estimatedRowHeight,
@@ -991,14 +847,14 @@ function DataGrid(props) {
991
847
  }),
992
848
  !isLoading && rows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
993
849
  className: "flex items-center justify-center py-16 text-sm text-muted-foreground",
994
- style: { minWidth: visibleColumnMetrics.totalWidth },
850
+ style: { minWidth: totalContentWidth },
995
851
  children: emptyState ?? strings.noData
996
852
  }),
997
853
  !isLoading && rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
998
854
  style: {
999
855
  height: rowVirtualizer.getTotalSize(),
1000
856
  width: "100%",
1001
- minWidth: visibleColumnMetrics.totalWidth,
857
+ minWidth: totalContentWidth,
1002
858
  position: "relative"
1003
859
  },
1004
860
  children: rowVirtualizer.getVirtualItems().map((virtualRow) => {
@@ -1015,12 +871,10 @@ function DataGrid(props) {
1015
871
  transform: `translateY(${virtualRow.start}px)`
1016
872
  },
1017
873
  onClick: (e) => {
1018
- if (shouldIgnoreRowClick(e)) return;
1019
- handleRowClick(row, rowId, e);
874
+ if (!shouldIgnoreRowClick(e)) handleRowClick(row, rowId, e);
1020
875
  },
1021
876
  onDoubleClick: (e) => {
1022
- if (shouldIgnoreRowClick(e)) return;
1023
- onRowDoubleClick?.(row, rowId, e);
877
+ if (!shouldIgnoreRowClick(e)) onRowDoubleClick?.(row, rowId, e);
1024
878
  },
1025
879
  role: "row",
1026
880
  "aria-rowindex": virtualRow.index + 2,
@@ -1032,7 +886,7 @@ function DataGrid(props) {
1032
886
  style: { width: 44 },
1033
887
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectionCheckbox, {
1034
888
  checked: isSelected,
1035
- onChange: (event) => handleRowSelectionCheckboxClick(row, rowId, event),
889
+ onChange: (event) => handleRowClick(row, rowId, event),
1036
890
  ariaLabel: `Select row ${rowId}`
1037
891
  })
1038
892
  }), visibleColumns.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DataCell, {
@@ -1056,7 +910,7 @@ function DataGrid(props) {
1056
910
  })
1057
911
  }),
1058
912
  footer !== false && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1059
- className: "relative z-10 shrink-0 bg-transparent",
913
+ className: "sticky bottom-0 z-30 shrink-0 overflow-hidden rounded-b-[calc(var(--radius)*2)] bg-white/90 dark:bg-background/60 backdrop-blur-xl",
1060
914
  children: [footer ? footer(footerCtx) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultFooter, {
1061
915
  ctx: footerCtx,
1062
916
  pagination: paginationMode,