@stackframe/dashboard-ui-components 2.8.86 → 2.8.89

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 (64) hide show
  1. package/dist/components/analytics-chart/analytics-chart-pie.js +3 -3
  2. package/dist/components/analytics-chart/analytics-chart-pie.js.map +1 -1
  3. package/dist/components/data-grid/data-grid-sizing.d.ts +2 -1
  4. package/dist/components/data-grid/data-grid-sizing.d.ts.map +1 -1
  5. package/dist/components/data-grid/data-grid-sizing.js +33 -4
  6. package/dist/components/data-grid/data-grid-sizing.js.map +1 -1
  7. package/dist/components/data-grid/data-grid-toolbar.js +18 -15
  8. package/dist/components/data-grid/data-grid-toolbar.js.map +1 -1
  9. package/dist/components/data-grid/data-grid.d.ts +35 -1
  10. package/dist/components/data-grid/data-grid.d.ts.map +1 -1
  11. package/dist/components/data-grid/data-grid.js +329 -127
  12. package/dist/components/data-grid/data-grid.js.map +1 -1
  13. package/dist/components/data-grid/data-grid.test.d.ts +1 -0
  14. package/dist/components/data-grid/data-grid.test.js +215 -0
  15. package/dist/components/data-grid/data-grid.test.js.map +1 -0
  16. package/dist/components/data-grid/index.d.ts +3 -2
  17. package/dist/components/data-grid/index.js +13 -0
  18. package/dist/components/data-grid/state.d.ts.map +1 -1
  19. package/dist/components/data-grid/state.js +24 -7
  20. package/dist/components/data-grid/state.js.map +1 -1
  21. package/dist/components/data-grid/types.d.ts +34 -3
  22. package/dist/components/data-grid/types.d.ts.map +1 -1
  23. package/dist/components/data-grid/use-data-source.d.ts +6 -0
  24. package/dist/components/data-grid/use-data-source.d.ts.map +1 -1
  25. package/dist/components/data-grid/use-data-source.js +10 -2
  26. package/dist/components/data-grid/use-data-source.js.map +1 -1
  27. package/dist/components/tabs.d.ts +5 -1
  28. package/dist/components/tabs.d.ts.map +1 -1
  29. package/dist/components/tabs.js +40 -27
  30. package/dist/components/tabs.js.map +1 -1
  31. package/dist/dashboard-ui-components.global.js +672 -368
  32. package/dist/dashboard-ui-components.global.js.map +4 -4
  33. package/dist/esm/components/analytics-chart/analytics-chart-pie.js +3 -3
  34. package/dist/esm/components/analytics-chart/analytics-chart-pie.js.map +1 -1
  35. package/dist/esm/components/data-grid/data-grid-sizing.d.ts +2 -1
  36. package/dist/esm/components/data-grid/data-grid-sizing.d.ts.map +1 -1
  37. package/dist/esm/components/data-grid/data-grid-sizing.js +33 -5
  38. package/dist/esm/components/data-grid/data-grid-sizing.js.map +1 -1
  39. package/dist/esm/components/data-grid/data-grid-toolbar.js +18 -15
  40. package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -1
  41. package/dist/esm/components/data-grid/data-grid.d.ts +35 -1
  42. package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -1
  43. package/dist/esm/components/data-grid/data-grid.js +329 -128
  44. package/dist/esm/components/data-grid/data-grid.js.map +1 -1
  45. package/dist/esm/components/data-grid/data-grid.test.d.ts +1 -0
  46. package/dist/esm/components/data-grid/data-grid.test.js +215 -0
  47. package/dist/esm/components/data-grid/data-grid.test.js.map +1 -0
  48. package/dist/esm/components/data-grid/index.d.ts +3 -2
  49. package/dist/esm/components/data-grid/index.js +3 -2
  50. package/dist/esm/components/data-grid/state.d.ts.map +1 -1
  51. package/dist/esm/components/data-grid/state.js +24 -7
  52. package/dist/esm/components/data-grid/state.js.map +1 -1
  53. package/dist/esm/components/data-grid/types.d.ts +34 -3
  54. package/dist/esm/components/data-grid/types.d.ts.map +1 -1
  55. package/dist/esm/components/data-grid/use-data-source.d.ts +6 -0
  56. package/dist/esm/components/data-grid/use-data-source.d.ts.map +1 -1
  57. package/dist/esm/components/data-grid/use-data-source.js +10 -2
  58. package/dist/esm/components/data-grid/use-data-source.js.map +1 -1
  59. package/dist/esm/components/tabs.d.ts +5 -1
  60. package/dist/esm/components/tabs.d.ts.map +1 -1
  61. package/dist/esm/components/tabs.js +40 -27
  62. package/dist/esm/components/tabs.js.map +1 -1
  63. package/dist/index.d.ts +3 -2
  64. package/package.json +4 -4
