@humanspeak/svelte-virtual-list 0.3.8 → 0.3.10

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.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Performance metrics utility for profiling virtual list operations.
3
+ *
4
+ * This lightweight profiling wrapper measures execution times for critical operations
5
+ * in the virtual list component. Enable by setting the environment variable:
6
+ * PUBLIC_SVELTE_VIRTUAL_LIST_PERF=true
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { perfMetrics, measureSync, measureAsync, isPerfEnabled } from './perfMetrics.js'
11
+ *
12
+ * // Measure synchronous operation
13
+ * const result = measureSync('visibleRange', () => calculateVisibleRange(...))
14
+ *
15
+ * // Get aggregated stats
16
+ * const stats = perfMetrics.getStats()
17
+ * console.log(stats.visibleRange.avg, stats.visibleRange.max)
18
+ * ```
19
+ */
20
+ export type MetricName = 'scrollHandler' | 'visibleRange' | 'transformY' | 'heightBatch' | 'frameTime' | 'displayItems' | 'resizeObserver' | 'initialRender';
21
+ export interface MetricEntry {
22
+ timestamp: number;
23
+ duration: number;
24
+ }
25
+ export interface MetricStats {
26
+ count: number;
27
+ total: number;
28
+ avg: number;
29
+ min: number;
30
+ max: number;
31
+ recent: number[];
32
+ }
33
+ export interface PerfMetrics {
34
+ scrollHandler: MetricEntry[];
35
+ visibleRange: MetricEntry[];
36
+ transformY: MetricEntry[];
37
+ heightBatch: MetricEntry[];
38
+ frameTime: MetricEntry[];
39
+ displayItems: MetricEntry[];
40
+ resizeObserver: MetricEntry[];
41
+ initialRender: MetricEntry[];
42
+ }
43
+ /**
44
+ * Check if performance profiling is enabled via environment variable
45
+ */
46
+ export declare const isPerfEnabled: () => boolean;
47
+ /**
48
+ * Measure a synchronous function's execution time
49
+ */
50
+ export declare const measureSync: <T>(name: MetricName, fn: () => T) => T;
51
+ /**
52
+ * Measure an async function's execution time
53
+ */
54
+ export declare const measureAsync: <T>(name: MetricName, fn: () => Promise<T>) => Promise<T>;
55
+ /**
56
+ * Start a manual timing measurement (for operations that span callbacks)
57
+ */
58
+ export declare const startMeasure: () => (() => number);
59
+ /**
60
+ * Record a pre-calculated duration
61
+ */
62
+ export declare const recordDuration: (name: MetricName, duration: number) => void;
63
+ /**
64
+ * Start FPS tracking during scroll
65
+ */
66
+ export declare const startFpsTracking: () => void;
67
+ /**
68
+ * Stop FPS tracking and return average FPS
69
+ */
70
+ export declare const stopFpsTracking: () => number;
71
+ /**
72
+ * Get current FPS (call during active tracking)
73
+ */
74
+ export declare const getCurrentFps: () => number;
75
+ /**
76
+ * Performance metrics interface with stats aggregation
77
+ */
78
+ export declare const perfMetrics: {
79
+ /**
80
+ * Get aggregated stats for all metrics
81
+ */
82
+ getStats: () => Record<MetricName, MetricStats>;
83
+ /**
84
+ * Get stats for a single metric
85
+ */
86
+ getMetricStats: (name: MetricName) => MetricStats;
87
+ /**
88
+ * Get raw metric entries
89
+ */
90
+ getRawMetrics: () => PerfMetrics;
91
+ /**
92
+ * Reset all metrics
93
+ */
94
+ reset: () => void;
95
+ /**
96
+ * Get a summary report suitable for console output
97
+ */
98
+ getSummary: () => string;
99
+ /**
100
+ * Log summary to console
101
+ */
102
+ logSummary: () => void;
103
+ };
104
+ /**
105
+ * Memory tracking utilities
106
+ */
107
+ export declare const getMemoryUsage: () => {
108
+ usedJSHeapSize: number;
109
+ totalJSHeapSize: number;
110
+ } | null;
111
+ /**
112
+ * Helper to format bytes to human-readable size
113
+ */
114
+ export declare const formatBytes: (bytes: number) => string;
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Performance metrics utility for profiling virtual list operations.
3
+ *
4
+ * This lightweight profiling wrapper measures execution times for critical operations
5
+ * in the virtual list component. Enable by setting the environment variable:
6
+ * PUBLIC_SVELTE_VIRTUAL_LIST_PERF=true
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { perfMetrics, measureSync, measureAsync, isPerfEnabled } from './perfMetrics.js'
11
+ *
12
+ * // Measure synchronous operation
13
+ * const result = measureSync('visibleRange', () => calculateVisibleRange(...))
14
+ *
15
+ * // Get aggregated stats
16
+ * const stats = perfMetrics.getStats()
17
+ * console.log(stats.visibleRange.avg, stats.visibleRange.max)
18
+ * ```
19
+ */
20
+ const MAX_SAMPLES = 1000; // Keep last N samples per metric
21
+ const RECENT_WINDOW = 100; // Number of recent samples for stats
22
+ /**
23
+ * Check if performance profiling is enabled via environment variable
24
+ */
25
+ export const isPerfEnabled = () => {
26
+ if (typeof process === 'undefined')
27
+ return false;
28
+ return (process?.env?.PUBLIC_SVELTE_VIRTUAL_LIST_PERF === 'true' ||
29
+ process?.env?.SVELTE_VIRTUAL_LIST_PERF === 'true');
30
+ };
31
+ const createEmptyMetrics = () => ({
32
+ scrollHandler: [],
33
+ visibleRange: [],
34
+ transformY: [],
35
+ heightBatch: [],
36
+ frameTime: [],
37
+ displayItems: [],
38
+ resizeObserver: [],
39
+ initialRender: []
40
+ });
41
+ let metrics = createEmptyMetrics();
42
+ /**
43
+ * Record a metric measurement
44
+ */
45
+ const record = (name, duration) => {
46
+ if (!isPerfEnabled())
47
+ return;
48
+ const entry = {
49
+ timestamp: performance.now(),
50
+ duration
51
+ };
52
+ metrics[name].push(entry);
53
+ // Keep array bounded
54
+ if (metrics[name].length > MAX_SAMPLES) {
55
+ metrics[name] = metrics[name].slice(-MAX_SAMPLES);
56
+ }
57
+ };
58
+ /**
59
+ * Measure a synchronous function's execution time
60
+ */
61
+ export const measureSync = (name, fn) => {
62
+ if (!isPerfEnabled()) {
63
+ return fn();
64
+ }
65
+ const start = performance.now();
66
+ try {
67
+ return fn();
68
+ }
69
+ finally {
70
+ const duration = performance.now() - start;
71
+ record(name, duration);
72
+ }
73
+ };
74
+ /**
75
+ * Measure an async function's execution time
76
+ */
77
+ export const measureAsync = async (name, fn) => {
78
+ if (!isPerfEnabled()) {
79
+ return fn();
80
+ }
81
+ const start = performance.now();
82
+ try {
83
+ return await fn();
84
+ }
85
+ finally {
86
+ const duration = performance.now() - start;
87
+ record(name, duration);
88
+ }
89
+ };
90
+ /**
91
+ * Start a manual timing measurement (for operations that span callbacks)
92
+ */
93
+ export const startMeasure = () => {
94
+ const start = performance.now();
95
+ return () => performance.now() - start;
96
+ };
97
+ /**
98
+ * Record a pre-calculated duration
99
+ */
100
+ export const recordDuration = (name, duration) => {
101
+ record(name, duration);
102
+ };
103
+ /**
104
+ * Calculate stats for a single metric
105
+ */
106
+ const calculateStats = (entries) => {
107
+ if (entries.length === 0) {
108
+ return { count: 0, total: 0, avg: 0, min: 0, max: 0, recent: [] };
109
+ }
110
+ const durations = entries.map((e) => e.duration);
111
+ const total = durations.reduce((a, b) => a + b, 0);
112
+ const recent = durations.slice(-RECENT_WINDOW);
113
+ return {
114
+ count: entries.length,
115
+ total,
116
+ avg: total / entries.length,
117
+ min: Math.min(...durations),
118
+ max: Math.max(...durations),
119
+ recent
120
+ };
121
+ };
122
+ /**
123
+ * Frame rate tracking for scroll performance
124
+ */
125
+ let frameTimestamps = [];
126
+ let rafId = null;
127
+ /**
128
+ * Start FPS tracking during scroll
129
+ */
130
+ export const startFpsTracking = () => {
131
+ if (!isPerfEnabled())
132
+ return;
133
+ if (rafId !== null)
134
+ return;
135
+ frameTimestamps = [];
136
+ const trackFrame = () => {
137
+ frameTimestamps.push(performance.now());
138
+ // Keep only last 2 seconds of frames
139
+ const cutoff = performance.now() - 2000;
140
+ frameTimestamps = frameTimestamps.filter((t) => t > cutoff);
141
+ rafId = requestAnimationFrame(trackFrame);
142
+ };
143
+ rafId = requestAnimationFrame(trackFrame);
144
+ };
145
+ /**
146
+ * Stop FPS tracking and return average FPS
147
+ */
148
+ export const stopFpsTracking = () => {
149
+ if (rafId !== null) {
150
+ cancelAnimationFrame(rafId);
151
+ rafId = null;
152
+ }
153
+ if (frameTimestamps.length < 2)
154
+ return 0;
155
+ const duration = frameTimestamps[frameTimestamps.length - 1] - frameTimestamps[0];
156
+ if (duration <= 0)
157
+ return 0;
158
+ return (frameTimestamps.length - 1) / (duration / 1000);
159
+ };
160
+ /**
161
+ * Get current FPS (call during active tracking)
162
+ */
163
+ export const getCurrentFps = () => {
164
+ if (frameTimestamps.length < 2)
165
+ return 0;
166
+ const now = performance.now();
167
+ const recentFrames = frameTimestamps.filter((t) => now - t < 1000);
168
+ if (recentFrames.length < 2)
169
+ return 0;
170
+ const duration = recentFrames[recentFrames.length - 1] - recentFrames[0];
171
+ if (duration <= 0)
172
+ return 0;
173
+ return (recentFrames.length - 1) / (duration / 1000);
174
+ };
175
+ /**
176
+ * Performance metrics interface with stats aggregation
177
+ */
178
+ export const perfMetrics = {
179
+ /**
180
+ * Get aggregated stats for all metrics
181
+ */
182
+ getStats: () => ({
183
+ scrollHandler: calculateStats(metrics.scrollHandler),
184
+ visibleRange: calculateStats(metrics.visibleRange),
185
+ transformY: calculateStats(metrics.transformY),
186
+ heightBatch: calculateStats(metrics.heightBatch),
187
+ frameTime: calculateStats(metrics.frameTime),
188
+ displayItems: calculateStats(metrics.displayItems),
189
+ resizeObserver: calculateStats(metrics.resizeObserver),
190
+ initialRender: calculateStats(metrics.initialRender)
191
+ }),
192
+ /**
193
+ * Get stats for a single metric
194
+ */
195
+ getMetricStats: (name) => calculateStats(metrics[name]),
196
+ /**
197
+ * Get raw metric entries
198
+ */
199
+ getRawMetrics: () => ({ ...metrics }),
200
+ /**
201
+ * Reset all metrics
202
+ */
203
+ reset: () => {
204
+ metrics = createEmptyMetrics();
205
+ },
206
+ /**
207
+ * Get a summary report suitable for console output
208
+ */
209
+ getSummary: () => {
210
+ const stats = perfMetrics.getStats();
211
+ const lines = ['=== Virtual List Performance Summary ==='];
212
+ for (const [name, stat] of Object.entries(stats)) {
213
+ if (stat.count > 0) {
214
+ lines.push(`${name}: avg=${stat.avg.toFixed(2)}ms, max=${stat.max.toFixed(2)}ms, count=${stat.count}`);
215
+ }
216
+ }
217
+ return lines.join('\n');
218
+ },
219
+ /**
220
+ * Log summary to console
221
+ */
222
+ logSummary: () => {
223
+ if (isPerfEnabled()) {
224
+ console.info(perfMetrics.getSummary());
225
+ }
226
+ }
227
+ };
228
+ /**
229
+ * Memory tracking utilities
230
+ */
231
+ export const getMemoryUsage = () => {
232
+ if (typeof performance !== 'undefined' &&
233
+ 'memory' in performance &&
234
+ performance.memory) {
235
+ const memory = performance.memory;
236
+ return {
237
+ usedJSHeapSize: memory.usedJSHeapSize,
238
+ totalJSHeapSize: memory.totalJSHeapSize
239
+ };
240
+ }
241
+ return null;
242
+ };
243
+ /**
244
+ * Helper to format bytes to human-readable size
245
+ */
246
+ export const formatBytes = (bytes) => {
247
+ if (bytes < 1024)
248
+ return `${bytes} B`;
249
+ if (bytes < 1024 * 1024)
250
+ return `${(bytes / 1024).toFixed(1)} KB`;
251
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
252
+ };
@@ -1,4 +1,39 @@
1
1
  import type { SvelteVirtualListMode, SvelteVirtualListScrollAlign } from '../types.js';
