@versini/ui-datagrid 0.3.1 → 0.3.3

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 (32) hide show
  1. package/dist/DataGrid/DataGrid.js +43 -28
  2. package/dist/DataGrid/DataGridContext.js +1 -1
  3. package/dist/DataGrid/index.js +1 -1
  4. package/dist/DataGrid/utilities.d.ts +12 -9
  5. package/dist/DataGrid/utilities.js +18 -27
  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 +67 -19
  10. package/dist/DataGridBody/index.js +1 -1
  11. package/dist/DataGridCell/DataGridCell.js +8 -6
  12. package/dist/DataGridCell/index.js +1 -1
  13. package/dist/DataGridCellSort/DataGridCellSort.js +2 -2
  14. package/dist/DataGridCellSort/index.js +1 -1
  15. package/dist/DataGridConstants/DataGridConstants.js +1 -1
  16. package/dist/DataGridConstants/index.js +1 -1
  17. package/dist/DataGridFooter/DataGridFooter.d.ts +3 -2
  18. package/dist/DataGridFooter/DataGridFooter.js +33 -9
  19. package/dist/DataGridFooter/index.js +1 -1
  20. package/dist/DataGridHeader/DataGridHeader.d.ts +8 -7
  21. package/dist/DataGridHeader/DataGridHeader.js +39 -15
  22. package/dist/DataGridHeader/index.js +1 -1
  23. package/dist/DataGridInfinite/InfiniteScrollMarker.js +1 -1
  24. package/dist/DataGridInfinite/index.js +1 -1
  25. package/dist/DataGridInfinite/useInfiniteScroll.d.ts +5 -5
  26. package/dist/DataGridInfinite/useInfiniteScroll.js +3 -3
  27. package/dist/DataGridRow/DataGridRow.js +33 -20
  28. package/dist/DataGridRow/index.js +1 -1
  29. package/dist/DataGridSorting/index.js +1 -1
  30. package/dist/DataGridSorting/sortingUtils.js +1 -1
  31. package/dist/common/utilities.js +1 -1
  32. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -29,16 +29,27 @@ import { getDataGridClasses } from "./utilities.js";
29
29
  * DataGrid (main component)
