@humanspeak/svelte-virtual-list 0.3.6 → 0.3.9

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.
@@ -1,5 +1,41 @@
1
1
  import type { SvelteVirtualListMode, SvelteVirtualListPreviousVisibleRange } from '../types.js';
2
2
  import type { VirtualListSetters, VirtualListState } from './types.js';
3
+ /**
4
+ * Validates a height value and returns it if valid, otherwise returns the fallback.
5
+ *
6
+ * A height is considered valid if it is a finite number greater than 0.
7
+ * This utility consolidates the repeated pattern of height validation
8
+ * found throughout the virtual list codebase.
9
+ *
10
+ * @param {unknown} height - The height value to validate
11
+ * @param {number} fallback - The fallback value to use if height is invalid
12
+ * @returns {number} The validated height or the fallback value
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const height = getValidHeight(heightCache[i], calculatedItemHeight)
17
+ * // Returns heightCache[i] if valid, otherwise calculatedItemHeight
18
+ * ```
19
+ */
20
+ export declare const getValidHeight: (height: unknown, fallback: number) => number;
21
+ /**
22
+ * Clamps a numeric value to be within a specified range.
23
+ *
24
+ * This utility consolidates the repeated `Math.max(min, Math.min(max, value))`
25
+ * pattern used throughout scroll calculations and positioning logic.
26
+ *
27
+ * @param {number} value - The value to clamp
28
+ * @param {number} min - The minimum allowed value
29
+ * @param {number} max - The maximum allowed value
30
+ * @returns {number} The clamped value
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const scrollTop = clampValue(targetScrollTop, 0, maxScrollTop)
35
+ * // Ensures scrollTop is between 0 and maxScrollTop
36
+ * ```
37
+ */
38
+ export declare const clampValue: (value: number, min: number, max: number) => number;
3
39
  /**
4
40
  * Calculates the maximum scroll position for a virtual list.
5
41
  *
@@ -151,3 +187,31 @@ onComplete: () => void) => Promise<void>;
151
187
  * const offset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, 12345, blockSums);
152
188
  */
153
189
  export declare const getScrollOffsetForIndex: (heightCache: Record<number, number>, calculatedItemHeight: number, idx: number, blockSums?: number[], blockSize?: number) => number;