2
+ /**
3
+ * Calculates the scroll target for aligning an item to a specific edge.
4
+ *
5
+ * This helper consolidates the shared alignment logic between bottomToTop
6
+ * and topToBottom scroll calculations, reducing code duplication.
7
+ *
8
+ * @param {number} itemTop - The top position of the item in pixels
9
+ * @param {number} itemBottom - The bottom position of the item in pixels
10
+ * @param {number} scrollTop - Current scroll position in pixels
11
+ * @param {number} viewportHeight - Height of the viewport in pixels
12
+ * @param {'top' | 'bottom' | 'nearest'} align - The alignment mode
13
+ * @returns {number | null} The scroll target position, or null if item is already visible (for 'nearest')
14
+ */
15
+ export declare const alignToEdge: (itemTop: number, itemBottom: number, scrollTop: number, viewportHeight: number, align: "top" | "bottom" | "nearest") => number | null;
16
+ /**
17
+ * Calculates the scroll target for aligning a visible item to its nearest edge.
18
+ *
19
+ * Unlike alignToEdge with 'nearest', this always returns a scroll position
20
+ * even when the item is visible. Used for 'auto' alignment mode when item
21
+ * is within the visible range.
22
+ *
23
+ * @param {number} itemTop - The top position of the item in pixels
24
+ * @param {number} itemBottom - The bottom position of the item in pixels
25
+ * @param {number} scrollTop - Current scroll position in pixels
26
+ * @param {number} viewportHeight - Height of the viewport in pixels
27
+ * @returns {number} The scroll target position aligned to nearest edge
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * // For a visible item, align to whichever edge is closer
32
+ * const scrollTarget = alignVisibleToNearestEdge(400, 450, 200, 400)
33
+ * viewportElement.scrollTo({ top: scrollTarget })
34
+ * ```
35
+ */
36
+ export declare const alignVisibleToNearestEdge: (itemTop: number, itemBottom: number, scrollTop: number, viewportHeight: number) => number;
2
37
  /**
3
38
  * Parameters for calculating scroll target position
4
39
  */
