@versini/ui-datagrid 0.3.8 → 0.4.1

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 (41) hide show
  1. package/README.md +106 -29
  2. package/dist/DataGrid/DataGrid.js +1 -1
  3. package/dist/DataGrid/DataGridContext.js +1 -1
  4. package/dist/DataGrid/DataGridTypes.d.ts +280 -0
  5. package/dist/DataGrid/DataGridTypes.js +9 -0
  6. package/dist/DataGrid/index.js +1 -1
  7. package/dist/DataGridAnimated/AnimatedWrapper.d.ts +11 -7
  8. package/dist/DataGridAnimated/AnimatedWrapper.js +12 -8
  9. package/dist/DataGridAnimated/index.js +1 -1
  10. package/dist/DataGridAnimated/useAnimatedHeight.js +1 -1
  11. package/dist/DataGridBody/DataGridBody.js +12 -56
  12. package/dist/DataGridBody/getBodyClass.d.ts +10 -0
  13. package/dist/DataGridBody/getBodyClass.js +24 -0
  14. package/dist/DataGridBody/index.js +1 -1
  15. package/dist/DataGridBody/useColumnMeasurement.d.ts +11 -0
  16. package/dist/DataGridBody/useColumnMeasurement.js +68 -0
  17. package/dist/DataGridCell/DataGridCell.js +1 -1
  18. package/dist/DataGridCell/index.js +1 -1
  19. package/dist/DataGridCellSort/DataGridCellSort.js +1 -1
  20. package/dist/DataGridCellSort/index.js +1 -1
  21. package/dist/DataGridConstants/DataGridConstants.js +1 -1
  22. package/dist/DataGridConstants/index.js +1 -1
  23. package/dist/DataGridFooter/DataGridFooter.js +1 -1
  24. package/dist/DataGridFooter/index.js +1 -1
  25. package/dist/DataGridHeader/DataGridHeader.js +1 -1
  26. package/dist/DataGridHeader/index.js +1 -1
  27. package/dist/DataGridInfinite/DataGridInfiniteBody.d.ts +52 -0
  28. package/dist/DataGridInfinite/DataGridInfiniteBody.js +335 -0
  29. package/dist/DataGridInfinite/index.d.ts +2 -4
  30. package/dist/DataGridInfinite/index.js +4 -8
  31. package/dist/DataGridRow/DataGridRow.js +5 -3
  32. package/dist/DataGridRow/index.js +1 -1
  33. package/dist/DataGridSorting/index.js +1 -1
  34. package/dist/DataGridSorting/sortingUtils.js +1 -1
  35. package/dist/utilities/classes.d.ts +6 -2
  36. package/dist/utilities/classes.js +35 -8
  37. package/package.json +2 -2
  38. package/dist/DataGridInfinite/InfiniteScrollMarker.d.ts +0 -31
  39. package/dist/DataGridInfinite/InfiniteScrollMarker.js +0 -54
  40. package/dist/DataGridInfinite/useInfiniteScroll.d.ts +0 -92
  41. package/dist/DataGridInfinite/useInfiniteScroll.js +0 -136
package/README.md CHANGED
@@ -36,10 +36,7 @@ import { DataGridCell } from "@versini/ui-datagrid/cell";
36
36
  import { DataGridCellSort } from "@versini/ui-datagrid/cell-sort";
37
37
 
38
38
  // Infinite scroll (progressive loading)
39
- import {
40
- useInfiniteScroll,
41
- InfiniteScrollMarker
42
- } from "@versini/ui-datagrid/infinite";
39
+ import { DataGridInfiniteBody } from "@versini/ui-datagrid/infinite";
43
40
 
44
41
  // Animated height wrapper
