@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
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Get the CSS class for a DataGrid body element. When columns are provided, use
3
+ * display:contents so the body doesn't interfere with the grid flow. Rows will
4
+ * use subgrid.
5
+ *
6
+ * @param hasColumns - Whether the DataGrid has columns defined
7
+ * @param className - Additional class name to merge
8
+ *
9
+ */
10
+ export declare function getBodyClass(hasColumns: boolean | undefined, className?: string): string;
@@ -0,0 +1,24 @@
1
+ /*!
2
+ @versini/ui-datagrid v0.4.1
3
+ © 2026 gizmette.com
4
+ */
5
+
6
+ import clsx from "clsx";
7
+
8
+ ;// CONCATENATED MODULE: external "clsx"
9
+
10
+ ;// CONCATENATED MODULE: ./src/DataGridBody/getBodyClass.ts
11
+
12
+ /**
13
+ * Get the CSS class for a DataGrid body element. When columns are provided, use
14
+ * display:contents so the body doesn't interfere with the grid flow. Rows will
15
+ * use subgrid.
16
+ *
17
+ * @param hasColumns - Whether the DataGrid has columns defined
18
+ * @param className - Additional class name to merge
19
+ *
20
+ */ function getBodyClass(hasColumns, className) {
21
+ return hasColumns ? clsx("contents", className) : clsx("flex flex-col", className);
22
+ }
23
+
24
+ export { getBodyClass };
@@ -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,11 @@
1
+ /**
2
+ * Hook to measure column widths from the first body row's cells. This is needed
3
+ * because sticky header/footer are absolutely positioned and can't use CSS
4
+ * subgrid. We measure the body cells (which ARE in the grid flow) and report
5
+ * the widths so header/footer can use the same pixel values.
6
+ *
7
+ * @param bodyRef - Ref to the body element containing the rows
8
+ * @param contentDependency - Dependency that changes when content changes (children or renderedContent)
9
+ *
10
+ */
11
+ export declare function useColumnMeasurement(bodyRef: React.RefObject<HTMLDivElement | null>, contentDependency: unknown): void;
@@ -0,0 +1,68 @@
1
+ /*!
2
+ @versini/ui-datagrid v0.4.1
3
+ © 2026 gizmette.com
4
+ */
5
+
6
+ import { useContext, useLayoutEffect } from "react";
7
+ import { DataGridContext } from "../DataGrid/DataGridContext.js";
8
+
9
+ ;// CONCATENATED MODULE: external "react"
10
+
11
+ ;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
12
+
13
+ ;// CONCATENATED MODULE: ./src/DataGridBody/useColumnMeasurement.ts
14
+
15
+
16
+ /**
17
+ * Hook to measure column widths from the first body row's cells. This is needed
18
+ * because sticky header/footer are absolutely positioned and can't use CSS
19
+ * subgrid. We measure the body cells (which ARE in the grid flow) and report
20
+ * the widths so header/footer can use the same pixel values.
21
+ *
22
+ * @param bodyRef - Ref to the body element containing the rows
23
+ * @param contentDependency - Dependency that changes when content changes (children or renderedContent)
24
+ *
25
+ */ function useColumnMeasurement(bodyRef, contentDependency) {
26
+ const ctx = useContext(DataGridContext);
27
+ // biome-ignore lint/correctness/useExhaustiveDependencies: contentDependency triggers remeasurement when rows change
28
+ useLayoutEffect(()=>{
29
+ const element = bodyRef.current;
30
+ const needsMeasurement = ctx.columns && (ctx.stickyHeader || ctx.stickyFooter);
31
+ if (!element || !needsMeasurement || !ctx.setMeasuredColumnWidths) {
32
+ return;
33
+ }
34
+ const firstRow = element.querySelector('[role="row"]');
35
+ if (!firstRow) {
36
+ return;
37
+ }
38
+ const cells = firstRow.querySelectorAll('[role="cell"], [role="columnheader"], [role="gridcell"]');
39
+ if (cells.length === 0) {
40
+ return;
41
+ }
42
+ const measureColumns = ()=>{
43
+ const widths = Array.from(cells).map((cell)=>cell.getBoundingClientRect().width);
44
+ ctx.setMeasuredColumnWidths?.(widths);
45
+ };
46
+ // Initial measurement.
47
+ measureColumns();
48
+ // Set up ResizeObserver to re-measure when cells resize.
49
+ const observer = new ResizeObserver(()=>{
50
+ measureColumns();
51
+ });
52
+ // Observe the body element for any size changes.
53
+ observer.observe(element);
54
+ // Also observe the first row's cells directly for more accurate updates.
55
+ for (const cell of cells){
56
+ observer.observe(cell);
57
+ }
58
+ return ()=>observer.disconnect();
59
+ }, [
60
+ ctx.columns,
61
+ ctx.stickyHeader,
62
+ ctx.stickyFooter,
63
+ ctx.setMeasuredColumnWidths,
64
+ contentDependency
65
+ ]);
66
+ }
67
+
68
+ export { useColumnMeasurement };
@@ -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,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,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,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,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,52 @@
1
+ export type DataGridInfiniteBodyProps<T> = {
2
+ /**
3
+ * The full dataset to render progressively.
4
+ */
5
+ data: T[];
6
+ /**
7
+ * Render function for each row. Should return a DataGridRow element. The
8
+ * consumer is responsible for providing the key prop on the returned element.
9
+ */
10
+ children: (item: T, index: number) => React.ReactNode;
11
+ /**
12
+ * Number of items to show initially and to add on each scroll.
13
+ * @default 20
14
+ */
15
+ batchSize?: number;
16
+ /**
17
+ * How many items to keep below the marker for seamless scrolling. The marker
18
+ * is placed `threshold` items before the end of visible items.
19
+ * @default 5
20
+ */
21
+ threshold?: number;
22
+ /**
23
+ * IntersectionObserver root margin.
24
+ * @default "20px"
25
+ */
26
+ rootMargin?: string;
27
+ /**
28
+ * Callback when visible count changes. Useful for displaying count in header.
29
+ */
30
+ onVisibleCountChange?: (visibleCount: number, totalItems: number) => void;
31
+ /**
32
+ * CSS class to apply to the body.
33
+ */
34
+ className?: string;
35
+ };
36
+ /**
37
+ * Ref handle for DataGridInfiniteBody, exposing imperative methods.
38
+ */
39
+ export type DataGridInfiniteBodyRef = {
40
+ /**
41
+ * Scroll to a specific row index with smooth animation. If the row is not yet
42
+ * visible, it will expand the visible count first.
43
+ * @param index - The index of the row to scroll to
44
+ */
45
+ scrollToIndex: (index: number) => void;
46
+ };
47
+ /**
48
+ * DataGridInfiniteBody with forwardRef support for imperative methods.
49
+ */
50
+ export declare const DataGridInfiniteBody: <T>(props: DataGridInfiniteBodyProps<T> & {
51
+ ref?: React.ForwardedRef<DataGridInfiniteBodyRef>;
52
+ }) => React.ReactElement;
@@ -0,0 +1,335 @@
1
+ /*!
2
+ @versini/ui-datagrid v0.4.1
3
+ © 2026 gizmette.com
4
+ */
5
+
6
+ import { jsx } from "react/jsx-runtime";
7
+ import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
8
+ import { DataGridContext } from "../DataGrid/DataGridContext.js";
9
+ import { getBodyClass } from "../DataGridBody/getBodyClass.js";
10
+ import { useColumnMeasurement } from "../DataGridBody/useColumnMeasurement.js";
11
+ import { CellWrapper } from "../DataGridConstants/index.js";
12
+
13
+ ;// CONCATENATED MODULE: external "react/jsx-runtime"
14
+
15
+ ;// CONCATENATED MODULE: external "react"
16
+
17
+ ;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
18
+
19
+ ;// CONCATENATED MODULE: external "../DataGridBody/getBodyClass.js"
20
+
21
+ ;// CONCATENATED MODULE: external "../DataGridBody/useColumnMeasurement.js"
22
+
23
+ ;// CONCATENATED MODULE: external "../DataGridConstants/index.js"
24
+
25
+ ;// CONCATENATED MODULE: ./src/DataGridInfinite/DataGridInfiniteBody.tsx
26
+
27
+
28
+
29
+
30
+
31
+
32
+ const DEFAULT_BATCH_SIZE = 20;
33
+ const DEFAULT_THRESHOLD = 5;
34
+ const DEFAULT_ROOT_MARGIN = "20px";
35
+ const ROW_INDEX_DATA_ATTR = "data-row-index";
36
+ /**
37
+ * Finds the nearest scrollable ancestor of an element. Returns null if no
38
+ * scrollable ancestor is found (uses viewport).
39
+ */ function findScrollableAncestor(element) {
40
+ let parent = element.parentElement;
41
+ while(parent){
42
+ const style = getComputedStyle(parent);
43
+ const overflowY = style.overflowY;
44
+ if (overflowY === "auto" || overflowY === "scroll") {
45
+ return parent;
46
+ }
47
+ parent = parent.parentElement;
48
+ }
49
+ return null;
50
+ }
51
+ /**
52
+ * A DataGridBody variant that handles infinite scroll internally.
53
+ *
54
+ * This component manages all the complexity of infinite scroll:
55
+ * - Progressive data loading with IntersectionObserver
56
+ * - Correct marker placement for seamless scrolling
57
+ * - Automatic data slicing and memoization
58
+ * - Programmatic scroll-to-row with smooth animation
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * const infiniteBodyRef = useRef<DataGridInfiniteBodyRef>(null);
63
+ *
64
+ * // Jump to a specific row
65
+ * const handleJumpToRow = () => {
66
+ * infiniteBodyRef.current?.scrollToIndex(134);
67
+ * };
68
+ *
69
+ * <DataGrid maxHeight="400px" stickyHeader>
70
+ * <DataGridHeader caption={`Showing ${visibleCount} of ${data.length}`}>
71
+ * <DataGridRow>
72
+ * <DataGridCell>Name</DataGridCell>
73
+ * <DataGridCell>Role</DataGridCell>
74
+ * </DataGridRow>
75
+ * </DataGridHeader>
76
+ *
77
+ * <DataGridInfiniteBody
78
+ * ref={infiniteBodyRef}
79
+ * data={largeData}
80
+ * batchSize={25}
81
+ * onVisibleCountChange={(count) => setVisibleCount(count)}
82
+ * >
83
+ * {(row) => (
84
+ * <DataGridRow key={row.id}>
85
+ * <DataGridCell>{row.name}</DataGridCell>
86
+ * <DataGridCell>{row.role}</DataGridCell>
87
+ * </DataGridRow>
88
+ * )}
89
+ * </DataGridInfiniteBody>
90
+ * </DataGrid>
91
+ * ```
92
+ *
93
+ */ function DataGridInfiniteBodyInner({ data, children: renderRow, batchSize = DEFAULT_BATCH_SIZE, threshold = DEFAULT_THRESHOLD, rootMargin = DEFAULT_ROOT_MARGIN, onVisibleCountChange, className }, ref) {
94
+ const ctx = useContext(DataGridContext);
95
+ const bodyRef = useRef(null);
96
+ const observerRef = useRef(null);
97
+ const pendingScrollRef = useRef(null);
98
+ const totalItems = data.length;
99
+ const initialCount = Math.min(batchSize + threshold, totalItems);
100
+ const [visibleCount, setVisibleCount] = useState(initialCount);
101
+ const hasMore = visibleCount < totalItems;
102
+ /**
103
+ * Scroll to a row by its index. Called after visibleCount updates.
104
+ * NOTE: We query for the wrapper with data-row-index, then scroll to the
105
+ * actual row element inside it (since the wrapper has display:contents and
106
+ * doesn't have a bounding box).
107
+ */ const scrollToRowElement = useCallback((index)=>{
108
+ const body = bodyRef.current;
109
+ if (!body) {
110
+ return;
111
+ }
112
+ const wrapper = body.querySelector(`[${ROW_INDEX_DATA_ATTR}="${index}"]`);
113
+ if (wrapper) {
114
+ // The wrapper has display:contents, so scroll to the actual row inside.
115
+ const row = wrapper.querySelector('[role="row"]');
116
+ if (row) {
117
+ row.scrollIntoView({
118
+ behavior: "smooth",
119
+ block: "center"
120
+ });
121
+ }
122
+ }
123
+ }, []);
124
+ /**
125
+ * Handle pending scroll after render (when visibleCount has been expanded).
126
+ */ useEffect(()=>{
127
+ if (pendingScrollRef.current !== null) {
128
+ const targetIndex = pendingScrollRef.current;
129
+ // Only scroll if the target is now within visible range.
130
+ if (targetIndex < visibleCount) {
131
+ // Use requestAnimationFrame to ensure DOM has updated.
132
+ requestAnimationFrame(()=>{
133
+ scrollToRowElement(targetIndex);
134
+ });
135
+ pendingScrollRef.current = null;
136
+ }
137
+ }
138
+ }, [
139
+ visibleCount,
140
+ scrollToRowElement
141
+ ]);
142
+ /**
143
+ * Expose imperative methods via ref.
144
+ */ useImperativeHandle(ref, ()=>({
145
+ scrollToIndex: (index)=>{
146
+ if (index < 0 || index >= totalItems) {
147
+ console.warn(`scrollToIndex: index ${index} is out of bounds (0-${totalItems - 1})`);
148
+ return;
149
+ }
150
+ // If the row is already visible, just scroll to it.
151
+ if (index < visibleCount) {
152
+ scrollToRowElement(index);
153
+ return;
154
+ }
155
+ /**
156
+ * Otherwise, expand visible count to include the target row. Add some
157
+ * buffer so the row isn't at the very edge.
158
+ */ const targetCount = Math.min(index + threshold + 1, totalItems);
159
+ pendingScrollRef.current = index;
160
+ setVisibleCount(targetCount);
161
+ }
162
+ }), [
163
+ totalItems,
164
+ visibleCount,
165
+ threshold,
166
+ scrollToRowElement
167
+ ]);
168
+ // Reset visible count when data changes significantly.
169
+ useEffect(()=>{
170
+ setVisibleCount(Math.min(batchSize + threshold, totalItems));
171
+ }, [
172
+ totalItems,
173
+ batchSize,
174
+ threshold
175
+ ]);
176
+ // Notify parent of visible count changes.
177
+ useEffect(()=>{
178
+ onVisibleCountChange?.(visibleCount, totalItems);
179
+ }, [
180
+ visibleCount,
181
+ totalItems,
182
+ onVisibleCountChange
183
+ ]);
184
+ /**
185
+ * IntersectionObserver callback - triggered when marker becomes visible. Loads
186
+ * the next batch of items.
187
+ */ const handleIntersection = useCallback((entries)=>{
188
+ const target = entries[0];
189
+ if (target?.isIntersecting) {
190
+ setVisibleCount((prev)=>Math.min(prev + batchSize, totalItems));
191
+ }
192
+ }, [
193
+ batchSize,
194
+ totalItems
195
+ ]);
196
+ /**
197
+ * Callback ref for the marker element. Sets up IntersectionObserver when
198
+ * marker mounts, cleans up when it unmounts. This pattern ensures the observer
199
+ * always watches the current marker element, even when visibleCount changes
200
+ * and a new marker is created at a different position.
201
+ */ const markerRefCallback = useCallback((node)=>{
202
+ // Clean up previous observer.
203
+ if (observerRef.current) {
204
+ observerRef.current.disconnect();
205
+ observerRef.current = null;
206
+ }
207
+ // Set up new observer if we have a marker and more items to load.
208
+ if (node && hasMore) {
209
+ const root = findScrollableAncestor(node);
210
+ observerRef.current = new IntersectionObserver(handleIntersection, {
211
+ root,
212
+ rootMargin
213
+ });
214
+ observerRef.current.observe(node);
215
+ }
216
+ }, [
217
+ hasMore,
218
+ handleIntersection,
219
+ rootMargin
220
+ ]);
221
+ // Clean up observer on unmount.
222
+ useEffect(()=>{
223
+ return ()=>{
224
+ observerRef.current?.disconnect();
225
+ };
226
+ }, []);
227
+ /**
228
+ * Calculate marker position. The marker should be placed `threshold` items
229
+ * from the end of visible items. This allows seamless scrolling - new items
230
+ * load while user scrolls through the remaining `threshold` items.
231
+ */ const markerPosition = useMemo(()=>{
232
+ if (!hasMore) {
233
+ return -1; // No marker needed when all items are loaded.
234
+ }
235
+ // Place marker at visibleCount - threshold, but ensure it's at least 0.
236
+ return Math.max(0, visibleCount - threshold);
237
+ }, [
238
+ hasMore,
239
+ visibleCount,
240
+ threshold
241
+ ]);
242
+ /**
243
+ * Context value for body rows (shared base, rowIndex added per-row).
244
+ */ const bodyContextBase = useMemo(()=>({
245
+ ...ctx,
246
+ cellWrapper: CellWrapper.BODY
247
+ }), [
248
+ ctx
249
+ ]);
250
+ /**
251
+ * Render items with marker at the correct position. Each row gets a
252
+ * data-row-index attribute for scrollToIndex functionality. Each row is
253
+ * wrapped with a context provider that includes the row index for proper
254
+ * odd/even styling (CSS :nth-child doesn't work with wrappers).
255
+ */ const renderedContent = useMemo(()=>{
256
+ const result = [];
257
+ // Determine the actual last visible index (for border styling).
258
+ const lastVisibleIndex = Math.min(visibleCount, totalItems) - 1;
259
+ for(let i = 0; i < visibleCount && i < totalItems; i++){
260
+ // Insert marker at the calculated position.
261
+ if (i === markerPosition) {
262
+ result.push(/*#__PURE__*/ jsx("div", {
263
+ ref: markerRefCallback,
264
+ "aria-hidden": "true",
265
+ style: {
266
+ height: "1px",
267
+ background: "transparent"
268
+ }
269
+ }, "__infinite-scroll-marker-inline__"));
270
+ }
271
+ /**
272
+ * Determine if this is the last row (only when all data is loaded). If
273
+ * hasMore is true, no row is "last" since more will be loaded.
274
+ */ const isLastRow = !hasMore && i === lastVisibleIndex;
275
+ /**
276
+ * Wrap row with context provider that includes rowIndex for proper odd/even
277
+ * styling. Using display:contents so the wrapper doesn't affect grid layout.
278
+ */ result.push(/*#__PURE__*/ jsx(DataGridContext.Provider, {
279
+ value: {
280
+ ...bodyContextBase,
281
+ rowIndex: i,
282
+ isLastRow
283
+ },
284
+ children: /*#__PURE__*/ jsx("div", {
285
+ [ROW_INDEX_DATA_ATTR]: i,
286
+ style: {
287
+ display: "contents"
288
+ },
289
+ children: renderRow(data[i], i)
290
+ })
291
+ }, i));
292
+ }
293
+ // If marker position is at the end (edge case with small data).
294
+ if (markerPosition === visibleCount && hasMore) {
295
+ result.push(/*#__PURE__*/ jsx("div", {
296
+ ref: markerRefCallback,
297
+ "aria-hidden": "true",
298
+ style: {
299
+ height: "1px",
300
+ background: "transparent"
301
+ }
302
+ }, "__infinite-scroll-marker-end__"));
303
+ }
304
+ return result;
305
+ }, [
306
+ data,
307
+ visibleCount,
308
+ totalItems,
309
+ markerPosition,
310
+ hasMore,
311
+ renderRow,
312
+ markerRefCallback,
313
+ bodyContextBase
314
+ ]);
315
+ // Measure column widths for sticky header/footer sync.
316
+ useColumnMeasurement(bodyRef, renderedContent);
317
+ const bodyClass = getBodyClass(Boolean(ctx.columns), className);
318
+ return /*#__PURE__*/ jsx(DataGridContext.Provider, {
319
+ value: {
320
+ ...ctx,
321
+ cellWrapper: CellWrapper.BODY
322
+ },
323
+ children: /*#__PURE__*/ jsx("div", {
324
+ ref: bodyRef,
325
+ role: "rowgroup",
326
+ className: bodyClass,
327
+ children: renderedContent
328
+ })
329
+ });
330
+ }
331
+ /**
332
+ * DataGridInfiniteBody with forwardRef support for imperative methods.
333
+ */ const DataGridInfiniteBody = /*#__PURE__*/ forwardRef(DataGridInfiniteBodyInner);
334
+
335
+ export { DataGridInfiniteBody };
@@ -1,4 +1,2 @@
1
- export type { InfiniteScrollMarkerProps } from "./InfiniteScrollMarker";
2
- export { InfiniteScrollMarker } from "./InfiniteScrollMarker";
3
- export type { UseInfiniteScrollOptions, UseInfiniteScrollReturn, } from "./useInfiniteScroll";
4
- export { useInfiniteScroll } from "./useInfiniteScroll";
1
+ export type { DataGridInfiniteBodyProps, DataGridInfiniteBodyRef, } from "./DataGridInfiniteBody";
2
+ export { DataGridInfiniteBody } from "./DataGridInfiniteBody";
@@ -1,17 +1,13 @@
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
- import { InfiniteScrollMarker } from "./InfiniteScrollMarker.js";
7
- import { useInfiniteScroll } from "./useInfiniteScroll.js";
6
+ import { DataGridInfiniteBody } from "./DataGridInfiniteBody.js";
8
7
 