@@ -1,4 +1,66 @@
1
- import { getScrollOffsetForIndex } from './virtualList.js';
1
+ import { clampValue, getScrollOffsetForIndex } from './virtualList.js';
2
+ /**
3
+ * Calculates the scroll target for aligning an item to a specific edge.
4
+ *
5
+ * This helper consolidates the shared alignment logic between bottomToTop
6
+ * and topToBottom scroll calculations, reducing code duplication.
7
+ *
8
+ * @param {number} itemTop - The top position of the item in pixels
9
+ * @param {number} itemBottom - The bottom position of the item in pixels
10
+ * @param {number} scrollTop - Current scroll position in pixels
11
+ * @param {number} viewportHeight - Height of the viewport in pixels
12
+ * @param {'top' | 'bottom' | 'nearest'} align - The alignment mode
13
+ * @returns {number | null} The scroll target position, or null if item is already visible (for 'nearest')
14
+ */
15
+ export const alignToEdge = (itemTop, itemBottom, scrollTop, viewportHeight, align) => {
16
+ if (align === 'top') {
17
+ return itemTop;
18
+ }
19
+ if (align === 'bottom') {
20
+ return clampValue(itemBottom - viewportHeight, 0, Infinity);
21
+ }
22
+ // 'nearest' alignment
23
+ const viewportBottom = scrollTop + viewportHeight;
24
+ const isVisible = itemTop < viewportBottom && itemBottom > scrollTop;
25
+ if (isVisible) {
26
+ // Already visible, no scroll needed
27
+ return null;
28
+ }
29
+ // Not visible - align to nearest edge
30
+ const distanceToTop = Math.abs(scrollTop - itemTop);
31
+ const distanceToBottom = Math.abs(viewportBottom - itemBottom);
32
+ return distanceToTop < distanceToBottom
33
+ ? itemTop
34
+ : clampValue(itemBottom - viewportHeight, 0, Infinity);
35
+ };
36
+ /**
37
+ * Calculates the scroll target for aligning a visible item to its nearest edge.
38
+ *
39
+ * Unlike alignToEdge with 'nearest', this always returns a scroll position
40
+ * even when the item is visible. Used for 'auto' alignment mode when item
41
+ * is within the visible range.
42
+ *
43
+ * @param {number} itemTop - The top position of the item in pixels
44
+ * @param {number} itemBottom - The bottom position of the item in pixels
45
+ * @param {number} scrollTop - Current scroll position in pixels
46
+ * @param {number} viewportHeight - Height of the viewport in pixels
47
+ * @returns {number} The scroll target position aligned to nearest edge
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * // For a visible item, align to whichever edge is closer
52
+ * const scrollTarget = alignVisibleToNearestEdge(400, 450, 200, 400)
53
+ * viewportElement.scrollTo({ top: scrollTarget })
54
+ * ```
55
+ */
56
+ export const alignVisibleToNearestEdge = (itemTop, itemBottom, scrollTop, viewportHeight) => {
57
+ const viewportBottom = scrollTop + viewportHeight;
58
+ const distanceToTop = Math.abs(scrollTop - itemTop);
59
+ const distanceToBottom = Math.abs(viewportBottom - itemBottom);
60
+ return distanceToTop < distanceToBottom
61
+ ? itemTop
62
+ : clampValue(itemBottom - viewportHeight, 0, Infinity);
63
+ };
2
64
  /**
3
65
  * Calculates the target scroll position for scrolling to a specific item index.
4
66
  *
@@ -74,42 +136,25 @@ const calculateBottomToTopScrollTarget = (params) => {
74
136
  const totalHeight = getScrollOffsetForIndex(heightCache, calculatedItemHeight, itemsLength);
75
137
  const itemOffset = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
76
138
  const itemHeight = calculatedItemHeight;
139
+ // Calculate item boundaries in bottomToTop coordinate space
140
+ const itemTop = totalHeight - (itemOffset + itemHeight);
141
+ const itemBottom = totalHeight - itemOffset;
77
142
  if (align === 'auto') {
78
143
  // If item is above the viewport, align to top
79
144
  if (targetIndex < firstVisibleIndex) {
80
- return Math.max(0, totalHeight - (itemOffset + itemHeight));
145
+ return alignToEdge(itemTop, itemBottom, scrollTop, height, 'top');
81
146
  }
82
147
  else if (targetIndex > lastVisibleIndex - 1) {
83
148
  // In bottomToTop, "below" means higher indices that need HIGHER scrollTop
84
- return Math.max(0, totalHeight - itemOffset - height);
149
+ return alignToEdge(itemTop, itemBottom, scrollTop, height, 'bottom');
85
150
  }
86
151
  else {
87
- const itemTop = totalHeight - (itemOffset + itemHeight);
88
- const itemBottom = totalHeight - itemOffset;
89
- const distanceToTop = Math.abs(scrollTop - itemTop);
90
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
91
- return distanceToTop < distanceToBottom ? itemTop : Math.max(0, itemBottom - height);
152
+ // Item is visible - align to nearest edge (always returns a value)
153
+ return alignVisibleToNearestEdge(itemTop, itemBottom, scrollTop, height);
92
154
  }
93
155
  }
94
- else if (align === 'top') {
95
- return Math.max(0, totalHeight - (itemOffset + itemHeight));
96
- }
97
- else if (align === 'bottom') {
98
- return Math.max(0, totalHeight - itemOffset - height);
99
- }
100
- else if (align === 'nearest') {
101
- const itemTop = totalHeight - (itemOffset + itemHeight);
102
- const itemBottom = totalHeight - itemOffset;
103
- if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
104
- // Not visible, align to nearest edge
105
- const distanceToTop = Math.abs(scrollTop - itemTop);
106
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
107
- return distanceToTop < distanceToBottom ? itemTop : Math.max(0, itemBottom - height);
108
- }
109
- else {
110
- // Already visible, do nothing
111
- return null;
112
- }
156
+ if (align === 'top' || align === 'bottom' || align === 'nearest') {
157
+ return alignToEdge(itemTop, itemBottom, scrollTop, height, align);
113
158
  }
114
159
  return null;
115
160
  };
@@ -126,58 +171,25 @@ const calculateBottomToTopScrollTarget = (params) => {
126
171
  */
