@versini/ui-datagrid 0.4.0 → 0.4.2

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 (34) hide show
  1. package/dist/DataGrid/DataGrid.js +1 -1
  2. package/dist/DataGrid/DataGridContext.js +1 -1
  3. package/dist/DataGrid/DataGridTypes.d.ts +280 -0
  4. package/dist/DataGrid/DataGridTypes.js +9 -0
  5. package/dist/DataGrid/index.js +1 -1
  6. package/dist/DataGridAnimated/AnimatedWrapper.js +1 -1
  7. package/dist/DataGridAnimated/index.js +1 -1
  8. package/dist/DataGridAnimated/useAnimatedHeight.js +1 -1
  9. package/dist/DataGridBody/DataGridBody.js +1 -1
  10. package/dist/DataGridBody/getBodyClass.d.ts +4 -3
  11. package/dist/DataGridBody/getBodyClass.js +5 -4
  12. package/dist/DataGridBody/index.js +1 -1
  13. package/dist/DataGridBody/useColumnMeasurement.d.ts +5 -4
  14. package/dist/DataGridBody/useColumnMeasurement.js +15 -7
  15. package/dist/DataGridCell/DataGridCell.js +1 -1
  16. package/dist/DataGridCell/index.js +1 -1
  17. package/dist/DataGridCellSort/DataGridCellSort.js +1 -1
  18. package/dist/DataGridCellSort/index.js +1 -1
  19. package/dist/DataGridConstants/DataGridConstants.js +1 -1
  20. package/dist/DataGridConstants/index.js +1 -1
  21. package/dist/DataGridFooter/DataGridFooter.js +1 -1
  22. package/dist/DataGridFooter/index.js +1 -1
  23. package/dist/DataGridHeader/DataGridHeader.js +1 -1
  24. package/dist/DataGridHeader/index.js +1 -1
  25. package/dist/DataGridInfinite/DataGridInfiniteBody.d.ts +6 -6
  26. package/dist/DataGridInfinite/DataGridInfiniteBody.js +55 -29
  27. package/dist/DataGridInfinite/index.js +1 -1
  28. package/dist/DataGridRow/DataGridRow.js +5 -3
  29. package/dist/DataGridRow/index.js +1 -1
  30. package/dist/DataGridSorting/index.js +1 -1
  31. package/dist/DataGridSorting/sortingUtils.js +1 -1
  32. package/dist/utilities/classes.d.ts +6 -2
  33. package/dist/utilities/classes.js +35 -8
  34. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
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.2
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.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Get the CSS class for a DataGrid body element.
3
- * When columns are provided, use display:contents so the body doesn't
4
- * interfere with the grid flow. Rows will use subgrid.
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
5
  *
6
6
  * @param hasColumns - Whether the DataGrid has columns defined
7
7
  * @param className - Additional class name to merge
8
+ *
8
9
  */
9
10
  export declare function getBodyClass(hasColumns: boolean | undefined, className?: string): string;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -10,12 +10,13 @@ import clsx from "clsx";
10
10
  ;// CONCATENATED MODULE: ./src/DataGridBody/getBodyClass.ts
11
11
 
12
12
  /**
13
- * Get the CSS class for a DataGrid body element.
14
- * When columns are provided, use display:contents so the body doesn't
15
- * interfere with the grid flow. Rows will use subgrid.
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
16
  *
17
17
  * @param hasColumns - Whether the DataGrid has columns defined
18
18
  * @param className - Additional class name to merge
19
+ *
19
20
  */ function getBodyClass(hasColumns, className) {
20
21
  return hasColumns ? clsx("contents", className) : clsx("flex flex-col", className);
21
22
  }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Hook to measure column widths from the first body row's cells.
3
- * This is needed because sticky header/footer are absolutely positioned
4
- * and can't use CSS subgrid. We measure the body cells (which ARE in the
5
- * grid flow) and report the widths so header/footer can use the same pixel values.
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
6
  *