190
+ /**
191
+ * Builds block prefix sums for heightCache to accelerate offset queries.
192
+ *
193
+ * This function precomputes cumulative height sums for blocks of items, enabling
194
+ * O(blockSize) offset calculations instead of O(n). The returned array contains
195
+ * the total height of all items up to and including each completed block.
196
+ *
197
+ * For example, with blockSize=1000:
198
+ * - Entry 0: sum of heights for items 0-999
199
+ * - Entry 1: sum of heights for items 0-1999
200
+ * - Entry 2: sum of heights for items 0-2999
201
+ *
202
+ * @param {Record<number, number>} heightCache - Cache of measured item heights.
203
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items.
204
+ * @param {number} totalItems - Total number of items in the list.
205
+ * @param {number} [blockSize=1000] - Number of items per block for memoization.
206
+ * @returns {number[]} Array of cumulative height sums for each completed block.
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const heightCache = { 0: 40, 1: 50, 2: 45 };
211
+ * const blockSums = buildBlockSums(heightCache, 40, 5000, 1000);
212
+ *
213
+ * // Use with getScrollOffsetForIndex for efficient lookups
214
+ * const offset = getScrollOffsetForIndex(heightCache, 40, 2500, blockSums);
215
+ * ```
216
+ */
217
+ export declare const buildBlockSums: (heightCache: Record<number, number>, calculatedItemHeight: number, totalItems: number, blockSize?: number) => number[];
@@ -1,3 +1,39 @@
1
+ /**
2
+ * Validates a height value and returns it if valid, otherwise returns the fallback.
3
+ *
4
+ * A height is considered valid if it is a finite number greater than 0.
5
+ * This utility consolidates the repeated pattern of height validation
6
+ * found throughout the virtual list codebase.
7
+ *
8
+ * @param {unknown} height - The height value to validate
9
+ * @param {number} fallback - The fallback value to use if height is invalid
10
+ * @returns {number} The validated height or the fallback value
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const height = getValidHeight(heightCache[i], calculatedItemHeight)
15
+ * // Returns heightCache[i] if valid, otherwise calculatedItemHeight
16
+ * ```
17
+ */
18
+ export const getValidHeight = (height, fallback) => Number.isFinite(height) && height > 0 ? height : fallback;
19
+ /**
20
+ * Clamps a numeric value to be within a specified range.
21
+ *
22
+ * This utility consolidates the repeated `Math.max(min, Math.min(max, value))`
23
+ * pattern used throughout scroll calculations and positioning logic.
24
+ *
25
+ * @param {number} value - The value to clamp
26
+ * @param {number} min - The minimum allowed value
27
+ * @param {number} max - The maximum allowed value
28
+ * @returns {number} The clamped value
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const scrollTop = clampValue(targetScrollTop, 0, maxScrollTop)
33
+ * // Ensures scrollTop is between 0 and maxScrollTop
34
+ * ```
35
+ */
36
+ export const clampValue = (value, min, max) => Math.max(min, Math.min(max, value));
1
37
  /**
2
38
  * Calculates the maximum scroll position for a virtual list.
3
39
  *
@@ -68,10 +104,7 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
68
104
  const adjustedEnd = totalItems;
69
105
  let startCore = adjustedEnd;
70
106
  let acc = 0;
71
- const getH = (i) => {
72
- const v = heightCache ? heightCache[i] : undefined;
73
- return Number.isFinite(v) && v > 0 ? v : itemHeight;
74
- };
107
+ const getH = (i) => getValidHeight(heightCache ? heightCache[i] : undefined, itemHeight);
75
108
  while (startCore > 0 && acc < viewportHeight) {
76
109
  const h = getH(startCore - 1);
77
110
  acc += h;
@@ -115,7 +148,8 @@ export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart,
115
148
  const basicTransform = (totalItems - visibleEnd) * itemHeight;
116
149
  // When content is smaller than viewport, push to bottom
117
150
  const bottomOffset = Math.max(0, effectiveViewport - actualTotalHeight);
118
- return basicTransform + bottomOffset;
151
+ // Snap to integer pixels to avoid subpixel oscillation
152
+ return Math.round(basicTransform + bottomOffset);
119
153
  }
120
154
  else {
121
155
  // For topToBottom, prefer precise offset using measured heights when available
@@ -123,7 +157,7 @@ export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart,
123
157
  const offset = getScrollOffsetForIndex(heightCache, itemHeight, visibleStart);
124
158
  return Math.max(0, Math.round(offset));
125
159
  }
126
- return visibleStart * itemHeight;
160
+ return Math.round(visibleStart * itemHeight);
127
161
  }
128
162
  };
129
163
  /**
@@ -365,9 +399,7 @@ export const getScrollOffsetForIndex = (heightCache, calculatedItemHeight, idx,
365
399
  // Fallback: O(n) for a single query
366
400
  let offset = 0;
367
401
  for (let i = 0; i < safeIdx; i++) {
368
- const raw = heightCache[i];
369
- const height = Number.isFinite(raw) && raw > 0 ? raw : calculatedItemHeight;
370
- offset += height;
402
+ offset += getValidHeight(heightCache[i], calculatedItemHeight);
371
403
  }
372
404
  return offset;
373
405
  }
@@ -380,9 +412,48 @@ export const getScrollOffsetForIndex = (heightCache, calculatedItemHeight, idx,
380
412
  let offset = offsetBase;
381
413
  const start = blockIdx * blockSize;
382
414
  for (let i = start; i < safeIdx; i++) {
383
- const raw = heightCache[i];
384
- const height = Number.isFinite(raw) && raw > 0 ? raw : calculatedItemHeight;
385
- offset += height;
415
+ offset += getValidHeight(heightCache[i], calculatedItemHeight);
386
416
  }
387
417
  return offset;
388
418
  };
419
+ /**
420
+ * Builds block prefix sums for heightCache to accelerate offset queries.
421
+ *
422
+ * This function precomputes cumulative height sums for blocks of items, enabling
423
+ * O(blockSize) offset calculations instead of O(n). The returned array contains
424
+ * the total height of all items up to and including each completed block.
425
+ *
426
+ * For example, with blockSize=1000:
427
+ * - Entry 0: sum of heights for items 0-999
428
+ * - Entry 1: sum of heights for items 0-1999
429
+ * - Entry 2: sum of heights for items 0-2999
430
+ *
431
+ * @param {Record<number, number>} heightCache - Cache of measured item heights.
432
+ * @param {number} calculatedItemHeight - Estimated height for unmeasured items.
433
+ * @param {number} totalItems - Total number of items in the list.
434
+ * @param {number} [blockSize=1000] - Number of items per block for memoization.
435
+ * @returns {number[]} Array of cumulative height sums for each completed block.
436
+ *
437
+ * @example
438
+ * ```typescript
439
+ * const heightCache = { 0: 40, 1: 50, 2: 45 };
440
+ * const blockSums = buildBlockSums(heightCache, 40, 5000, 1000);
441
+ *
442
+ * // Use with getScrollOffsetForIndex for efficient lookups
443
+ * const offset = getScrollOffsetForIndex(heightCache, 40, 2500, blockSums);
444
+ * ```
445
+ */
446
+ export const buildBlockSums = (heightCache, calculatedItemHeight, totalItems, blockSize = 1000) => {
447
+ const blocks = Math.ceil(totalItems / blockSize);
448
+ const sums = new Array(Math.max(0, blocks - 1));
449
+ let running = 0;
450
+ for (let b = 0; b < blocks - 1; b++) {
451
+ const start = b * blockSize;
452
+ const end = start + blockSize;
453
+ for (let i = start; i < end; i++) {
454
+ running += getValidHeight(heightCache[i], calculatedItemHeight);
455
+ }
456
+ sums[b] = running;
457
+ }
458
+ return sums;
459
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humanspeak/svelte-virtual-list",
3
- "version": "0.3.6",
3
+ "version": "0.3.9",
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",
@@ -59,51 +59,51 @@
59
59
  "esm-env": "^1.2.2"
60
60
  },
