@humanspeak/svelte-virtual-list 0.2.6 → 0.3.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +50 -13
  2. package/dist/SvelteVirtualList.svelte +619 -179
  3. package/dist/SvelteVirtualList.svelte.d.ts +156 -65
  4. package/dist/reactive-height-manager/INTEGRATION_EXAMPLE.md +136 -0
  5. package/dist/reactive-height-manager/README.md +324 -0
  6. package/dist/reactive-height-manager/ReactiveHeightManager.svelte.d.ts +116 -0
  7. package/dist/reactive-height-manager/ReactiveHeightManager.svelte.js +200 -0
  8. package/dist/reactive-height-manager/benchmark.d.ts +5 -0
  9. package/dist/reactive-height-manager/benchmark.js +25 -0
  10. package/dist/reactive-height-manager/index.d.ts +50 -0
  11. package/dist/reactive-height-manager/index.js +55 -0
  12. package/dist/reactive-height-manager/test/TestComponent.svelte +78 -0
  13. package/dist/reactive-height-manager/test/TestComponent.svelte.d.ts +23 -0
  14. package/dist/reactive-height-manager/types.d.ts +41 -0
  15. package/dist/reactive-height-manager/types.js +1 -0
  16. package/dist/types.d.ts +24 -5
  17. package/dist/utils/heightCalculation.d.ts +18 -8
  18. package/dist/utils/heightCalculation.js +18 -11
  19. package/dist/utils/heightChangeDetection.d.ts +12 -0
  20. package/dist/utils/heightChangeDetection.js +20 -0
  21. package/dist/utils/resizeObserver.d.ts +89 -0
  22. package/dist/utils/resizeObserver.js +119 -0
  23. package/dist/utils/scrollCalculation.d.ts +47 -0
  24. package/dist/utils/scrollCalculation.js +167 -0
  25. package/dist/utils/throttle.d.ts +95 -0
  26. package/dist/utils/throttle.js +155 -0
  27. package/dist/utils/types.d.ts +0 -6
  28. package/dist/utils/virtualList.d.ts +20 -23
  29. package/dist/utils/virtualList.js +153 -61
  30. package/dist/utils/virtualListDebug.d.ts +12 -7
  31. package/dist/utils/virtualListDebug.js +19 -9
  32. package/package.json +33 -31
@@ -1,4 +1,4 @@
1
- import type { SvelteVirtualListMode } from '../types.js';
1
+ import type { SvelteVirtualListMode, SvelteVirtualListPreviousVisibleRange } from '../types.js';
2
2
  import type { VirtualListSetters, VirtualListState } from './types.js';
3
3
  /**
4
4
  * Calculates the maximum scroll position for a virtual list.
@@ -26,12 +26,9 @@ export declare const calculateScrollPosition: (totalItems: number, itemHeight: n
26
26
  * @param {number} totalItems - Total number of items in the list
27
27
  * @param {number} bufferSize - Number of items to render outside the visible area
28
28
  * @param {SvelteVirtualListMode} mode - Scroll direction mode
29
- * @returns {{ start: number, end: number }} Range of indices to render
29
+ * @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
30
30
  */
31
- export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode) => {
32
- start: number;
33
- end: number;
34
- };
31
+ export declare const calculateVisibleRange: (scrollTop: number, viewportHeight: number, itemHeight: number, totalItems: number, bufferSize: number, mode: SvelteVirtualListMode, atBottom: boolean, wasAtBottomBeforeHeightChange: boolean, lastVisibleRange: SvelteVirtualListPreviousVisibleRange | null, totalContentHeight?: number) => SvelteVirtualListPreviousVisibleRange;
35
32
  /**
36
33
  * Calculates the CSS transform value for positioning the virtual list items.
37
34
  *
@@ -44,9 +41,10 @@ export declare const calculateVisibleRange: (scrollTop: number, viewportHeight:
44
41
  * @param {number} visibleEnd - Index of the last visible item
45
42
  * @param {number} visibleStart - Index of the first visible item
46
43
  * @param {number} itemHeight - Height of each list item in pixels
44
+ * @param {number} viewportHeight - Height of the viewport in pixels
47
45
  * @returns {number} The calculated transform Y value in pixels
48
46
  */
