feature/comparison-slider #19
@@ -10,9 +10,10 @@ import { appliedFontsManager } from '../../model';
|
|||||||
|
|
||||||
interface Props extends Omit<ComponentProps<typeof VirtualList<T>>, 'onVisibleItemsChange'> {
|
interface Props extends Omit<ComponentProps<typeof VirtualList<T>>, 'onVisibleItemsChange'> {
|
||||||
onVisibleItemsChange?: (items: T[]) => void;
|
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[]) {
|
function handleInternalVisibleChange(visibleItems: T[]) {
|
||||||
// Auto-register fonts with the manager
|
// Auto-register fonts with the manager
|
||||||
@@ -22,12 +23,18 @@ function handleInternalVisibleChange(visibleItems: T[]) {
|
|||||||
// Forward the call to any external listener
|
// Forward the call to any external listener
|
||||||
onVisibleItemsChange?.(visibleItems);
|
onVisibleItemsChange?.(visibleItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleNearBottom(lastVisibleIndex: number) {
|
||||||
|
// Forward the call to any external listener
|
||||||
|
onNearBottom?.(lastVisibleIndex);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<VirtualList
|
<VirtualList
|
||||||
{items}
|
{items}
|
||||||
{...rest}
|
{...rest}
|
||||||
onVisibleItemsChange={handleInternalVisibleChange}
|
onVisibleItemsChange={handleInternalVisibleChange}
|
||||||
|
onNearBottom={handleNearBottom}
|
||||||
>
|
>
|
||||||
{#snippet children(scope)}
|
{#snippet children(scope)}
|
||||||
{@render children(scope)}
|
{@render children(scope)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<!--
|
<!--
|
||||||
Component: SuggestedFonts
|
Component: SuggestedFonts
|
||||||
Renders a list of suggested fonts in a virtualized list to improve performance.
|
Renders a list of suggested fonts in a virtualized list to improve performance.
|
||||||
|
Includes pagination with auto-loading when scrolling near the bottom.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
@@ -8,9 +9,58 @@ import {
|
|||||||
FontVirtualList,
|
FontVirtualList,
|
||||||
unifiedFontStore,
|
unifiedFontStore,
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load more fonts by moving to the next page
|
||||||
|
*/
|
||||||
|
function loadMore() {
|
||||||
|
if (!unifiedFontStore.pagination.hasMore || unifiedFontStore.isFetching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unifiedFontStore.nextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle scroll near bottom - auto-load next page
|
||||||
|
*
|
||||||
|
* Triggered when the user scrolls within 5 items of the end of the list.
|
||||||
|
* Only fetches if there are more pages available and not already fetching.
|
||||||
|
*/
|
||||||
|
function handleNearBottom(lastVisibleIndex: number) {
|
||||||
|
const { hasMore, total } = unifiedFontStore.pagination;
|
||||||
|
const itemsRemaining = total - lastVisibleIndex;
|
||||||
|
|
||||||
|
// Only trigger if within 5 items of the end, more data exists, and not already fetching
|
||||||
|
if (itemsRemaining <= 5 && hasMore && !unifiedFontStore.isFetching) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate display range for pagination info
|
||||||
|
*/
|
||||||
|
const displayRange = $derived.by(() => {
|
||||||
|
const { offset, limit, total } = unifiedFontStore.pagination;
|
||||||
|
const loadedCount = Math.min(offset + limit, total);
|
||||||
|
return `Showing ${loadedCount} of ${total} fonts`;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FontVirtualList items={unifiedFontStore.fonts}>
|
{#if unifiedFontStore.pagination.total > 0 && !unifiedFontStore.isLoading}
|
||||||
|
<div class="text-sm text-muted-foreground px-2 py-2">
|
||||||
|
{displayRange}
|
||||||
|
{#if unifiedFontStore.isFetching}
|
||||||
|
<span class="ml-2 text-xs text-muted-foreground/70">(Loading...)</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<FontVirtualList
|
||||||
|
items={unifiedFontStore.fonts}
|
||||||
|
total={unifiedFontStore.pagination.total}
|
||||||
|
onNearBottom={handleNearBottom}
|
||||||
|
>
|
||||||
{#snippet children({ item: font, isVisible, proximity })}
|
{#snippet children({ item: font, isVisible, proximity })}
|
||||||
<FontListItem {font} {isVisible} {proximity} />
|
<FontListItem {font} {isVisible} {proximity} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -19,6 +19,20 @@ interface Props {
|
|||||||
* @template T - The type of items in the list
|
* @template T - The type of items in the list
|
||||||
*/
|
*/
|
||||||
items: T[];
|
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
|
||||||
|
* <VirtualList items={loadedFonts} total={1920}>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
total?: number;
|
||||||
/**
|
/**
|
||||||
* Height for each item, either as a fixed number
|
* Height for each item, either as a fixed number
|
||||||
* or a function that returns height per index.
|
* or a function that returns height per index.
|
||||||
@@ -40,6 +54,24 @@ interface Props {
|
|||||||
* @param items - Loaded items
|
* @param items - Loaded items
|
||||||
*/
|
*/
|
||||||
onVisibleItemsChange?: (items: T[]) => void;
|
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.
|
* Snippet for rendering individual list items.
|
||||||
*
|
*
|
||||||
@@ -55,10 +87,19 @@ interface Props {
|
|||||||
children: Snippet<[{ item: T; index: number; isVisible: boolean; proximity: number }]>;
|
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(() => ({
|
const virtualizer = createVirtualizer(() => ({
|
||||||
count: items.length,
|
count: total,
|
||||||
data: items,
|
data: items,
|
||||||
estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight,
|
estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight,
|
||||||
overscan,
|
overscan,
|
||||||
@@ -67,6 +108,16 @@ const virtualizer = createVirtualizer(() => ({
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
const visibleItems = virtualizer.items.map(item => items[item.index]);
|
const visibleItems = virtualizer.items.map(item => items[item.index]);
|
||||||
onVisibleItemsChange?.(visibleItems);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -96,12 +147,14 @@ $effect(() => {
|
|||||||
class="absolute top-0 left-0 w-full"
|
class="absolute top-0 left-0 w-full"
|
||||||
style:transform="translateY({item.start}px)"
|
style:transform="translateY({item.start}px)"
|
||||||
>
|
>
|
||||||
|
{#if item.index < items.length}
|
||||||
{@render children({
|
{@render children({
|
||||||
item: items[item.index],
|
item: items[item.index],
|
||||||
index: item.index,
|
index: item.index,
|
||||||
isVisible: item.isVisible,
|
isVisible: item.isVisible,
|
||||||
proximity: item.proximity,
|
proximity: item.proximity,
|
||||||
})}
|
})}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user