7
7
  * @param bodyRef - Ref to the body element containing the rows
8
8
  * @param contentDependency - Dependency that changes when content changes (children or renderedContent)
9
+ *
9
10
  */
10
11
  export declare function useColumnMeasurement(bodyRef: React.RefObject<HTMLDivElement | null>, contentDependency: unknown): void;
@@ -1,9 +1,9 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
6
- import { useContext, useLayoutEffect } from "react";
6
+ import { useContext, useLayoutEffect, useRef } from "react";
7
7
  import { DataGridContext } from "../DataGrid/DataGridContext.js";
8
8
 
9
9
  ;// CONCATENATED MODULE: external "react"
@@ -14,15 +14,17 @@ import { DataGridContext } from "../DataGrid/DataGridContext.js";
14
14
 
15
15
 
16
16
  /**
17
- * Hook to measure column widths from the first body row's cells.
18
- * This is needed because sticky header/footer are absolutely positioned
19
- * and can't use CSS subgrid. We measure the body cells (which ARE in the
20
- * grid flow) and report the widths so header/footer can use the same pixel values.
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
21
  *
22
22
  * @param bodyRef - Ref to the body element containing the rows
23
23
  * @param contentDependency - Dependency that changes when content changes (children or renderedContent)
24
+ *
24
25
  */ function useColumnMeasurement(bodyRef, contentDependency) {
25
26
  const ctx = useContext(DataGridContext);
27
+ const prevWidthsRef = useRef([]);
26
28
  // biome-ignore lint/correctness/useExhaustiveDependencies: contentDependency triggers remeasurement when rows change
27
29
  useLayoutEffect(()=>{
28
30
  const element = bodyRef.current;
@@ -40,7 +42,13 @@ import { DataGridContext } from "../DataGrid/DataGridContext.js";
40
42
  }
41
43
  const measureColumns = ()=>{
42
44
  const widths = Array.from(cells).map((cell)=>cell.getBoundingClientRect().width);
43
- ctx.setMeasuredColumnWidths?.(widths);
45
+ // Only update state if widths have actually changed to prevent infinite loops.
46
+ const prevWidths = prevWidthsRef.current;
47
+ const hasChanged = widths.length !== prevWidths.length || widths.some((w, i)=>w !== prevWidths[i]);
48
+ if (hasChanged) {
49
+ prevWidthsRef.current = widths;
50
+ ctx.setMeasuredColumnWidths?.(widths);
51
+ }
44
52
  };
45
53
  // Initial measurement.
46
54
  measureColumns();
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -4,8 +4,8 @@ export type DataGridInfiniteBodyProps<T> = {
4
4
  */
5
5
  data: T[];
6
6
  /**
7
- * Render function for each row. Should return a DataGridRow element.
8
- * The consumer is responsible for providing the key prop on the returned element.
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
9
  */
10
10
  children: (item: T, index: number) => React.ReactNode;
11
11
  /**
@@ -14,8 +14,8 @@ export type DataGridInfiniteBodyProps<T> = {
14
14
  */
15
15
  batchSize?: number;
16
16
  /**
17
- * How many items to keep below the marker for seamless scrolling.
18
- * The marker is placed `threshold` items before the end of visible items.
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
19
  * @default 5
20
20
  */
21
21
  threshold?: number;
@@ -38,8 +38,8 @@ export type DataGridInfiniteBodyProps<T> = {
38
38
  */
39
39
  export type DataGridInfiniteBodyRef = {
40
40
  /**
41
- * Scroll to a specific row index with smooth animation.
42
- * If the row is not yet visible, it will expand the visible count first.
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
43
  * @param index - The index of the row to scroll to
44
44
  */
45
45
  scrollToIndex: (index: number) => void;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -34,8 +34,8 @@ const DEFAULT_THRESHOLD = 5;
34
34
  const DEFAULT_ROOT_MARGIN = "20px";
35
35
  const ROW_INDEX_DATA_ATTR = "data-row-index";
36
36
  /**
37
- * Finds the nearest scrollable ancestor of an element.
38
- * Returns null if no scrollable ancestor is found (uses viewport).
37
+ * Finds the nearest scrollable ancestor of an element. Returns null if no
38
+ * scrollable ancestor is found (uses viewport).
39
39
  */ function findScrollableAncestor(element) {
40
40
  let parent = element.parentElement;
41
41
  while(parent){
@@ -89,6 +89,7 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
89
89
  * </DataGridInfiniteBody>
90
90
  * </DataGrid>
91
91
  * ```
92
+ *
92
93
  */ function DataGridInfiniteBodyInner({ data, children: renderRow, batchSize = DEFAULT_BATCH_SIZE, threshold = DEFAULT_THRESHOLD, rootMargin = DEFAULT_ROOT_MARGIN, onVisibleCountChange, className }, ref) {
93
94
  const ctx = useContext(DataGridContext);
94
95
  const bodyRef = useRef(null);
@@ -100,9 +101,9 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
100
101
  const hasMore = visibleCount < totalItems;
101
102
  /**
102
103
  * Scroll to a row by its index. Called after visibleCount updates.
103
- * Note: We query for the wrapper with data-row-index, then scroll to the
104
- * actual row element inside it (since the wrapper has display:contents
105
- * and doesn't have a bounding box).
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).
106
107
  */ const scrollToRowElement = useCallback((index)=>{
107
108
  const body = bodyRef.current;
108
109
  if (!body) {
@@ -151,9 +152,10 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
151
152
  scrollToRowElement(index);
152
153
  return;
153
154
  }
154
- // Otherwise, expand visible count to include the target row.
155
- // Add some buffer so the row isn't at the very edge.
156
- const targetCount = Math.min(index + threshold + 1, totalItems);
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);
157
159
  pendingScrollRef.current = index;
158
160
  setVisibleCount(targetCount);
159
161
  }
@@ -180,8 +182,8 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
180
182
  onVisibleCountChange
181
183
  ]);
182
184
  /**
183
- * IntersectionObserver callback - triggered when marker becomes visible.
184
- * Loads the next batch of items.
185
+ * IntersectionObserver callback - triggered when marker becomes visible. Loads
186
+ * the next batch of items.
185
187
  */ const handleIntersection = useCallback((entries)=>{
186
188
  const target = entries[0];
187
189
  if (target?.isIntersecting) {
@@ -192,10 +194,10 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
192
194
  totalItems
193
195
  ]);
194
196
  /**
195
- * Callback ref for the marker element.
196
- * Sets up IntersectionObserver when marker mounts, cleans up when it unmounts.
197
- * This pattern ensures the observer always watches the current marker element,
198
- * even when visibleCount changes and a new marker is created at a different position.
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.
199
201
  */ const markerRefCallback = useCallback((node)=>{
200
202
  // Clean up previous observer.
201
203
  if (observerRef.current) {
@@ -223,10 +225,9 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
223
225
  };
224
226
  }, []);
225
227
  /**
226
- * Calculate marker position.
227
- * The marker should be placed `threshold` items from the end of visible items.
228
- * This allows seamless scrolling - new items load while user scrolls through
229
- * the remaining `threshold` items.
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.
230
231
  */ const markerPosition = useMemo(()=>{
231
232
  if (!hasMore) {
232
233
  return -1; // No marker needed when all items are loaded.
@@ -239,10 +240,22 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
239
240
  threshold
240
241
  ]);
241
242
  /**
242
- * Render items with marker at the correct position.
243
- * Each row gets a data-row-index attribute for scrollToIndex functionality.
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).
244
255
  */ const renderedContent = useMemo(()=>{
245
256
  const result = [];
257
+ // Determine the actual last visible index (for border styling).
258
+ const lastVisibleIndex = Math.min(visibleCount, totalItems) - 1;
246
259
  for(let i = 0; i < visibleCount && i < totalItems; i++){
247
260
  // Insert marker at the calculated position.
248
261
  if (i === markerPosition) {
@@ -255,14 +268,26 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
255
268
  }
256
269
  }, "__infinite-scroll-marker-inline__"));
257
270
  }
258
- // Wrap row with data attribute for scrollToIndex lookup.
259
- // Using display:contents so the wrapper doesn't affect grid layout.
260
- result.push(/*#__PURE__*/ jsx("div", {
261
- [ROW_INDEX_DATA_ATTR]: i,
262
- style: {
263
- display: "contents"
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
264
283
  },
265
- children: renderRow(data[i], i)
284
+ children: /*#__PURE__*/ jsx("div", {
285
+ [ROW_INDEX_DATA_ATTR]: i,
286
+ style: {
287
+ display: "contents"
288
+ },
289
+ children: renderRow(data[i], i)
290
+ })
266
291
  }, i));
267
292
  }
268
293
  // If marker position is at the end (edge case with small data).
@@ -284,7 +309,8 @@ const ROW_INDEX_DATA_ATTR = "data-row-index";
284
309
  markerPosition,
285
310
  hasMore,
286
311
  renderRow,
287
- markerRefCallback
312
+ markerRefCallback,
313
+ bodyContextBase
288
314
  ]);
289
315
  // Measure column widths for sticky header/footer sync.
290
316
  useColumnMeasurement(bodyRef, renderedContent);
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
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.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -124,12 +124,16 @@ export declare const getFooterClasses: ({ className, stickyFooter, mode, blurEff
124
124
  stickyFooter?: boolean;
125
125
  }) => string;
126
126
  /**
127
- * Generates classes for DataGridRow.
127
+ * Generates classes for DataGridRow. When rowIndex is provided (e.g., from
128
+ * DataGridInfiniteBody), explicit odd/even classes are used instead of CSS
129
+ * :nth-child selectors, which don't work with wrapper elements.
128
130
  */
129
- export declare const getRowClasses: ({ mode, className, cellWrapper, }: {
131
+ export declare const getRowClasses: ({ mode, className, cellWrapper, rowIndex, isLastRow, }: {
130
132
  mode: ThemeMode;
131
133
  cellWrapper?: CellWrapperType;
132
134
  className?: string;
135
+ rowIndex?: number;
136
+ isLastRow?: boolean;
133
137
  }) => string;
134
138
  /**
135
139
  * Generates classes for DataGridCell.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.4.0
2
+ @versini/ui-datagrid v0.4.2
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -181,22 +181,49 @@ import { BlurEffects, CellWrapper } from "../DataGridConstants/DataGridConstants
181
181
  }), className);
182
182
  };
183
183
  /**
184
- * Generates classes for DataGridRow.
185
- */ const getRowClasses = ({ mode, className, cellWrapper })=>{
184
+ * Generates classes for DataGridRow. When rowIndex is provided (e.g., from
185
+ * DataGridInfiniteBody), explicit odd/even classes are used instead of CSS
186
+ * :nth-child selectors, which don't work with wrapper elements.
187
+ */ const getRowClasses = ({ mode, className, cellWrapper, rowIndex, isLastRow })=>{
186
188
  const layoutClass = "group grid items-center";
187
189
  if (cellWrapper === CellWrapper.HEADER || cellWrapper === CellWrapper.FOOTER) {
188
190
  return clsx(layoutClass, className);
189
191
  }
190
- return clsx(layoutClass, "border-b last:border-0", getBorderClasses({
192
+ /**
193
+ * When rowIndex is provided, use explicit classes instead of CSS :nth-child
194
+ * selectors. CSS :nth-child doesn't work correctly when rows are wrapped
195
+ * (e.g., in DataGridInfiniteBody).
196
+ */ const hasExplicitIndex = rowIndex !== undefined;
197
+ const isOdd = hasExplicitIndex && rowIndex % 2 === 0; // 0-based index: 0,2,4 are visually "odd" rows (1st, 3rd, 5th)
198
+ const isEven = hasExplicitIndex && rowIndex % 2 === 1;
199
+ /**
200
+ * Border classes: use explicit border-0 for last row when isLastRow is
201
+ * provided, otherwise fall back to CSS :last-child selector.
202
+ */ const borderClasses = isLastRow !== undefined ? isLastRow ? "border-b border-b-transparent" // Last row: transparent border to maintain spacing
203
+ : "border-b" : "border-b last:border-0"; // Fallback to CSS :last-child
204
+ return clsx(layoutClass, borderClasses, getBorderClasses({
191
205
  mode
192
206
  }), {
193
- "odd:bg-table-dark-odd even:bg-table-dark-even": mode === "dark",
207
+ // Explicit odd/even when rowIndex is provided.
208
+ "bg-table-dark-odd": hasExplicitIndex && isOdd && mode === "dark",
209
+ "bg-table-dark-even": hasExplicitIndex && isEven && mode === "dark",
210
+ "bg-table-light-odd": hasExplicitIndex && isOdd && mode === "light",
211
+ "bg-table-light-even": hasExplicitIndex && isEven && mode === "light",
212
+ // System mode with explicit index.
213
+ "bg-table-dark-odd dark:bg-table-light-odd": hasExplicitIndex && isOdd && mode === "system",
214
+ "bg-table-dark-even dark:bg-table-light-even": hasExplicitIndex && isEven && mode === "system",
215
+ // Alt-system mode with explicit index.
216
+ "bg-table-light-odd dark:bg-table-dark-odd": hasExplicitIndex && isOdd && mode === "alt-system",
217
+ "bg-table-light-even dark:bg-table-dark-even": hasExplicitIndex && isEven && mode === "alt-system",
218
+ // CSS :nth-child selectors (original behavior when rowIndex not provided).
219
+ "odd:bg-table-dark-odd even:bg-table-dark-even": !hasExplicitIndex && mode === "dark",
220
+ "odd:bg-table-light-odd even:bg-table-light-even": !hasExplicitIndex && mode === "light",
221
+ "odd:bg-table-dark-odd even:bg-table-dark-even dark:odd:bg-table-light-odd dark:even:bg-table-light-even": !hasExplicitIndex && mode === "system",
222
+ "odd:bg-table-light-odd even:bg-table-light-even dark:odd:bg-table-dark-odd dark:even:bg-table-dark-even": !hasExplicitIndex && mode === "alt-system",
223
+ // Hover effects (same for both modes).
194
224
  "hover:bg-table-dark-hover": mode === "dark",
195
- "odd:bg-table-light-odd even:bg-table-light-even": mode === "light",
196
225
  "hover:bg-table-light-hover": mode === "light",
197
- "odd:bg-table-dark-odd even:bg-table-dark-even dark:odd:bg-table-light-odd dark:even:bg-table-light-even": mode === "system",
198
226
  "hover:bg-table-dark-hover dark:hover:bg-table-light-hover": mode === "system",
199
- "odd:bg-table-light-odd even:bg-table-light-even dark:odd:bg-table-dark-odd dark:even:bg-table-dark-even": mode === "alt-system",
200
227
  "hover:bg-table-light-hover dark:hover:bg-table-dark-hover": mode === "alt-system"
201
228
  }, className);
202
229
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-datagrid",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -94,5 +94,5 @@
94
94
  "sideEffects": [
95
95
  "**/*.css"
96
96
  ],
97
- "gitHead": "fa25a9204c1b506f6eed099cc11e321b53bb0f05"
97
+ "gitHead": "26d6f863e034c26395f8a67bb203297770864d52"
98
98
  }