@humanspeak/svelte-virtual-list 0.4.6 → 0.5.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.
@@ -2,14 +2,13 @@
2
2
  @component SvelteVirtualList
3
3
 
4
4
  A high-performance, memory-efficient virtualized list component for Svelte 5.
5
- Renders only visible items plus a buffer, supporting dynamic item heights,
6
- bi-directional (top-to-bottom and bottom-to-top) scrolling, and programmatic control.
5
+ Renders only visible items plus a buffer, supporting dynamic item heights
6
+ and programmatic control.
7
7
 
8
8
  =============================
9
9
  == Key Features ==
10
10
  =============================
11
11
  - Dynamic item height support (no fixed height required)
12
- - Top-to-bottom and bottom-to-top (chat-style) scrolling
13
12
  - Programmatic scrolling with flexible alignment (top, bottom, auto)
14
13
  - Smooth scrolling and buffer size configuration
15
14
  - SSR compatible and hydration-friendly
@@ -25,7 +24,6 @@
25
24
  ```svelte
26
25
  <SvelteVirtualList
27
26
  items={data}
28
- mode="bottomToTop"
29
27
  bind:this={listRef}
30
28
  >
31
29
  {#snippet renderItem(item)}
@@ -43,7 +41,6 @@
43
41
  - Handles resize events and dynamic content changes
44
42
  - Optimized for very large lists through virtualization
45
43
  - Modular architecture with extracted utility functions
46
- - Bi-directional support: mode="topToBottom" or "bottomToTop"
47
44
  - Designed for extensibility and easy debugging
48
45
 
49
46
  =============================
@@ -76,49 +73,44 @@
76
73
  * - Implemented debounced measurements
77
74
  * - Created height averaging mechanism for performance
78
75
  *
79
- * 3. Bidirectional Scrolling
80
- * - Added bottomToTop mode
81
- * - Solved complex initialization issues with flexbox
82
- * - Implemented careful scroll position management
83
- *
84
- * 4. Performance Optimizations ✓
76
+ * 3. Performance Optimizations
85
77
  * - Added element recycling through keyed each blocks
86
78
  * - Implemented RAF for smooth animations
87
79
  * - Optimized DOM updates with transform translations
88
80
  *
89
- * 5. Stability Improvements ✓
81
+ * 4. Stability Improvements ✓
90
82
  * - Added ResizeObserver for responsive updates
91
83
  * - Implemented proper cleanup on component destruction
92
84
  * - Added debug mode for development assistance
93
85
  *
94
- * 6. Large Dataset Optimizations ✓
86
+ * 5. Large Dataset Optimizations ✓
95
87
  * - Implemented chunked processing for 10k+ items
96
88
  * - Added progressive initialization system
97
89
  * - Deferred height calculations for better initial load
98
90
  * - Optimized memory usage for large lists
99
91
  * - Added progress tracking for initialization
100
92
  *
101
- * 7. Size Management Improvements ✓
93
+ * 6. Size Management Improvements ✓
102
94
  * - Implemented height caching system for measured items
103
95
  * - Added smart height estimation for unmeasured items
104
96
  * - Optimized resize handling with debouncing
105
97
  * - Added height recalculation on content changes
106
98
  * - Implemented progressive height adjustments
107
99
  *
108
- * 8. Code Quality & Maintainability ✓
100
+ * 7. Code Quality & Maintainability ✓
109
101
  * - Extracted debug utilities for better testing
110
102
  * - Improved type safety throughout
111
103
  * - Added comprehensive documentation
112
104
  * - Optimized debug output to reduce noise
113
105
  *
114
- * 9. Architecture Refactoring ✓
106
+ * 8. Architecture Refactoring ✓
115
107
  * - Extracted scroll calculation logic to scrollCalculation.ts utility
116
108
  * - Extracted ResizeObserver utilities to resizeObserver.ts
117
109
  * - Added comprehensive test coverage for extracted utilities
118
110
  * - Improved separation of concerns and maintainability
119
111
  * - Simplified initialization (removed unnecessary chunked processing)
120
112
  *
121
- * 10. Future Improvements (Planned)
113
+ * 9. Future Improvements (Planned)
122
114
  * - Add horizontal scrolling support
123
115
  * - Implement variable-sized item caching
124
116
  * - Add keyboard navigation support
@@ -126,7 +118,6 @@
126
118
  * - Add accessibility enhancements
127
119
  *
128
120
  * Technical Challenges Solved:
129
- * - Bottom-to-top scrolling in flexbox layouts
130
121
  * - Dynamic height calculations without layout thrashing
131
122
  * - Smooth scrolling on various devices
132
123
  * - Memory management for large lists
@@ -166,8 +157,7 @@
166
157
  calculateTransformY,
167
158
  calculateVisibleRange,
168
159
  clampValue,
169
- updateHeightAndScroll as utilsUpdateHeightAndScroll,
170
- getScrollOffsetForIndex
160
+ updateHeightAndScroll as utilsUpdateHeightAndScroll
171
161
  } from './utils/virtualList.js'
172
162
  import { createDebugInfo, shouldShowDebugInfo } from './utils/virtualListDebug.js'
173
163
  import { calculateScrollTarget } from './utils/scrollCalculation.js'
@@ -178,11 +168,7 @@
178
168
 
179
169
  const rafSchedule = createRafScheduler()
180
170
  // Timing constants
181
- const GLOBAL_CORRECTION_COOLDOWN_MS = 16
182
- const SCROLL_IDLE_DELAY_MS = 250
183
- const SUPPRESSION_WINDOW_MS = 450
184
171
  const HEIGHT_DEBOUNCE_MS = 100
185
- const lastCorrectionTimestampByViewport = new WeakMap<HTMLElement, number>()
186
172
  // Package-specific debug flag - safe for library distribution
187
173
  // Enable with: PUBLIC_SVELTE_VIRTUAL_LIST_DEBUG=true (preferred) or SVELTE_VIRTUAL_LIST_DEBUG=true
188
174
  // Avoid SvelteKit-only $env imports so library works in non-Kit/Vitest contexts
@@ -191,20 +177,6 @@
191
177
  (process?.env?.PUBLIC_SVELTE_VIRTUAL_LIST_DEBUG === 'true' ||
192
178
  process?.env?.SVELTE_VIRTUAL_LIST_DEBUG === 'true')
193
179
  )
194
- // Feature flags - default off; enable via env for incremental rollout
195
- const anchorModeEnabled = Boolean(
196
- typeof process !== 'undefined' &&
197
- (process?.env?.PUBLIC_SVL_ANCHOR_MODE === 'true' ||
198
- process?.env?.SVL_ANCHOR_MODE === 'true')
199
- )
200
- const idleCorrectionsOnly = Boolean(
201
- typeof process !== 'undefined' &&
202
- (process?.env?.PUBLIC_SVL_IDLE_ONLY === 'true' || process?.env?.SVL_IDLE_ONLY === 'true')
203
- )
204
- const batchUpdatesEnabled = Boolean(
205
- typeof process !== 'undefined' &&
206
- (process?.env?.PUBLIC_SVL_BATCH === 'true' || process?.env?.SVL_BATCH === 'true')
207
- )
208
180
  /**
209
181
  * Core configuration props with default values
210
182
  * @type {SvelteVirtualListProps<TItem>}
@@ -219,7 +191,6 @@
219
191
  contentClass, // Custom class for the content wrapper
220
192
  itemsClass, // Custom class for the items wrapper
221
193
  debugFunction, // Custom debug logging function
222
- mode = 'topToBottom', // Scroll direction mode
223
194
  bufferSize = 20, // Number of items to render outside visible area
224
195
  testId, // Base test ID for component elements (undefined = no data-testid attributes)
225
196
  onLoadMore, // Callback when more data needed (supports sync and async)
@@ -243,102 +214,7 @@
243
214
 
244
215
  const isCalculatingHeight = $state(false) // Prevents concurrent height calculations
245
216
  let isLoadingMore = $state(false) // Prevents concurrent onLoadMore calls
246
- let isScrolling = $state(false) // Tracks active scrolling state
247
- let scrollIdleTimer: number | null = null
248
- // Anchor state (read-only capture; used when anchorModeEnabled)
249
- let lastAnchorIndex = $state(0)
250
- let lastAnchorOffset = $state(0) // offset within anchored item (px)
251
- let pendingAnchorReconcile = $state(false)
252
- let batchDepth = $state(0)
253
-
254
- const captureAnchor = () => {
255
- if (!heightManager.viewportElement) return
256
- const vr = visibleItems
257
- const anchorIndex = Math.max(0, vr.start)
258
- const cache = heightManager.getHeightCache()
259
- const est = heightManager.averageHeight
260
- const maxScrollTop = Math.max(0, totalHeight - (height || 0))
261
- // Offset from start to anchored item
262
- const blockSums = heightManager.getBlockSums()
263
- const offsetToIndex = getScrollOffsetForIndex(cache, est, anchorIndex, blockSums)
264
- const currentTop = heightManager.viewport.scrollTop
265
- let offsetWithin: number
266
- if (mode === 'bottomToTop') {
267
- // Convert distance-from-end to distance-from-start
268
- const distanceFromStart = maxScrollTop - currentTop
269
- offsetWithin = distanceFromStart - offsetToIndex
270
- } else {
271
- offsetWithin = currentTop - offsetToIndex
272
- }
273
- lastAnchorIndex = anchorIndex
274
- lastAnchorOffset = Math.max(0, Math.round(offsetWithin))
275
- // Expose for tests
276
- ;(heightManager.viewport as unknown as Record<string, unknown>).__svlAnchor = {
277
- index: lastAnchorIndex,
278
- offset: lastAnchorOffset
279
- }
280
- pendingAnchorReconcile = true
281
- }
282
-
283
- const reconcileToAnchorIfEnabled = () => {
284
- if (!anchorModeEnabled || !heightManager.viewportElement) return
285
- if (!pendingAnchorReconcile) return
286
- const cache = heightManager.getHeightCache()
287
- const est = heightManager.averageHeight
288
- const blockSums = heightManager.getBlockSums()
289
- const offsetToIndex = getScrollOffsetForIndex(
290
- cache,
291
- est,
292
- Math.max(0, lastAnchorIndex),
293
- blockSums
294
- )
295
- const maxScrollTop = clampValue(totalHeight - (height || 0), 0, Infinity)
296
- let targetTop: number
297
- if (mode === 'bottomToTop') {
298
- const distanceFromStart = clampValue(offsetToIndex + lastAnchorOffset, 0, Infinity)
299
- targetTop = clampValue(Math.round(maxScrollTop - distanceFromStart), 0, maxScrollTop)
300
- } else {
301
- targetTop = clampValue(Math.round(offsetToIndex + lastAnchorOffset), 0, maxScrollTop)
302
- }
303
- if (Math.abs(heightManager.viewport.scrollTop - targetTop) >= 2) {
304
- syncScrollTop(targetTop)
305
- }
306
- pendingAnchorReconcile = false
307
- }
308
-
309
- /**
310
- * Runs a batch of updates with scroll corrections coalesced until the batch completes.
311
- *
312
- * Use this method when making multiple changes to the items array to prevent
313
- * intermediate scroll corrections. The scroll position reconciliation is deferred
314
- * until the batch exits, ensuring smooth visual updates.
315
- *
316
- * @param {() => void} fn - The function containing batch updates to execute.
317
- * @returns {void}
318
- *
319
- * @example
320
- * ```typescript
321
- * // Add multiple items without intermediate scroll corrections
322
- * list.runInBatch(() => {
323
- * items.push(newItem1);
324
- * items.push(newItem2);
325
- * items.push(newItem3);
326
- * });
327
- * ```
328
- */
329
- export const runInBatch = (fn: () => void): void => {
330
- batchDepth += 1
331
- try {
332
- fn()
333
- } finally {
334
- batchDepth = Math.max(0, batchDepth - 1)
335
- if (batchUpdatesEnabled && batchDepth === 0) {
336
- reconcileToAnchorIfEnabled()
337
- }
338
- }
339
- }
340
217
  let lastMeasuredIndex = $state(-1) // Index of last measured item
341
- let lastScrollTopSnapshot = $state(0) // Previous scroll position snapshot
342
218
 
343
219
  /**
344
220
  * Timers and Observers
@@ -352,15 +228,12 @@
352
228
  */
353
229
  const dirtyItems = $state(new Set<number>()) // Set of item indices that need height recalculation
354
230
  let dirtyItemsCount = $state(0) // Reactive count of dirty items
355
- // Fallback measurement used only when height has not been established yet
356
- let measuredFallbackHeight = $state(0)
357
231
  // Scroll delta threshold optimization - track last scroll position used for range calculation
358
232
  let lastProcessedScrollTop = $state(0)
359
233
 
360
234
  let prevVisibleRange = $state<SvelteVirtualListPreviousVisibleRange | null>(null)
361
235
  let prevHeight = $state<number>(0)
362
236
  let prevTotalHeightForScrollCorrection = $state<number>(0)
363
- let lastBottomDistance = $state<number | null>(null)
364
237
 
365
238
  /**
366
239
  * Reactive Height Manager - O(1) height calculation system
@@ -401,187 +274,11 @@
401
274
  heightManager.scrollTop = scrollValue
402
275
  }
403
276
 
404
- // Dynamic update coordination to avoid UA scroll anchoring interference
405
- let suppressBottomAnchoringUntilMs = $state(0)
406
-
407
- /**
408
- * Handles scroll position corrections when item heights change, ensuring proper positioning
409
- * relative to the user's scroll context. This function calculates the cumulative impact of
410
- * height changes above the current viewport and adjusts the scroll position accordingly.
411
- *
412
- * The correction logic considers:
413
- * - Height changes occurring above the visible area (which would shift content)
414
- * - The current scroll position and visible range
415
- * - Whether height changes warrant a scroll adjustment
416
- *
417
- * This prevents jarring jumps when items resize, maintaining the user's visual context
418
- * and where they are positioned relative to the current scroll position.
419
- */
420
- const handleHeightChangesScrollCorrection = (
421
- heightChanges: Array<{ index: number; oldHeight: number; newHeight: number; delta: number }>
422
- ) => {
423
- if (!heightManager.viewportElement || !heightManager.initialized || userHasScrolledAway) {
424
- return
425
- }
426
- // Coalesce adjustments during active scroll; apply on idle
427
- if (isScrolling) {
428
- // Accumulate net change above viewport and defer application
429
- let pending = 0
430
- const currentVisibleRange = visibleItems
431
- for (const change of heightChanges) {
432
- if (change.index < currentVisibleRange.start) pending += change.delta
433
- }
434
- if (pending !== 0) {
435
- // Store on the viewport element to avoid extra module globals
436
- const key = '__svl_pendingHeightAdj__' as unknown as keyof HTMLElement
437
- const prev = (heightManager.viewport as unknown as Record<string, number>)[
438
- key as string
439
- ] as number | undefined
440
- ;(heightManager.viewport as unknown as Record<string, number>)[key as string] =
441
- (prev ?? 0) + pending
442
- }
443
- return
444
- }
445
-
446
- /**
447
- * CRITICAL: BottomToTop Mode Height Change Fix
448
- * ============================================
449
- *
450
- * Problem: In bottomToTop mode, when items change height while user is at bottom,
451
- * the list would jump to middle positions (e.g. items 1032-1096) instead of
452
- * staying anchored at bottom showing Item 0.
453
- *
454
- * Root Cause: Height calculations using simple averages (items.length * calculatedItemHeight)
455
- * were drastically skewed by single item changes. Example:
456
- * - 1 item changes from 20px to 100px (+80px actual change)
457
- * - Average jumps from 20px to 22.35px (+2.35px per item)
458
- * - Across 10,000 items: 2.35px × 10,000 = 23,500px total height error!
459
- * - This caused massive scroll position overshoots and incorrect positioning
460
- *
461
- * Solution: Two-step native scrollIntoView approach
462
- * 1. Fixed skewed height calculations using actual heightCache measurements (see totalHeight)
463
- * 2. When wasAtBottomBeforeHeightChange=true (captured before any height processing):
464
- * a) First scroll to approximate bottom position to render Item 0 in virtual viewport
465
- * b) Use native scrollIntoView() with block:'end' for precise bottom alignment
466
- *
467
- * Why This Works:
468
- * - Uses browser's native scroll logic instead of error-prone manual calculations
469
- * - Two-step ensures Item 0 exists in DOM before attempting to scroll to it
470
- * - Native scrollIntoView handles all edge cases (subpixel precision, browser differences)
471
- * - Eliminates complex math that was accumulating rounding errors
472
- * - Smooth behavior provides better UX than instant jumps
473
- *
474
- * Dependencies:
475
- * - wasAtBottomBeforeHeightChange: Set to true when first item marked dirty, prevents cascading corrections
476
- * - totalHeight: Uses actual heightCache measurements instead of skewed averages
477
- * - Aggressive scroll correction: Blocked when wasAtBottomBeforeHeightChange=true
478
- *
479
- * ⚠️ DO NOT MODIFY WITHOUT EXTENSIVE TESTING ⚠️
480
- * This fix resolves a complex interaction between:
481
- * - Virtual list rendering (only ~20 items visible, rest virtualized)
482
- * - Height change calculations (prone to average skewing with large datasets)
483
- * - Multiple scroll correction mechanisms (specific vs aggressive)
484
- * - Bottom anchor positioning in reversed list mode (bottomToTop)
485
- *
486
- * Test coverage: tests/bottomToTop/firstItemHeightChange.spec.ts (45 comprehensive tests)
487
- * Related fixes: See aggressive scroll correction logic ~line 410 with !wasAtBottomBeforeHeightChange
488
- */
489
- if (
490
- mode === 'bottomToTop' &&
491
- wasAtBottomBeforeHeightChange &&
492
- !programmaticScrollInProgress &&
493
- performance.now() >= suppressBottomAnchoringUntilMs
494
- ) {
495
- // Prevent same-frame corrections; defer if this viewport just corrected
496
- const now = performance.now()
497
- const viewportEl = heightManager.viewport
498
- const lastCorrectionMs = lastCorrectionTimestampByViewport.get(viewportEl) ?? 0
499
- if (now - lastCorrectionMs < GLOBAL_CORRECTION_COOLDOWN_MS) {
500
- suppressBottomAnchoringUntilMs = now + 50
501
- return
502
- }
503
- lastCorrectionTimestampByViewport.set(viewportEl, now)
504
-
505
- // Step 1: Scroll to approximate position to ensure Item 0 gets rendered in virtual viewport
506
- const approximateScrollTop = Math.max(0, totalHeight - height)
507
- log('[SVL] b2t-correction-approx', { approximateScrollTop })
508
- syncScrollTop(approximateScrollTop)
509
-
510
- // Step 2: Use native scrollIntoView for precise bottom-edge positioning after DOM updates
511
- tick().then(() => {
512
- const item0Element = heightManager.viewport.querySelector(
513
- '[data-original-index="0"]'
514
- )
515
- if (item0Element) {
516
- // Verify alignment via rects; if off, perform one-time scrollIntoView
517
- const contRect = heightManager.viewport.getBoundingClientRect()
518
- const itemRect = (item0Element as HTMLElement).getBoundingClientRect()
519
- const tol = 4
520
- const aligned =
521
- Math.abs(contRect.y + contRect.height - (itemRect.y + itemRect.height)) <=
522
- tol
523
- if (!aligned) {
524
- // Use manual scrollTop instead of scrollIntoView to prevent parent scroll
525
- // (scrollIntoView scrolls all ancestor containers, not just the viewport)
526
- // Note: `container: 'nearest'` option could replace this once browser support improves
527
- const currentScrollTop = heightManager.viewport.scrollTop
528
- const offset = itemRect.bottom - contRect.bottom
529
- syncScrollTop(currentScrollTop + offset)
530
- log('[SVL] b2t-correction-manual', { offset })
531
- } else {
532
- // Sync our internal scroll state with actual DOM position
533
- heightManager.scrollTop = heightManager.viewport.scrollTop
534
- }
535
- // After peer correction, delay further corrections briefly
536
- suppressBottomAnchoringUntilMs = performance.now() + 200
537
- }
538
- })
539
-
540
- return // Skip remaining scroll correction logic - we've handled bottomToTop case
541
- }
542
-
543
- const currentScrollTop = heightManager.viewport.scrollTop
544
- const maxScrollTop = Math.max(0, totalHeight - height)
545
-
546
- // Calculate total height change impact above current visible area
547
- let heightChangeAboveViewport = 0
548
- const currentVisibleRange = visibleItems
549
-
550
- for (const change of heightChanges) {
551
- // Only consider items that are above the current visible range
552
- if (change.index < currentVisibleRange.start) {
553
- heightChangeAboveViewport += change.delta
554
- }
555
- }
556
-
557
- // If there are height changes above the viewport, adjust scroll to maintain position
558
- // Include any pending coalesced delta (when scrolling)
559
- {
560
- const key = '__svl_pendingHeightAdj__' as unknown as keyof HTMLElement
561
- const pending =
562
- (heightManager.viewport as unknown as Record<string, number>)[key as string] ?? 0
563
- if (pending) {
564
- heightChangeAboveViewport += pending
565
- ;(heightManager.viewport as unknown as Record<string, number>)[key as string] = 0
566
- }
567
- }
568
- if (Math.abs(heightChangeAboveViewport) > 2) {
569
- const newScrollTop = clampValue(
570
- currentScrollTop + heightChangeAboveViewport,
571
- 0,
572
- maxScrollTop
573
- )
574
- syncScrollTop(newScrollTop)
575
- }
576
- }
577
-
578
277
  // Height update function - removed throttling to fix race condition on initial load
579
278
  // Create throttled height update function with trailing execution to ensure measurement always happens
580
279
  const triggerHeightUpdate = createAdvancedThrottledCallback(
581
280
  () => {
582
281
  if (BROWSER && dirtyItemsCount > 0) {
583
- // Capture bottom state before any height processing to prevent cascading corrections
584
- wasAtBottomBeforeHeightChange = atBottom
585
282
  heightManager.startDynamicUpdate()
586
283
  updateHeight()
587
284
  }
@@ -601,14 +298,11 @@
601
298
  // Keep height manager synchronized with items length
602
299
  $effect(() => {
603
300
  heightManager.updateItemLength(items.length)
604
- stabilizedContentHeight = 0
605
301
  })
606
302
 
607
303
  // Infinite scroll: trigger onLoadMore when approaching end of list
608
304
  $effect(() => {
609
305
  if (!BROWSER || !onLoadMore || !hasMore || isLoadingMore) return
610
- // Skip loading during bottomToTop initialization (init path renders all items artificially)
611
- if (mode === 'bottomToTop' && !bottomToTopScrollComplete) return
612
306
 
613
307
  const range = visibleItems
614
308
  const atLoadingEdge = range.end >= items.length - loadMoreThreshold
@@ -623,7 +317,7 @@
623
317
  })
624
318
 
625
319
  const updateHeight = () => {
626
- // Capture previous total height for scroll correction (topToBottom anchoring)
320
+ // Capture previous total height for scroll correction.
627
321
  prevTotalHeightForScrollCorrection = heightManager.totalHeight
628
322
  heightUpdateTimeout = calculateAverageHeightDebounced(
629
323
  isCalculatingHeight,
@@ -650,15 +344,8 @@
650
344
  heightManager.processDirtyHeights(result.heightChanges)
651
345
  }
652
346
 
653
- // Handle height changes for scroll correction (manager totals already updated)
654
- if (result.heightChanges.length > 0 && mode === 'bottomToTop') {
655
- // Run correction after dynamic update finishes to avoid blocking conditions
656
- const changes = result.heightChanges
657
- queueMicrotask(() => handleHeightChangesScrollCorrection(changes))
658
- }
659
-
660
- // TopToBottom: maintain bottom anchoring when total height changes
661
- if (mode === 'topToBottom' && heightManager.isReady && heightManager.initialized) {
347
+ // Keep the end item visually stable when total height changes at the end.
348
+ if (heightManager.isReady && heightManager.initialized) {
662
349
  const oldTotal = prevTotalHeightForScrollCorrection
663
350
  const newTotal = heightManager.totalHeight
664
351
  const deltaTotal = newTotal - oldTotal
@@ -669,7 +356,7 @@
669
356
  const currentScrollTop = heightManager.viewport.scrollTop
670
357
  const isAtBottom = Math.abs(currentScrollTop - maxScrollTop) <= tolerance
671
358
  if (isAtBottom) {
672
- // Adjust scrollTop by total height delta to hold bottom anchor
359
+ // Adjust scrollTop by total height delta to keep the same end position.
673
360
  const adjusted = clampValue(
674
361
  currentScrollTop + deltaTotal,
675
362
  0,
@@ -685,38 +372,21 @@
685
372
  // Clear processed dirty items (all dirty items were processed)
686
373
  dirtyItems.clear()
687
374
  dirtyItemsCount = 0
688
-
689
- // Reset bottom state flag
690
- wasAtBottomBeforeHeightChange = false
691
375
  })
692
376
  heightManager.endDynamicUpdate()
693
377
  },
694
378
  lastMeasuredIndex < 0 || dirtyItems.size > 0 ? 0 : HEIGHT_DEBOUNCE_MS,
695
379
  dirtyItems, // Pass dirty items for processing
696
380
  0, // Don't pass ReactiveListManager state - let each system manage its own totals
697
- 0, // Don't pass ReactiveListManager state - let each system manage its own totals
698
- mode // Pass mode for correct element indexing
381
+ 0 // Don't pass ReactiveListManager state - let each system manage its own totals
699
382
  )
700
383
  }
701
384
 
702
- // Add new effect to handle height changes
703
- // Track if user has scrolled away from bottom to prevent snap-back
704
- let userHasScrolledAway = $state(false)
705
- let programmaticScrollInProgress = $state(false) // Prevent bottom-anchoring during programmatic scrolls
706
- let lastCalculatedHeight = $state(0)
707
- let lastItemsLength = $state(0)
708
- // Track last observed total height to compute precise deltas on item count changes
709
- let lastTotalHeightObserved = $state(0)
710
- // For bottomToTop mode: keep init path active until scroll positioning is complete
711
- // This ensures Item 0 stays in the DOM throughout initialization
712
- let bottomToTopScrollComplete = $state(false)
713
-
714
385
  /**
715
386
  * CRITICAL: O(1) Reactive Total Height Calculation
716
387
  * ===============================================
717
388
  *
718
389
  * Uses ReactiveListManager for O(1) height calculations instead of O(n) loops.
719
- * This fixes the root cause of massive scroll jumps in bottomToTop mode.
720
390
  *
721
391
  * Problem with Previous O(n) Approach:
722
392
  * - Looped through ALL items on every reactive update
@@ -740,12 +410,10 @@
740
410
  * - Updates incrementally using processDirtyHeights() instead of recalculating all
741
411
  *
742
412
  * This getter is reactive and updates whenever heightManager's internal state changes.
743
- * Used by: atBottom calculation, scroll corrections, maxScrollTop calculations
413
+ * Used by scroll corrections and maxScrollTop calculations.
744
414
  */
745
415
  const totalHeight = $derived(heightManager.totalHeight)
746
416
 
747
- const atBottom = $derived(heightManager.scrollTop >= totalHeight - height - 1)
748
- let wasAtBottomBeforeHeightChange = false
749
417
  let lastVisibleRange: SvelteVirtualListPreviousVisibleRange | null = null
750
418
 
751
419
  function updateDebugTailDistance() {
@@ -756,174 +424,14 @@
756
424
  if (!last) return
757
425
  const v = heightManager.viewport.getBoundingClientRect()
758
426
  const r = last.getBoundingClientRect()
759
- lastBottomDistance = Math.round(Math.abs(r.bottom - v.bottom))
427
+ const bottomDistance = Math.round(Math.abs(r.bottom - v.bottom))
760
428
  if (INTERNAL_DEBUG) {
761
- console.info('[SVL] bottomDistance(px):', lastBottomDistance)
429
+ console.info('[SVL] bottomDistance(px):', bottomDistance)
762
430
  }
763
431
  }
764
432
 
765
433
  // no UI export; rely on console logs when debug=true
766
434
 
767
- // $inspect('scrollState: atTop', atTop)
768
- // $inspect('scrollState: atBottom', atBottom)
769
-
770
- $effect(() => {
771
- if (
772
- BROWSER &&
773
- heightManager.initialized &&
774
- mode === 'bottomToTop' &&
775
- heightManager.viewportElement
776
- ) {
777
- const targetScrollTop = Math.max(0, totalHeight - height)
778
- const currentScrollTop = heightManager.viewport.scrollTop
779
- const scrollDifference = Math.abs(currentScrollTop - targetScrollTop)
780
-
781
- // Only correct scroll if:
782
- // 1. Item height changed significantly (not just user scrolling)
783
- // 2. User hasn't intentionally scrolled away from bottom
784
- // 3. We're significantly off target
785
- // 4. We're not at the bottom (where height changes should be handled more carefully)
786
- const heightChanged = Math.abs(heightManager.averageHeight - lastCalculatedHeight) > 1
787
- const maxScrollTop = Math.max(0, totalHeight - height)
788
-
789
- // In bottomToTop mode, we're "at bottom" when scroll is at max position
790
- const isAtBottom =
791
- Math.abs(currentScrollTop - maxScrollTop) < heightManager.averageHeight
792
- const shouldCorrect =
793
- heightChanged &&
794
- !userHasScrolledAway &&
795
- !isAtBottom && // Don't apply aggressive correction when at bottom
796
- !isScrolling && // Skip aggressive corrections during active scroll
797
- !programmaticScrollInProgress && // Don't interfere with programmatic scrolls
798
- performance.now() >= suppressBottomAnchoringUntilMs &&
799
- !heightManager.isDynamicUpdateInProgress &&
800
- scrollDifference > heightManager.averageHeight * 3
801
-
802
- if (shouldCorrect) {
803
- // Round to avoid subpixel positioning issues in bottomToTop mode
804
- syncScrollTop(targetScrollTop, true)
805
- }
806
-
807
- // Track if user has scrolled significantly away from bottom
808
- if (scrollDifference > heightManager.averageHeight * 5) {
809
- userHasScrolledAway = true
810
- }
811
-
812
- lastCalculatedHeight = heightManager.averageHeight
813
- }
814
- })
815
-
816
- // Handle items being added/removed in bottomToTop mode
817
- $effect(() => {
818
- // Only track items.length to prevent re-runs on other reactive changes
819
- const currentItemsLength = items.length
820
-
821
- if (
822
- BROWSER &&
823
- heightManager.initialized &&
824
- mode === 'bottomToTop' &&
825
- heightManager.isReady &&
826
- lastItemsLength > 0
827
- ) {
828
- const itemsAdded = currentItemsLength - lastItemsLength
829
-
830
- if (itemsAdded !== 0) {
831
- // Capture all reactive values immediately to prevent re-triggering
832
- const currentScrollTop = heightManager.viewport.scrollTop
833
- const currentCalculatedItemHeight = heightManager.averageHeight
834
- const currentHeight = height
835
- const currentTotalHeight = totalHeight
836
- const prevTotalHeight =
837
- lastTotalHeightObserved ||
838
- currentTotalHeight - itemsAdded * currentCalculatedItemHeight
839
- const prevMaxScrollTop = Math.max(0, prevTotalHeight - currentHeight)
840
- const nextMaxScrollTop = Math.max(0, currentTotalHeight - currentHeight)
841
- const deltaMax = nextMaxScrollTop - prevMaxScrollTop
842
- log('[SVL] items-length-change:before', {
843
- instanceId,
844
- itemsAdded,
845
- lastItemsLength,
846
- currentItemsLength,
847
- currentScrollTop,
848
- prevTotalHeight,
849
- currentTotalHeight,
850
- prevMaxScrollTop,
851
- nextMaxScrollTop,
852
- deltaMax,
853
- averageItemHeight: currentCalculatedItemHeight
854
- })
855
-
856
- // Maintain visual position for ALL cases by advancing scrollTop by deltaMax.
857
- // If near the bottom, this naturally pins to the new max; otherwise it preserves the current content.
858
- programmaticScrollInProgress = true
859
- void heightManager.runDynamicUpdate(() => {
860
- const newScrollTop = clampValue(
861
- currentScrollTop + deltaMax,
862
- 0,
863
- nextMaxScrollTop
864
- )
865
- syncScrollTop(newScrollTop)
866
- log('[SVL] items-length-change:applied', {
867
- instanceId,
868
- previousScrollTop: currentScrollTop,
869
- appliedScrollTop: newScrollTop,
870
- prevMaxScrollTop,
871
- nextMaxScrollTop,
872
- deltaMax
873
- })
874
-
875
- // We are explicitly managing position; consider this a programmatic action.
876
- // Do not flip userHasScrolledAway here; it should reflect user intent only.
877
-
878
- // Reconcile on next frame in case measured heights adjust totals
879
- requestAnimationFrame(() => {
880
- const beforeReconcileScrollTop = heightManager.viewport.scrollTop
881
- const reconciledNextMax = clampValue(totalHeight - height, 0, Infinity)
882
- const reconciledDeltaMaxChange = reconciledNextMax - nextMaxScrollTop
883
- // Desired position is to maintain distance-from-end; equivalently keep (max - scrollTop) constant.
884
- const desiredScrollTop = clampValue(
885
- newScrollTop + reconciledDeltaMaxChange,
886
- 0,
887
- reconciledNextMax
888
- )
889
- // Snap to integer pixels to prevent oscillation due to subpixel rounding
890
- const desiredRounded = Math.round(desiredScrollTop)
891
- const diffToDesired = desiredRounded - heightManager.viewport.scrollTop
892
- if (Math.abs(diffToDesired) >= 2) {
893
- const adjusted = clampValue(desiredRounded, 0, reconciledNextMax)
894
- syncScrollTop(adjusted)
895
- log('[SVL] items-length-change:reconciled', {
896
- instanceId,
897
- beforeReconcileScrollTop,
898
- adjustedScrollTop: adjusted,
899
- reconciledNextMax,
900
- reconciledDeltaMaxChange,
901
- desiredScrollTop,
902
- desiredRounded,
903
- diffToDesired
904
- })
905
- } else {
906
- log('[SVL] items-length-change:reconciled-skip', {
907
- instanceId,
908
- beforeReconcileScrollTop,
909
- reconciledNextMax,
910
- reconciledDeltaMaxChange,
911
- desiredScrollTop,
912
- desiredRounded,
913
- diffToDesired
914
- })
915
- }
916
- programmaticScrollInProgress = false
917
- })
918
- })
919
- }
920
- }
921
-
922
- lastItemsLength = currentItemsLength
923
- // Update last observed total height at the end of the effect
924
- lastTotalHeightObserved = totalHeight
925
- })
926
-
927
435
  // Update container height continuously to reflect layout changes that
928
436
  // may occur outside ResizeObserver timing (keeps buffers correct across engines)
929
437
  $effect(() => {
@@ -933,17 +441,6 @@
933
441
  }
934
442
  })
935
443
 
936
- // One-time fallback measurement when height hasn't been established yet
937
-
938
- // Provide a one-time synchronous measurement only when height is still 0,
939
- // to avoid DOM reads inside render-time expressions.
940
- $effect(() => {
941
- if (BROWSER && height === 0 && heightManager.isReady) {
942
- const h = heightManager.container.getBoundingClientRect().height
943
- if (Number.isFinite(h) && h > 0) measuredFallbackHeight = h
944
- }
945
- })
946
-
947
444
  /**
948
445
  * Calculates the range of items that should be rendered based on current scroll position.
949
446
  *
@@ -953,7 +450,6 @@
953
450
  * - Viewport height
954
451
  * - Item height estimates
955
452
  * - Buffer size
956
- * - Scroll direction mode
957
453
  *
958
454
  * @example
959
455
  * ```typescript