127
172
  const calculateTopToBottomScrollTarget = (params) => {
128
173
  const { align, targetIndex, calculatedItemHeight, height, scrollTop, firstVisibleIndex, lastVisibleIndex, heightCache } = params;
174
+ // Calculate item boundaries
175
+ const itemTop = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
176
+ const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
129
177
  if (align === 'auto') {
130
178
  // If item is above the viewport, align to top
131
179
  if (targetIndex < firstVisibleIndex) {
132
- const scrollTarget = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
133
- return scrollTarget;
180
+ return alignToEdge(itemTop, itemBottom, scrollTop, height, 'top');
134
181
  }
135
182
  // If item is below the viewport, align to bottom
136
183
  else if (targetIndex > lastVisibleIndex - 1) {
137
- const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
138
- const scrollTarget = Math.max(0, itemBottom - height);
139
- return scrollTarget;
184
+ return alignToEdge(itemTop, itemBottom, scrollTop, height, 'bottom');
140
185
  }
141
186
  else {
142
- // Item is visible but not aligned: align to nearest edge
143
- const itemTop = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
144
- const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
145
- const distanceToTop = Math.abs(scrollTop - itemTop);
146
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
147
- if (distanceToTop < distanceToBottom) {
148
- return itemTop;
149
- }
150
- else {
151
- return Math.max(0, itemBottom - height);
152
- }
187
+ // Item is visible - align to nearest edge (always returns a value)
188
+ return alignVisibleToNearestEdge(itemTop, itemBottom, scrollTop, height);
153
189
  }
154
190
  }
155
- else if (align === 'top') {
156
- const scrollTarget = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
157
- return scrollTarget;
158
- }
159
- else if (align === 'bottom') {
160
- const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
161
- return Math.max(0, itemBottom - height);
162
- }
163
- else if (align === 'nearest') {
164
- const itemTop = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex);
165
- const itemBottom = getScrollOffsetForIndex(heightCache, calculatedItemHeight, targetIndex + 1);
166
- if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
167
- // Not visible, align to nearest edge
168
- const distanceToTop = Math.abs(scrollTop - itemTop);
169
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom);
170
- if (distanceToTop < distanceToBottom) {
171
- return itemTop;
172
- }
173
- else {
174
- return Math.max(0, itemBottom - height);
175
- }
176
- }
177
- else {
178
- // Already visible, do nothing
179
- return null;
180
- }
191
+ if (align === 'top' || align === 'bottom' || align === 'nearest') {
192
+ return alignToEdge(itemTop, itemBottom, scrollTop, height, align);
181
193
  }
182
194
  return null;
183
195
  };
@@ -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
  *