@versini/ui-datagrid 0.3.7 → 0.4.0

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 (46) hide show
  1. package/README.md +106 -29
  2. package/dist/DataGrid/DataGrid.js +7 -7
  3. package/dist/DataGrid/DataGridContext.js +1 -1
  4. package/dist/DataGrid/index.js +1 -1
  5. package/dist/DataGridAnimated/AnimatedWrapper.d.ts +11 -7
  6. package/dist/DataGridAnimated/AnimatedWrapper.js +12 -8
  7. package/dist/DataGridAnimated/index.js +1 -1
  8. package/dist/DataGridAnimated/useAnimatedHeight.js +1 -1
  9. package/dist/DataGridBody/DataGridBody.js +12 -54
  10. package/dist/DataGridBody/getBodyClass.d.ts +9 -0
  11. package/dist/DataGridBody/getBodyClass.js +23 -0
  12. package/dist/DataGridBody/index.js +1 -1
  13. package/dist/DataGridBody/useColumnMeasurement.d.ts +10 -0
  14. package/dist/DataGridBody/useColumnMeasurement.js +67 -0
  15. package/dist/DataGridCell/DataGridCell.d.ts +0 -10
  16. package/dist/DataGridCell/DataGridCell.js +4 -58
  17. package/dist/DataGridCell/index.js +1 -1
  18. package/dist/DataGridCellSort/DataGridCellSort.js +3 -3
  19. package/dist/DataGridCellSort/index.js +1 -1
  20. package/dist/DataGridConstants/DataGridConstants.js +1 -1
  21. package/dist/DataGridConstants/index.js +1 -1
  22. package/dist/DataGridFooter/DataGridFooter.d.ts +0 -15
  23. package/dist/DataGridFooter/DataGridFooter.js +6 -42
  24. package/dist/DataGridFooter/index.js +1 -1
  25. package/dist/DataGridHeader/DataGridHeader.d.ts +0 -27
  26. package/dist/DataGridHeader/DataGridHeader.js +6 -54
  27. package/dist/DataGridHeader/index.js +1 -1
  28. package/dist/DataGridInfinite/DataGridInfiniteBody.d.ts +52 -0
  29. package/dist/DataGridInfinite/DataGridInfiniteBody.js +309 -0
  30. package/dist/DataGridInfinite/index.d.ts +2 -4
  31. package/dist/DataGridInfinite/index.js +4 -8
  32. package/dist/DataGridRow/DataGridRow.js +10 -34
  33. package/dist/DataGridRow/index.js +1 -1
  34. package/dist/DataGridSorting/index.js +1 -1
  35. package/dist/DataGridSorting/sortingUtils.js +1 -1
  36. package/dist/utilities/classes.d.ts +150 -0
  37. package/dist/utilities/classes.js +249 -0
  38. package/package.json +2 -2
  39. package/dist/DataGrid/utilities.d.ts +0 -44
  40. package/dist/DataGrid/utilities.js +0 -92
  41. package/dist/DataGridInfinite/InfiniteScrollMarker.d.ts +0 -31
  42. package/dist/DataGridInfinite/InfiniteScrollMarker.js +0 -54
  43. package/dist/DataGridInfinite/useInfiniteScroll.d.ts +0 -92
  44. package/dist/DataGridInfinite/useInfiniteScroll.js +0 -136
  45. package/dist/common/utilities.d.ts +0 -15
  46. package/dist/common/utilities.js +0 -48
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,13 +1,13 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.7
2
+ @versini/ui-datagrid v0.4.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { useCallback, useMemo, useState } from "react";
8
8
  import { BlurEffects } from "../DataGridConstants/DataGridConstants.js";
9
+ import { getDataGridClasses } from "../utilities/classes.js";
9
10
  import { DataGridContext } from "./DataGridContext.js";
10
- import { getDataGridClasses } from "./utilities.js";
11
11
 
12
12
  ;// CONCATENATED MODULE: external "react/jsx-runtime"
13
13
 