30
30
  * ========================================================================== */ const DataGrid = ({ className, wrapperClassName, children, mode = "system", compact = false, stickyHeader = false, stickyFooter = false, blurEffect = BlurEffects.NONE, maxHeight, disabled = false, columns, ...rest })=>{
31
31
  /**
32
- * Track registered header/footer components via context registration.
33
- * Uses counter-based tracking to properly handle multiple instances.
34
- * Components register themselves when they mount, regardless of nesting depth.
35
- * This replaces the previous displayName-based child inspection approach.
32
+ * Track registered header/footer components via context registration. Uses
33
+ * counter-based tracking to properly handle multiple instances. Components
34
+ * register themselves when they mount, regardless of nesting depth. This
35
+ * replaces the previous displayName-based child inspection approach.
36
36
  */ const [headerCount, setHeaderCount] = useState(0);
37
37
  const [footerCount, setFooterCount] = useState(0);
38
38
  /**
39
- * Registration callbacks with stable references.
40
- * Called by DataGridHeader/DataGridFooter on mount/unmount.
41
- * Uses increment/decrement to handle multiple instances correctly.
39
+ * Track measured heights of header/footer for dynamic padding. Reported by
40
+ * DataGridHeader/Footer via ResizeObserver. This replaces the brittle
41
+ * hard-coded Tailwind padding classes.
42
+ */ const [headerHeight, setHeaderHeight] = useState(0);
43
+ const [footerHeight, setFooterHeight] = useState(0);
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.
48
+ */ const [measuredColumnWidths, setMeasuredColumnWidths] = useState([]);
49
+ /**
50
+ * Registration callbacks with stable references. Called by
51
+ * DataGridHeader/DataGridFooter on mount/unmount. Uses increment/decrement to
52
+ * handle multiple instances correctly.
42
53
  */ const registerHeader = useCallback(()=>setHeaderCount((c)=>c + 1), []);
43
54
  const unregisterHeader = useCallback(()=>setHeaderCount((c)=>c - 1), []);
44
55
  const registerFooter = useCallback(()=>setFooterCount((c)=>c + 1), []);
@@ -46,23 +57,18 @@ import { getDataGridClasses } from "./utilities.js";
46
57
  const hasRegisteredHeader = headerCount > 0;
47
58
  const hasRegisteredFooter = footerCount > 0;
48
59
  /**
49
- * Only apply sticky behavior if both the prop is true AND the
50
- * corresponding component exists. This prevents adding padding/styles
51
- * for non-existent headers/footers.
60
+ * Only apply sticky behavior if both the prop is true AND the corresponding
61
+ * component exists. This prevents adding padding/styles for non-existent
62
+ * headers/footers.
52
63
  */ const effectiveStickyHeader = stickyHeader && hasRegisteredHeader;
53
64
  const effectiveStickyFooter = stickyFooter && hasRegisteredFooter;
54
65
  /**
55
- * State to hold the caption ID registered by DataGridHeader.
56
- * Used for aria-labelledby on the grid element for accessibility.
57
- * Also used to determine if a caption exists (for padding calculations).
66
+ * State to hold the caption ID registered by DataGridHeader. Used for
67
+ * aria-labelledby on the grid element for accessibility.
58
68
  */ const [captionId, setCaptionId] = useState(undefined);
59
69
  const handleSetCaptionId = useCallback((id)=>{
60
70
  setCaptionId(id);
61
71
  }, []);
62
- /**
63
- * Determine if caption exists based on registered captionId.
64
- * This replaces the previous child inspection approach.
65
- */ const hasCaption = Boolean(captionId);
66
72
  const classes = useMemo(()=>getDataGridClasses({
67
73
  mode,
68
74
  className,
@@ -70,9 +76,7 @@ import { getDataGridClasses } from "./utilities.js";
70
76
  stickyHeader: effectiveStickyHeader,
71
77
  stickyFooter: effectiveStickyFooter,
72
78
  disabled,
73
- hasCaption,
74
- hasColumns: Boolean(columns),
75
- compact
79
+ hasColumns: Boolean(columns)
76
80
  }), [
77
81
  mode,
78
82
  className,
@@ -80,9 +84,7 @@ import { getDataGridClasses } from "./utilities.js";
80
84
  effectiveStickyHeader,
81
85
  effectiveStickyFooter,
82
86
  disabled,
83
- hasCaption,
84
- columns,
85
- compact
87
+ columns
86
88
  ]);
87
89
  const contextValue = useMemo(()=>({
88
90
  mode,
@@ -91,11 +93,15 @@ import { getDataGridClasses } from "./utilities.js";
91
93
  stickyFooter: effectiveStickyFooter,
92
94
  blurEffect,
93
95
  columns,
96
+ measuredColumnWidths,
94
97
  setCaptionId: handleSetCaptionId,
95
98
  registerHeader,
96
99
  unregisterHeader,
97
100
  registerFooter,
98
- unregisterFooter
101
+ unregisterFooter,
102
+ setHeaderHeight,
103
+ setFooterHeight,
104
+ setMeasuredColumnWidths
99
105
  }), [
100
106
  mode,
101
107
  compact,
@@ -103,6 +109,7 @@ import { getDataGridClasses } from "./utilities.js";
103
109
  effectiveStickyFooter,
104
110
  blurEffect,
105
111
  columns,
112
+ measuredColumnWidths,
106
113
  handleSetCaptionId,
107
114
  registerHeader,
108
115
  unregisterHeader,
@@ -112,14 +119,22 @@ import { getDataGridClasses } from "./utilities.js";
112
119
  const wrapperStyle = maxHeight ? {
113
120
  maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight
114
121
  } : undefined;
122
+ /**
123
+ * Dynamic padding for scrollable content based on measured header/footer
124
+ * heights. This replaces the brittle hard-coded Tailwind padding classes.
125
+ */ const scrollableContentStyle = {
126
+ ...wrapperStyle,
127
+ paddingTop: effectiveStickyHeader ? headerHeight : undefined,
128
+ paddingBottom: effectiveStickyFooter ? footerHeight : undefined
129
+ };
115
130
  /**
116
131
  * When sticky header/footer is enabled, use Panel-like structure: - Outer
117
132
  * wrapper has overflow-hidden - Scrollable content area in the middle with
118
133
  * padding - Header/footer are absolutely positioned.
119
134
  */ const hasSticky = effectiveStickyHeader || effectiveStickyFooter;
120
135
  /**
121
- * When columns are provided, apply grid-template-columns at the grid level
122
- * so all rows can use subgrid to inherit the same column sizing.
136
+ * When columns are provided, apply grid-template-columns at the grid level so
137
+ * all rows can use subgrid to inherit the same column sizing.
123
138
  */ const gridStyle = columns ? {
124
139
  gridTemplateColumns: columns.join(" ")
125
140
  } : undefined;
@@ -160,7 +175,7 @@ import { getDataGridClasses } from "./utilities.js";
160
175
  style: wrapperStyle,
161
176
  children: hasSticky ? /*#__PURE__*/ jsx("div", {
162
177
  className: classes.scrollableContent,
163
- style: wrapperStyle,
178
+ style: scrollableContentStyle,
164
179
  children: gridContent
165
180
  }) : gridContent
166
181
  })
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -3,34 +3,37 @@ import { type ThemeMode } from "../DataGridConstants/DataGridConstants";
3
3
  * Generates classes for the main DataGrid wrapper and grid. When sticky
4
4
  * header/footer is enabled:
5
5
  * - Outer wrapper has overflow-hidden
6
- * - Scrollable area is a separate inner container with padding for header/footer
6
+ * - Scrollable area is a separate inner container with dynamic padding
7
7
  * - Header/footer use position:absolute to overlay scrollbar
8
- * - Padding is auto-calculated based on whether caption is present
8
+ * - Padding is dynamically set via inline styles based on measured heights
9
9
  *
10
10
  * When columns prop is provided, the grid uses CSS Grid layout with subgrid
11
11
  * support for proper column alignment across all rows.
12
+ *
12
13
  */
13
- export declare const getDataGridClasses: ({ mode, className, wrapperClassName, stickyHeader, stickyFooter, disabled, hasCaption, hasColumns, compact, }: {
14
+ export declare const getDataGridClasses: ({ mode, className, wrapperClassName, stickyHeader, stickyFooter, disabled, hasColumns, }: {
14
15
  mode: ThemeMode;
15
16
  className?: string;
16
17
  disabled?: boolean;
17
- hasCaption?: boolean;
18
18
  hasColumns?: boolean;
19
19
  stickyFooter?: boolean;
20
20
  stickyHeader?: boolean;
21
21
  wrapperClassName?: string;
22
- compact?: boolean;
23
22
  }) => {
24
23
  overlay: string;
25
24
  inner: string;
26
25
  spinnerWrapper: string;
27
26
  spinner: string;
27
+ /**
28
+ * Outer wrapper - overflow-hidden when sticky, like Panel's outerWrapper.
29
+ * Uses flex layout when sticky to allow scrollableContent to respect
30
+ * max-height.
31
+ */
28
32
  wrapper: string;
29
33
  /**
30
- * Scrollable content area with padding for absolute-positioned header/footer.
31
- * Header height varies based on whether caption is present:
32
- * - pt-10 (40px): header row only
33
- * - pt-19 (76px): header row + caption
34
+ * Scrollable content area for absolute-positioned header/footer. Padding is
35
+ * applied via inline styles based on measured header/footer heights,
36
+ * eliminating the need for hard-coded values that break when styles change.
34
37
  */
35
38
  scrollableContent: string;
36
39
  /**
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -35,13 +35,14 @@ import clsx from "clsx";
35
35
  * Generates classes for the main DataGrid wrapper and grid. When sticky
36
36
  * header/footer is enabled:
37
37
  * - Outer wrapper has overflow-hidden
38
- * - Scrollable area is a separate inner container with padding for header/footer
38
+ * - Scrollable area is a separate inner container with dynamic padding
39
39
  * - Header/footer use position:absolute to overlay scrollbar
40
- * - Padding is auto-calculated based on whether caption is present
40
+ * - Padding is dynamically set via inline styles based on measured heights
41
41
  *
42
42
  * When columns prop is provided, the grid uses CSS Grid layout with subgrid
43
43
  * support for proper column alignment across all rows.
44
- */ const getDataGridClasses = ({ mode, className, wrapperClassName, stickyHeader, stickyFooter, disabled, hasCaption, hasColumns, compact })=>{
44
+ *
45
+ */ const getDataGridClasses = ({ mode, className, wrapperClassName, stickyHeader, stickyFooter, disabled, hasColumns })=>{
45
46
  const overlayClasses = disabled ? getOverlayClasses({
46
47
  mode
47
48
  }) : null;
@@ -51,9 +52,11 @@ import clsx from "clsx";
51
52
  inner: overlayClasses?.inner ?? "",
52
53
  spinnerWrapper: overlayClasses?.spinnerWrapper ?? "",
53
54
  spinner: overlayClasses?.spinner ?? "",
54
- // Outer wrapper - overflow-hidden when sticky, like Panel's outerWrapper.
55
- // Uses flex layout when sticky to allow scrollableContent to respect max-height.
56
- wrapper: clsx("not-prose relative w-full rounded-lg shadow-md", {
55
+ /**
56
+ * Outer wrapper - overflow-hidden when sticky, like Panel's outerWrapper.
57
+ * Uses flex layout when sticky to allow scrollableContent to respect
58
+ * max-height.
59
+ */ wrapper: clsx("not-prose relative w-full rounded-lg shadow-md", {
57
60
  "overflow-x-auto": !hasSticky && !disabled,
58
61
  "overflow-hidden flex flex-col": hasSticky || disabled,
59
62
  "bg-surface-darker": mode === "dark" || mode === "system",
@@ -66,26 +69,14 @@ import clsx from "clsx";
66
69
  "text-copy-dark dark:text-copy-light": mode === "alt-system"
67
70
  }, wrapperClassName),
68
71
  /**
69
- * Scrollable content area with padding for absolute-positioned header/footer.
70
- * Header height varies based on whether caption is present:
71
- * - pt-10 (40px): header row only
72
- * - pt-19 (76px): header row + caption
73
- */ scrollableContent: clsx(// flex-1 + min-h-0 allows this to shrink within flex parent and enable scrolling.
74
- // min-h-0 overrides the default min-height:auto that prevents flex items from shrinking.
75
- "overflow-y-auto overflow-x-hidden rounded-[inherit] flex-1 min-h-0", {
76
- // Padding adjusted for header + caption when compact is false
77
- "pt-20": stickyHeader && hasCaption && !compact,
78
- // Padding adjusted for header row only when compact is false
79
- "pt-11": stickyHeader && !hasCaption && !compact,
80
- // Padding adjusted for footer row when compact is false
81
- "pb-11": stickyFooter && !compact,
82
- // Padding adjusted for header row only when compact is true
83
- "pt-7": stickyHeader && !hasCaption && compact,
84
- // Padding adjusted for header + caption when compact is true
85
- "pt-16": stickyHeader && hasCaption && compact,
86
- // Padding adjusted for footer row when compact is true
87
- "pb-7": stickyFooter && compact
88
- }),
72
+ * Scrollable content area for absolute-positioned header/footer. Padding is
73
+ * applied via inline styles based on measured header/footer heights,
74
+ * eliminating the need for hard-coded values that break when styles change.
75
+ */ scrollableContent: clsx(/**
76
+ * flex-1 + min-h-0 allows this to shrink within flex parent and enable
77
+ * scrolling. min-h-0 overrides the default min-height:auto that prevents
78
+ * flex items from shrinking.
79
+ */ "overflow-y-auto overflow-x-hidden rounded-[inherit] flex-1 min-h-0"),
89
80
  /**
90
81
  * When columns are provided, use CSS Grid so rows can use subgrid for
91
82
  * consistent column alignment. Otherwise, use flexbox.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,10 +1,11 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  import clsx from "clsx";
8
+ import { useContext, useLayoutEffect, useRef } from "react";
8
9
  import { DataGridContext } from "../DataGrid/DataGridContext.js";
9
10
  import { CellWrapper } from "../DataGridConstants/index.js";
10
11
 
@@ -12,6 +13,8 @@ import { CellWrapper } from "../DataGridConstants/index.js";
12
13
 
13
14
  ;// CONCATENATED MODULE: external "clsx"
14
15
 
16
+ ;// CONCATENATED MODULE: external "react"
17
+
15
18
  ;// CONCATENATED MODULE: external "../DataGrid/DataGridContext.js"
16
19
 
17
20
  ;// CONCATENATED MODULE: external "../DataGridConstants/index.js"
@@ -21,28 +24,73 @@ import { CellWrapper } from "../DataGridConstants/index.js";
21
24
 
22
25
 
23
26
 
27
+
24
28
  /* =============================================================================
25
29
  * DataGridBody
26
30
  * ========================================================================== */ const DataGridBody = ({ className, children, ...rest })=>{
27
- return /*#__PURE__*/ jsx(DataGridContext.Consumer, {
28
- children: (ctx)=>{
29
- /**
30
- * When columns are provided, use display:contents so the body
31
- * doesn't interfere with the grid flow. Rows will use subgrid.
32
- */ const bodyClass = ctx.columns ? clsx("contents", className) : clsx("flex flex-col", className);
33
- return /*#__PURE__*/ jsx(DataGridContext.Provider, {
34
- value: {
35
- ...ctx,
36
- cellWrapper: CellWrapper.BODY
37
- },
38
- children: /*#__PURE__*/ jsx("div", {
39
- role: "rowgroup",
40
- className: bodyClass,
41
- ...rest,
42
- children: children
43
- })
44
- });
31
+ const ctx = useContext(DataGridContext);
32
+ 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);
45
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);
82
+ return /*#__PURE__*/ jsx(DataGridContext.Provider, {
83
+ value: {
84
+ ...ctx,
85
+ cellWrapper: CellWrapper.BODY
86
+ },
87
+ children: /*#__PURE__*/ jsx("div", {
88
+ ref: bodyRef,
89
+ role: "rowgroup",
90
+ className: bodyClass,
91
+ ...rest,
92
+ children: children
93
+ })
46
94
  });
47
95
  };
48
96
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -42,9 +42,10 @@ const getCellClasses = ({ cellWrapper, className, compact, align, active, mode,
42
42
  "before:bg-table-active-light": active && mode === "light",
43
43
  "before:bg-table-active-dark dark:before:bg-table-active-light": active && mode === "system",
44
44
  "before:bg-table-active-light dark:before:bg-table-active-dark": active && mode === "alt-system"
45
- }, // Vertical borders.
46
- // self-stretch ensures border spans full row height in grid layout.
47
- {
45
+ }, /**
46
+ * Vertical borders. self-stretch ensures border spans full row height in grid
47
+ * layout.
48
+ */ {
48
49
  "self-stretch": borderLeft || borderRight,
49
50
  "border-l border-l-table-dark": borderLeft && mode === "dark",
50
51
  "border-l border-l-table-light": borderLeft && mode === "light",
@@ -58,7 +59,8 @@ const getCellClasses = ({ cellWrapper, className, compact, align, active, mode,
58
59
  return mainClasses;
59
60
  };
60
61
  /**
61
- * Returns the appropriate ARIA role for the cell based on the cell wrapper type.
62
+ * Returns the appropriate ARIA role for the cell based on the cell wrapper
63
+ * type.
62
64
  */ const getCellRole = (cellWrapper)=>{
63
65
  if (cellWrapper === CellWrapper.HEADER) {
64
66
  return "columnheader";
@@ -81,7 +83,7 @@ const getCellClasses = ({ cellWrapper, className, compact, align, active, mode,
81
83
  borderRight
82
84
  });
83
85
  const role = getCellRole(cellWrapper);
84
- // Apply grid-column span for colSpan > 1
86
+ // Apply grid-column span for colSpan > 1.
85
87
  const cellStyle = colSpan && colSpan > 1 ? {
86
88
  ...style,
87
89
  gridColumn: `span ${colSpan}`
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -76,7 +76,7 @@ import { getCellClasses } from "../DataGridCell/DataGridCell.js";
76
76
  compact,
77
77
  align
78
78
  });
79
- // Flex container for alignment of button within the cell
79
+ // Flex container for alignment of button within the cell.
80
80
  const contentClasses = clsx("flex", {
81
81
  "justify-start": align === "left" || !align,
82
82
  "justify-center": align === "center",
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -3,8 +3,9 @@ import { type BlurEffect, type ThemeMode } from "../DataGridConstants/DataGridCo
3
3
  /**
4
4
  * Generates classes for the DataGridFooter.
5
5
  *
6
- * When columns are provided (subgrid mode), uses display:contents so the
7
- * footer doesn't break the parent grid flow.
6
+ * When columns are provided (subgrid mode), uses display:contents so the footer
7
+ * doesn't break the parent grid flow.
8
+ *
8
9
  */
9
10
  export declare const getFooterClasses: ({ className, stickyFooter, mode, blurEffect, hasColumns, }: {
10
11
  mode: ThemeMode;
@@ -1,11 +1,11 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  import clsx from "clsx";
8
- import { useEffect } from "react";
8
+ import { useEffect, useLayoutEffect, useRef } from "react";
9
9
  import { getHeaderFooterBackgroundClasses, getStickyBlurClasses } from "../common/utilities.js";
10
10
  import { DataGridContext } from "../DataGrid/DataGridContext.js";
11
11
  import { BlurEffects, CellWrapper } from "../DataGridConstants/DataGridConstants.js";
@@ -32,8 +32,9 @@ import { BlurEffects, CellWrapper } from "../DataGridConstants/DataGridConstants
32
32
  /**
33
33
  * Generates classes for the DataGridFooter.
34
34
  *
35
- * When columns are provided (subgrid mode), uses display:contents so the
36
- * footer doesn't break the parent grid flow.
35
+ * When columns are provided (subgrid mode), uses display:contents so the footer
36
+ * doesn't break the parent grid flow.
37
+ *
37
38
  */ const getFooterClasses = ({ className, stickyFooter, mode, blurEffect, hasColumns })=>{
38
39
  /**
39
40
  * When columns are provided and not sticky, use display:contents so the
@@ -74,13 +75,15 @@ import { BlurEffects, CellWrapper } from "../DataGridConstants/DataGridConstants
74
75
  };
75
76
  DataGridFooter.displayName = "DataGridFooter";
76
77
  /**
77
- * Inner component to handle the useEffect for registering footer.
78
- * Separated to avoid hooks inside Consumer render prop.
78
+ * Inner component to handle the useEffect for registering footer. Separated to
79
+ * avoid hooks inside Consumer render prop.
79
80
  */ const DataGridFooterInner = ({ className, ctx, children, ...rest })=>{
80
81
  const hasColumns = Boolean(ctx.columns);
81
- // Register this footer with the parent DataGrid on mount.
82
- // This enables sticky footer behavior regardless of nesting depth.
83
- useEffect(()=>{
82
+ const footerRef = useRef(null);
83
+ /**
84
+ * Register this footer with the parent DataGrid on mount. This enables sticky
85
+ * footer behavior regardless of nesting depth.
86
+ */ useEffect(()=>{
84
87
  ctx.registerFooter?.();
85
88
  return ()=>{
86
89
  ctx.unregisterFooter?.();
@@ -89,12 +92,33 @@ DataGridFooter.displayName = "DataGridFooter";
89
92
  ctx.registerFooter,
90
93
  ctx.unregisterFooter
91
94
  ]);
95
+ /**
96
+ * Measure and report footer height for dynamic padding calculation. Uses
97
+ * ResizeObserver to handle dynamic content changes.
98
+ */ useLayoutEffect(()=>{
99
+ const element = footerRef.current;
100
+ if (!element || !ctx.stickyFooter || !ctx.setFooterHeight) {
101
+ return;
102
+ }
103
+ const observer = new ResizeObserver((entries)=>{
104
+ const height = entries[0]?.borderBoxSize?.[0]?.blockSize;
105
+ if (height !== undefined) {
106
+ ctx.setFooterHeight?.(height);
107
+ }
108
+ });
109
+ observer.observe(element);
110
+ return ()=>observer.disconnect();
111
+ }, [
112
+ ctx.stickyFooter,
113
+ ctx.setFooterHeight
114
+ ]);
92
115
  return /*#__PURE__*/ jsx(DataGridContext.Provider, {
93
116
  value: {
94
117
  ...ctx,
95
118
  cellWrapper: CellWrapper.FOOTER
96
119
  },
97
120
  children: /*#__PURE__*/ jsx("div", {
121
+ ref: footerRef,
98
122
  role: "rowgroup",
99
123
  className: getFooterClasses({
100
124
  className,
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -3,12 +3,13 @@ import { type BlurEffect, type ThemeMode } from "../DataGridConstants/DataGridCo
3
3
  /**
4
4
  * Generates classes for the DataGridHeader wrapper.
5
5
  *
6
- * When stickyHeader is true, uses position:absolute so the header overlays
7
- * the scrollbar area. The parent DataGrid auto-detects caption presence and
8
- * applies appropriate padding to the scrollable content.
6
+ * When stickyHeader is true, uses position:absolute so the header overlays the
7
+ * scrollbar area. The parent DataGrid auto-detects caption presence and applies
8
+ * appropriate padding to the scrollable content.
9
+ *
10
+ * When columns are provided (subgrid mode) and not sticky, uses
11
+ * display:contents so the header doesn't break the parent grid flow.
9
12
  *
10
- * When columns are provided (subgrid mode) and not sticky, uses display:contents
11
- * so the header doesn't break the parent grid flow.
12
13
  */
13
14
  export declare const getHeaderClasses: ({ className, stickyHeader, mode, blurEffect, hasColumns, }: {
14
15
  mode: ThemeMode;
@@ -18,8 +19,8 @@ export declare const getHeaderClasses: ({ className, stickyHeader, mode, blurEff
18
19
  stickyHeader?: boolean;
19
20
  }) => string;
20
21
  /**
21
- * Generates classes for the caption element inside DataGridHeader.
22
- * When hasColumns is true (subgrid mode), adds col-span-full to span all columns.
22
+ * Generates classes for the caption element inside DataGridHeader. When
23
+ * hasColumns is true (subgrid mode), adds col-span-full to span all columns.
23
24
  */
24
25
  export declare const getCaptionClasses: ({ captionClassName, hasColumns, }: {
25
26
  captionClassName?: string;
@@ -1,11 +1,11 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  import clsx from "clsx";
8
- import { useEffect, useId } from "react";
8
+ import { useEffect, useId, useLayoutEffect, useRef } from "react";
9
9
  import { getHeaderFooterBackgroundClasses, getHeaderFooterCopyClasses, getStickyBlurClasses } from "../common/utilities.js";
10
10
  import { DataGridContext } from "../DataGrid/DataGridContext.js";
11
11
  import { BlurEffects, CellWrapper } from "../DataGridConstants/DataGridConstants.js";
@@ -32,12 +32,13 @@ import { BlurEffects, CellWrapper } from "../DataGridConstants/DataGridConstants
32
32
  /**
33
33
  * Generates classes for the DataGridHeader wrapper.
34
34
  *
35
- * When stickyHeader is true, uses position:absolute so the header overlays
36
- * the scrollbar area. The parent DataGrid auto-detects caption presence and
37
- * applies appropriate padding to the scrollable content.
35
+ * When stickyHeader is true, uses position:absolute so the header overlays the
36
+ * scrollbar area. The parent DataGrid auto-detects caption presence and applies
37
+ * appropriate padding to the scrollable content.
38
+ *
39
+ * When columns are provided (subgrid mode) and not sticky, uses
40
+ * display:contents so the header doesn't break the parent grid flow.
38
41
  *
39
- * When columns are provided (subgrid mode) and not sticky, uses display:contents
40
- * so the header doesn't break the parent grid flow.
41
42
  */ const getHeaderClasses = ({ className, stickyHeader, mode, blurEffect, hasColumns })=>{
42
43
  const hasBlur = Boolean(blurEffect && blurEffect !== BlurEffects.NONE);
43
44
  /**
@@ -53,7 +54,7 @@ import { BlurEffects, CellWrapper } from "../DataGridConstants/DataGridConstants
53
54
  }), className);
54
55
  }
55
56
  return clsx("flex flex-col", {
56
- // Absolute positioning to overlay scrollbar, matching footer behavior
57
+ // Absolute positioning to overlay scrollbar, matching footer behavior.
57
58
  "absolute left-0 right-0 z-20 top-0 rounded-t-lg": stickyHeader
58
59
  }, getHeaderFooterBackgroundClasses({
59
60
  mode,
@@ -66,10 +67,10 @@ import { BlurEffects, CellWrapper } from "../DataGridConstants/DataGridConstants
66
67
  }), className);
67
68
  };
68
69
  /**
69
- * Generates classes for the caption element inside DataGridHeader.
70
- * When hasColumns is true (subgrid mode), adds col-span-full to span all columns.
70
+ * Generates classes for the caption element inside DataGridHeader. When
71
+ * hasColumns is true (subgrid mode), adds col-span-full to span all columns.
71
72
  */ const getCaptionClasses = ({ captionClassName, hasColumns })=>{
72
- return clsx("py-2 text-sm text-center font-bold", // In subgrid mode, caption needs to span all columns
73
+ return clsx("py-2 text-sm text-center font-bold", // In subgrid mode, caption needs to span all columns.
73
74
  hasColumns && "col-span-full", captionClassName);
74
75
  };
75
76
  /* =============================================================================
@@ -94,9 +95,11 @@ DataGridHeader.displayName = "DataGridHeader";
94
95
  * Separated to avoid hooks inside Consumer render prop.
95
96
  */ const DataGridHeaderInner = ({ caption, captionClassName, captionId, className, ctx, children, ...rest })=>{
96
97
  const hasColumns = Boolean(ctx.columns);
97
- // Register this header with the parent DataGrid on mount.
98
- // This enables sticky header behavior regardless of nesting depth.
99
- useEffect(()=>{
98
+ const headerRef = useRef(null);
99
+ /**
100
+ * Register this header with the parent DataGrid on mount. This enables sticky
101
+ * header behavior regardless of nesting depth.
102
+ */ useEffect(()=>{
100
103
  ctx.registerHeader?.();
101
104
  return ()=>{
102
105
  ctx.unregisterHeader?.();
@@ -105,7 +108,7 @@ DataGridHeader.displayName = "DataGridHeader";
105
108
  ctx.registerHeader,
106
109
  ctx.unregisterHeader
107
110
  ]);
108
- // Register the caption ID with the parent DataGrid for aria-labelledby
111
+ // Register the caption ID with the parent DataGrid for aria-labelledby.
109
112
  useEffect(()=>{
110
113
  if (caption && ctx.setCaptionId) {
111
114
  ctx.setCaptionId(captionId);
@@ -120,12 +123,33 @@ DataGridHeader.displayName = "DataGridHeader";
120
123
  captionId,
121
124
  ctx.setCaptionId
122
125
  ]);
126
+ /**
127
+ * Measure and report header height for dynamic padding calculation. Uses
128
+ * ResizeObserver to handle dynamic content changes (text wrapping, etc.).
129
+ */ useLayoutEffect(()=>{
130
+ const element = headerRef.current;
131
+ if (!element || !ctx.stickyHeader || !ctx.setHeaderHeight) {
132
+ return;
133
+ }
134
+ const observer = new ResizeObserver((entries)=>{
135
+ const height = entries[0]?.borderBoxSize?.[0]?.blockSize;
136
+ if (height !== undefined) {
137
+ ctx.setHeaderHeight?.(height);
138
+ }
139
+ });
140
+ observer.observe(element);
141
+ return ()=>observer.disconnect();
142
+ }, [
143
+ ctx.stickyHeader,
144
+ ctx.setHeaderHeight
145
+ ]);
123
146
  return /*#__PURE__*/ jsx(DataGridContext.Provider, {
124
147
  value: {
125
148
  ...ctx,
126
149
  cellWrapper: CellWrapper.HEADER
127
150
  },
128
151
  children: /*#__PURE__*/ jsxs("div", {
152
+ ref: headerRef,
129
153
  role: "rowgroup",
130
154
  className: getHeaderClasses({
131
155
  className,
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Finds the nearest scrollable ancestor of an element.
3
- * Returns null if no scrollable ancestor is found (uses viewport).
2
+ * Finds the nearest scrollable ancestor of an element. Returns null if no
3
+ * scrollable ancestor is found (uses viewport).
4
4
  */
5
5
  export declare function findScrollableAncestor(element: HTMLElement): Element | null;
6
6
  export type UseInfiniteScrollOptions = {
@@ -34,9 +34,9 @@ export type UseInfiniteScrollOptions = {
34
34
  */
35
35
  onLoadMore?: (newVisibleCount: number) => void;
36
36
  /**
37
- * The scroll container element for IntersectionObserver root.
38
- * Required when the DataGrid is inside a scrollable container (e.g., with maxHeight).
39
- * Pass null to use the viewport as root.
37
+ * The scroll container element for IntersectionObserver root. Required when
38
+ * the DataGrid is inside a scrollable container (e.g., with maxHeight). Pass
39
+ * null to use the viewport as root.
40
40
  */
41
41
  scrollContainer?: Element | null;
42
42
  };
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -13,8 +13,8 @@ const DEFAULT_BATCH_SIZE = 20;
13
13
  const DEFAULT_THRESHOLD = 5;
14
14
  const DEFAULT_ROOT_MARGIN = "20px";
15
15
  /**
16
- * Finds the nearest scrollable ancestor of an element.
17
- * Returns null if no scrollable ancestor is found (uses viewport).
16
+ * Finds the nearest scrollable ancestor of an element. Returns null if no
17
+ * scrollable ancestor is found (uses viewport).
18
18
  */ function findScrollableAncestor(element) {
19
19
  let parent = element.parentElement;
20
20
  while(parent){
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -26,7 +26,7 @@ import { CellWrapper } from "../DataGridConstants/index.js";
26
26
 
27
27
 
28
28
  const getRowClasses = ({ mode, className, cellWrapper })=>{
29
- // Always use CSS Grid for proper column alignment, with vertical centering
29
+ // Always use CSS Grid for proper column alignment, with vertical centering.
30
30
  const layoutClass = "grid items-center";
31
31
  if (cellWrapper === CellWrapper.HEADER || cellWrapper === CellWrapper.FOOTER) {
32
32
  /**
@@ -52,43 +52,56 @@ const getRowClasses = ({ mode, className, cellWrapper })=>{
52
52
  /* =============================================================================
53
53
  * DataGridRow
54
54
  * ========================================================================== */ const DataGridRow = ({ className, children, style: userStyle, ...rest })=>{
55
- // Count the number of direct children to determine column count
55
+ // Count the number of direct children to determine column count.
56
56
  const columnCount = react.Children.count(children);
57
57
  return /*#__PURE__*/ jsx(DataGridContext.Consumer, {
58
- children: ({ mode, cellWrapper, stickyHeader, stickyFooter, columns })=>{
58
+ children: ({ mode, cellWrapper, stickyHeader, stickyFooter, columns, measuredColumnWidths })=>{
59
59
  /**
60
- * Determine if this row is inside a sticky header/footer.
61
- * Sticky elements are absolutely positioned and outside the main grid
62
- * flow, so they can't use subgrid.
60
+ * Determine if this row is inside a sticky header/footer. Sticky elements
61
+ * are absolutely positioned and outside the main grid flow, so they can't
62
+ * use subgrid.
63
63
  */ const isInStickyHeader = cellWrapper === CellWrapper.HEADER && stickyHeader;
64
64
  const isInStickyFooter = cellWrapper === CellWrapper.FOOTER && stickyFooter;
65
65
  const isInStickyContext = isInStickyHeader || isInStickyFooter;
66
66
  /**
67
- * When columns are provided AND the row is inside the grid flow
68
- * (not in a sticky header/footer), use CSS subgrid to inherit the
69
- * column sizing from the parent grid.
67
+ * When columns are provided AND the row is inside the grid flow (not in a
68
+ * sticky header/footer), use CSS subgrid to inherit the column sizing from
69
+ * the parent grid.
70
70
  *
71
- * For sticky headers/footers, use the explicit column template since
72
- * they're absolutely positioned outside the main grid.
71
+ * For sticky headers/footers, use measured pixel widths from the body if
72
+ * available. This syncs the header/footer columns with the body columns
73
+ * since absolutely positioned elements can't use subgrid.
74
+ *
75
+ * When columns are not provided, fall back to the original behavior where
76
+ * each row defines its own equal-width columns.
73
77
  *
74
- * When columns are not provided, fall back to the original behavior
75
- * where each row defines its own equal-width columns.
76
78
  */ let rowStyle;
77
79
  if (columns) {
78
80
  if (isInStickyContext) {
79
- // Sticky elements can't use subgrid, use explicit template
80
- rowStyle = {
81
- gridTemplateColumns: columns.join(" ")
82
- };
81
+ // Sticky elements can't use subgrid.
82
+ // Use measured widths from body if available for perfect alignment.
83
+ // Also check that widths are valid (non-zero) - in test environments
84
+ // or before layout, measurements may be zero.
85
+ const hasValidMeasurements = measuredColumnWidths && measuredColumnWidths.length === columns.length && measuredColumnWidths.some((w)=>w > 0);
86
+ if (hasValidMeasurements) {
87
+ rowStyle = {
88
+ gridTemplateColumns: measuredColumnWidths.map((w)=>`${w}px`).join(" ")
89
+ };
90
+ } else {
91
+ // Fallback to explicit template while measurements are pending.
92
+ rowStyle = {
93
+ gridTemplateColumns: columns.join(" ")
94
+ };
95
+ }
83
96
  } else {
84
- // Normal flow: use subgrid to inherit from parent grid
97
+ // Normal flow: use subgrid to inherit from parent grid.
85
98
  rowStyle = {
86
99
  gridColumn: "1 / -1",
87
100
  gridTemplateColumns: "subgrid"
88
101
  };
89
102
  }
90
103
  } else {
91
- // No columns prop: use equal-width columns
104
+ // No columns prop: use equal-width columns.
92
105
  rowStyle = {
93
106
  gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`
94
107
  };
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v0.3.1
2
+ @versini/ui-datagrid v0.3.3
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-datagrid",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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": "30fcda9a659ee83caeaf29475095ff6f192b65c2"
97
+ "gitHead": "92252168e1ae927c8c874cd6549dc54922805ae7"
98
98
  }