45
42
  import {
@@ -138,52 +135,122 @@ Supported column values include:
138
135
 
139
136
  ## Infinite Scroll
140
137
 
141
- For datasets with hundreds to thousands of rows, use progressive loading:
138
+ For datasets with hundreds to thousands of rows, use `DataGridInfiniteBody` for progressive loading:
142
139
 
143
140
  ```tsx
141
+ import { useState } from "react";
144
142
  import { DataGrid } from "@versini/ui-datagrid/datagrid";
145
143
  import { DataGridHeader } from "@versini/ui-datagrid/header";
146
- import { DataGridBody } from "@versini/ui-datagrid/body";
147
144
  import { DataGridRow } from "@versini/ui-datagrid/row";
148
145
  import { DataGridCell } from "@versini/ui-datagrid/cell";
149
- import {
150
- useInfiniteScroll,
151
- InfiniteScrollMarker
152
- } from "@versini/ui-datagrid/infinite";
146
+ import { DataGridInfiniteBody } from "@versini/ui-datagrid/infinite";
153
147
  import { AnimatedWrapper } from "@versini/ui-datagrid/animated";
154
148
 
155
149
  function MyInfiniteTable({ data }) {
156
- const { visibleCount, hasMore, markerRef } = useInfiniteScroll({
157
- totalItems: data.length,
158
- batchSize: 25
159
- });
160
-
161
- const visibleData = data.slice(0, visibleCount);
150
+ const [visibleCount, setVisibleCount] = useState(0);
162
151
 
163
152
  return (
164
153
  <AnimatedWrapper dependency={visibleCount}>
165
154
  <DataGrid maxHeight="500px" stickyHeader>
166
- <DataGridHeader>
155
+ <DataGridHeader caption={`Showing ${visibleCount} of ${data.length}`}>
167
156
  <DataGridRow>
168
157
  <DataGridCell>Name</DataGridCell>
169
158
  <DataGridCell>Date</DataGridCell>
170
159
  </DataGridRow>
171
160
  </DataGridHeader>
172
- <DataGridBody>
173
- {visibleData.map((item) => (
161
+
162
+ <DataGridInfiniteBody
163
+ data={data}
164
+ batchSize={25}
165
+ threshold={5}
166
+ onVisibleCountChange={(count) => setVisibleCount(count)}
167
+ >
168
+ {(item) => (
174
169
  <DataGridRow key={item.id}>
175
170
  <DataGridCell>{item.name}</DataGridCell>
176
171
  <DataGridCell>{item.date}</DataGridCell>
177
172
  </DataGridRow>
178
- ))}
179
- {hasMore && <InfiniteScrollMarker ref={markerRef} colSpan={2} />}
180
- </DataGridBody>
173
+ )}
174
+ </DataGridInfiniteBody>
181
175
  </DataGrid>
182
176
  </AnimatedWrapper>
183
177
  );
184
178
  }
185
179
  ```
186
180
 
181
+ The `DataGridInfiniteBody` component handles all the complexity internally:
182
+ - Progressive loading with IntersectionObserver
183
+ - Correct marker placement for seamless scrolling (marker is placed `threshold` items before the end)
184
+ - Automatic data slicing and memoization
185
+
186
+ ### Jump to Row
187
+
188
+ Use the `scrollToIndex` imperative method to programmatically scroll to any row, even if it's not yet loaded:
189
+
190
+ ```tsx
191
+ import { useRef, useState } from "react";
192
+ import { DataGrid } from "@versini/ui-datagrid/datagrid";
193
+ import { DataGridHeader } from "@versini/ui-datagrid/header";
194
+ import { DataGridRow } from "@versini/ui-datagrid/row";
195
+ import { DataGridCell } from "@versini/ui-datagrid/cell";
196
+ import {
197
+ DataGridInfiniteBody,
198
+ type DataGridInfiniteBodyRef
199
+ } from "@versini/ui-datagrid/infinite";
200
+
201
+ function MyInfiniteTableWithJump({ data }) {
202
+ const [activeRowId, setActiveRowId] = useState(null);
203
+ const infiniteBodyRef = useRef<DataGridInfiniteBodyRef>(null);
204
+
205
+ const handleJumpToRow = (targetId) => {
206
+ // Find the index of the row with the target id
207
+ const targetIndex = data.findIndex((row) => row.id === targetId);
208
+ if (targetIndex !== -1) {
209
+ setActiveRowId(targetId);
210
+ infiniteBodyRef.current?.scrollToIndex(targetIndex);
211
+ }
212
+ };
213
+
214
+ return (
215
+ <div>
216
+ <button onClick={() => handleJumpToRow(1341)}>
217
+ Jump to row with id=1341
218
+ </button>
219
+
220
+ <DataGrid maxHeight="400px" stickyHeader>
221
+ <DataGridHeader>
222
+ <DataGridRow>
223
+ <DataGridCell>ID</DataGridCell>
224
+ <DataGridCell>Name</DataGridCell>
225
+ </DataGridRow>
226
+ </DataGridHeader>
227
+
228
+ <DataGridInfiniteBody
229
+ ref={infiniteBodyRef}
230
+ data={data}
231
+ batchSize={25}
232
+ >
233
+ {(item) => (
234
+ <DataGridRow
235
+ key={item.id}
236
+ active={activeRowId === item.id}
237
+ onClick={() => setActiveRowId(item.id)}
238
+ >
239
+ <DataGridCell>{item.id}</DataGridCell>
240
+ <DataGridCell>{item.name}</DataGridCell>
241
+ </DataGridRow>
242
+ )}
243
+ </DataGridInfiniteBody>
244
+ </DataGrid>
245
+ </div>
246
+ );
247
+ }
248
+ ```
249
+
250
+ The `scrollToIndex` method:
251
+ - If the row is already visible → smooth scrolls to it immediately
252
+ - If the row is not yet loaded → expands visible count first, then scrolls after render
253
+
187
254
  ## Sorting
188
255
 
189
256
  ```tsx
@@ -328,14 +395,24 @@ function MySortableTable({ data }) {
328
395
  | `className` | `string` | - | CSS class for the cell |
329
396
  | `buttonClassName` | `string` | - | CSS class for the sort button |
330
397
 
331
- ### useInfiniteScroll
398
+ ### DataGridInfiniteBody
399
+
400
+ | Prop | Type | Default | Description |
401
+ | ---------------------- | ------------------------------------------- | ------- | ------------------------------------------------- |
402
+ | `data` | `T[]` | **required** | The full dataset to render progressively |
403
+ | `children` | `(item: T, index: number) => ReactNode` | **required** | Render function for each row |
404
+ | `batchSize` | `number` | `20` | Items to show initially and add per scroll |
405
+ | `threshold` | `number` | `5` | Items before marker to allow seamless scrolling |
406
+ | `rootMargin` | `string` | `'20px'`| IntersectionObserver margin |
407
+ | `onVisibleCountChange` | `(visibleCount: number, total: number) => void` | - | Callback when visible count changes |
408
+ | `className` | `string` | - | CSS class for the body element |
409
+ | `ref` | `React.Ref<DataGridInfiniteBodyRef>` | - | Ref to access imperative methods |
410
+
411
+ ### DataGridInfiniteBodyRef (Imperative Handle)
332
412
 
333
- | Option | Type | Default | Description |
334
- | ------------ | -------- | ------------ | -------------------------------- |
335
- | `totalItems` | `number` | **required** | Total number of items |
336
- | `batchSize` | `number` | `20` | Items to load per batch |
337
- | `threshold` | `number` | `5` | Items before end to trigger load |
338
- | `rootMargin` | `string` | `'20px'` | IntersectionObserver margin |
413
+ | Method | Signature | Description |
414
+ | --------------- | ---------------------------- | -------------------------------------------------------- |
415
+ | `scrollToIndex` | `(index: number) => void` | Scroll to a row by index. Expands visible count if needed, then smooth scrolls. |
339
416
 
340
417
  ## License
341
418
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.8
2
+ @versini/ui-datagrid v0.4.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.8
2
+ @versini/ui-datagrid v0.4.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -0,0 +1,280 @@
1
+ import type React from "react";
2
+ type CSSLength = `${number}px` | `${number}rem` | `${number}em` | `${number}ch` | `${number}vw` | `${number}vh` | `${number}cm` | `${number}mm` | `${number}in` | `${number}pt` | `${number}pc`;
3
+ type CSSPercentage = `${number}%`;
4
+ type CSSFraction = `${number}fr`;
5
+ type CSSVar = `var(${string})`;
6
+ type CSSCalc = `calc(${string})`;
7
+ type CSSClamp = `clamp(${string})`;
8
+ type CSSFitContent = `fit-content(${CSSLength | CSSPercentage})`;
9
+ type GridTrackPrimitive = CSSLength | CSSPercentage | CSSFraction | "auto" | "min-content" | "max-content" | "subgrid" | CSSFitContent | CSSVar | CSSCalc | CSSClamp;
10
+ type CSSMinMax = `minmax(${GridTrackPrimitive}, ${GridTrackPrimitive})`;
11
+ type CSSRepeat = `repeat(${number | "auto-fill" | "auto-fit"}, ${GridTrackPrimitive})`;
12
+ export type DataGridColumnSize = GridTrackPrimitive | CSSMinMax | CSSRepeat;
13
+ import type { BlurEffect, CellWrapperType, SortDirection, ThemeMode } from "../DataGridConstants/DataGridConstants";
14
+ export type DataGridProps = {
15
+ /**
16
+ * The content of the DataGrid (DataGridHeader, DataGridBody, DataGridFooter).
17
+ */
18
+ children: React.ReactNode;
19
+ /**
20
+ * CSS class to apply to the data grid.
21
+ */
22
+ className?: string;
23
+ /**
24
+ * If true, the data grid will have reduced padding.
25
+ */
26
+ compact?: boolean;
27
+ /**
28
+ * If true, a frosty overlay will be placed above the data grid, blocking all
29
+ * interactions (sorting, scrolling, etc.). The content remains visible but
30
+ * slightly blurred with a spinner.
31
+ * @default false
32
+ */
33
+ disabled?: boolean;
34
+ /**
35
+ * The max height of the data grid. Required for sticky header/footer to work.
36
+ * Accepts any valid CSS max-height value.
37
+ */
38
+ maxHeight?: string;
39
+ /**
40
+ * The theme mode of the data grid. Controls colors and styling.
41
+ * @default "system"
42
+ */
43
+ mode?: ThemeMode;
44
+ /**
45
+ * If true, the data grid header will be sticky (stays visible when scrolling).
46
+ * Requires maxHeight to be set or wrapperClassName to be set with a
47
+ * max-height.
48
+ */
49
+ stickyHeader?: boolean;
50
+ /**
51
+ * If true, the data grid footer will be sticky (stays visible when scrolling).
52
+ * Requires maxHeight to be set or wrapperClassName to be set with a
53
+ * max-height.
54
+ */
55
+ stickyFooter?: boolean;
56
+ /**
57
+ * The blur effect intensity for sticky header/footer backgrounds.
58
+ * @default "none"
59
+ */
60
+ blurEffect?: BlurEffect;
61
+ /**
62
+ * This attribute defines an alternative text that summarizes the content of
63
+ * the data grid. Not visible but read by screen readers.
64
+ */
65
+ summary?: string;
66
+ /**
67
+ * CSS class to apply to the data grid wrapper container.
68
+ */
69
+ wrapperClassName?: string;
70
+ /**
71
+ * An array of CSS Grid track sizes that define the width of each column.
72
+ * Template-literal union covers common grid values (fr, %, px/em/rem/etc.,
73
+ * minmax(), fit-content(), repeat(auto-fill/fit, ...), var(), calc()) so
74
+ * obviously invalid strings are caught at compile time.
75
+ * @example ["1fr", "1fr", "auto"] - Two equal columns and one auto-sized column
76
+ * @example ["200px", "1fr", "min-content"] - Fixed, flexible, and content-sized columns
77
+ */
78
+ columns?: ReadonlyArray<DataGridColumnSize>;
79
+ };
80
+ export type DataGridHeaderProps = {
81
+ /**
82
+ * The content of the header (DataGridRow elements).
83
+ */
84
+ children: React.ReactNode;
85
+ /**
86
+ * This attribute defines the caption (or title) of the data grid. When
87
+ * provided, it will be rendered above the header row as part of the same
88
+ * block, so they stay together whether sticky or not.
89
+ */
90
+ caption?: React.ReactNode;
91
+ /**
92
+ * CSS class to apply to the caption element.
93
+ */
94
+ captionClassName?: string;
95
+ /**
96
+ * CSS class to apply to the header.
97
+ */
98
+ className?: string;
99
+ } & React.HTMLAttributes<HTMLDivElement>;
100
+ export type DataGridBodyProps = {
101
+ /**
102
+ * The content of the body (DataGridRow elements).
103
+ */
104
+ children: React.ReactNode;
105
+ /**
106
+ * CSS class to apply to the body.
107
+ */
108
+ className?: string;
109
+ } & React.HTMLAttributes<HTMLDivElement>;
110
+ export type DataGridFooterProps = {
111
+ /**
112
+ * The content of the footer (DataGridRow elements).
113
+ */
114
+ children: React.ReactNode;
115
+ /**
116
+ * CSS class to apply to the footer.
117
+ */
118
+ className?: string;
119
+ } & React.HTMLAttributes<HTMLDivElement>;
120
+ export type DataGridRowProps = {
121
+ /**
122
+ * The content of the row (DataGridCell elements).
123
+ */
124
+ children: React.ReactNode;
125
+ /**
126
+ * CSS class to apply to the row.
127
+ */
128
+ className?: string;
129
+ /**
130
+ * If true, the row will display a left border indicator to show it's active.
131
+ * @default false
132
+ */
133
+ active?: boolean;
134
+ } & React.HTMLAttributes<HTMLDivElement>;
135
+ export type DataGridCellProps = {
136
+ /**
137
+ * The content of the cell.
138
+ */
139
+ children?: React.ReactNode;
140
+ /**
141
+ * CSS class to apply to the cell.
142
+ */
143
+ className?: string;
144
+ /**
145
+ * Horizontal alignment of the cell content.
146
+ */
147
+ align?: "left" | "center" | "right";
148
+ /**
149
+ * If true, adds a vertical border on the left side of the cell.
150
+ */
151
+ borderLeft?: boolean;
152
+ /**
153
+ * If true, adds a vertical border on the right side of the cell.
154
+ */
155
+ borderRight?: boolean;
156
+ /**
157
+ * Number of columns the cell should span. Defaults to 1.
158
+ */
159
+ colSpan?: number;
160
+ } & React.HTMLAttributes<HTMLDivElement>;
161
+ export type DataGridCellSortProps = {
162
+ /**
163
+ * Unique identifier for the cell. Used to determine which column is sorted.
164
+ */
165
+ cellId: string;
166
+ /**
167
+ * The label text for the sortable column header.
168
+ */
169
+ children: string;
170
+ /**
171
+ * Handler called when the sort button is clicked. Receives the cellId and
172
+ * current direction (if sorted).
173
+ */
174
+ onSort?: (cellId: string, currentDirection?: SortDirection) => void;
175
+ /**
176
+ * The current sort direction.
177
+ */
178
+ sortDirection: SortDirection | false;
179
+ /**
180
+ * The cellId of the currently sorted column.
181
+ */
182
+ sortedCell: string;
183
+ /**
184
+ * Horizontal alignment of the cell content.
185
+ */
186
+ align?: "left" | "center" | "right";
187
+ /**
188
+ * CSS class to apply to the sort button.
189
+ */
190
+ buttonClassName?: string;
191
+ /**
192
+ * CSS class to apply to the cell.
193
+ */
194
+ className?: string;
195
+ /**
196
+ * The focus mode for the sort button.
197
+ * @default "alt-system"
198
+ */
199
+ focusMode?: ThemeMode;
200
+ /**
201
+ * The theme mode for the sort button.
202
+ * @default "alt-system"
203
+ */
204
+ mode?: ThemeMode;
205
+ /**
206
+ * Content to display on the left side of the label.
207
+ */
208
+ slotLeft?: React.ReactNode;
209
+ /**
210
+ * Content to display on the right side of the label.
211
+ */
212
+ slotRight?: React.ReactNode;
213
+ } & React.HTMLAttributes<HTMLDivElement>;
214
+ export type DataGridContextValue = {
215
+ mode: ThemeMode;
216
+ stickyHeader?: boolean;
217
+ stickyFooter?: boolean;
218
+ compact?: boolean;
219
+ blurEffect?: BlurEffect;
220
+ cellWrapper?: CellWrapperType;
221
+ /**
222
+ * CSS grid column sizes passed from DataGrid to rows.
223
+ */
224
+ columns?: ReadonlyArray<DataGridColumnSize>;
225
+ /**
226
+ * Measured column widths in pixels, reported by DataGridBody. Used by sticky
227
+ * header/footer to sync column widths since they can't use subgrid (being
228
+ * absolutely positioned).
229
+ */
230
+ measuredColumnWidths?: number[];
231
+ /**
232
+ * Callback for DataGridHeader to register its caption ID for accessibility.
233
+ * DataGrid uses this to set aria-labelledby on the grid element.
234
+ */
235
+ setCaptionId?: (id: string | undefined) => void;
236
+ /**
237
+ * Callback for DataGridHeader to register itself with the parent DataGrid.
238
+ * Used to enable sticky header behavior regardless of nesting depth.
239
+ */
240
+ registerHeader?: () => void;
241
+ /**
242
+ * Callback for DataGridHeader to unregister itself when unmounting.
243
+ */
244
+ unregisterHeader?: () => void;
245
+ /**
246
+ * Callback for DataGridFooter to register itself with the parent DataGrid.
247
+ * Used to enable sticky footer behavior regardless of nesting depth.
248
+ */
249
+ registerFooter?: () => void;
250
+ /**
251
+ * Callback for DataGridFooter to unregister itself when unmounting.
252
+ */
253
+ unregisterFooter?: () => void;
254
+ /**
255
+ * Callback for DataGridHeader to report its measured height. Used for dynamic
256
+ * padding calculation instead of hard-coded values.
257
+ */
258
+ setHeaderHeight?: (height: number) => void;
259
+ /**
260
+ * Callback for DataGridFooter to report its measured height. Used for dynamic
261
+ * padding calculation instead of hard-coded values.
262
+ */
263
+ setFooterHeight?: (height: number) => void;
264
+ /**
265
+ * Callback for DataGridBody to report measured column widths. Used to sync
266
+ * sticky header/footer columns with body columns.
267
+ */
268
+ setMeasuredColumnWidths?: (widths: number[]) => void;
269
+ /**
270
+ * Row index for explicit odd/even styling. Used by DataGridInfiniteBody where
271
+ * CSS :nth-child selectors don't work due to wrapper elements.
272
+ */
273
+ rowIndex?: number;
274
+ /**
275
+ * Whether this is the last row (for explicit border removal). Used by
276
+ * DataGridInfiniteBody where CSS :last-child doesn't work.
277
+ */
278
+ isLastRow?: boolean;
279
+ };
280
+ export {};
@@ -0,0 +1,9 @@
1
+ /*!
2
+ @versini/ui-datagrid v0.4.1
3
+ © 2026 gizmette.com
4
+ */
5
+
6
+
7
+ ;// CONCATENATED MODULE: ./src/DataGrid/DataGridTypes.ts
8
+
9
+
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.8
2
+ @versini/ui-datagrid v0.4.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -28,16 +28,20 @@ export type AnimatedWrapperProps = {
28
28
  *
29
29
  * @example
30
30
  * ```tsx
31
- * const { visibleCount } = useInfiniteScroll({ totalItems: data.length });
31
+ * const [visibleCount, setVisibleCount] = useState(0);
32
32
  *
33
33
  * return (
34
34
  * <AnimatedWrapper dependency={visibleCount}>
35
- * <DataGrid>
36
- * <DataGridBody>
37
- * {data.slice(0, visibleCount).map((item) => (
38
- * <DataGridRow key={item.id}>...</DataGridRow>
39
- * ))}
40
- * </DataGridBody>
35
+ * <DataGrid maxHeight="400px" stickyHeader>
36
+ * <DataGridInfiniteBody
37
+ * data={largeData}
38
+ * batchSize={25}
39
+ * onVisibleCountChange={(count) => setVisibleCount(count)}
40
+ * >
41
+ * {(row) => (
42
+ * <DataGridRow key={row.id}>...</DataGridRow>
43
+ * )}
44
+ * </DataGridInfiniteBody>
41
45
  * </DataGrid>
42
46
  * </AnimatedWrapper>
43
47
  * );
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.8
2
+ @versini/ui-datagrid v0.4.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -18,16 +18,20 @@ import { useAnimatedHeight } from "./useAnimatedHeight.js";
18
18
  *
19
19
  * @example
20
20
  * ```tsx
21
- * const { visibleCount } = useInfiniteScroll({ totalItems: data.length });
21
+ * const [visibleCount, setVisibleCount] = useState(0);
22
22
  *
23
23
  * return (
24
24
  * <AnimatedWrapper dependency={visibleCount}>
25
- * <DataGrid>
26
- * <DataGridBody>
27
- * {data.slice(0, visibleCount).map((item) => (
28
- * <DataGridRow key={item.id}>...</DataGridRow>
29
- * ))}
30
- * </DataGridBody>
25
+ * <DataGrid maxHeight="400px" stickyHeader>
26
+ * <DataGridInfiniteBody
27
+ * data={largeData}
28
+ * batchSize={25}
29
+ * onVisibleCountChange={(count) => setVisibleCount(count)}
30
+ * >
31
+ * {(row) => (
32
+ * <DataGridRow key={row.id}>...</DataGridRow>
33
+ * )}
34
+ * </DataGridInfiniteBody>
31
35
  * </DataGrid>
32
36
  * </AnimatedWrapper>
33
37
  * );
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.8
2
+ @versini/ui-datagrid v0.4.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.8
2
+ @versini/ui-datagrid v0.4.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,86 +1,42 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.8
2
+ @versini/ui-datagrid v0.4.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import { jsx } from "react/jsx-runtime";
7
- import clsx from "clsx";
8
- import { useContext, useLayoutEffect, useRef } from "react";
7
+ import { useContext, useRef } from "react";
9
8
  import { DataGridContext } from "../DataGrid/DataGridContext.js";
10
9
  import { CellWrapper } from "../DataGridConstants/index.js";
10
+ import { getBodyClass } from "./getBodyClass.js";
11
+ import { useColumnMeasurement } from "./useColumnMeasurement.js";
11
12
 
12
13
  ;// CONCATENATED MODULE: external "react/jsx-runtime"
13
14
 
14
- ;// CONCATENATED MODULE: external "clsx"
15
-
16
15
  ;// CONCATENATED MODULE: external "react"
17
16
 
18
17
  ;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
19
18
 
20
19
  ;// CONCATENATED MODULE: external "../DataGridConstants/index.js"
21
20
 
21
+ ;// CONCATENATED MODULE: external "./getBodyClass.js"
22
+
23
+ ;// CONCATENATED MODULE: external "./useColumnMeasurement.js"
24
+
22
25
  ;// CONCATENATED MODULE: ./src/DataGridBody/DataGridBody.tsx
23
26
 
24
27
 
25
28
 
26
29
 
27
30
 
31
+
28
32
  /* =============================================================================
29
33
  * DataGridBody
30
34
  * ========================================================================== */ const DataGridBody = ({ className, children, ...rest })=>{
31
35
  const ctx = useContext(DataGridContext);
32
36
  const bodyRef = useRef(null);
33
- /**
34
- * Measure column widths from the first body row's cells. This is needed
35
- * because sticky header/footer are absolutely positioned and can't use CSS
36
- * subgrid. We measure the body cells (which ARE in the grid flow) and report
37
- * the widths so header/footer can use the same pixel values.
38
- */ useLayoutEffect(()=>{
39
- const element = bodyRef.current;
40
- const needsMeasurement = ctx.columns && (ctx.stickyHeader || ctx.stickyFooter);
41
- if (!element || !needsMeasurement || !ctx.setMeasuredColumnWidths) {
42
- return;
43
- }
44
- /**
45
- * Find the first row and its cells once, reuse for both measurement and
46
- * observation.
47
- */ const firstRow = element.querySelector('[role="row"]');
48
- if (!firstRow) {
49
- return;
50
- }
51
- const cells = firstRow.querySelectorAll('[role="cell"], [role="columnheader"], [role="gridcell"]');
52
- if (cells.length === 0) {
53
- return;
54
- }
55
- const measureColumns = ()=>{
56
- // Measure each cell's width.
57
- const widths = Array.from(cells).map((cell)=>cell.getBoundingClientRect().width);
58
- ctx.setMeasuredColumnWidths?.(widths);
59
- };
60
- // Initial measurement.
61
- measureColumns();
62
- // Set up ResizeObserver to re-measure when cells resize.
63
- const observer = new ResizeObserver(()=>{
64
- measureColumns();
65
- });
66
- // Observe the body element for any size changes.
67
- observer.observe(element);
68
- // Also observe the first row's cells directly for more accurate updates.
69
- for (const cell of cells){
70
- observer.observe(cell);
71
- }
72
- return ()=>observer.disconnect();
73
- }, [
74
- ctx.columns,
75
- ctx.stickyHeader,
76
- ctx.stickyFooter,
77
- ctx.setMeasuredColumnWidths,
78
- children
79
- ]);
80
- /**
81
- * When columns are provided, use display:contents so the body doesn't
82
- * interfere with the grid flow. Rows will use subgrid.
83
- */ const bodyClass = ctx.columns ? clsx("contents", className) : clsx("flex flex-col", className);
37
+ // Measure column widths for sticky header/footer sync.
38
+ useColumnMeasurement(bodyRef, children);
39
+ const bodyClass = getBodyClass(Boolean(ctx.columns), className);
84
40
  return /*#__PURE__*/ jsx(DataGridContext.Provider, {
85
41
  value: {
86
42
  ...ctx,