@@ -967,29 +463,11 @@
967
463
  if (!items.length) return { start: 0, end: 0 } as SvelteVirtualListPreviousVisibleRange
968
464
  const viewportHeight = height || 0
969
465
 
970
- // For bottomToTop mode, always render items starting from index 0 during initialization
971
- // This ensures Item 0 is in the DOM so we can use scrollIntoView for precise positioning
972
- // The scrollIntoView in updateHeightAndScroll will handle correct alignment after heights are measured
973
- // Use bottomToTopScrollComplete (not just initialized) to keep init path active until scroll is done
974
- if (mode === 'bottomToTop' && !bottomToTopScrollComplete) {
975
- // Use a reasonable default if viewport height isn't measured yet
976
- const effectiveViewport = viewportHeight || 400
977
- const visibleCount = Math.ceil(effectiveViewport / heightManager.averageHeight) + 1
978
- lastVisibleRange = {
979
- start: 0,
980
- end: Math.min(items.length, visibleCount + bufferSize * 2)
981
- } as SvelteVirtualListPreviousVisibleRange
982
-
983
- return lastVisibleRange
984
- }
985
-
986
466
  // Scroll delta threshold optimization: skip recalculation if scroll delta is less than
987
467
  // half the average item height and we have a cached range. This reduces unnecessary
