@humanspeak/svelte-virtual-list 0.2.6-beta.0 → 0.2.6-beta.2

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.
@@ -152,16 +152,16 @@
152
152
  calculateScrollPosition,
153
153
  calculateTransformY,
154
154
  calculateVisibleRange,
155
- getScrollOffsetForIndex,
156
- processChunked,
157
155
  updateHeightAndScroll as utilsUpdateHeightAndScroll
158
156
  } from './utils/virtualList.js'
159
157
  import { createDebugInfo, shouldShowDebugInfo } from './utils/virtualListDebug.js'
158
+ import { calculateScrollTarget } from './utils/scrollCalculation.js'
159
+ import { initializeVirtualList } from './utils/initialization.js'
160
160
  import { BROWSER } from 'esm-env'
161
161
  import { onMount, tick } from 'svelte'
162
162
 
163
163
  const rafSchedule = createRafScheduler()
164
-
164
+ const INTERNAL_DEBUG = false
165
165
  /**
166
166
  * Core configuration props with default values
167
167
  * @type {SvelteVirtualListProps}
@@ -241,25 +241,74 @@
241
241
  calculatedItemHeight = result.newHeight
242
242
  lastMeasuredIndex = result.newLastMeasuredIndex
243
243
  heightCache = result.updatedHeightCache
244
- }
244
+
245
+ // Update running totals for precise height calculation (only when significant changes)
246
+ if (result.clearedDirtyItems.size > 10) {
247
+ const heights = Object.values(heightCache)
248
+ totalMeasuredHeight = heights.reduce((sum, h) => sum + h, 0)
249
+ measuredCount = heights.length
250
+ }
251
+
252
+ // Clear processed dirty items
253
+ result.clearedDirtyItems.forEach((index) => {
254
+ dirtyItems.delete(index)
255
+ })
256
+
257
+ if (INTERNAL_DEBUG && result.clearedDirtyItems.size > 0) {
258
+ console.log(
259
+ `Cleared ${result.clearedDirtyItems.size} dirty items:`,
260
+ Array.from(result.clearedDirtyItems)
261
+ )
262
+ }
263
+ },
264
+ 100, // debounceTime
265
+ dirtyItems // Pass dirty items for processing
245
266
  )
246
267
  }
247
268
 
248
269
  // Add new effect to handle height changes
270
+ // Track if user has scrolled away from bottom to prevent snap-back
271
+ let userHasScrolledAway = $state(false)
272
+ let lastCalculatedHeight = $state(0)
273
+
249
274
  $effect(() => {
250
275
  if (BROWSER && initialized && mode === 'bottomToTop' && viewportElement) {
251
276
  const totalHeight = Math.max(0, items.length * calculatedItemHeight)
252
277
  const targetScrollTop = Math.max(0, totalHeight - height)
278
+ const currentScrollTop = viewportElement.scrollTop
279
+ const scrollDifference = Math.abs(currentScrollTop - targetScrollTop)
280
+
281
+ // Only correct scroll if:
282
+ // 1. Item height changed significantly (not just user scrolling)
283
+ // 2. User hasn't intentionally scrolled away from bottom
284
+ // 3. We're significantly off target
285
+ const heightChanged = Math.abs(calculatedItemHeight - lastCalculatedHeight) > 1
286
+ const shouldCorrect =
287
+ heightChanged && !userHasScrolledAway && scrollDifference > calculatedItemHeight * 3
288
+
289
+ if (shouldCorrect) {
290
+ if (INTERNAL_DEBUG) {
291
+ console.log(
292
+ '🔄 Correcting scroll position from',
293
+ currentScrollTop,
294
+ 'to',
295
+ targetScrollTop,
296
+ 'diff:',
297
+ scrollDifference,
298
+ 'heightChanged:',
299
+ heightChanged
300
+ )
301
+ }
302
+ viewportElement.scrollTop = targetScrollTop
303
+ scrollTop = targetScrollTop
304
+ }
253
305
 
254
- // Only update if the difference is significant
255
- if (Math.abs(viewportElement.scrollTop - targetScrollTop) > calculatedItemHeight) {
256
- requestAnimationFrame(() => {
257
- if (viewportElement) {
258
- viewportElement.scrollTop = targetScrollTop
259
- scrollTop = targetScrollTop
260
- }
261
- })
306
+ // Track if user has scrolled significantly away from bottom
307
+ if (scrollDifference > calculatedItemHeight * 5) {
308
+ userHasScrolledAway = true
262
309
  }
310
+
311
+ lastCalculatedHeight = calculatedItemHeight
263
312
  }
264
313
  })
265
314
 
@@ -303,6 +352,23 @@
303
352
  }
304
353
  })
305
354
 
355
+ /**
356
+ * Calculate precise item height based on actual measurements when available
357
+ */
358
+ // Running totals for efficient precise height calculation
359
+ let totalMeasuredHeight = $state(0)
360
+ let measuredCount = $state(0)
361
+ const preciseItemHeight = $derived(() => {
362
+ if (measuredCount > 100) {
363
+ const avgHeight = totalMeasuredHeight / measuredCount
364
+ // Only use if the difference is significant (more than 0.5px)
365
+ if (Math.abs(avgHeight - calculatedItemHeight) > 0.5) {
366
+ return avgHeight
367
+ }
368
+ }
369
+ return calculatedItemHeight
370
+ })
371
+
306
372
  /**
307
373
  * Calculates the range of items that should be rendered based on current scroll position.
308
374
  *
@@ -326,7 +392,27 @@
326
392
  if (!items.length) return { start: 0, end: 0 } as SvelteVirtualListPreviousVisibleRange
327
393
  const viewportHeight = height || 0
328
394
 
329
- return calculateVisibleRange(
395
+ // For bottomToTop mode, don't calculate visible range until properly initialized
396
+ // This prevents showing wrong items when scrollTop starts at 0
397
+ if (mode === 'bottomToTop' && !initialized && scrollTop === 0 && viewportHeight > 0) {
398
+ // Calculate what the correct scroll position should be
399
+ const totalHeight = items.length * calculatedItemHeight
400
+ const targetScrollTop = Math.max(0, totalHeight - viewportHeight)
401
+
402
+ // Use the target scroll position for visible range calculation
403
+ const result = calculateVisibleRange(
404
+ targetScrollTop,
405
+ viewportHeight,
406
+ calculatedItemHeight,
407
+ items.length,
408
+ bufferSize,
409
+ mode
410
+ )
411
+
412
+ return result
413
+ }
414
+
415
+ const result = calculateVisibleRange(
330
416
  scrollTop,
331
417
  viewportHeight,
332
418
  calculatedItemHeight,
@@ -334,6 +420,8 @@
334
420
  bufferSize,
335
421
  mode
336
422
  )
423
+
424
+ return result
337
425
  })
338
426
 
339
427
  /**
@@ -442,50 +530,15 @@
442
530
  )
443
531
  }
444
532
 
445
- /**
446
- * Initializes large datasets in chunks to prevent UI blocking.
447
- *
448
- * This function processes items in smaller chunks using setTimeout to yield
449
- * to the main thread, allowing other UI operations to remain responsive.
450
- * Progress is tracked and reported through the processedItems state.
451
- *
452
- * For datasets larger than 1000 items, this method is automatically used
453
- * instead of immediate initialization. The chunk size is controlled by the
454
- * component's chunkSize state (default: 50).
455
- *
456
- * @async
457
- * @example
458
- * ```typescript
459
- * // Component initialization
460
- * $effect(() => {
461
- * if (BROWSER && items.length > 1000) {
462
- * initializeChunked()
463
- * } else {
464
- * initialized = true
465
- * }
466
- * })
467
- * ```
468
- *
469
- * @throws {Error} If processChunked fails to complete initialization
470
- * @returns {Promise<void>} Resolves when all chunks have been processed
471
- */
472
- const initializeChunked = async () => {
473
- if (!items.length) return
474
-
475
- await processChunked(
476
- items,
477
- chunkSize,
478
- (processed) => (processedItems = processed),
479
- () => (initialized = true)
480
- )
481
- }
482
-
483
- // Modify the mount effect to use chunked initialization
533
+ // Initialize the virtual list when items change
484
534
  $effect(() => {
485
- if (BROWSER && items.length > 1000) {
486
- initializeChunked()
487
- } else {
488
- initialized = true
535
+ if (BROWSER) {
536
+ initializeVirtualList({
537
+ items,
538
+ chunkSize,
539
+ onProgress: (processed) => (processedItems = processed),
540
+ onComplete: () => (initialized = true)
541
+ })
489
542
  }
490
543
  })
491
544
 
@@ -495,7 +548,7 @@
495
548
  itemResizeObserver = new ResizeObserver((entries) => {
496
549
  let shouldRecalculate = false
497
550
 
498
- if (debug) {
551
+ if (INTERNAL_DEBUG) {
499
552
  console.log(`ResizeObserver fired for ${entries.length} entries`)
500
553
  }
501
554
 
@@ -510,7 +563,7 @@
510
563
  dirtyItems.add(actualIndex)
511
564
  shouldRecalculate = true
512
565
 
513
- if (debug) {
566
+ if (INTERNAL_DEBUG) {
514
567
  console.log(
515
568
  `Item ${actualIndex} marked dirty (resized), queue size: ${dirtyItems.size}`
516
569
  )
@@ -556,7 +609,7 @@
556
609
 
557
610
  // Add the effect in the script section
558
611
  $effect(() => {
559
- if (debug) {
612
+ if (INTERNAL_DEBUG) {
560
613
  prevVisibleRange = visibleItems()
561
614
  prevHeight = calculatedItemHeight
562
615
  }
@@ -661,144 +714,30 @@
661
714
  }
662
715
 
663
716
  const { start: firstVisibleIndex, end: lastVisibleIndex } = visibleItems()
664
- let scrollTarget: number | null = null
665
717
 
666
- if (mode === 'bottomToTop') {
667
- const totalHeight = items.length * calculatedItemHeight
668
- const itemOffset = targetIndex * calculatedItemHeight
669
- const itemHeight = calculatedItemHeight
670
- if (align === 'auto') {
671
- // If item is above the viewport, align to top
672
- if (targetIndex < firstVisibleIndex) {
673
- scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
674
- // If item is below the viewport, align to bottom
675
- } else if (targetIndex > lastVisibleIndex - 1) {
676
- scrollTarget = Math.max(0, totalHeight - itemOffset - height)
677
- } else {
678
- // Item is visible but not aligned: align to nearest edge
679
- // Calculate the offset of the item relative to the viewport
680
- const itemTop = totalHeight - (itemOffset + itemHeight)
681
- const itemBottom = totalHeight - itemOffset
682
- const distanceToTop = Math.abs(scrollTop - itemTop)
683
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
684
- if (distanceToTop < distanceToBottom) {
685
- // Closer to top, align to top
686
- scrollTarget = itemTop
687
- } else {
688
- // Closer to bottom, align to bottom
689
- scrollTarget = Math.max(0, itemBottom - height)
690
- }
691
- }
692
- } else if (align === 'top') {
693
- // Align to top
694
- scrollTarget = Math.max(0, totalHeight - (itemOffset + itemHeight))
695
- } else if (align === 'bottom') {
696
- // Align to bottom
697
- scrollTarget = Math.max(0, totalHeight - itemOffset - height)
698
- } else if (align === 'nearest') {
699
- // If not visible, align to nearest edge; if visible, do nothing
700
- const itemTop = totalHeight - (itemOffset + itemHeight)
701
- const itemBottom = totalHeight - itemOffset
702
- if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
703
- // Not visible, align to nearest edge
704
- const distanceToTop = Math.abs(scrollTop - itemTop)
705
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
706
- if (distanceToTop < distanceToBottom) {
707
- scrollTarget = itemTop
708
- } else {
709
- scrollTarget = Math.max(0, itemBottom - height)
710
- }
711
- } else {
712
- // Already visible, do nothing
713
- return
714
- }
715
- }
716
- } else {
717
- // topToBottom (default)
718
- if (align === 'auto') {
719
- // If item is above the viewport, align to top
720
- if (targetIndex < firstVisibleIndex) {
721
- scrollTarget = getScrollOffsetForIndex(
722
- heightCache,
723
- calculatedItemHeight,
724
- targetIndex
725
- )
726
- // If item is below the viewport, align to bottom
727
- } else if (targetIndex > lastVisibleIndex - 1) {
728
- const itemBottom = getScrollOffsetForIndex(
729
- heightCache,
730
- calculatedItemHeight,
731
- targetIndex + 1
732
- )
733
- scrollTarget = Math.max(0, itemBottom - height)
734
- } else {
735
- // Item is visible but not aligned: align to nearest edge
736
- const itemTop = getScrollOffsetForIndex(
737
- heightCache,
738
- calculatedItemHeight,
739
- targetIndex
740
- )
741
- const itemBottom = getScrollOffsetForIndex(
742
- heightCache,
743
- calculatedItemHeight,
744
- targetIndex + 1
745
- )
746
- const distanceToTop = Math.abs(scrollTop - itemTop)
747
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
748
- if (distanceToTop < distanceToBottom) {
749
- // Closer to top, align to top
750
- scrollTarget = itemTop
751
- } else {
752
- // Closer to bottom, align to bottom
753
- scrollTarget = Math.max(0, itemBottom - height)
754
- }
755
- }
756
- } else if (align === 'top') {
757
- scrollTarget = getScrollOffsetForIndex(
758
- heightCache,
759
- calculatedItemHeight,
760
- targetIndex
761
- )
762
- } else if (align === 'bottom') {
763
- const itemBottom = getScrollOffsetForIndex(
764
- heightCache,
765
- calculatedItemHeight,
766
- targetIndex + 1
767
- )
768
- scrollTarget = Math.max(0, itemBottom - height)
769
- } else if (align === 'nearest') {
770
- const itemTop = getScrollOffsetForIndex(
771
- heightCache,
772
- calculatedItemHeight,
773
- targetIndex
774
- )
775
- const itemBottom = getScrollOffsetForIndex(
776
- heightCache,
777
- calculatedItemHeight,
778
- targetIndex + 1
779
- )
780
- if (itemBottom <= scrollTop || itemTop >= scrollTop + height) {
781
- // Not visible, align to nearest edge
782
- const distanceToTop = Math.abs(scrollTop - itemTop)
783
- const distanceToBottom = Math.abs(scrollTop + height - itemBottom)
784
- if (distanceToTop < distanceToBottom) {
785
- scrollTarget = itemTop
786
- } else {
787
- scrollTarget = Math.max(0, itemBottom - height)
788
- }
789
- } else {
790
- // Already visible, do nothing
791
- return
792
- }
793
- }
794
- }
718
+ // Use extracted scroll calculation utility
719
+ const scrollTarget = calculateScrollTarget({
720
+ mode,
721
+ align,
722
+ targetIndex,
723
+ itemsLength: items.length,
724
+ calculatedItemHeight,
725
+ height,
726
+ scrollTop,
727
+ firstVisibleIndex,
728
+ lastVisibleIndex,
729
+ heightCache
730
+ })
795
731
 
796
- if (scrollTarget !== null) {
797
- viewportElement.scrollTo({
798
- top: scrollTarget,
799
- behavior: smoothScroll ? 'smooth' : 'auto'
800
- })
732
+ // Handle early return for 'nearest' alignment when item is already visible
733
+ if (scrollTarget === null) {
734
+ return
801
735
  }
736
+
737
+ viewportElement.scrollTo({
738
+ top: scrollTarget,
739
+ behavior: smoothScroll ? 'smooth' : 'auto'
740
+ })
802
741
  }
803
742
 
804
743
  /**
@@ -811,7 +750,7 @@
811
750
  function autoObserveItemResize(element: HTMLElement) {
812
751
  if (itemResizeObserver) {
813
752
  itemResizeObserver.observe(element)
814
- if (debug) {
753
+ if (INTERNAL_DEBUG) {
815
754
  console.log(
816
755
  'Started observing element:',
817
756
  element,
@@ -819,7 +758,7 @@
819
758
  element.getBoundingClientRect().height
820
759
  )
821
760
  }
822
- } else if (debug) {
761
+ } else if (INTERNAL_DEBUG) {
823
762
  console.log('itemResizeObserver not available for element:', element)
824
763
  }
825
764
 
@@ -827,7 +766,7 @@
827
766
  destroy() {
828
767
  if (itemResizeObserver) {
829
768
  itemResizeObserver.unobserve(element)
830
- if (debug) {
769
+ if (INTERNAL_DEBUG) {
831
770
  console.log('Stopped observing element:', element)
832
771
  }
833
772
  }
@@ -862,24 +801,36 @@
862
801
  id="virtual-list-content"
863
802
  {...testId ? { 'data-testid': `${testId}-content` } : {}}
864
803
  class={contentClass ?? 'virtual-list-content'}
865
- style:height="{Math.max(height, items.length * calculatedItemHeight)}px"
804
+ style:height="{(() => {
805
+ // Use precise height when available for better cross-browser compatibility
806
+ const totalActualHeight = items.length * preciseItemHeight()
807
+ return Math.max(height, totalActualHeight)
808
+ })()}px"
866
809
  >
867
810
  <!-- Items container is translated to show correct items -->
868
811
  <div
869
812
  id="virtual-list-items"
870
813
  {...testId ? { 'data-testid': `${testId}-items` } : {}}
871
814
  class={itemsClass ?? 'virtual-list-items'}
872
- style:transform="translateY({calculateTransformY(
873
- mode,
874
- items.length,
875
- visibleItems().end,
876
- visibleItems().start,
877
- calculatedItemHeight
878
- )}px)"
815
+ style:transform="translateY({(() => {
816
+ const transform = calculateTransformY(
817
+ mode,
818
+ items.length,
819
+ visibleItems().end,
820
+ visibleItems().start,
821
+ calculatedItemHeight
822
+ )
823
+
824
+ return transform
825
+ })()}px)"
879
826
  >
880
- {#each mode === 'bottomToTop' ? items
881
- .slice(visibleItems().start, visibleItems().end)
882
- .reverse() : items.slice(visibleItems().start, visibleItems().end) as currentItem, i (currentItem?.id ?? i)}
827
+ {#each (() => {
828
+ const slice = mode === 'bottomToTop' ? items
829
+ .slice(visibleItems().start, visibleItems().end)
830
+ .reverse() : items.slice(visibleItems().start, visibleItems().end)
831
+
832
+ return slice
833
+ })() as currentItem, i (currentItem?.id ?? i)}
883
834
  <!-- Only debug when visible range or average height changes -->
884
835
  {#if debug && i === 0 && shouldShowDebugInfo(prevVisibleRange, visibleItems(), prevHeight, calculatedItemHeight)}
885
836
  {@const debugInfo = createDebugInfo(
@@ -74,4 +74,5 @@ export declare const calculateAverageHeightDebounced: (isCalculatingHeight: bool
74
74
  newHeight: number;
75
75
  newLastMeasuredIndex: number;
76
76
  updatedHeightCache: Record<number, number>;
77
- }) => void, debounceTime?: number) => NodeJS.Timeout | null;
77
+ clearedDirtyItems: Set<number>;
78
+ }) => void, debounceTime: number, dirtyItems: Set<number>) => NodeJS.Timeout | null;
@@ -71,20 +71,23 @@ import { BROWSER } from 'esm-env';
71
71
  */
72
72
  export const calculateAverageHeightDebounced = (isCalculatingHeight, heightUpdateTimeout, visibleItemsGetter, itemElements, heightCache, lastMeasuredIndex, calculatedItemHeight,
73
73
  /* trunk-ignore(eslint/no-unused-vars) */
74
- onUpdate, debounceTime = 200) => {
75
- if (!BROWSER || isCalculatingHeight || heightUpdateTimeout)
74
+ onUpdate, debounceTime, dirtyItems) => {
75
+ if (!BROWSER || isCalculatingHeight)
76
76
  return null;
77
77
  const visibleRange = visibleItemsGetter();
78
78
  const currentIndex = visibleRange.start;
79
79
  if (currentIndex === lastMeasuredIndex)
80
80
  return null;
81
+ if (heightUpdateTimeout)
82
+ clearTimeout(heightUpdateTimeout);
81
83
  return setTimeout(() => {
82
- const { newHeight, newLastMeasuredIndex, updatedHeightCache } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight);
83
- if (Math.abs(newHeight - calculatedItemHeight) > 1) {
84
+ const { newHeight, newLastMeasuredIndex, updatedHeightCache, clearedDirtyItems } = calculateAverageHeight(itemElements, visibleRange, heightCache, calculatedItemHeight, dirtyItems);
85
+ if (Math.abs(newHeight - calculatedItemHeight) > 1 || dirtyItems.size > 0) {
84
86
  onUpdate({
85
87
  newHeight,
86
88
  newLastMeasuredIndex,
87
- updatedHeightCache
89
+ updatedHeightCache,
90
+ clearedDirtyItems
88
91
  });
89
92
  }
90
93
  }, debounceTime);
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Configuration for virtual list initialization
3
+ */
4
+ export interface InitializationConfig {
5
+ /** Array of items to initialize */
6
+ items: unknown[];
7
+ /** Number of items to process in each chunk */
8
+ chunkSize: number;
9
+ /** Threshold above which to use chunked initialization */
10
+ chunkThreshold?: number;
11
+ /** Callback called with progress updates during chunked initialization */
12
+ onProgress?: (processedItems: number, totalItems: number) => void;
13
+ /** Callback called when initialization is complete */
14
+ onComplete?: () => void;
15
+ }
16
+ /**
17
+ * Determines whether to use chunked initialization based on item count and threshold.
18
+ *
19
+ * @param itemCount - Number of items to initialize
20
+ * @param threshold - Threshold above which chunked initialization is used (default: 1000)
21
+ * @returns True if chunked initialization should be used
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const useChunked = shouldUseChunkedInitialization(5000) // true
26
+ * const useImmediate = shouldUseChunkedInitialization(500) // false
27
+ * ```
28
+ */
29
+ export declare const shouldUseChunkedInitialization: (itemCount: number, threshold?: number) => boolean;
30
+ /**
31
+ * Initializes a virtual list with items, using chunked processing for large datasets.
32
+ *
33
+ * This function automatically determines whether to use immediate or chunked initialization
34
+ * based on the number of items. For large datasets, it processes items in chunks to
35
+ * prevent UI blocking, yielding to the main thread between chunks.
36
+ *
37
+ * @param config - Configuration object for initialization
38
+ * @returns Promise that resolves when initialization is complete
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * import { initializeVirtualList } from './initialization.js'
43
+ *
44
+ * // Initialize with progress tracking
45
+ * await initializeVirtualList({
46
+ * items: largeDataset,
47
+ * chunkSize: 50,
48
+ * onProgress: (processed, total) => {
49
+ * console.log(`Progress: ${processed}/${total}`)
50
+ * },
51
+ * onComplete: () => {
52
+ * console.log('Initialization complete!')
53
+ * }
54
+ * })
55
+ * ```
56
+ */
57
+ export declare const initializeVirtualList: (config: InitializationConfig) => Promise<void>;
58
+ /**
59
+ * Calculates the optimal chunk size for initialization based on item count and device capabilities.
60
+ *
61
+ * This function provides a heuristic for determining an appropriate chunk size that balances
62
+ * performance and responsiveness. It considers both the total number of items and the
63
+ * estimated processing time per item.
64
+ *
65
+ * @param itemCount - Total number of items to process
66
+ * @param baseChunkSize - Base chunk size to use as a starting point (default: 50)
67
+ * @returns Recommended chunk size
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const chunkSize = calculateOptimalChunkSize(10000) // Returns optimized chunk size
72
+ * const smallChunkSize = calculateOptimalChunkSize(100) // Returns smaller chunk size
73
+ * ```
74
+ */
75
+ export declare const calculateOptimalChunkSize: (itemCount: number, baseChunkSize?: number) => number;
76
+ /**
77
+ * Progress information for initialization
78
+ */
79
+ export interface InitializationProgress {
80
+ /** Number of items processed */
81
+ processed: number;
82
+ /** Total number of items */
83
+ total: number;
84
+ /** Percentage complete (0-100) */
85
+ percentage: number;
86
+ /** Whether initialization is complete */
87
+ isComplete: boolean;
88
+ }
89
+ /**
90
+ * Creates a progress tracking object for initialization.
91
+ *
92
+ * @param processed - Number of items processed
93
+ * @param total - Total number of items
94
+ * @returns Progress information object
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const progress = createProgressInfo(750, 1000)
99
+ * console.log(progress.percentage) // 75
100
+ * console.log(progress.isComplete) // false
101
+ * ```
102
+ */
103
+ export declare const createProgressInfo: (processed: number, total: number) => InitializationProgress;