diff --git a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte index 65102db..ede09df 100644 --- a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte +++ b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte @@ -10,9 +10,10 @@ import { appliedFontsManager } from '../../model'; interface Props extends Omit>, 'onVisibleItemsChange'> { onVisibleItemsChange?: (items: T[]) => void; + onNearBottom?: (lastVisibleIndex: number) => void; } -let { items, children, onVisibleItemsChange, ...rest }: Props = $props(); +let { items, children, onVisibleItemsChange, onNearBottom, ...rest }: Props = $props(); function handleInternalVisibleChange(visibleItems: T[]) { // Auto-register fonts with the manager @@ -22,12 +23,18 @@ function handleInternalVisibleChange(visibleItems: T[]) { // Forward the call to any external listener onVisibleItemsChange?.(visibleItems); } + +function handleNearBottom(lastVisibleIndex: number) { + // Forward the call to any external listener + onNearBottom?.(lastVisibleIndex); +} {#snippet children(scope)} {@render children(scope)} diff --git a/src/features/GetFonts/ui/SuggestedFonts/SuggestedFonts.svelte b/src/features/GetFonts/ui/SuggestedFonts/SuggestedFonts.svelte index d4e4a14..f477731 100644 --- a/src/features/GetFonts/ui/SuggestedFonts/SuggestedFonts.svelte +++ b/src/features/GetFonts/ui/SuggestedFonts/SuggestedFonts.svelte @@ -1,6 +1,7 @@ - +{#if unifiedFontStore.pagination.total > 0 && !unifiedFontStore.isLoading} +
+ {displayRange} + {#if unifiedFontStore.isFetching} + (Loading...) + {/if} +
+{/if} + + {#snippet children({ item: font, isVisible, proximity })} {/snippet} diff --git a/src/shared/ui/VirtualList/VirtualList.svelte b/src/shared/ui/VirtualList/VirtualList.svelte index 3534351..55926bb 100644 --- a/src/shared/ui/VirtualList/VirtualList.svelte +++ b/src/shared/ui/VirtualList/VirtualList.svelte @@ -19,6 +19,20 @@ interface Props { * @template T - The type of items in the list */ items: T[]; + /** + * Total number of items (including not-yet-loaded items for pagination). + * If not provided, defaults to items.length. + * + * Use this when implementing pagination to ensure the scrollbar + * reflects the total count of items, not just the loaded ones. + * + * @example + * ```ts + * // Pagination scenario: 1920 total fonts, but only 50 loaded + * + * ``` + */ + total?: number; /** * Height for each item, either as a fixed number * or a function that returns height per index. @@ -40,6 +54,24 @@ interface Props { * @param items - Loaded items */ onVisibleItemsChange?: (items: T[]) => void; + /** + * An optional callback that will be called when user scrolls near the end of the list. + * Useful for triggering auto-pagination. + * + * The callback receives the index of the last visible item. You can use this + * to determine if you should load more data. + * + * @example + * ```ts + * onNearBottom={(lastVisibleIndex) => { + * const itemsRemaining = total - lastVisibleIndex; + * if (itemsRemaining < 5 && hasMore && !isFetching) { + * loadMore(); + * } + * }} + * ``` + */ + onNearBottom?: (lastVisibleIndex: number) => void; /** * Snippet for rendering individual list items. * @@ -55,10 +87,19 @@ interface Props { children: Snippet<[{ item: T; index: number; isVisible: boolean; proximity: number }]>; } -let { items, itemHeight = 80, overscan = 5, class: className, onVisibleItemsChange, children }: Props = $props(); +let { + items, + total = items.length, + itemHeight = 80, + overscan = 5, + class: className, + onVisibleItemsChange, + onNearBottom, + children, +}: Props = $props(); const virtualizer = createVirtualizer(() => ({ - count: items.length, + count: total, data: items, estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight, overscan, @@ -67,6 +108,16 @@ const virtualizer = createVirtualizer(() => ({ $effect(() => { const visibleItems = virtualizer.items.map(item => items[item.index]); onVisibleItemsChange?.(visibleItems); + + // Trigger onNearBottom when user scrolls near the end (within 5 items) + if (virtualizer.items.length > 0 && onNearBottom) { + const lastVisibleItem = virtualizer.items[virtualizer.items.length - 1]; + const itemsRemaining = total - lastVisibleItem.index; + + if (itemsRemaining <= 5) { + onNearBottom(lastVisibleItem.index); + } + } }); @@ -96,12 +147,14 @@ $effect(() => { class="absolute top-0 left-0 w-full" style:transform="translateY({item.start}px)" > - {@render children({ + {#if item.index < items.length} + {@render children({ item: items[item.index], index: item.index, isVisible: item.isVisible, proximity: item.proximity, })} + {/if} {/each}