49
- export declare const calculateTransformY: (mode: SvelteVirtualListMode, totalItems: number, visibleEnd: number, visibleStart: number, itemHeight: number) => number;
47
+ export declare const calculateTransformY: (mode: SvelteVirtualListMode, totalItems: number, visibleEnd: number, visibleStart: number, itemHeight: number, viewportHeight: number, totalContentHeight?: number) => number;
50
48
  /**
51
49
  * Updates the virtual list's height and scroll position when necessary.
52
50
  *
@@ -64,19 +62,19 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
64
62
  * Calculates the average height of visible items in a virtual list.
65
63
  *
66
64
  * This function optimizes performance by:
67
- * 1. Using a height cache to store measured item heights
65
+ * 1. Using a height cache to store measured item heights with dirty tracking
68
66
  * 2. Only measuring new items not in the cache
69
67
  * 3. Calculating a running average of all measured heights
70
68
  *
71
69
  * @param {HTMLElement[]} itemElements - Array of currently rendered item elements
72
70
  * @param {{ start: number }} visibleRange - Object containing the start index of visible items
73
- * @param {Record<number, number>} heightCache - Cache of previously measured item heights
71
+ * @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
74
72
  * @param {number} currentItemHeight - Current average item height being used
75
73
  *
76
74
  * @returns {{
77
75
  * newHeight: number,
78
76
  * newLastMeasuredIndex: number,
79
- * updatedHeightCache: Record<number, number>
77
+ * updatedHeightCache: HeightCache
80
78
  * }} Object containing new calculated height, last measured index, and updated cache
81
79
  *
82
80
  * @example
@@ -89,10 +87,20 @@ export declare const updateHeightAndScroll: (state: VirtualListState, setters: V
89
87
  */
90
88
  export declare const calculateAverageHeight: (itemElements: HTMLElement[], visibleRange: {
91
89
  start: number;
92
- }, heightCache: Record<number, number>, currentItemHeight: number) => {
90
+ end: number;
91
+ }, heightCache: Record<number, number>, currentItemHeight: number, dirtyItems: Set<number>, currentTotalHeight?: number, currentValidCount?: number, mode?: SvelteVirtualListMode) => {
93
92
  newHeight: number;
94
93
  newLastMeasuredIndex: number;
95
94
  updatedHeightCache: Record<number, number>;
95
+ clearedDirtyItems: Set<number>;
96
+ newTotalHeight: number;
97
+ newValidCount: number;
98
+ heightChanges: Array<{
99
+ index: number;
100
+ oldHeight: number;
101
+ newHeight: number;
102
+ delta: number;
103
+ }>;
96
104
  };
97
105
  /**
98
106
  * Processes large arrays in chunks to prevent UI blocking.
@@ -121,17 +129,6 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
121
129
  export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
122
130
  chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
123
131
  onComplete: () => void) => Promise<void>;
124
- /**
125
- * Builds a block sum array for fast offset calculation in large virtual lists.
126
- * Each entry in the array is the total height up to the end of that block (exclusive).
127
- *
128
- * @param {Record<number, number>} heightCache - Map of measured item heights
129
- * @param {number} calculatedItemHeight - Estimated height for unmeasured items
130
- * @param {number} totalItems - Total number of items in the list
131
- * @param {number} blockSize - Number of items per block
132
- * @returns {number[]} Array of prefix sums at each block boundary
133
- */
134
- export declare const buildBlockSums: (heightCache: Record<number, number>, calculatedItemHeight: number, totalItems: number, blockSize?: number) => number[];
135
132
  /**
136
133
  * Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
137
134
  *
@@ -141,7 +138,7 @@ export declare const buildBlockSums: (heightCache: Record<number, number>, calcu
141
138
  * - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
142
139
  * - For small indices, falls back to the original logic.
143
140
  *
144
- * @param {Record<number, number>} heightCache - Map of measured item heights
141
+ * @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
145
142
  * @param {number} calculatedItemHeight - Estimated height for unmeasured items
146
143
  * @param {number} idx - The index to scroll to (exclusive)
147
144
  * @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
@@ -29,24 +29,56 @@ export const calculateScrollPosition = (totalItems, itemHeight, containerHeight)
29
29
  * @param {number} totalItems - Total number of items in the list
30
30
  * @param {number} bufferSize - Number of items to render outside the visible area
31
31
  * @param {SvelteVirtualListMode} mode - Scroll direction mode
32
- * @returns {{ start: number, end: number }} Range of indices to render
32
+ * @returns {SvelteVirtualListPreviousVisibleRange} Range of indices to render
33
33
  */
