@versini/ui-datagrid 4.0.0 → 4.1.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.
package/dist/197.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
package/dist/430.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
package/dist/799.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
package/dist/91.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,7 +1,7 @@
1
1
  export type DataGridInfiniteBodyProps<T> = ({
2
2
  /**
3
- * If true, displays an empty state instead of the grid rows. When noData
4
- * is true, data and children are optional.
3
+ * If true, displays an empty state instead of the grid rows. When noData is
4
+ * true, data and children are optional.
5
5
  */
6
6
  noData: true;
7
7
  /**
@@ -58,6 +58,22 @@ export type DataGridInfiniteBodyProps<T> = ({
58
58
  */
59
59
  className?: string;
60
60
  };
61
+ /**
62
+ * Options for {@link DataGridInfiniteBodyRef.scrollToIndex}.
63
+ */
64
+ export type ScrollToIndexOptions = {
65
+ /**
66
+ * How the grid scrolls to the row. `"smooth"` (the default) glides to the
67
+ * target; `"instant"` (or `"auto"`) jumps directly with no animation. A user
68
+ * `prefers-reduced-motion` setting always forces a direct jump, whatever is
69
+ * requested. Spelled out as a literal union rather than the DOM
70
+ * `ScrollBehavior` type, whose definition (notably `"instant"`) varies across
71
+ * TypeScript `lib.dom` versions — so the documented values are always
72
+ * available.
73
+ * @default "smooth"
74
+ */
75
+ behavior?: "auto" | "instant" | "smooth";
76
+ };
61
77
  /**
62
78
  * Ref handle for DataGridInfiniteBody, exposing imperative methods.
63
79
  */
@@ -65,8 +81,10 @@ export type DataGridInfiniteBodyRef = {
65
81
  /**
66
82
  * Scroll a row into view (centered). Works for any index, mounted or not.
67
83
  * @param index - The index of the row to scroll to
84
+ * @param options - Scroll options; defaults to a smooth scroll. Pass
85
+ * `{ behavior: "instant" }` for a direct jump.
68
86
  */
69
- scrollToIndex: (index: number) => void;
87
+ scrollToIndex: (index: number, options?: ScrollToIndexOptions) => void;
70
88
  };
71
89
  /**
72
90
  * A virtualized DataGrid body for large datasets.
@@ -74,8 +92,8 @@ export type DataGridInfiniteBodyRef = {
74
92
  * Only the rows near the viewport stay mounted (windowing via
75
93
  * `@tanstack/react-virtual`), so the DOM stays small and scrolling stays smooth
76
94
  * at any row count. Rows are rendered in normal grid flow between two full-span
77
- * spacer elements, so `columns`/subgrid (including intrinsic `auto` tracks) keep
78
- * working without any consumer change. The virtualizer windows against the
95
+ * spacer elements, so `columns`/subgrid (including intrinsic `auto` tracks)
96
+ * keep working without any consumer change. The virtualizer windows against the
79
97
  * DataGrid's own scroll container (created by `maxHeight` or a sticky
80
98
  * header/footer), or the page (window scroll) when the grid has neither.
81
99
  *
@@ -1,2 +1,2 @@
1
- export type { DataGridInfiniteBodyProps, DataGridInfiniteBodyRef, } from "./DataGridInfiniteBody";
1
+ export type { DataGridInfiniteBodyProps, DataGridInfiniteBodyRef, ScrollToIndexOptions, } from "./DataGridInfiniteBody";
2
2
  export { DataGridInfiniteBody } from "./DataGridInfiniteBody";
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -23,14 +23,25 @@ import { getBodyClass, useColumnMeasurement } from "../197.js";
23
23
  const DEFAULT_OVERSCAN = 8;
24
24
  const DEFAULT_ESTIMATED_ROW_HEIGHT = 44;
25
25
  const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
26
+ /**
27
+ * Fallback used to restore the normal window after a smooth jump when the
28
+ * browser does not fire `scrollend` (e.g. Safari < 17.4) or the target was
29
+ * already centered.
30
+ */ const JUMP_SETTLE_FALLBACK_MS = 1000;
31
+ /**
32
+ * A smooth jump animates reliably over a short hop, but a long one outruns the
33
+ * animation budget and races react-virtual's mid-flight measurement, landing
34
+ * unpredictably. Past this many rows of reach we jump instantly instead — it
35
+ * lands exactly (no animation to race) at the cost of the glide.
36
+ */ const FAR_JUMP_THRESHOLD = 30;
26
37
  /**
27
38
  * A virtualized DataGrid body for large datasets.
28
39
  *
29
40
  * Only the rows near the viewport stay mounted (windowing via
30
41
  * `@tanstack/react-virtual`), so the DOM stays small and scrolling stays smooth
31
42
  * at any row count. Rows are rendered in normal grid flow between two full-span
32
- * spacer elements, so `columns`/subgrid (including intrinsic `auto` tracks) keep
33
- * working without any consumer change. The virtualizer windows against the
43
+ * spacer elements, so `columns`/subgrid (including intrinsic `auto` tracks)
44
+ * keep working without any consumer change. The virtualizer windows against the
34
45
  * DataGrid's own scroll container (created by `maxHeight` or a sticky
35
46
  * header/footer), or the page (window scroll) when the grid has neither.
36
47
  *
@@ -61,18 +72,44 @@ const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
61
72
  */ function DataGridInfiniteBody({ data = [], children: renderRow, overscan = DEFAULT_OVERSCAN, estimatedRowHeight, onVisibleCountChange, className, noData = false, noDataText, ref }) {
62
73
  const ctx = useContext(DataGridContext);
63
74
  const bodyRef = useRef(null);
64
- // A zero-height grid item at the list start. The display:contents body has no
65
- // box, so we measure this real element to find the list's scroll-margin.
66
- const sentinelRef = useRef(null);
75
+ /**
76
+ * A zero-height grid item at the list start. The display:contents body has no
77
+ * box, so we measure this real element to find the list's scroll-margin.
78
+ */ const sentinelRef = useRef(null);
67
79
  const totalItems = data.length;
68
80
  const headerRows = ctx.headerRows ?? 0;
69
81
  const footerRows = ctx.footerRows ?? 0;
70
82
  /**
71
83
  * Default the row-height estimate from the grid's density when the consumer
72
84
  * doesn't set it: `compact` rows are much shorter (~29px vs ~44px), and an
73
- * estimate far from the real height hurts scrollbar accuracy and scrollToIndex.
85
+ * estimate far from the real height hurts scrollbar accuracy and
86
+ * scrollToIndex.
74
87
  */ const resolvedEstimatedRowHeight = estimatedRowHeight ?? (ctx.compact ? DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT : DEFAULT_ESTIMATED_ROW_HEIGHT);
75
88
  const [scrollMargin, setScrollMargin] = useState(0);
89
+ /**
90
+ * Widen-window jump-to-row state. A default (`"smooth"`) `scrollToIndex` first
91
+ * widens the virtualized window (`jump.overscan`) so every row between the
92
+ * current view and the target is mounted with real, laid-out heights, then the
93
+ * layout effect scrolls to the real target element and restores the window
94
+ * once the scroll settles.
95
+ *
96
+ * `instant` is set for a far jump (reach past `FAR_JUMP_THRESHOLD`); the
97
+ * effect then jumps instantly rather than animating (it also forces instant
98
+ * for a `prefers-reduced-motion` user, but that is read live in the effect,
99
+ * not stored here). Scrolling over real rows always lands exactly; the
100
+ * animation is what raced react-virtual's mid-flight measurement over a long
101
+ * distance, so far jumps drop it. A direct jump (`behavior: "instant"`) skips
102
+ * this state entirely and delegates to the virtualizer (see `scrollToIndex`
103
+ * below).
104
+ *
105
+ * `seq` makes every request a fresh object: two jumps that happen to compute
106
+ * the same `overscan` still re-run the jump effect (a plain number would be a
107
+ * no-op setState and the second jump would be dropped). `handledSeqRef`
108
+ * records the last seq the effect acted on, so it scrolls once per request.
109
+ *
110
+ */ const [jump, setJump] = useState(null);
111
+ const jumpSeqRef = useRef(0);
112
+ const handledSeqRef = useRef(-1);
76
113
  /**
77
114
  * The scroll target is provided by the parent DataGrid: its internal bounded
78
115
  * scroller (sticky / maxHeight), or the page (window scroll) when it owns
@@ -81,9 +118,10 @@ const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
81
118
  */ const useWindow = ctx.pageScroll === true;
82
119
  const scrollEl = useWindow ? null : ctx.scrollContainer ?? null;
83
120
  const resolved = useWindow || scrollEl !== null;
84
- // Measure the list's offset within the scroller (= sticky-header padding /
85
- // inline-header height / page offset). Keeps the window origin correct.
86
- useLayoutEffect(()=>{
121
+ /**
122
+ * Measure the list's offset within the scroller (= sticky-header padding /
123
+ * inline-header height / page offset). Keeps the window origin correct.
124
+ */ useLayoutEffect(()=>{
87
125
  if (!resolved || noData) {
88
126
  return;
89
127
  }
@@ -110,27 +148,64 @@ const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
110
148
  scrollEl,
111
149
  noData
112
150
  ]);
151
+ /**
152
+ * Keep the element scroller's `scroll-padding-top` equal to the header height
153
+ * (`scrollMargin`). A smooth jump centers via native `scrollIntoView`, which
154
+ * otherwise centers in the full box and lands the row half a header-height too
155
+ * high under a sticky header; the inset shrinks the centering region to the
156
+ * visible area. Declared here (not mutated per-jump) so it is not call-order
157
+ * dependent, and the previous inline value is restored on cleanup so the grid
158
+ * never permanently overrides a consumer's own `scroll-padding-top`.
159
+ */ useLayoutEffect(()=>{
160
+ if (!scrollEl) {
161
+ return;
162
+ }
163
+ const previous = scrollEl.style.scrollPaddingTop;
164
+ scrollEl.style.scrollPaddingTop = `${scrollMargin}px`;
165
+ return ()=>{
166
+ scrollEl.style.scrollPaddingTop = previous;
167
+ };
168
+ }, [
169
+ scrollEl,
170
+ scrollMargin
171
+ ]);
113
172
  const virtualizer = useVirtualizer({
114
173
  count: totalItems,
115
- // In window mode the scroll element IS `window` (the window observers read
116
- // window.innerHeight / window.scrollY off it); cast bridges the type since
117
- // useVirtualizer's element constraint excludes Window.
118
- getScrollElement: ()=>useWindow ? typeof window !== "undefined" ? window : null : scrollEl,
174
+ /**
175
+ * In window mode the scroll element IS `window` (the window observers read
176
+ * window.innerHeight / window.scrollY off it); cast bridges the type since
177
+ * useVirtualizer's element constraint excludes Window.
178
+ */ getScrollElement: ()=>useWindow ? typeof window !== "undefined" ? window : null : scrollEl,
119
179
  estimateSize: ()=>resolvedEstimatedRowHeight,
120
- overscan,
180
+ /**
181
+ * While a jump is in flight, `jump.overscan` widens the window to mount the
182
+ * rows along the path; otherwise the consumer's (or default) overscan.
183
+ */ overscan: jump?.overscan ?? overscan,
121
184
  scrollMargin,
185
+ /**
186
+ * Direct jumps center via the virtualizer (`scrollToIndex` with align
187
+ * "center"). Like the sticky-header inset on the smooth path, the row must
188
+ * center in the visible area below the header, not the full box.
189
+ * react-virtual shifts the target offset by `scrollPaddingStart` and then
190
+ * centers, so half the header height lands the row at the visible center.
191
+ * (The smooth path uses CSS `scroll-padding-top`, which shrinks the centering
192
+ * box instead, so that one takes the full header height — hence the /2
193
+ * asymmetry.)
194
+ */ scrollPaddingStart: scrollMargin / 2,
122
195
  // React 19: let React batch the scroll-driven updates natively.
123
196
  useFlushSync: false,
124
- // Window vs element observers are selected to match getScrollElement; the
125
- // casts bridge the Element|Window union react-virtual can't narrow here.
126
- observeElementRect: useWindow ? observeWindowRect : observeElementRect,
197
+ /**
198
+ * Window vs element observers are selected to match getScrollElement; the
199
+ * casts bridge the Element|Window union react-virtual can't narrow here.
200
+ */ observeElementRect: useWindow ? observeWindowRect : observeElementRect,
127
201
  observeElementOffset: useWindow ? observeWindowOffset : observeElementOffset,
128
202
  scrollToFn: useWindow ? windowScroll : elementScroll,
129
- // Window mode: seed the offset from the live scroll position (mirrors
130
- // useWindowVirtualizer). Without this, on mount the virtualizer's offset is
131
- // 0, so it scrolls an already-scrolled page to the top and computes the
132
- // wrong initial window.
133
- initialOffset: useWindow ? ()=>typeof window !== "undefined" ? window.scrollY : 0 : undefined
203
+ /**
204
+ * Window mode: seed the offset from the live scroll position (mirrors
205
+ * useWindowVirtualizer). Without this, on mount the virtualizer's offset is
206
+ * 0, so it scrolls an already-scrolled page to the top and computes the wrong
207
+ * initial window.
208
+ */ initialOffset: useWindow ? ()=>typeof window !== "undefined" ? window.scrollY : 0 : undefined
134
209
  });
135
210
  const virtualItems = virtualizer.getVirtualItems();
136
211
  const totalSize = virtualizer.getTotalSize();
@@ -142,9 +217,10 @@ const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
142
217
  * scroller that isn't height-bounded (every row mounts). Runs after paint so
143
218
  * the measurements are real; stripped from production builds.
144
219
  */ useEffect(()=>{
145
- // Treat a missing `process` (a non-Node runtime whose bundler didn't replace
146
- // this expression) as production, so reading NODE_ENV can never throw.
147
- const isProduction = typeof process === "undefined" || process.env.NODE_ENV === "production";
220
+ /**
221
+ * Treat a missing `process` (a non-Node runtime whose bundler didn't replace
222
+ * this expression) as production, so reading NODE_ENV can never throw.
223
+ */ const isProduction = typeof process === "undefined" || process.env.NODE_ENV === "production";
148
224
  if (isProduction || noData || totalItems === 0 || typeof window === "undefined") {
149
225
  return;
150
226
  }
@@ -168,27 +244,129 @@ const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
168
244
  totalItems
169
245
  ]);
170
246
  /**
171
- * Expose scrollToIndex via ref; delegate to the virtualizer (centers the row,
172
- * mounting it first if needed).
247
+ * Expose scrollToIndex via ref. Works for any index, mounted or not.
248
+ *
249
+ * A direct jump (`behavior: "instant"`/`"auto"`) delegates straight to the
250
+ * virtualizer: it scrolls to the target's estimated offset and mounts only the
251
+ * window around it — lightweight, but the landing is only as accurate as
252
+ * `estimatedRowHeight` (the rows above stay estimated).
253
+ *
254
+ * A default (`"smooth"`) jump instead widens the window to mount every row
255
+ * from the current view to the target with real heights, so it lands exactly.
256
+ * The layout effect then animates a near jump, or — past `FAR_JUMP_THRESHOLD`
257
+ * — jumps instantly (a long animation races the virtualizer's mid-flight
258
+ * measurement and strands the jump short).
259
+ *
173
260
  */ useImperativeHandle(ref, ()=>({
174
- scrollToIndex: (index)=>{
261
+ scrollToIndex: (index, options)=>{
175
262
  if (index < 0 || index >= totalItems) {
176
263
  console.warn(`scrollToIndex: index ${index} is out of bounds${totalItems === 0 ? " (the grid is empty)" : ` (0-${totalItems - 1})`}`);
177
264
  return;
178
265
  }
179
- // Smooth scroll, unless the user prefers reduced motion. Smoothness is
180
- // best when `estimatedRowHeight` is close to the real row height; a far-
181
- // off estimate makes the virtualizer re-correct the offset as rows are
182
- // measured, which reads as a stutter near the target (a react-virtual
183
- // limitation with dynamically measured rows).
184
- const reduceMotion = typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
185
- virtualizer.scrollToIndex(index, {
186
- align: "center",
187
- behavior: reduceMotion ? "auto" : "smooth"
266
+ const behavior = options?.behavior ?? "smooth";
267
+ if (behavior !== "smooth") {
268
+ /**
269
+ * If a widened smooth jump is still in flight, cancel it so its (possibly
270
+ * large) path window doesn't stay mounted until its own settle. Safe
271
+ * unconditionally: `setJump(null)` no-ops when there is no pending jump.
272
+ */ setJump(null);
273
+ /**
274
+ * Direct jump: delegate to the virtualizer with an explicit "instant".
275
+ * Forwarding "auto" instead would defer to the scroller's CSS
276
+ * `scroll-behavior` — under a global `scroll-behavior: smooth` that
277
+ * animates through the un-widened window (the very stutter the smooth path
278
+ * exists to avoid), breaking the documented "jumps directly" contract.
279
+ */ virtualizer.scrollToIndex(index, {
280
+ align: "center",
281
+ behavior: "instant"
282
+ });
283
+ return;
284
+ }
285
+ const items = virtualizer.getVirtualItems();
286
+ const first = items.length > 0 ? items[0].index : 0;
287
+ const last = items.length > 0 ? items[items.length - 1].index : 0;
288
+ /**
289
+ * Overscan is symmetric, so widen by the larger gap to either edge of the
290
+ * current window; that guarantees the target (and the rows between) mount.
291
+ * The jump effect scrolls once they render, then restores the normal
292
+ * window. A jump reaching past `FAR_JUMP_THRESHOLD` lands instantly (a long
293
+ * smooth animation races the virtualizer's mid-flight measurement and
294
+ * strands the jump short).
295
+ */ const reach = Math.max(index - first, last - index, 0);
296
+ jumpSeqRef.current += 1;
297
+ setJump({
298
+ index,
299
+ overscan: reach + overscan,
300
+ instant: reach > FAR_JUMP_THRESHOLD,
301
+ seq: jumpSeqRef.current
188
302
  });
189
303
  }
190
304
  }), [
191
305
  totalItems,
306
+ virtualizer,
307
+ overscan
308
+ ]);
309
+ /**
310
+ * Perform a pending jump once the widened window has mounted the target row
311
+ * (react-virtual recomputes the window synchronously, so it is mounted in the
312
+ * commit that follows the `jump` change), then restore the normal window.
313
+ *
314
+ * Two flavors:
315
+ * - **Instant** (far jump or reduced motion): scroll instantly, measure the
316
+ * path rows so the window-collapse keeps their real heights (not the
317
+ * estimate, which would shift the target), then collapse — all synchronously.
318
+ * No animation means nothing races the measurement, so it lands exactly.
319
+ * - **Smooth** (near jump): animate, and collapse once the scroll settles
320
+ * (`scrollend`, or the timeout fallback for the no-op / unsupported case). A
321
+ * short hop finishes before measurement can interfere, so it stays exact.
322
+ *
323
+ */ useLayoutEffect(()=>{
324
+ if (jump === null || handledSeqRef.current === jump.seq) {
325
+ return;
326
+ }
327
+ handledSeqRef.current = jump.seq;
328
+ const rowEl = bodyRef.current?.querySelector(`[data-index="${jump.index}"]`);
329
+ if (!rowEl) {
330
+ /**
331
+ * The widened window should always mount the target; if it somehow did not,
332
+ * don't stay stuck in widened mode — restore the normal window so later
333
+ * jumps aren't dropped.
334
+ */ setJump(null);
335
+ return;
336
+ }
337
+ const reduceMotion = typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
338
+ if (jump.instant || reduceMotion) {
339
+ rowEl.scrollIntoView({
340
+ behavior: "instant",
341
+ block: "center"
342
+ });
343
+ /**
344
+ * Cache the path rows' real heights before the window collapses, so the rows
345
+ * above the target don't fall back to `estimatedRowHeight` and shift it.
346
+ * Safe to do synchronously here: the instant scroll has already landed, so
347
+ * there is no in-flight animation for the measurement to disturb.
348
+ */ bodyRef.current?.querySelectorAll("[data-index]").forEach((el)=>virtualizer.measureElement(el));
349
+ setJump(null);
350
+ return;
351
+ }
352
+ rowEl.scrollIntoView({
353
+ behavior: "smooth",
354
+ block: "center"
355
+ });
356
+ const scroller = useWindow ? typeof window !== "undefined" ? window : null : scrollEl;
357
+ const settle = ()=>setJump(null);
358
+ scroller?.addEventListener("scrollend", settle, {
359
+ once: true
360
+ });
361
+ const timer = setTimeout(settle, JUMP_SETTLE_FALLBACK_MS);
362
+ return ()=>{
363
+ clearTimeout(timer);
364
+ scroller?.removeEventListener("scrollend", settle);
365
+ };
366
+ }, [
367
+ jump,
368
+ useWindow,
369
+ scrollEl,
192
370
  virtualizer
193
371
  ]);
194
372
  /**
@@ -255,10 +433,11 @@ const DEFAULT_COMPACT_ESTIMATED_ROW_HEIGHT = 29;
255
433
  }), [
256
434
  ctx
257
435
  ]);
258
- // Measure column widths for sticky header/footer sync. Re-key on the first
259
- // mounted row so the sticky header re-syncs as the window (and the auto-column
260
- // width) slides.
261
- useColumnMeasurement(bodyRef, virtualItems[0]?.index, noData);
436
+ /**
437
+ * Measure column widths for sticky header/footer sync. Re-key on the first
438
+ * mounted row so the sticky header re-syncs as the window (and the auto-column
439
+ * width) slides.
440
+ */ useColumnMeasurement(bodyRef, virtualItems[0]?.index, noData);
262
441
  const bodyClass = getBodyClass(className);
263
442
  const paddingTop = virtualItems.length > 0 ? Math.max(0, virtualItems[0].start - scrollMargin) : 0;
264
443
  const paddingBottom = virtualItems.length > 0 ? Math.max(0, totalSize - virtualItems[virtualItems.length - 1].end) : 0;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-datagrid v4.0.0
2
+ @versini/ui-datagrid v4.1.0
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": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -87,8 +87,8 @@
87
87
  },
88
88
  "devDependencies": {
89
89
  "@testing-library/jest-dom": "6.9.1",
90
- "@versini/ui-button": "15.0.5",
91
- "@versini/ui-types": "10.0.0"
90
+ "@versini/ui-button": "15.1.0",
91
+ "@versini/ui-types": "10.1.0"
92
92
  },
93
93
  "dependencies": {
94
94
  "@tanstack/react-virtual": "3.14.4",
@@ -99,5 +99,5 @@
99
99
  "sideEffects": [
100
100
  "**/*.css"
101
101
  ],
102
- "gitHead": "4f7d84ec0f33c46dd0757121a5f04da7071b2d4c"
102
+ "gitHead": "fbfcead0c4637fdf2cf9c8f6a701fe5da9508d4b"
103
103
  }