988
468
  // calculations during smooth scrolling.
989
- // Note: Only applied in topToBottom mode - bottomToTop has complex scroll correction
990
- // logic that requires precise visible range calculations.
991
469
  // Note: We use lastProcessedScrollTop read-only here; it's updated in the scroll handler
992
- if (mode === 'topToBottom') {
470
+ {
993
471
  const scrollDelta = Math.abs(heightManager.scrollTop - lastProcessedScrollTop)
994
472
  const threshold = heightManager.averageHeight * 0.5
995
473
  if (lastVisibleRange && scrollDelta < threshold && scrollDelta > 0) {
@@ -1004,7 +482,6 @@
1004
482
  itemHeight: heightManager.averageHeight,
1005
483
  totalItems: items.length,
1006
484
  bufferSize,
1007
- mode,
1008
485
  totalContentHeight: totalHeight,
1009
486
  heightCache: heightManager.getHeightCache()
1010
487
  })
@@ -1016,74 +493,34 @@
1016
493
  * Computed content height for the virtual list.
1017
494
  * Uses the maximum of container height and total content height to ensure
1018
495
  * proper scrolling behavior.
1019
- *
1020
- * In bottomToTop mode during active scroll, contentHeight is "ratcheted" —
1021
- * it can grow but never shrink. This prevents a feedback loop where
1022
- * averageHeight oscillation causes scrollHeight to bounce, triggering
1023
- * browser scrollTop adjustments that fire new scroll events.
1024
- * When scrolling stops (isScrolling goes false), it snaps to the true value.
1025
496
  */
1026
- let stabilizedContentHeight = 0
1027
-
1028
- const contentHeight = $derived.by(() => {
1029
- const raw = Math.max(height, totalHeight)
1030
-
1031
- if (mode !== 'bottomToTop' || !isScrolling) {
1032
- stabilizedContentHeight = raw
1033
- return raw
1034
- }
1035
-
1036
- // During active scroll in bottomToTop: only allow growth (ratchet)
1037
- // Prevents shrink → scrollTop adjust → new scroll event feedback loop
1038
- if (raw > stabilizedContentHeight) {
1039
- stabilizedContentHeight = raw
1040
- }
1041
-
1042
- return stabilizedContentHeight
1043
- })
497
+ const contentHeight = $derived(Math.max(height, totalHeight))
1044
498
 
1045
499
  /**
1046
500
  * Computed transform Y value for positioning the visible items.
1047
501
  * Extracted from inline IIFE for better performance and readability.
1048
502
  */
1049
503
  const transformY = $derived.by(() => {
1050
- const viewportHeight = height || measuredFallbackHeight || 0
1051
504
  const visibleRange = visibleItems
1052
505
 
1053
- // Avoid synchronous DOM reads here; fall back once if height is 0
1054
- const effectiveHeight = viewportHeight === 0 ? 400 : viewportHeight
1055
-
1056
506
  // Use precise offset using measured heights when available.
1057
- // For bottomToTop, pass ratcheted contentHeight so the transform stays
1058
- // stable while scrollHeight is stabilized (prevents visual shift).
1059
507
  return Math.round(
1060
508
  calculateTransformY(
1061
- mode,
1062
509
  items.length,
1063
- visibleRange.end,
1064
510
  visibleRange.start,
1065
511
  heightManager.averageHeight,
1066
- effectiveHeight,
1067
- mode === 'bottomToTop' ? contentHeight : totalHeight,
1068
- heightManager.getHeightCache(),
1069
- measuredFallbackHeight
512
+ heightManager.getHeightCache()
1070
513
  )
1071
514
  )
1072
515
  })
1073
516
 
1074
517
  const displayItems = $derived.by(() => {
1075
518
  const visibleRange = visibleItems
1076
- const slice =
1077
- mode === 'bottomToTop'
1078
- ? items.slice(visibleRange.start, visibleRange.end).reverse()
1079
- : items.slice(visibleRange.start, visibleRange.end)
519
+ const slice = items.slice(visibleRange.start, visibleRange.end)
1080
520
 
1081
521
  return slice.map((item, sliceIndex) => ({
1082
522
  item,
1083
- originalIndex:
1084
- mode === 'bottomToTop'
1085
- ? visibleRange.end - 1 - sliceIndex
1086
- : visibleRange.start + sliceIndex,
523
+ originalIndex: visibleRange.start + sliceIndex,
1087
524
  sliceIndex
1088
525
  }))
1089
526
  })
@@ -1096,8 +533,7 @@
1096
533
  * smooth scrolling performance.
1097
534
  *
1098
535
  * Implementation details:
1099
- * - Uses isScrolling flag to prevent multiple RAF calls
1100
- * - Updates scrollTop state only when scrolling has settled
536
+ * - Updates scrollTop state through RAF scheduling
1101
537
  * - Browser-only functionality
1102
538
  *
1103
539
  * @example
@@ -1112,31 +548,8 @@
1112
548
  const handleScroll = () => {
1113
549
  if (!BROWSER || !heightManager.viewportElement) return
1114
550
 
1115
- // Mark active scrolling and debounce idle transition (~120ms)
1116
- isScrolling = true
1117
- if (scrollIdleTimer) {
1118
- clearTimeout(scrollIdleTimer)
1119
- scrollIdleTimer = null
1120
- }
1121
- scrollIdleTimer = window.setTimeout(() => {
1122
- isScrolling = false
1123
- // Apply deferred anchor correction on idle
1124
- if (idleCorrectionsOnly || anchorModeEnabled) {
1125
- reconcileToAnchorIfEnabled()
1126
- }
1127
- }, SCROLL_IDLE_DELAY_MS)
1128
-
1129
551
  rafSchedule(() => {
1130
552
  const current = heightManager.viewport.scrollTop
1131
- if (mode === 'bottomToTop') {
1132
- const delta = lastScrollTopSnapshot - current
1133
- if (delta > 0.5) {
1134
- // Widen suppression to avoid fighting peer instance corrections
1135
- suppressBottomAnchoringUntilMs = performance.now() + SUPPRESSION_WINDOW_MS
1136
- userHasScrolledAway = true
1137
- }
1138
- }
1139
- lastScrollTopSnapshot = current
1140
553
  heightManager.scrollTop = current
1141
554
  // Update last processed scroll position for delta threshold optimization
1142
555
  // Only update when we actually process a scroll (i.e., recalculate visible range)
@@ -1146,13 +559,9 @@
1146
559
  lastProcessedScrollTop = current
1147
560
  }
1148
561
  updateDebugTailDistance()
1149
- if (anchorModeEnabled) {
1150
- captureAnchor()
1151
- }
1152
562
  if (INTERNAL_DEBUG) {
1153
563
  const vr = visibleItems
1154
564
  log('[SVL] scroll', {
1155
- mode,
1156
565
  scrollTop: heightManager.scrollTop,
1157
566
  height,
1158
567
  totalHeight: totalHeight,
@@ -1160,109 +569,23 @@
1160
569
  visibleRange: vr
1161
570
  })
1162
571
  }
1163
- // isScrolling cleared by idle timer
1164
572
  })
1165
573
  }