9
- ;// CONCATENATED MODULE: external "./InfiniteScrollMarker.js"
10
-
11
- ;// CONCATENATED MODULE: external "./useInfiniteScroll.js"
8
+ ;// CONCATENATED MODULE: external "./DataGridInfiniteBody.js"
12
9
 
13
10
  ;// CONCATENATED MODULE: ./src/DataGridInfinite/index.ts
14
11
 
15
12
 
16
-
17
- export { InfiniteScrollMarker, useInfiniteScroll };
13
+ export { DataGridInfiniteBody };
@@ -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
 
@@ -31,7 +31,7 @@ import { getRowClasses } from "../utilities/classes.js";
31
31
  // Count the number of direct children to determine column count.
32
32
  const columnCount = react.Children.count(children);
33
33
  return /*#__PURE__*/ jsx(DataGridContext.Consumer, {
34
- children: ({ mode, cellWrapper, stickyHeader, stickyFooter, columns, measuredColumnWidths })=>{
34
+ children: ({ mode, cellWrapper, stickyHeader, stickyFooter, columns, measuredColumnWidths, rowIndex, isLastRow })=>{
35
35
  /**
36
36
  * Determine if this row is inside a sticky header/footer. Sticky elements
37
37
  * are absolutely positioned and outside the main grid flow, so they can't
@@ -88,7 +88,9 @@ import { getRowClasses } from "../utilities/classes.js";
88
88
  className: getRowClasses({
89
89
  mode,
90
90
  className,
91
- cellWrapper
91
+ cellWrapper,
92
+ rowIndex,
93
+ isLastRow
92
94
  }),
93
95
  style: {
94
96
  ...rowStyle,
@@ -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,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