34
- export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode) => {
34
+ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, totalItems, bufferSize, mode, atBottom, wasAtBottomBeforeHeightChange, lastVisibleRange, totalContentHeight) => {
35
35
  if (mode === 'bottomToTop') {
36
36
  const visibleCount = Math.ceil(viewportHeight / itemHeight) + 1;
37
- const bottomIndex = totalItems - Math.floor(scrollTop / itemHeight);
37
+ // In bottomToTop mode, scrollTop represents distance from the total content end
38
+ // scrollTop = 0 means we're at the beginning (showing first items)
39
+ // scrollTop = maxScrollTop means we're at the end (showing last items)
40
+ const totalHeight = totalContentHeight ?? totalItems * itemHeight;
41
+ const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
42
+ // Convert scrollTop to "distance from start" for bottomToTop
43
+ const distanceFromStart = maxScrollTop - scrollTop;
44
+ const startIndex = Math.floor(distanceFromStart / itemHeight);
45
+ // Safeguard: handle edge cases
46
+ if (startIndex < 0) {
47
+ // We're scrolled beyond the maximum (showing first items)
48
+ const start = 0;
49
+ const end = Math.min(totalItems, visibleCount + bufferSize * 2);
50
+ return { start, end };
51
+ }
38
52
  // Add buffer to both ends
39
- const start = Math.max(0, bottomIndex - visibleCount - bufferSize);
40
- const end = Math.min(totalItems, bottomIndex + bufferSize);
53
+ const start = Math.max(0, startIndex - bufferSize);
54
+ const end = Math.min(totalItems, startIndex + visibleCount + bufferSize);
41
55
  return { start, end };
42
56
  }
43
57
  else {
44
58
  const start = Math.floor(scrollTop / itemHeight);
45
59
  const end = Math.min(totalItems, start + Math.ceil(viewportHeight / itemHeight) + 1);
60
+ // Safeguard for topToBottom: ensure last item is fully visible when at max scroll
61
+ const totalHeight = totalContentHeight ?? totalItems * itemHeight;
62
+ const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
63
+ // Add dynamic tolerance based on item height for browser rendering precision
64
+ const tolerance = Math.max(itemHeight, 10); // At least one full item height or 10px minimum
65
+ const isAtBottom = Math.abs(scrollTop - maxScrollTop) <= tolerance;
66
+ if (isAtBottom) {
67
+ // When at the bottom, ensure we include all items up to the end
68
+ const adjustedEnd = totalItems;
69
+ const visibleItemCount = Math.ceil(viewportHeight / itemHeight) + bufferSize + 1;
70
+ const adjustedStart = Math.max(0, adjustedEnd - visibleItemCount);
71
+ return {
72
+ start: adjustedStart,
73
+ end: adjustedEnd
74
+ };
75
+ }
46
76
  // Add buffer to both ends
77
+ const finalStart = Math.max(0, start - bufferSize);
78
+ const finalEnd = Math.min(totalItems, end + bufferSize);
47
79
  return {
48
- start: Math.max(0, start - bufferSize),
49
- end: Math.min(totalItems, end + bufferSize)
80
+ start: finalStart,
81
+ end: finalEnd
50
82
  };
51
83
  }
52
84
  };
@@ -62,12 +94,22 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
62
94
  * @param {number} visibleEnd - Index of the last visible item
63
95
  * @param {number} visibleStart - Index of the first visible item
64
96
  * @param {number} itemHeight - Height of each list item in pixels
97
+ * @param {number} viewportHeight - Height of the viewport in pixels
65
98
  * @returns {number} The calculated transform Y value in pixels
66
99
  */