@@ -6,10 +6,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
7
7
  import { clearSelection, exportToCsv, formatGridDate, getSortDirection, getSortIndex, isColumnVisible, resolveColumnValue, resolveColumnWidth, selectAll, toggleRowSelection, toggleSort } from "./state.js";
8
8
  import { resolveDataGridStrings } from "./strings.js";
9
+ import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
9
10
  import { useVirtualizer } from "@tanstack/react-virtual";
10
11
  import { DesignSkeleton } from "../skeleton.js";
11
- import { DataGridToolbar } from "./data-grid-toolbar.js";
12
12
  import { applyDraggedColumnWidth, clampColumnWidth, createGridSizingStyle, getColumnSizingStyle } from "./data-grid-sizing.js";
13
+ import { DataGridToolbar } from "./data-grid-toolbar.js";
13
14
 
14
15
  //#region src/components/data-grid/data-grid.tsx
15
16
  function ResizeHandle({ onResize, onResizeEnd }) {
@@ -69,6 +70,37 @@ function ResizeHandle({ onResize, onResizeEnd }) {
69
70
  onPointerDown
70
71
  });
71
72
  }
73
+ function getNearestVerticalScrollElement(element) {
74
+ let current = element?.parentElement ?? null;
75
+ while (current) {
76
+ const style = window.getComputedStyle(current);
77
+ const overflowY = style.overflowY === "visible" ? style.overflow : style.overflowY;
78
+ if ((overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && current.scrollHeight > current.clientHeight + 1) return current;
79
+ current = current.parentElement;
80
+ }
81
+ return window;
82
+ }
83
+ function getEventTargetElement(target) {
84
+ if (target instanceof Element) return target;
85
+ if (target instanceof Node) return target.parentElement;
86
+ return null;
87
+ }
88
+ function isDataGridInteractiveRowClickTarget(target) {
89
+ return getEventTargetElement(target)?.closest([
90
+ "a",
91
+ "button",
92
+ "input",
93
+ "select",
94
+ "textarea",
95
+ "[role=\"button\"]",
96
+ "[role=\"menuitem\"]",
97
+ "[contenteditable]:not([contenteditable=\"false\"])",
98
+ "[data-no-row-click]"
99
+ ].join(",")) != null;
100
+ }
101
+ function shouldIgnoreRowClick(event) {
102
+ return event.defaultPrevented || isDataGridInteractiveRowClickTarget(event.target);
103
+ }
72
104
  function HeaderCell({ col, isSorted, sortIndex, resizable, onSort, onResize, onResizeEnd }) {
73
105
  const ctx = {
74
106
  columnId: col.id,
@@ -79,7 +111,7 @@ function HeaderCell({ col, isSorted, sortIndex, resizable, onSort, onResize, onR
79
111
  const label = typeof col.header === "function" ? col.header(ctx) : col.header;
80
112
  const sortable = col.sortable !== false;
81
113
  return /* @__PURE__ */ jsxs("div", {
82
- className: cn("group/header relative flex items-center gap-1.5 px-3 select-none bg-transparent", "border-r border-black/[0.04] dark:border-white/[0.04] last:border-r-0", sortable && "cursor-pointer"),
114
+ className: 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"),
83
115
  style: getColumnSizingStyle(col),
84
116
  "data-col-id": col.id,
85
117
  onClick: (e) => sortable && onSort(col.id, e.metaKey || e.ctrlKey),
@@ -87,7 +119,7 @@ function HeaderCell({ col, isSorted, sortIndex, resizable, onSort, onResize, onR
87
119
  "aria-sort": isSorted === "asc" ? "ascending" : isSorted === "desc" ? "descending" : "none",
88
120
  children: [
89
121
  /* @__PURE__ */ jsx("span", {
90
- className: cn("flex-1 truncate text-xs font-semibold uppercase tracking-wider text-muted-foreground", col.align === "center" && "text-center", col.align === "right" && "text-right"),
122
+ className: 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"),
91
123
  children: label
92
124
  }),
93
125
  isSorted && /* @__PURE__ */ jsxs("span", {
@@ -137,8 +169,9 @@ function DataCell({ col, row, rowId, rowIndex, isSelected, dateDisplay }) {
137
169
  else if (isDateCol) content = renderDateCell(value, dateDisplay, col);
138
170
  else content = formatCellValue(value);
139
171
  const hasCellClick = col.onCellClick || col.onCellDoubleClick;
172
+ const isWrap = col.cellOverflow === "wrap";
140
173
  return /* @__PURE__ */ jsx("div", {
141
- className: cn("flex items-center px-3 truncate bg-transparent", "border-r border-black/[0.04] dark:border-white/[0.04] last:border-r-0", "text-sm text-foreground", col.align === "center" && "justify-center", col.align === "right" && "justify-end", hasCellClick && "cursor-pointer"),
174
+ className: 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"),
142
175
  style: getColumnSizingStyle(col),
143
176
  "data-col-id": col.id,
144
177
  role: "gridcell",
@@ -150,7 +183,10 @@ function DataCell({ col, row, rowId, rowIndex, isSelected, dateDisplay }) {
150
183
  e.stopPropagation();
151
184
  col.onCellDoubleClick(ctx, e);
152
185
  } : void 0,
153
- children: content
186
+ children: /* @__PURE__ */ jsx("div", {
187
+ className: cn("min-w-0", isWrap ? "flex-1" : "truncate"),
188
+ children: content
189
+ })
154
190
  });
155
191
  }
156
192
  function formatCellValue(value) {
@@ -190,6 +226,11 @@ function renderDateCell(value, dateDisplay, col) {
190
226
  children: display
191
227
  });
192
228
  }
229
+ function hashStringToInt(value) {
230
+ let hash = 0;
231
+ for (let i = 0; i < value.length; i++) hash = (hash << 5) - hash + value.charCodeAt(i) | 0;
232
+ return Math.abs(hash);
233
+ }
193
234
  function SkeletonRow({ columns, height, showCheckbox }) {
194
235
  return /* @__PURE__ */ jsxs("div", {
195
236
  className: "flex",
@@ -204,7 +245,7 @@ function SkeletonRow({ columns, height, showCheckbox }) {
204
245
  style: getColumnSizingStyle(col),
205
246
  children: /* @__PURE__ */ jsx(DesignSkeleton, {
206
247
  className: "h-3.5 rounded-md",
207
- style: { width: `${40 + Math.random() * 40}%` }
248
+ style: { width: `${40 + hashStringToInt(col.id) % 40}%` }
208
249
  })
209
250
  }, col.id))]
210
251
  });
@@ -226,17 +267,21 @@ function SelectionCheckbox({ checked, indeterminate, onChange, ariaLabel }) {
226
267
  })
227
268
  });
228
269
  }
229
- function InfiniteScrollSentinel({ onIntersect, isLoading, strings }) {
270
+ const NOOP = () => {};
271
+ function InfiniteScrollSentinel({ onIntersect, isLoading, rootRef, strings }) {
230
272
  const ref = useRef(null);
231
273
  useEffect(() => {
232
274
  const el = ref.current;
233
275
  if (!el) return;
234
276
  const observer = new IntersectionObserver((entries) => {
235
277
  if (entries[0]?.isIntersecting) onIntersect();
236
- }, { rootMargin: "200px" });
278
+ }, {
279
+ root: rootRef?.current ?? null,
280
+ rootMargin: "200px"
281
+ });
237
282
  observer.observe(el);
238
283
  return () => observer.disconnect();
239
- }, [onIntersect]);
284
+ }, [onIntersect, rootRef]);
240
285
  return /* @__PURE__ */ jsx("div", {
241
286
  ref,
242
287
  className: "flex items-center justify-center py-4",
@@ -513,6 +558,39 @@ function DefaultFooter({ ctx, pagination, onChange }) {
513
558
  * and footer all call it for you. You do not need to wire any of this
514
559
  * manually.
515
560
  *
561
+ * ## Cell overflow and dynamic row heights
562
+ *
563
+ * By default every cell truncates its content with an ellipsis
564
+ * (`cellOverflow: "truncate"`). For columns whose content should wrap
565
+ * — badge lists, multi-line text, permission chips — set
566
+ * `cellOverflow: "wrap"` on the column definition.
567
+ *
568
+ * To let rows grow to fit their tallest cell, set `rowHeight="auto"`
569
+ * on the grid. The virtualizer will measure each row after render and
570
+ * adjust scroll positions accordingly. Pair with `estimatedRowHeight`
571
+ * (default 44) for better scroll-position estimates before measurement.
572
+ *
573
+ * ```tsx
574
+ * // Columns: UUIDs truncate, auth-method badges wrap
575
+ * const columns = [
576
+ * { id: "userId", header: "User ID", width: 130 }, // default truncate
577
+ * { id: "auth", header: "Auth methods", width: 150, cellOverflow: "wrap",
578
+ * renderCell: ({ row }) => (
579
+ * <div className="flex flex-wrap gap-1">
580
+ * {row.authTypes.map((t) => <Badge key={t}>{t}</Badge>)}
581
+ * </div>
582
+ * ),
583
+ * },
584
+ * ];
585
+ *
586
+ * <DataGrid columns={columns} rowHeight="auto" estimatedRowHeight={48} ... />
587
+ * ```
588
+ *
589
+ * With a fixed numeric `rowHeight` (the default), `cellOverflow: "wrap"`
590
+ * still lets content wrap within the row, but anything exceeding the
591
+ * fixed height is clipped. This is useful when you want controlled
592
+ * wrapping without variable row heights.
593
+ *
516
594
  * ## Height and scrolling
517
595
  *
518
596
  * DataGrid is NOT a card. It has no border, rounded corners, or shadow of
@@ -553,7 +631,10 @@ function DefaultFooter({ ctx, pagination, onChange }) {
553
631
  * toggle for `date` / `dateTime` columns.
554
632
  */
555
633
  function DataGrid(props) {
556
- const { columns: allColumns, rows, getRowId, totalRowCount, isLoading = false, isRefetching = false, hasMore = false, isLoadingMore = false, onLoadMore, state, onChange, paginationMode = "paginated", selectionMode = "none", resizable = true, rowHeight = 44, headerHeight = 44, overscan = 5, maxHeight, toolbar, toolbarExtra, emptyState, loadingState, footer, footerExtra, exportFilename = "export", strings: stringsOverride, className, onRowClick, onRowDoubleClick, onSelectionChange, onSortChange } = props;
634
+ 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;
635
+ const isDynamicRowHeight = rowHeightProp === "auto";
636
+ const fixedRowHeight = isDynamicRowHeight ? void 0 : rowHeightProp;
637
+ const estimatedRowHeight = estimatedRowHeightProp ?? fixedRowHeight ?? 44;
557
638
  const strings = useMemo(() => resolveDataGridStrings(stringsOverride), [stringsOverride]);
558
639
  const visibleColumns = useMemo(() => (state.columnOrder.length > 0 ? state.columnOrder.map((id) => allColumns.find((c) => c.id === id)).filter(Boolean) : allColumns).filter((col) => isColumnVisible(col.id, state.columnVisibility)), [
559
640
  allColumns,
@@ -582,15 +663,17 @@ function DataGrid(props) {
582
663
  const resizeRef = useRef(null);
583
664
  const gridRef = useRef(null);
584
665
  const handleSort = useCallback((columnId, multi) => {
585
- onChange((s) => {
586
- const next = toggleSort(s.sorting, columnId, multi);
587
- onSortChange?.(next);
588
- return {
589
- ...s,
590
- sorting: next
591
- };
592
- });
593
- }, [onChange, onSortChange]);
666
+ const next = toggleSort(state.sorting, columnId, multi);
667
+ onChange((s) => ({
668
+ ...s,
669
+ sorting: next
670
+ }));
671
+ onSortChange?.(next);
672
+ }, [
673
+ onChange,
674
+ onSortChange,
675
+ state.sorting
676
+ ]);
594
677
  const handleResize = useCallback((columnId, delta) => {
595
678
  const col = allColumns.find((c) => c.id === columnId);
596
679
  if (!col) return;
@@ -628,17 +711,17 @@ function DataGrid(props) {
628
711
  }));
629
712
  }, [onChange]);
630
713
  const handleRowClick = useCallback((row, rowId, event) => {
631
- if (selectionMode !== "none") onChange((s) => {
632
- const next = toggleRowSelection(s.selection, rowId, selectionMode, event.shiftKey, event.metaKey || event.ctrlKey, rowIds);
714
+ if (selectionMode !== "none") {
715
+ const next = toggleRowSelection(state.selection, rowId, selectionMode, event.shiftKey, event.metaKey || event.ctrlKey, rowIds);
716
+ onChange((s) => ({
717
+ ...s,
718
+ selection: next
719
+ }));
633
720
  if (onSelectionChange) {
634
721
  const selectedRows = rows.filter((r) => next.selectedIds.has(getRowId(r)));
635
- setTimeout(() => onSelectionChange(next.selectedIds, selectedRows), 0);
722
+ onSelectionChange(next.selectedIds, selectedRows);
636
723
  }
637
- return {
638
- ...s,
639
- selection: next
640
- };
641
- });
724
+ }
642
725
  onRowClick?.(row, rowId, event);
643
726
  }, [
644
727
  selectionMode,
@@ -647,29 +730,27 @@ function DataGrid(props) {
647
730
  onSelectionChange,
648
731
  rowIds,
649
732
  rows,
650
- getRowId
733
+ getRowId,
734
+ state.selection
651
735
  ]);
652
736
  const handleRowSelectionCheckboxClick = useCallback((row, rowId, event) => {
653
737
  handleRowClick(row, rowId, event);
654
738
  }, [handleRowClick]);
655
739
  const handleSelectAll = useCallback(() => {
656
- onChange((s) => {
657
- const allSelected = rowIds.every((id) => s.selection.selectedIds.has(id));
658
- const next = allSelected ? clearSelection() : selectAll(rowIds);
659
- if (onSelectionChange) {
660
- const selectedRows = allSelected ? [] : rows;
661
- setTimeout(() => onSelectionChange(next.selectedIds, [...selectedRows]), 0);
662
- }
663
- return {
664
- ...s,
665
- selection: next
666
- };
667
- });
740
+ const allSelectedNow = rowIds.every((id) => state.selection.selectedIds.has(id));
741
+ const next = allSelectedNow ? clearSelection() : selectAll(rowIds);
742
+ const selectedRows = allSelectedNow ? [] : rows;
743
+ onChange((s) => ({
744
+ ...s,
745
+ selection: next
746
+ }));
747
+ if (onSelectionChange) onSelectionChange(next.selectedIds, [...selectedRows]);
668
748
  }, [
669
749
  onChange,
670
750
  rowIds,
671
751
  rows,
672
- onSelectionChange
752
+ onSelectionChange,
753
+ state.selection
673
754
  ]);
674
755
  const handleExportCsv = useCallback(() => {
675
756
  exportToCsv(rows, visibleColumns, exportFilename);
@@ -680,12 +761,118 @@ function DataGrid(props) {
680
761
  ]);
681
762
  const scrollContainerRef = useRef(null);
682
763
  const headerScrollRef = useRef(null);
764
+ const stickyChromeRef = useRef(null);
765
+ const rowsClipRef = useRef(null);
766
+ const measureElementFn = useCallback((el) => el.getBoundingClientRect().height, []);
683
767
  const rowVirtualizer = useVirtualizer({
684
768
  count: rows.length,
685
769
  getScrollElement: () => scrollContainerRef.current,
686
- estimateSize: () => rowHeight,
687
- overscan
770
+ estimateSize: () => estimatedRowHeight,
771
+ overscan,
772
+ getItemKey: (index) => {
773
+ const row = rows[index];
774
+ return row != null ? String(getRowId(row)) : index;
775
+ },
776
+ ...isDynamicRowHeight ? { measureElement: measureElementFn } : {}
688
777
  });
778
+ useLayoutEffect(() => {
779
+ const grid = gridRef.current;
780
+ const stickyEl = stickyChromeRef.current;
781
+ if (!grid || !stickyEl) return;
782
+ const parseRgba = (raw) => {
783
+ const rgbaMatch = raw.match(/rgba?\(\s*([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?\s*\)/);
784
+ if (!rgbaMatch) return null;
785
+ const alphaRaw = rgbaMatch[4];
786
+ return [
787
+ Number(rgbaMatch[1]),
788
+ Number(rgbaMatch[2]),
789
+ Number(rgbaMatch[3]),
790
+ alphaRaw === void 0 ? 1 : Number(alphaRaw)
791
+ ];
792
+ };
793
+ const blendOver = (base, top) => {
794
+ const [tr, tg, tb, ta] = top;
795
+ const [br, bg, bb, ba] = base;
796
+ const outA = ta + ba * (1 - ta);
797
+ if (outA === 0) return [
798
+ 0,
799
+ 0,
800
+ 0,
801
+ 0
802
+ ];
803
+ return [
804
+ (tr * ta + br * ba * (1 - ta)) / outA,
805
+ (tg * ta + bg * ba * (1 - ta)) / outA,
806
+ (tb * ta + bb * ba * (1 - ta)) / outA,
807
+ outA
808
+ ];
809
+ };
810
+ const detect = () => {
811
+ const layers = [];
812
+ let ancestor = grid.parentElement;
813
+ while (ancestor) {
814
+ const parsed = parseRgba(getComputedStyle(ancestor).backgroundColor);
815
+ if (parsed && parsed[3] > 0) {
816
+ layers.push(parsed);
817
+ if (parsed[3] >= 1) break;
818
+ }
819
+ ancestor = ancestor.parentElement;
820
+ }
821
+ if (layers.length === 0) {
822
+ stickyEl.style.backgroundColor = "";
823
+ return;
824
+ }
825
+ let result = layers[layers.length - 1];
826
+ for (let i = layers.length - 2; i >= 0; i--) result = blendOver(result, layers[i]);
827
+ const [r, g, b] = result;
828
+ stickyEl.style.backgroundColor = `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;
829
+ };
830
+ detect();
831
+ const observer = new MutationObserver(detect);
832
+ observer.observe(document.documentElement, {
833
+ attributes: true,
834
+ attributeFilter: ["class"]
835
+ });
836
+ return () => observer.disconnect();
837
+ }, []);
838
+ useLayoutEffect(() => {
839
+ const gridEl = gridRef.current;
840
+ const stickyEl = stickyChromeRef.current;
841
+ const bodyEl = scrollContainerRef.current;
842
+ const clipEl = rowsClipRef.current;
843
+ if (!gridEl || !stickyEl || !bodyEl || !clipEl) return;
844
+ const verticalScrollEl = fillHeight ? bodyEl : getNearestVerticalScrollElement(gridEl);
845
+ let extraObservedScrollEl = null;
846
+ if (verticalScrollEl instanceof HTMLElement && verticalScrollEl !== bodyEl) extraObservedScrollEl = verticalScrollEl;
847
+ const updateClip = () => {
848
+ const stickyRect = stickyEl.getBoundingClientRect();
849
+ const clipRect = clipEl.getBoundingClientRect();
850
+ const overlap = Math.max(0, stickyRect.bottom - clipRect.top);
851
+ const clipValue = overlap > 0 ? `inset(${overlap}px 0 0 0)` : "";
852
+ const maskValue = overlap > 0 ? `linear-gradient(to bottom, transparent 0px, transparent ${overlap}px, black ${overlap}px, black 100%)` : "";
853
+ clipEl.style.clipPath = clipValue;
854
+ clipEl.style.setProperty("-webkit-clip-path", clipValue);
855
+ clipEl.style.maskImage = maskValue;
856
+ clipEl.style.setProperty("-webkit-mask-image", maskValue);
857
+ };
858
+ updateClip();
859
+ bodyEl.addEventListener("scroll", updateClip);
860
+ if (verticalScrollEl === window) window.addEventListener("scroll", updateClip, true);
861
+ else if (extraObservedScrollEl) extraObservedScrollEl.addEventListener("scroll", updateClip);
862
+ window.addEventListener("resize", updateClip);
863
+ const ro = new ResizeObserver(updateClip);
864
+ ro.observe(gridEl);
865
+ ro.observe(stickyEl);
866
+ ro.observe(bodyEl);
867
+ if (extraObservedScrollEl) ro.observe(extraObservedScrollEl);
868
+ return () => {
869
+ bodyEl.removeEventListener("scroll", updateClip);
870
+ if (verticalScrollEl === window) window.removeEventListener("scroll", updateClip, true);
871
+ else if (extraObservedScrollEl) extraObservedScrollEl.removeEventListener("scroll", updateClip);
872
+ window.removeEventListener("resize", updateClip);
873
+ ro.disconnect();
874
+ };
875
+ }, [fillHeight]);
689
876
  const handleBodyScroll = useCallback(() => {
690
877
  const body = scrollContainerRef.current;
691
878
  const header = headerScrollRef.current;
@@ -725,9 +912,10 @@ function DataGrid(props) {
725
912
  ]);
726
913
  const allSelected = rowIds.length > 0 && rowIds.every((id) => state.selection.selectedIds.has(id));
727
914
  const someSelected = !allSelected && rowIds.some((id) => state.selection.selectedIds.has(id));
915
+ const infiniteScrollRootRef = paginationMode === "infinite" && (fillHeight || maxHeight != null) ? scrollContainerRef : void 0;
728
916
  return /* @__PURE__ */ jsxs("div", {
729
917
  ref: gridRef,
730
- className: cn("flex flex-col h-full min-h-0 bg-transparent", className),
918
+ className: 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),
731
919
  style: maxHeight != null ? {
732
920
  ...gridSizingStyle,
733
921
  maxHeight
@@ -736,23 +924,24 @@ function DataGrid(props) {
736
924
  "aria-rowcount": totalRowCount ?? rows.length,
737
925
  "aria-colcount": visibleColumns.length,
738
926
  children: [
739
- toolbar !== false && /* @__PURE__ */ jsx("div", {
740
- className: "relative shrink-0 bg-transparent",
741
- children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DataGridToolbar, {
742
- ctx: toolbarCtx,
743
- extra: typeof toolbarExtra === "function" ? toolbarExtra(toolbarCtx) : toolbarExtra
744
- })
745
- }),
746
927
  /* @__PURE__ */ jsxs("div", {
747
- className: "relative flex min-h-0 flex-1 flex-col",
748
- children: [
749
- isRefetching && /* @__PURE__ */ jsx("div", {
928
+ ref: stickyChromeRef,
929
+ className: "sticky z-20 w-full min-w-0 shrink-0 rounded-t-[calc(var(--radius)*2)] bg-background",
930
+ style: { top: stickyTop ?? "var(--data-grid-sticky-top, 0px)" },
931
+ children: [toolbar !== false && /* @__PURE__ */ jsx("div", {
932
+ className: "relative bg-transparent",
933
+ children: toolbar ? toolbar(toolbarCtx) : /* @__PURE__ */ jsx(DataGridToolbar, {
934
+ ctx: toolbarCtx,
935
+ extra: typeof toolbarExtra === "function" ? toolbarExtra(toolbarCtx) : toolbarExtra
936
+ })
937
+ }), /* @__PURE__ */ jsxs("div", {
938
+ className: "relative",
939
+ children: [isRefetching && /* @__PURE__ */ jsx("div", {
750
940
  className: "absolute top-0 left-0 right-0 h-0.5 z-30 bg-foreground/[0.04] overflow-hidden",
751
941
  children: /* @__PURE__ */ jsx("div", { className: "h-full w-1/3 bg-blue-500/60 rounded-full animate-pulse" })
752
- }),
753
- /* @__PURE__ */ jsx("div", {
942
+ }), /* @__PURE__ */ jsx("div", {
754
943
  ref: headerScrollRef,
755
- className: "overflow-hidden shrink-0 border-b border-foreground/[0.06]",
944
+ className: "w-full min-w-0 shrink-0 overflow-hidden border-b border-foreground/[0.06]",
756
945
  children: /* @__PURE__ */ jsxs("div", {
757
946
  className: "flex",
758
947
  style: {
@@ -779,77 +968,89 @@ function DataGrid(props) {
779
968
  onResizeEnd: handleResizeEnd
780
969
  }, col.id))]
781
970
  })
782
- }),
783
- /* @__PURE__ */ jsxs("div", {
784
- ref: scrollContainerRef,
785
- className: cn("min-h-0 overflow-auto flex-1 bg-transparent", "[&::-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]"),
786
- onScroll: handleBodyScroll,
787
- children: [
788
- isLoading && /* @__PURE__ */ jsx("div", {
789
- style: { minWidth: visibleColumnMetrics.totalWidth },
790
- children: loadingState ?? Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(SkeletonRow, {
791
- columns: visibleColumns,
792
- height: rowHeight,
793
- showCheckbox: selectionMode !== "none"
794
- }, i))
795
- }),
796
- !isLoading && rows.length === 0 && /* @__PURE__ */ jsx("div", {
797
- className: "flex items-center justify-center py-16 text-sm text-muted-foreground",
798
- style: { minWidth: visibleColumnMetrics.totalWidth },
799
- children: emptyState ?? strings.noData
800
- }),
801
- !isLoading && rows.length > 0 && /* @__PURE__ */ jsx("div", {
802
- style: {
803
- height: rowVirtualizer.getTotalSize(),
804
- width: "100%",
805
- minWidth: visibleColumnMetrics.totalWidth,
806
- position: "relative"
807
- },
808
- children: rowVirtualizer.getVirtualItems().map((virtualRow) => {
809
- const row = rows[virtualRow.index];
810
- const rowId = getRowId(row);
811
- const isSelected = state.selection.selectedIds.has(rowId);
812
- const isOddRow = virtualRow.index % 2 === 1;
813
- return /* @__PURE__ */ jsxs("div", {
814
- className: cn("absolute left-0 w-full flex", "border-b border-black/[0.03] dark:border-white/[0.03]", "transition-colors duration-75", isSelected ? "bg-blue-500/[0.06] dark:bg-blue-400/[0.08] hover:bg-blue-500/[0.08] dark:hover:bg-blue-400/[0.1]" : isOddRow ? "bg-foreground/[0.02] dark:bg-foreground/[0.03] hover:bg-foreground/[0.04] dark:hover:bg-foreground/[0.06]" : "hover:bg-foreground/[0.025] dark:hover:bg-foreground/[0.04]", selectionMode !== "none" && "cursor-pointer"),
815
- style: {
816
- height: rowHeight,
817
- transform: `translateY(${virtualRow.start}px)`
818
- },
819
- onClick: (e) => handleRowClick(row, rowId, e),
820
- onDoubleClick: (e) => onRowDoubleClick?.(row, rowId, e),
821
- role: "row",
822
- "aria-rowindex": virtualRow.index + 2,
823
- "aria-selected": isSelected,
824
- "data-row-id": rowId,
825
- "data-state": isSelected ? "selected" : void 0,
826
- children: [selectionMode !== "none" && /* @__PURE__ */ jsx("div", {
827
- className: "flex items-center justify-center border-r border-black/[0.04] dark:border-white/[0.04]",
828
- style: { width: 44 },
829
- children: /* @__PURE__ */ jsx(SelectionCheckbox, {
830
- checked: isSelected,
831
- onChange: (event) => handleRowSelectionCheckboxClick(row, rowId, event),
832
- ariaLabel: `Select row ${rowId}`
833
- })
834
- }), visibleColumns.map((col) => /* @__PURE__ */ jsx(DataCell, {
835
- col,
836
- row,
837
- rowId,
838
- rowIndex: virtualRow.index,
839
- isSelected,
840
- dateDisplay: state.dateDisplay
841
- }, col.id))]
842
- }, rowId);
843
- })
844
- }),
845
- paginationMode === "infinite" && hasMore && !isLoading && /* @__PURE__ */ jsx(InfiniteScrollSentinel, {
846
- onIntersect: onLoadMore ?? (() => {}),
847
- isLoading: isLoadingMore,
848
- strings
971
+ })]
972
+ })]
973
+ }),
974
+ /* @__PURE__ */ jsx("div", {
975
+ ref: scrollContainerRef,
976
+ className: 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]"),
977
+ onScroll: handleBodyScroll,
978
+ children: /* @__PURE__ */ jsxs("div", {
979
+ ref: rowsClipRef,
980
+ children: [
981
+ isLoading && /* @__PURE__ */ jsx("div", {
982
+ style: { minWidth: visibleColumnMetrics.totalWidth },
983
+ children: loadingState ?? Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(SkeletonRow, {
984
+ columns: visibleColumns,
985
+ height: estimatedRowHeight,
986
+ showCheckbox: selectionMode !== "none"
987
+ }, i))
988
+ }),
989
+ !isLoading && rows.length === 0 && /* @__PURE__ */ jsx("div", {
990
+ className: "flex items-center justify-center py-16 text-sm text-muted-foreground",
991
+ style: { minWidth: visibleColumnMetrics.totalWidth },
992
+ children: emptyState ?? strings.noData
993
+ }),
994
+ !isLoading && rows.length > 0 && /* @__PURE__ */ jsx("div", {
995
+ style: {
996
+ height: rowVirtualizer.getTotalSize(),
997
+ width: "100%",
998
+ minWidth: visibleColumnMetrics.totalWidth,
999
+ position: "relative"
1000
+ },
1001
+ children: rowVirtualizer.getVirtualItems().map((virtualRow) => {
1002
+ const row = rows[virtualRow.index] ?? throwErr(`DataGrid: virtualized row index ${virtualRow.index} out of range (rows.length=${rows.length})`);
1003
+ const rowId = getRowId(row);
1004
+ const isSelected = state.selection.selectedIds.has(rowId);
1005
+ const isOddRow = virtualRow.index % 2 === 1;
1006
+ return /* @__PURE__ */ jsxs("div", {
1007
+ ref: isDynamicRowHeight ? rowVirtualizer.measureElement : void 0,
1008
+ "data-index": virtualRow.index,
1009
+ className: cn("absolute left-0 w-full flex", "border-b border-black/[0.03] dark:border-white/[0.03]", "transition-colors duration-75", isSelected ? "bg-blue-500/[0.06] dark:bg-blue-400/[0.08] hover:bg-blue-500/[0.08] dark:hover:bg-blue-400/[0.1]" : isOddRow ? "bg-foreground/[0.02] dark:bg-foreground/[0.03] hover:bg-foreground/[0.04] dark:hover:bg-foreground/[0.06]" : "hover:bg-foreground/[0.025] dark:hover:bg-foreground/[0.04]", (selectionMode !== "none" || onRowClick) && "cursor-pointer"),
1010
+ style: {
1011
+ ...isDynamicRowHeight ? { minHeight: estimatedRowHeight } : { height: fixedRowHeight },
1012
+ transform: `translateY(${virtualRow.start}px)`
1013
+ },
1014
+ onClick: (e) => {
1015
+ if (shouldIgnoreRowClick(e)) return;
1016
+ handleRowClick(row, rowId, e);
1017
+ },
1018
+ onDoubleClick: (e) => {
1019
+ if (shouldIgnoreRowClick(e)) return;
1020
+ onRowDoubleClick?.(row, rowId, e);
1021
+ },
1022
+ role: "row",
1023
+ "aria-rowindex": virtualRow.index + 2,
1024
+ "aria-selected": isSelected,
1025
+ "data-row-id": rowId,
1026
+ "data-state": isSelected ? "selected" : void 0,
1027
+ children: [selectionMode !== "none" && /* @__PURE__ */ jsx("div", {
1028
+ className: "flex items-center justify-center border-r border-black/[0.04] dark:border-white/[0.04]",
1029
+ style: { width: 44 },
1030
+ children: /* @__PURE__ */ jsx(SelectionCheckbox, {
1031
+ checked: isSelected,
1032
+ onChange: (event) => handleRowSelectionCheckboxClick(row, rowId, event),
1033
+ ariaLabel: `Select row ${rowId}`
1034
+ })
1035
+ }), visibleColumns.map((col) => /* @__PURE__ */ jsx(DataCell, {
1036
+ col,
1037
+ row,
1038
+ rowId,
1039
+ rowIndex: virtualRow.index,
1040
+ isSelected,
1041
+ dateDisplay: state.dateDisplay
1042
+ }, col.id))]
1043
+ }, rowId);
849
1044
  })
850
- ]
851
- })
852
- ]
1045
+ }),
1046
+ paginationMode === "infinite" && hasMore && !isLoading && /* @__PURE__ */ jsx(InfiniteScrollSentinel, {
1047
+ onIntersect: onLoadMore ?? NOOP,
1048
+ isLoading: isLoadingMore,
1049
+ rootRef: infiniteScrollRootRef,
1050
+ strings
1051
+ })
1052
+ ]
1053
+ })
853
1054
  }),
854
1055
  footer !== false && /* @__PURE__ */ jsxs("div", {
855
1056
  className: "relative z-10 shrink-0 bg-transparent",
@@ -864,5 +1065,5 @@ function DataGrid(props) {
864
1065
  }
865
1066
 
866
1067
  //#endregion
867
- export { DataGrid };
1068
+ export { DataGrid, isDataGridInteractiveRowClickTarget };
868
1069
  //# sourceMappingURL=data-grid.js.map