61
61
  "devDependencies": {
62
- "@eslint/compat": "^1.4.1",
63
- "@eslint/js": "^9.39.1",
64
- "@faker-js/faker": "^10.1.0",
65
- "@playwright/test": "^1.56.1",
62
+ "@eslint/compat": "^2.0.1",
63
+ "@eslint/js": "^9.39.2",
64
+ "@faker-js/faker": "^10.2.0",
65
+ "@playwright/test": "^1.58.0",
66
66
  "@sveltejs/adapter-auto": "^7.0.0",
67
- "@sveltejs/kit": "^2.48.4",
68
- "@sveltejs/package": "^2.5.4",
69
- "@sveltejs/vite-plugin-svelte": "^6.2.1",
70
- "@tailwindcss/vite": "^4.1.17",
67
+ "@sveltejs/kit": "^2.50.1",
68
+ "@sveltejs/package": "^2.5.7",
69
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
70
+ "@tailwindcss/vite": "^4.1.18",
71
71
  "@testing-library/jest-dom": "^6.9.1",
72
- "@testing-library/svelte": "^5.2.8",
72
+ "@testing-library/svelte": "^5.3.1",
73
73
  "@testing-library/user-event": "^14.6.1",
74
- "@types/node": "^24.10.1",
75
- "@typescript-eslint/eslint-plugin": "^8.46.4",
76
- "@typescript-eslint/parser": "^8.46.4",
77
- "@vitest/coverage-v8": "^4.0.8",
74
+ "@types/node": "^25.1.0",
75
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
76
+ "@typescript-eslint/parser": "^8.54.0",
77
+ "@vitest/coverage-v8": "^4.0.18",
78
78
  "concurrently": "^9.2.1",
79
- "eslint": "^9.39.1",
79
+ "eslint": "^9.39.2",
80
80
  "eslint-config-prettier": "^10.1.8",
81
81
  "eslint-plugin-import": "^2.32.0",
82
- "eslint-plugin-svelte": "^3.13.0",
82
+ "eslint-plugin-svelte": "^3.14.0",
83
83
  "eslint-plugin-unused-imports": "^4.3.0",
84
- "globals": "^16.5.0",
84
+ "globals": "^17.2.0",
85
85
  "husky": "^9.1.7",
86
- "jsdom": "^27.2.0",
87
- "prettier": "^3.6.2",
86
+ "jsdom": "^27.4.0",
87
+ "prettier": "^3.8.1",
88
88
  "prettier-plugin-organize-imports": "^4.3.0",
89
- "prettier-plugin-sort-json": "^4.1.1",
90
- "prettier-plugin-svelte": "^3.4.0",
91
- "prettier-plugin-tailwindcss": "^0.7.1",
92
- "publint": "^0.3.15",
93
- "svelte": "^5.43.6",
94
- "svelte-check": "^4.3.4",
95
- "tailwindcss": "^4.1.17",
89
+ "prettier-plugin-sort-json": "^4.2.0",
90
+ "prettier-plugin-svelte": "^3.4.1",
91
+ "prettier-plugin-tailwindcss": "^0.7.2",
92
+ "publint": "^0.3.17",
93
+ "svelte": "^5.48.5",
94
+ "svelte-check": "^4.3.5",
95
+ "tailwindcss": "^4.1.18",
96
96
  "tw-animate-css": "^1.4.0",
97
97
  "typescript": "^5.9.3",
98
- "typescript-eslint": "^8.46.4",
99
- "vite": "^7.2.2",
100
- "vitest": "^4.0.8"
98
+ "typescript-eslint": "^8.54.0",
99
+ "vite": "^7.3.1",
100
+ "vitest": "^4.0.18"
101
101
  },
102
102
  "peerDependencies": {
103
103
  "svelte": "^5.0.0"
104
104
  },
105
105
  "volta": {
106
- "node": "22.18.0"
106
+ "node": "24.13.0"
107
107
  },
108
108
  "publishConfig": {
109
109
  "access": "public"