67
- export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart, itemHeight) => {
68
- return mode === 'bottomToTop'
69
- ? (totalItems - visibleEnd) * itemHeight
70
- : visibleStart * itemHeight;
100
+ export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart, itemHeight, viewportHeight, totalContentHeight) => {
101
+ if (mode === 'bottomToTop') {
102
+ // In bottomToTop mode, position items so they stack from bottom up
103
+ const actualTotalHeight = totalContentHeight ?? totalItems * itemHeight;
104
+ // Calculate transform to position visible items correctly
105
+ const basicTransform = (totalItems - visibleEnd) * itemHeight;
106
+ // When content is smaller than viewport, push to bottom
107
+ const bottomOffset = Math.max(0, viewportHeight - actualTotalHeight);
108
+ return basicTransform + bottomOffset;
109
+ }
110
+ else {
111
+ return visibleStart * itemHeight;
112
+ }
71
113
  };
72
114
  /**
73
115
  * Updates the virtual list's height and scroll position when necessary.
@@ -101,19 +143,19 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
101
143
  * Calculates the average height of visible items in a virtual list.
102
144
  *
103
145
  * This function optimizes performance by:
104
- * 1. Using a height cache to store measured item heights
146
+ * 1. Using a height cache to store measured item heights with dirty tracking
105
147
  * 2. Only measuring new items not in the cache
106
148
  * 3. Calculating a running average of all measured heights
107
149
  *
108
150
  * @param {HTMLElement[]} itemElements - Array of currently rendered item elements
109
151
  * @param {{ start: number }} visibleRange - Object containing the start index of visible items
110
- * @param {Record<number, number>} heightCache - Cache of previously measured item heights
152
+ * @param {HeightCache} heightCache - Cache of previously measured item heights with dirty tracking
111
153
  * @param {number} currentItemHeight - Current average item height being used
112
154
  *
113
155
  * @returns {{
114
156
  * newHeight: number,
115
157
  * newLastMeasuredIndex: number,
116
- * updatedHeightCache: Record<number, number>
158
+ * updatedHeightCache: HeightCache
117
159
  * }} Object containing new calculated height, last measured index, and updated cache
118
160
  *
119
161
  * @example
@@ -124,39 +166,113 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
124
166
  * 40
125
167
  * )
126
168
  */