@@ -15,9 +15,9 @@ import { getDataGridClasses } from "./utilities.js";
15
15
 
16
16
  ;// CONCATENATED MODULE: external "../DataGridConstants/DataGridConstants.js"
17
17
 
18
- ;// CONCATENATED MODULE: external "./DataGridContext.js"
18
+ ;// CONCATENATED MODULE: external "../utilities/classes.js"
19
19
 
20
- ;// CONCATENATED MODULE: external "./utilities.js"
20
+ ;// CONCATENATED MODULE: external "./DataGridContext.js"
21
21
 
22
22
  ;// CONCATENATED MODULE: ./src/DataGrid/DataGrid.tsx
23
23
 
@@ -42,9 +42,9 @@ import { getDataGridClasses } from "./utilities.js";
42
42
  */ const [headerHeight, setHeaderHeight] = useState(0);
43
43
  const [footerHeight, setFooterHeight] = useState(0);
44
44
  /**
45
- * Track measured column widths from the body. Used by sticky header/footer
46
- * to sync column widths since absolutely positioned elements can't use
47
- * CSS subgrid.
45
+ * Track measured column widths from the body. Used by sticky header/footer to
46
+ * sync column widths since absolutely positioned elements can't use CSS
47
+ * subgrid.
48
48
  */ const [measuredColumnWidths, setMeasuredColumnWidths] = useState([]);