1166
574
 
1167
575
  /**
1168
576
  * Updates the height and scroll position of the virtual list.
1169
577
  *
1170
- * This function handles two scenarios:
1171
- * 1. Initial setup (critical for bottomToTop mode in flexbox layouts)
1172
- * 2. Subsequent resize events
1173
- *
1174
- * For bottomToTop mode, we need to ensure:
1175
- * - The flexbox layout is fully calculated
1176
- * - The height measurements are accurate
1177
- * - The scroll position starts at the bottom
1178
- *
1179
578
  * @param immediate - Whether to skip the delay (used for resize events)
1180
579
  */
1181
580
  const updateHeightAndScroll = (immediate = false) => {
1182
581
  log('updateHeightAndScroll-enter', {
1183
582
  immediate,
1184
- initialized: heightManager.initialized,
1185
- mode
583
+ initialized: heightManager.initialized
1186
584
  })
1187
- if (!heightManager.initialized && mode === 'bottomToTop') {
1188
- // bottomToTop initialization: use scrollIntoView on Item 0 for precise positioning
1189
- // visibleItems guarantees Item 0 is rendered during initialization
1190
- tick().then(() => {
1191
- requestAnimationFrame(() => {
1192
- requestAnimationFrame(() => {
1193
- if (!heightManager.isReady) return
1194
- const measuredHeight =
1195
- heightManager.container.getBoundingClientRect().height
1196
- height = measuredHeight
1197
-
1198
- // Instance jitter to avoid same-frame collisions when two lists init together
1199
- const cleanedId = String(instanceId)
1200
- .toLowerCase()
1201
- .replace(/[^a-z0-9]/g, '')
1202
- const suffix = cleanedId.slice(-4)
1203
- const parsed = parseInt(suffix, 36)
1204
- const jitterMs = Number.isNaN(parsed)
1205
- ? Math.floor(Math.random() * 3)
1206
- : parsed % 3
1207
-
1208
- setTimeout(() => {
1209
- // Step 1: Set initialized (for other purposes like scroll event handling)
1210
- // The init path in visibleItems stays active until bottomToTopScrollComplete
1211
- if (!heightManager.initialized) {
1212
- heightManager.initialized = true
1213
- }
1214
-
1215
- // Step 2: Use scrollIntoView on Item 0 for precise positioning
1216
- // Use double RAF to ensure heights are measured and layout is stable
1217
- requestAnimationFrame(() => {
1218
- requestAnimationFrame(() => {
1219
- // Item 0 is guaranteed to be in DOM due to init path
1220
- // Skip if user has already scrolled (scrollTop significantly != 0)
1221
- const currentScroll = heightManager.viewport.scrollTop
1222
- const userHasScrolled =
1223
- currentScroll > heightManager.averageHeight
1224
- const el = heightManager.viewport.querySelector(
1225
- '[data-original-index="0"]'
1226
- ) as HTMLElement | null
1227
-
1228
- if (el && !userHasScrolled) {
1229
- // Use manual scrollTop instead of scrollIntoView to prevent parent scroll
1230
- // (scrollIntoView scrolls all ancestor containers, not just the viewport)
1231
- // Note: `container: 'nearest'` option could replace this once browser support improves
1232
- const viewportRect =
1233
- heightManager.viewport.getBoundingClientRect()
1234
- const elRect = el.getBoundingClientRect()
1235
- const offset = elRect.bottom - viewportRect.bottom
1236
- heightManager.viewport.scrollTop += offset
1237
- heightManager.scrollTop = heightManager.viewport.scrollTop
1238
- } else if (userHasScrolled) {
1239
- // Sync internal state with current scroll
1240
- heightManager.scrollTop = currentScroll
1241
- }
1242
-
1243
- // Step 3: Mark scroll complete - switches visibleItems to normal mode
1244
- requestAnimationFrame(() => {
1245
- bottomToTopScrollComplete = true
1246
- // Reset bottom-anchoring flag to prevent stale state from init
1247
- // affecting later operations (e.g., adding items while scrolled away)
1248
- wasAtBottomBeforeHeightChange = false
1249
- // Suppress bottom-anchoring briefly to let heights stabilize
1250
- // after switching to normal mode
1251
- suppressBottomAnchoringUntilMs = performance.now() + 200
1252
- })
1253
- })
1254
- })
1255
- }, jitterMs)
1256
- })
1257
- })
1258
- })
1259
- return
1260
- }
1261
585
 
1262
586
  utilsUpdateHeightAndScroll(
1263
587
  {
1264
588
  initialized: heightManager.initialized,
1265
- mode,
1266
589
  containerElement: heightManager.containerElement,
1267
590
  viewportElement: heightManager.viewportElement,
1268
591
  calculatedItemHeight: heightManager.averageHeight,
@@ -1309,11 +632,6 @@
1309
632
 
1310
633
  // Only mark as dirty if height change is significant
1311
634
  if (isSignificant) {
1312
- // Capture bottom state when FIRST item gets marked dirty
1313
- if (dirtyItemsCount === 0) {
1314
- wasAtBottomBeforeHeightChange = atBottom
1315
- }
1316
-
1317
635
  dirtyItems.add(actualIndex)
1318
636
  dirtyItemsCount = dirtyItems.size
1319
637
  shouldRecalculate = true
@@ -1334,7 +652,7 @@
1334
652
  onMount(() => {
1335
653
  if (BROWSER) {
1336
654
  // Initial setup of heights and scroll position
1337
- log('onMount-enter', { mode, items: items.length })
655
+ log('onMount-enter', { items: items.length })
1338
656
  updateHeightAndScroll()
1339
657
  // Ensure one initial measurement pass even if no ResizeObserver fires
1340
658
  tick().then(() =>
@@ -1514,7 +832,6 @@
1514
832
 
1515
833
  // Use extracted scroll calculation utility
1516
834
  const scrollTarget = calculateScrollTarget({
1517
- mode,
1518
835
  align: align || 'auto',
1519
836
  targetIndex,
1520
837
  itemsLength: items.length,
@@ -1531,9 +848,6 @@
1531
848
  return
1532
849
  }
1533
850
 
1534
- // Prevent bottom-anchoring logic from interfering with programmatic scroll
1535
- programmaticScrollInProgress = true
1536
-
1537
851
  if (INTERNAL_DEBUG && heightManager.viewportElement) {
1538
852
  const domMax = Math.max(
1539
853
  0,
@@ -1550,32 +864,6 @@
1550
864
  })
1551
865
  }
1552
866
 
1553
- // CROSS-BROWSER COMPATIBILITY FIX:
1554
- // All major browsers (Chrome, Firefox, Safari) have inconsistent behavior with scrollTo()
1555
- // in bottomToTop mode when using smooth scrolling. Using scrollIntoView() on the highest
1556
- // visible element provides consistent cross-browser smooth scrolling behavior.
1557
- // This approach works universally and maintains the user's expected smooth scroll experience.
1558
- if (mode === 'bottomToTop' && smoothScroll) {
1559
- // Find the element with the highest original-index in the current viewport
1560
- const visibleElements = heightManager.viewport.querySelectorAll('[data-original-index]')
1561
- let maxIndex = -1
1562
- let maxElement: HTMLElement | null = null
1563
- for (const el of visibleElements) {
1564
- const index = parseInt(el.getAttribute('data-original-index') || '-1')
1565
- if (index > maxIndex) {
1566
- maxIndex = index
1567
- maxElement = el as HTMLElement
1568
- }
1569
- }
1570
-
1571
- maxElement?.scrollIntoView({
1572
- behavior: 'smooth'
1573
- })
1574
- await tick()
1575
- await new Promise((resolve) => setTimeout(resolve, 100))
1576
- await tick()
1577
- }
1578
-
1579
867
  heightManager.viewport.scrollTo({
1580
868
  top: scrollTarget,
1581
869
  behavior: smoothScroll ? 'smooth' : 'auto'
@@ -1597,14 +885,6 @@
1597
885
  })
1598
886
 
1599
887
  // No extra alignment step here; allow native smooth scroll to reach DOM max scrollTop
1600
-
1601
- // Clear the flag after scroll completes
1602
- setTimeout(
1603
- () => {
1604
- programmaticScrollInProgress = false
1605
- },
1606
- smoothScroll ? 500 : 100
1607
- )
1608
888
  }
1609
889
 
1610
890
  /**