127
- export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight) => {
169
+ export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight, dirtyItems, currentTotalHeight = 0, currentValidCount = 0, mode = 'topToBottom') => {
128
170
  const validElements = itemElements.filter((el) => el);
129
171
  if (validElements.length === 0) {
130
172
  return {
131
173
  newHeight: currentItemHeight,
132
174
  newLastMeasuredIndex: visibleRange.start,
133
- updatedHeightCache: heightCache
175
+ updatedHeightCache: heightCache,
176
+ clearedDirtyItems: new Set(),
177
+ newTotalHeight: currentTotalHeight,
178
+ newValidCount: currentValidCount,
179
+ heightChanges: []
134
180
  };
135
181
  }
136
182
  const newHeightCache = { ...heightCache };
137
- // Cache heights for new items
138
- validElements.forEach((el, i) => {
139
- const itemIndex = visibleRange.start + i;
140
- if (!newHeightCache[itemIndex]) {
141
- try {
142
- const height = el.getBoundingClientRect().height;
143
- if (Number.isFinite(height) && height > 0) {
144
- newHeightCache[itemIndex] = height;
183
+ const clearedDirtyItems = new Set();
184
+ const heightChanges = [];
185
+ // Start with current running totals (O(1) instead of O(n))
186
+ let totalValidHeight = currentTotalHeight;
187
+ let validHeightCount = currentValidCount;
188
+ // Process only dirty items if they exist, otherwise process all visible items
189
+ if (dirtyItems.size > 0) {
190
+ // Process only dirty items
191
+ dirtyItems.forEach((itemIndex) => {
192
+ // Map original item index to position in itemElements array
193
+ let elementIndex;
194
+ if (mode === 'bottomToTop') {
195
+ // In bottomToTop, itemElements is reversed relative to the visible range
196
+ // elementIndex should be based on position within the actual array, not theoretical end
197
+ elementIndex = validElements.length - 1 - (itemIndex - visibleRange.start);
198
+ }
199
+ else {
200
+ // In topToBottom, itemElements is normal: [item0, item1, ..., item44, item45]
201
+ elementIndex = itemIndex - visibleRange.start;
202
+ }
203
+ const element = validElements[elementIndex];
204
+ if (element && elementIndex >= 0 && elementIndex < validElements.length) {
205
+ try {
206
+ // await tick()
207
+ void element.offsetHeight;
208
+ const height = element.getBoundingClientRect().height;
209
+ const oldHeight = newHeightCache[itemIndex];
210
+ if (Number.isFinite(height) && height > 0) {
211
+ // Only update if height actually changed (use smaller tolerance for precision)
212
+ if (!oldHeight || Math.abs(oldHeight - height) >= 0.1) {
213
+ // Track the height change for scroll correction
214
+ const actualOldHeight = oldHeight || currentItemHeight;
215
+ const delta = height - actualOldHeight;
216
+ heightChanges.push({
217
+ index: itemIndex,
218
+ oldHeight: actualOldHeight,
219
+ newHeight: height,
220
+ delta
221
+ });
222
+ // Update running totals
223
+ if (oldHeight && Number.isFinite(oldHeight) && oldHeight > 0) {
224
+ // Replace old height with new height in running total
225
+ totalValidHeight = totalValidHeight - oldHeight + height;
226
+ }
227
+ else {
228
+ // Add new height to running total
229
+ totalValidHeight += height;
230
+ validHeightCount++;
231
+ }
232
+ newHeightCache[itemIndex] = height;
233
+ }
234
+ }
235
+ clearedDirtyItems.add(itemIndex);
236
+ }
237
+ catch {
238
+ // Skip invalid measurements but still clear from dirty
239
+ clearedDirtyItems.add(itemIndex);
145
240
  }
146
241
  }
147
- catch {
148
- // Skip invalid measurements
242
+ else {
243
+ clearedDirtyItems.add(itemIndex); // Still clear it from dirty items
149
244
  }
150
- }
151
- });
152
- // Calculate average from valid cached heights
153
- const validHeights = Object.values(newHeightCache).filter((h) => Number.isFinite(h) && h > 0);
245
+ });
246
+ }
247
+ else {
248
+ // Original behavior: process all visible items
249
+ validElements.forEach((el, i) => {
250
+ const itemIndex = visibleRange.start + i;
251
+ if (!newHeightCache[itemIndex]) {
252
+ try {
253
+ const height = el.getBoundingClientRect().height;
254
+ if (Number.isFinite(height) && height > 0) {
255
+ // Add new height to running totals
256
+ totalValidHeight += height;
257
+ validHeightCount++;
258
+ newHeightCache[itemIndex] = height;
259
+ }
260
+ }
261
+ catch {
262
+ // Skip invalid measurements
263
+ }
264
+ }
265
+ });
266
+ }
267
+ // O(1) average calculation using running totals!
154
268
  return {
155
- newHeight: validHeights.length > 0
156
- ? validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length
157
- : currentItemHeight,
269
+ newHeight: validHeightCount > 0 ? totalValidHeight / validHeightCount : currentItemHeight,
158
270
  newLastMeasuredIndex: visibleRange.start,
159
- updatedHeightCache: newHeightCache
271
+ updatedHeightCache: newHeightCache,
272
+ clearedDirtyItems,
273
+ newTotalHeight: totalValidHeight,
274
+ newValidCount: validHeightCount,
275
+ heightChanges
160
276
  };
161
277
  };
162
278
  /**
@@ -202,31 +318,6 @@ onComplete) => {
202
318
  };
203
319
  await processChunk(0);
204
320
  };
205
- /**
206
- * Builds a block sum array for fast offset calculation in large virtual lists.
207
- * Each entry in the array is the total height up to the end of that block (exclusive).
208
- *
209
- * @param {Record<number, number>} heightCache - Map of measured item heights
210
- * @param {number} calculatedItemHeight - Estimated height for unmeasured items
211
- * @param {number} totalItems - Total number of items in the list
212
- * @param {number} blockSize - Number of items per block
213
- * @returns {number[]} Array of prefix sums at each block boundary
214
- */
215
- export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, blockSize = 1000) => {
216
- const blockSums = [];
217
- let sum = 0;
218
- for (let i = 0; i < totalItems; i++) {
219
- sum += heightCache[i] ?? calculatedItemHeight;
220
- if ((i + 1) % blockSize === 0) {
221
- blockSums.push(sum);
222
- }
223
- }
224
- // Push the last partial block if needed
225
- if (totalItems % blockSize !== 0) {
226
- blockSums.push(sum);
227
- }
228
- return blockSums;
229
- };
230
321
  /**
231
322
  * Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
232
323
  *
@@ -236,7 +327,7 @@ export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, bl
236
327
  * - For indices >= blockSize, sums the block prefix, then only iterates the tail within the block.
237
328
  * - For small indices, falls back to the original logic.
238
329
  *
239
- * @param {Record<number, number>} heightCache - Map of measured item heights
330
+ * @param {HeightCache} heightCache - Map of measured item heights with dirty tracking
240
331
  * @param {number} calculatedItemHeight - Estimated height for unmeasured items
241
332
  * @param {number} idx - The index to scroll to (exclusive)
242
333
  * @param {number[]} [blockSums] - Optional precomputed block sums (for repeated queries)
@@ -255,7 +346,8 @@ export const getScrollOffsetForIndex = (heightCache, calculatedItemHeight, idx,
255
346
  // Fallback: O(n) for a single query
256
347
  let offset = 0;
257
348
  for (let i = 0; i < idx; i++) {
258
- offset += heightCache[i] ?? calculatedItemHeight;
349
+ const height = heightCache[i] ?? calculatedItemHeight;
350
+ offset += height;
259
351
  }
260
352
  return offset;
261
353
  }
@@ -40,9 +40,10 @@ export declare function shouldShowDebugInfo(prevRange: {
40
40
  *
41
41
  * This utility function generates a structured debug object that captures the complete
42
42
  * state of a virtual list at any given moment. It includes critical metrics such as
43
- * visible item count, viewport boundaries, total items, processing progress, and
44
- * height calculations. This information is essential for performance monitoring,
45
- * debugging scroll behavior, and optimizing virtual list configurations.
43
+ * visible item count, viewport boundaries, total items, processed items with measured
44
+ * heights, height calculations, scroll position, and total content dimensions.
45
+ * This information is essential for performance monitoring, debugging scroll behavior,
46
+ * and optimizing virtual list configurations.
46
47
  *
47
48
  * Performance considerations:
48
49
  * - All calculations are O(1)
@@ -51,16 +52,20 @@ export declare function shouldShowDebugInfo(prevRange: {
51
52
  *
52
53
  * @param visibleRange - Current visible range object containing start and end indices
53
54
  * @param totalItems - Total number of items in the virtual list
54
- * @param processedItems - Number of items that have been processed/measured
55
+ * @param processedItems - Number of items with measured heights (heightCache.length)
55
56
  * @param averageItemHeight - Current calculated average height per item in pixels
57
+ * @param scrollTop - Current scroll position in pixels
58
+ * @param viewportHeight - Height of the viewport in pixels
56
59
  * @returns {SvelteVirtualListDebugInfo} A structured debug information object
57
60
  *
58
61
  * @example
59
62
  * const debugInfo = createDebugInfo(
60
63
  * { start: 0, end: 10 },
61
64
  * 1000,
62
- * 100,
63
- * 50
65
+ * 50,
66
+ * 45,
67
+ * 200,
68
+ * 400
64
69
  * );
65
70
  * console.log('Virtual List State:', debugInfo);
66
71
  *
@@ -69,4 +74,4 @@ export declare function shouldShowDebugInfo(prevRange: {
69
74
  export declare function createDebugInfo(visibleRange: {
70
75
  start: number;
71
76
  end: number;
72
- }, totalItems: number, processedItems: number, averageItemHeight: number): SvelteVirtualListDebugInfo;
77
+ }, totalItems: number, processedItems: number, averageItemHeight: number, scrollTop: number, viewportHeight: number, totalHeight: number): SvelteVirtualListDebugInfo;
@@ -39,9 +39,10 @@ export function shouldShowDebugInfo(prevRange, currentRange, prevHeight, current
39
39
  *
40
40
  * This utility function generates a structured debug object that captures the complete
41
41
  * state of a virtual list at any given moment. It includes critical metrics such as
42
- * visible item count, viewport boundaries, total items, processing progress, and
43
- * height calculations. This information is essential for performance monitoring,
44
- * debugging scroll behavior, and optimizing virtual list configurations.
42
+ * visible item count, viewport boundaries, total items, processed items with measured
43
+ * heights, height calculations, scroll position, and total content dimensions.
44
+ * This information is essential for performance monitoring, debugging scroll behavior,
45
+ * and optimizing virtual list configurations.
45
46
  *
46
47
  * Performance considerations:
47
48
  * - All calculations are O(1)
@@ -50,28 +51,37 @@ export function shouldShowDebugInfo(prevRange, currentRange, prevHeight, current
50
51
  *
51
52
  * @param visibleRange - Current visible range object containing start and end indices
52
53
  * @param totalItems - Total number of items in the virtual list
53
- * @param processedItems - Number of items that have been processed/measured
54
+ * @param processedItems - Number of items with measured heights (heightCache.length)
54
55
  * @param averageItemHeight - Current calculated average height per item in pixels
56
+ * @param scrollTop - Current scroll position in pixels
57
+ * @param viewportHeight - Height of the viewport in pixels
55
58
  * @returns {SvelteVirtualListDebugInfo} A structured debug information object
56
59
  *
57
60
  * @example
58
61
  * const debugInfo = createDebugInfo(
59
62
  * { start: 0, end: 10 },
60
63
  * 1000,
61
- * 100,
62
- * 50
64
+ * 50,
65
+ * 45,
66
+ * 200,
67
+ * 400
63
68
  * );
64
69
  * console.log('Virtual List State:', debugInfo);
65
70
  *
66
71
  * @throws {Error} Will throw if end index is less than start index in visibleRange
67
72
  */
68
- export function createDebugInfo(visibleRange, totalItems, processedItems, averageItemHeight) {
73
+ export function createDebugInfo(visibleRange, totalItems, processedItems, averageItemHeight, scrollTop, viewportHeight, totalHeight) {
74
+ const atTop = scrollTop <= 1; // Small tolerance for floating point precision
75
+ const atBottom = scrollTop >= totalHeight - viewportHeight - 1; // Small tolerance
69
76
  return {
70
77
  visibleItemsCount: visibleRange.end - visibleRange.start,
71
78
  startIndex: visibleRange.start,
72
79
  endIndex: visibleRange.end,
73
80
  totalItems,
74
- processedItems,
75
- averageItemHeight
81
+ processedItems, // Number of items with measured heights in heightCache
82
+ averageItemHeight,
83
+ atTop,
84
+ atBottom,
85
+ totalHeight
76
86
  };
77
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-virtual-list",
3
- "version": "0.2.6",
3
+ "version": "0.3.1-beta.1",
4
4
  "description": "A lightweight, high-performance virtual list component for Svelte 5 that renders large datasets with minimal memory usage. Features include dynamic height support, smooth scrolling, TypeScript support, and efficient DOM recycling. Ideal for infinite scrolling lists, data tables, chat interfaces, and any application requiring the rendering of thousands of items without compromising performance. Zero dependencies and fully customizable.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -55,16 +55,17 @@
55
55
  "lint": "prettier --check . && eslint .",
56
56
  "lint:fix": "npm run format && eslint . --fix",
57
57
  "package": "svelte-kit sync && svelte-package && publint",
58
+ "prepare": "husky",
58
59
  "prepublishOnly": "npm run package",
59
60
  "preview": "vite preview",
60
- "test": "vitest run --coverage",
61
+ "test": "vitest run --coverage --",
61
62
  "test:all": "npm run test && npm run test:e2e",
62
- "test:e2e": "playwright test",
63
- "test:e2e:debug": "playwright test --debug",
63
+ "test:e2e": "playwright test --",
64
+ "test:e2e:debug": "playwright test --debug --",
64
65
  "test:e2e:report": "playwright show-report",
65
- "test:e2e:ui": "playwright test --ui",
66
- "test:only": "vitest run",
67
- "test:watch": "vitest"
66
+ "test:e2e:ui": "playwright test --ui --",
67
+ "test:only": "vitest run --",
68
+ "test:watch": "vitest --"
68
69
  },
69
70
  "overrides": {
70
71
  "@sveltejs/kit": {
@@ -76,45 +77,46 @@
76
77
  },
77
78
  "devDependencies": {
78
79
  "@eslint/compat": "^1.3.1",
79
- "@eslint/js": "^9.29.0",
80
- "@faker-js/faker": "^9.8.0",
81
- "@playwright/test": "^1.53.1",
82
- "@sveltejs/adapter-auto": "^6.0.1",
83
- "@sveltejs/kit": "^2.22.2",
84
- "@sveltejs/package": "^2.3.12",
85
- "@sveltejs/vite-plugin-svelte": "^5.1.0",
86
- "@testing-library/jest-dom": "^6.6.3",
80
+ "@eslint/js": "^9.32.0",
81
+ "@faker-js/faker": "^9.9.0",
82
+ "@playwright/test": "^1.54.2",
83
+ "@sveltejs/adapter-auto": "^6.0.2",
84
+ "@sveltejs/kit": "^2.27.3",
85
+ "@sveltejs/package": "^2.4.1",
86
+ "@sveltejs/vite-plugin-svelte": "^6.1.0",
87
+ "@testing-library/jest-dom": "^6.6.4",
87
88
  "@testing-library/svelte": "^5.2.8",
88
89
  "@testing-library/user-event": "^14.6.1",
89
- "@types/node": "^24.0.4",
90
- "@typescript-eslint/eslint-plugin": "^8.35.0",
91
- "@typescript-eslint/parser": "^8.35.0",
90
+ "@types/node": "^24.2.0",
91
+ "@typescript-eslint/eslint-plugin": "^8.39.0",
92
+ "@typescript-eslint/parser": "^8.39.0",
92
93
  "@vitest/coverage-v8": "^3.2.4",
93
- "eslint": "^9.29.0",
94
- "eslint-config-prettier": "^10.1.5",
94
+ "eslint": "^9.32.0",
95
+ "eslint-config-prettier": "^10.1.8",
95
96
  "eslint-plugin-import": "^2.32.0",
96
- "eslint-plugin-svelte": "^3.10.0",
97
+ "eslint-plugin-svelte": "^3.11.0",
97
98
  "eslint-plugin-unused-imports": "^4.1.4",
98
- "globals": "^16.2.0",
99
+ "globals": "^16.3.0",
100
+ "husky": "^9.1.7",
99
101
  "jsdom": "^26.1.0",
100
- "prettier": "^3.6.1",
101
- "prettier-plugin-organize-imports": "^4.1.0",
102
+ "prettier": "^3.6.2",
103
+ "prettier-plugin-organize-imports": "^4.2.0",
102
104
  "prettier-plugin-sort-json": "^4.1.1",
103
105
  "prettier-plugin-svelte": "^3.4.0",
104
- "prettier-plugin-tailwindcss": "^0.6.13",
106
+ "prettier-plugin-tailwindcss": "^0.6.14",
105
107
  "publint": "^0.3.12",
106
- "svelte": "^5.34.8",
107
- "svelte-check": "^4.2.2",
108
- "typescript": "^5.8.3",
109
- "typescript-eslint": "^8.35.0",
110
- "vite": "^6.3.5",
108
+ "svelte": "^5.38.0",
109
+ "svelte-check": "^4.3.1",
110
+ "typescript": "^5.9.2",
111
+ "typescript-eslint": "^8.39.0",
112
+ "vite": "^7.1.1",
111
113
  "vitest": "^3.2.4"
112
114
  },
113
115
  "peerDependencies": {
114
116
  "svelte": "^5.0.0"
115
117
  },
116
118
  "volta": {
117
- "node": "22.16.0"
119
+ "node": "22.18.0"
118
120
  },
119
121
  "publishConfig": {
120
122
  "access": "public"