49
49
  /**
50
50
  * Registration callbacks with stable references. Called by
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.7
2
+ @versini/ui-datagrid v0.4.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.7
2
+ @versini/ui-datagrid v0.4.0
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.7
2
+ @versini/ui-datagrid v0.4.0
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.7
2
+ @versini/ui-datagrid v0.4.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.7
2
+ @versini/ui-datagrid v0.4.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,84 +1,42 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.7
2
+ @versini/ui-datagrid v0.4.0
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
36
- * CSS subgrid. We measure the body cells (which ARE in the grid flow)
37
- * and report 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
- // Find the first row and its cells once, reuse for both measurement and observation
45
- const firstRow = element.querySelector('[role="row"]');
46
- if (!firstRow) {
47
- return;
48
- }
49
- const cells = firstRow.querySelectorAll('[role="cell"], [role="columnheader"], [role="gridcell"]');
50
- if (cells.length === 0) {
51
- return;
52
- }
53
- const measureColumns = ()=>{
54
- // Measure each cell's width
55
- const widths = Array.from(cells).map((cell)=>cell.getBoundingClientRect().width);
56
- ctx.setMeasuredColumnWidths?.(widths);
57
- };
58
- // Initial measurement
59
- measureColumns();
60
- // Set up ResizeObserver to re-measure when cells resize
61
- const observer = new ResizeObserver(()=>{
62
- measureColumns();
63
- });
64
- // Observe the body element for any size changes
65
- observer.observe(element);
66
- // Also observe the first row's cells directly for more accurate updates
67
- for (const cell of cells){
68
- observer.observe(cell);
69
- }
70
- return ()=>observer.disconnect();
71
- }, [
72
- ctx.columns,
73
- ctx.stickyHeader,
74
- ctx.stickyFooter,
75
- ctx.setMeasuredColumnWidths,
76
- children
77
- ]);
78
- /**
79
- * When columns are provided, use display:contents so the body doesn't
80
- * interfere with the grid flow. Rows will use subgrid.
81
- */ 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);
82
40
  return /*#__PURE__*/ jsx(DataGridContext.Provider, {
83
41
  value: {
84
42
  ...ctx,
@@ -0,0 +1,9 @@
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.
5
+ *
6
+ * @param hasColumns - Whether the DataGrid has columns defined
7
+ * @param className - Additional class name to merge
8
+ */
9
+ export declare function getBodyClass(hasColumns: boolean | undefined, className?: string): string;
@@ -0,0 +1,23 @@
1
+ /*!
2
+ @versini/ui-datagrid v0.4.0
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.
14
+ * When columns are provided, use display:contents so the body doesn't
15
+ * interfere with the grid flow. Rows will use subgrid.
16
+ *
17
+ * @param hasColumns - Whether the DataGrid has columns defined
18
+ * @param className - Additional class name to merge
19
+ */ function getBodyClass(hasColumns, className) {
20
+ return hasColumns ? clsx("contents", className) : clsx("flex flex-col", className);
21
+ }
22
+
23
+ export { getBodyClass };
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.7
2
+ @versini/ui-datagrid v0.4.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -0,0 +1,10 @@
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.
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
+ export declare function useColumnMeasurement(bodyRef: React.RefObject<HTMLDivElement | null>, contentDependency: unknown): void;
@@ -0,0 +1,67 @@
1
+ /*!
2
+ @versini/ui-datagrid v0.4.0
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.
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.
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
+ */ function useColumnMeasurement(bodyRef, contentDependency) {
25
+ const ctx = useContext(DataGridContext);
26
+ // biome-ignore lint/correctness/useExhaustiveDependencies: contentDependency triggers remeasurement when rows change
27
+ useLayoutEffect(()=>{
28
+ const element = bodyRef.current;
29
+ const needsMeasurement = ctx.columns && (ctx.stickyHeader || ctx.stickyFooter);
30
+ if (!element || !needsMeasurement || !ctx.setMeasuredColumnWidths) {
31
+ return;
32
+ }
33
+ const firstRow = element.querySelector('[role="row"]');
34
+ if (!firstRow) {
35
+ return;
36
+ }
37
+ const cells = firstRow.querySelectorAll('[role="cell"], [role="columnheader"], [role="gridcell"]');
38
+ if (cells.length === 0) {
39
+ return;
40
+ }
41
+ const measureColumns = ()=>{
42
+ const widths = Array.from(cells).map((cell)=>cell.getBoundingClientRect().width);
43
+ ctx.setMeasuredColumnWidths?.(widths);
44
+ };
45
+ // Initial measurement.
46
+ measureColumns();
47
+ // Set up ResizeObserver to re-measure when cells resize.
48
+ const observer = new ResizeObserver(()=>{
49
+ measureColumns();
50
+ });
51
+ // Observe the body element for any size changes.
52
+ observer.observe(element);
53
+ // Also observe the first row's cells directly for more accurate updates.
54
+ for (const cell of cells){
55
+ observer.observe(cell);
56
+ }
57
+ return ()=>observer.disconnect();
58
+ }, [
59
+ ctx.columns,
60
+ ctx.stickyHeader,
61
+ ctx.stickyFooter,
62
+ ctx.setMeasuredColumnWidths,
63
+ contentDependency
64
+ ]);
65
+ }
66
+
67
+ export { useColumnMeasurement };
@@ -1,12 +1,2 @@
1
1
  import type { DataGridCellProps } from "../DataGrid/DataGridTypes";
2
- import { type CellWrapperType, type ThemeMode } from "../DataGridConstants";
3
- export declare const getCellClasses: ({ cellWrapper, className, compact, align, mode, borderLeft, borderRight, }: {
4
- cellWrapper?: CellWrapperType;
5
- className?: string;
6
- mode?: ThemeMode;
7
- compact?: boolean;
8
- align?: "left" | "center" | "right";
9
- borderLeft?: boolean;
10
- borderRight?: boolean;
11
- }) => string;
12
2
  export declare const DataGridCell: ({ className, children, align, borderLeft, borderRight, colSpan, style, ...rest }: DataGridCellProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,76 +1,22 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.7
2
+ @versini/ui-datagrid v0.4.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import { jsx } from "react/jsx-runtime";
7
- import { clsx } from "clsx";
8
7
  import { DataGridContext } from "../DataGrid/DataGridContext.js";
9
- import { CellWrapper } from "../DataGridConstants/index.js";
8
+ import { getCellClasses, getCellRole } from "../utilities/classes.js";
10
9
 
11
10
  ;// CONCATENATED MODULE: external "react/jsx-runtime"
12
11
 
13
- ;// CONCATENATED MODULE: external "clsx"
14
-
15
12
  ;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
16
13
 
17
- ;// CONCATENATED MODULE: external "../DataGridConstants/index.js"
14
+ ;// CONCATENATED MODULE: external "../utilities/classes.js"
18
15
 
19
16
  ;// CONCATENATED MODULE: ./src/DataGridCell/DataGridCell.tsx
20
17
 
21
18
 
22
19
 
23
-
24
- const getCellClasses = ({ cellWrapper, className, compact, align, mode, borderLeft, borderRight })=>{
25
- const isHeader = cellWrapper === CellWrapper.HEADER;
26
- const mainClasses = clsx(// Base padding.
27
- {
28
- "px-2 py-1": compact,
29
- "px-4 py-3": !compact
30
- }, // Text alignment.
31
- {
32
- "text-left justify-start": align === "left" || !align,
33
- "text-center justify-center": align === "center",
34
- "text-right justify-end": align === "right"
35
- }, // Header/footer specific styles.
36
- {
37
- "font-semibold": isHeader
38
- }, /**
39
- * Active row indicator (left border on first cell only).
40
- * Uses CSS group-data-[active] to detect when parent row has data-active attribute.
41
- * The `first:` variant ensures only the first cell in the row shows the indicator.
42
- */ "first:group-data-[active]:relative", "first:group-data-[active]:before:absolute first:group-data-[active]:before:left-0 first:group-data-[active]:before:top-0 first:group-data-[active]:before:bottom-0 first:group-data-[active]:before:w-1", "first:group-data-[active]:self-stretch first:group-data-[active]:flex first:group-data-[active]:items-center", // Active indicator color based on theme mode.
43
- {
44
- "first:group-data-[active]:before:bg-table-active-dark": mode === "dark",
45
- "first:group-data-[active]:before:bg-table-active-light": mode === "light",
46
- "first:group-data-[active]:before:bg-table-active-dark dark:first:group-data-[active]:before:bg-table-active-light": mode === "system",
47
- "first:group-data-[active]:before:bg-table-active-light dark:first:group-data-[active]:before:bg-table-active-dark": mode === "alt-system"
48
- }, /**
49
- * Vertical borders. self-stretch ensures border spans full row height in
50
- * grid layout. When stretching, use flex + items-center to maintain
51
- * vertical centering of content.
52
- */ {
53
- "self-stretch flex items-center": borderLeft || borderRight,
54
- "border-l border-l-table-dark": borderLeft && mode === "dark",
55
- "border-l border-l-table-light": borderLeft && mode === "light",
56
- "border-l border-l-table-dark dark:border-l-table-light": borderLeft && mode === "system",
57
- "border-l border-l-table-light dark:border-l-table-dark": borderLeft && mode === "alt-system",
58
- "border-r border-r-table-dark": borderRight && mode === "dark",
59
- "border-r border-r-table-light": borderRight && mode === "light",
60
- "border-r border-r-table-dark dark:border-r-table-light": borderRight && mode === "system",
61
- "border-r border-r-table-light dark:border-r-table-dark": borderRight && mode === "alt-system"
62
- }, className);
63
- return mainClasses;
64
- };
65
- /**
66
- * Returns the appropriate ARIA role for the cell based on the cell wrapper
67
- * type.
68
- */ const getCellRole = (cellWrapper)=>{
69
- if (cellWrapper === CellWrapper.HEADER) {
70
- return "columnheader";
71
- }
72
- return "gridcell";
73
- };
74
20
  /* =============================================================================
75
21
  * DataGridCell
76
22
  * ========================================================================== */ const DataGridCell = ({ className, children, align, borderLeft, borderRight, colSpan, style, ...rest })=>{
@@ -102,4 +48,4 @@ const getCellClasses = ({ cellWrapper, className, compact, align, mode, borderLe
102
48
  });
103
49
  };
104
50
 
105
- export { DataGridCell, getCellClasses };
51
+ export { DataGridCell };
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.7
2
+ @versini/ui-datagrid v0.4.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5