refactor(Font): rename fontStore and appliedFontsManager
Both names were vague or overloaded: - fontStore / FontStore -> fontCatalogStore / FontCatalogStore Three font-related stores live in this slice; the new name names the paginated catalog specifically. - appliedFontsManager / AppliedFontsManager -> fontLifecycleManager / FontLifecycleManager "Applied" collided with the filter-side appliedFilterStore (different meaning). The class actually orchestrates a load-use-evict lifecycle with FontBufferCache + FontEvictionPolicy + FontLoadQueue collaborators, so "Manager" is justified. Companion types file moved alongside (appliedFonts.ts -> fontLifecycle.ts). Directories, file basenames, factory (createFontStore -> createFontCatalogStore), and the AppliedFontsManagerDeps interface all renamed. All consumers (ComparisonView, SampleList, FontList, FontApplicator, FontVirtualList, FilterAndSortFonts bindings, createFontRowSizeResolver, mocks) updated.
This commit is contained in:
1 parent
07d044f4d6
commit
728380498b
38 files changed
+105
-105
No files matched your search
@@ -667,10 +667,10 @@ export const MOCK_STORES = {
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Create a mock FontStore object
|
||||
* Matches FontStore's public API for Storybook use
|
||||
* Create a mock FontCatalogStore object
|
||||
* Matches FontCatalogStore's public API for Storybook use
|
||||
*/
|
||||
fontStore: (config: {
|
||||
fontCatalogStore: (config: {
|
||||
/**
|
||||
* Preset font list
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TextLayoutEngine } from '$shared/lib';
|
||||
import { generateFontKey } from '../../model/store/appliedFontsStore/utils/generateFontKey/generateFontKey';
|
||||
import { generateFontKey } from '../../model/store/fontLifecycleManager/utils/generateFontKey/generateFontKey';
|
||||
import type {
|
||||
FontLoadStatus,
|
||||
UnifiedFont,
|
||||
@@ -41,7 +41,7 @@ export interface FontRowSizeResolverOptions {
|
||||
/**
|
||||
* Returns the font load status for a given font key (`'{id}@{weight}'` or `'{id}@vf'`).
|
||||
*
|
||||
* In production: `(key) => appliedFontsManager.statuses.get(key)`.
|
||||
* In production: `(key) => fontLifecycleManager.statuses.get(key)`.
|
||||
* Injected for testability — avoids a module-level singleton dependency in tests.
|
||||
* The call to `.get()` on a `SvelteMap` must happen inside a `$derived.by` context
|
||||
* for reactivity to work. This is satisfied when `itemHeight` is called by
|
||||
@@ -108,7 +108,7 @@ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions):
|
||||
// generateFontKey: '{id}@{weight}' for static fonts, '{id}@vf' for variable fonts.
|
||||
const fontKey = generateFontKey({ id: font.id, weight, isVariable: font.features?.isVariable });
|
||||
|
||||
// Reading via getStatus() allows the caller to pass appliedFontsManager.statuses.get(),
|
||||
// Reading via getStatus() allows the caller to pass fontLifecycleManager.statuses.get(),
|
||||
// which creates a Svelte 5 reactive dependency when called inside $derived.by.
|
||||
const status = options.getStatus(fontKey);
|
||||
if (status !== 'loaded') {
|
||||
|
||||
+9
-9
@@ -17,7 +17,7 @@ import {
|
||||
generateMockFonts,
|
||||
} from '../../../lib/mocks/fonts.mock';
|
||||
import type { UnifiedFont } from '../../types';
|
||||
import { FontStore } from './fontStore.svelte';
|
||||
import { FontCatalogStore } from './fontCatalogStore.svelte';
|
||||
|
||||
vi.mock('$shared/api/queryClient', () => ({
|
||||
queryClient: new QueryClient({
|
||||
@@ -44,7 +44,7 @@ const makeResponse = (
|
||||
});
|
||||
|
||||
function makeStore(params = {}) {
|
||||
return new FontStore({ limit: 10, ...params });
|
||||
return new FontCatalogStore({ limit: 10, ...params });
|
||||
}
|
||||
|
||||
async function fetchedStore(params = {}, fonts = generateMockFonts(5), meta: Parameters<typeof makeResponse>[1] = {}) {
|
||||
@@ -55,7 +55,7 @@ async function fetchedStore(params = {}, fonts = generateMockFonts(5), meta: Par
|
||||
return store;
|
||||
}
|
||||
|
||||
describe('FontStore', () => {
|
||||
describe('FontCatalogStore', () => {
|
||||
afterEach(() => {
|
||||
queryClient.clear();
|
||||
vi.resetAllMocks();
|
||||
@@ -69,7 +69,7 @@ describe('FontStore', () => {
|
||||
});
|
||||
|
||||
it('defaults limit to 50 when not provided', () => {
|
||||
const store = new FontStore();
|
||||
const store = new FontCatalogStore();
|
||||
expect(store.params.limit).toBe(50);
|
||||
store.destroy();
|
||||
});
|
||||
@@ -390,11 +390,11 @@ describe('FontStore', () => {
|
||||
});
|
||||
|
||||
describe('nextPage', () => {
|
||||
let store: FontStore;
|
||||
let store: FontCatalogStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
fetch.mockResolvedValue(makeResponse(generateMockFonts(10), { total: 30, limit: 10, offset: 0 }));
|
||||
store = new FontStore({ limit: 10 });
|
||||
store = new FontCatalogStore({ limit: 10 });
|
||||
await store.refetch();
|
||||
flushSync();
|
||||
});
|
||||
@@ -415,7 +415,7 @@ describe('FontStore', () => {
|
||||
// Set up a store where all fonts fit in one page (hasMore = false)
|
||||
queryClient.clear();
|
||||
fetch.mockResolvedValue(makeResponse(generateMockFonts(10), { total: 10, limit: 10, offset: 0 }));
|
||||
store = new FontStore({ limit: 10 });
|
||||
store = new FontCatalogStore({ limit: 10 });
|
||||
await store.refetch();
|
||||
flushSync();
|
||||
|
||||
@@ -454,7 +454,7 @@ describe('FontStore', () => {
|
||||
describe('getCachedData / setQueryData', () => {
|
||||
it('getCachedData returns undefined before any fetch', () => {
|
||||
queryClient.clear();
|
||||
const store = new FontStore({ limit: 10 });
|
||||
const store = new FontCatalogStore({ limit: 10 });
|
||||
expect(store.getCachedData()).toBeUndefined();
|
||||
store.destroy();
|
||||
});
|
||||
@@ -502,7 +502,7 @@ describe('FontStore', () => {
|
||||
});
|
||||
|
||||
describe('filter shortcut methods', () => {
|
||||
let store: FontStore;
|
||||
let store: FontCatalogStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = makeStore();
|
||||
+4
-4
@@ -25,7 +25,7 @@ type FontStoreParams = Omit<ProxyFontsParams, 'offset'>;
|
||||
|
||||
type FontStoreResult = InfiniteQueryObserverResult<InfiniteData<ProxyFontsResponse, PageParam>, Error>;
|
||||
|
||||
export class FontStore {
|
||||
export class FontCatalogStore {
|
||||
#params = $state<FontStoreParams>({ limit: 50 });
|
||||
#result = $state<FontStoreResult>({} as FontStoreResult);
|
||||
#observer: InfiniteQueryObserver<
|
||||
@@ -459,8 +459,8 @@ export class FontStore {
|
||||
}
|
||||
}
|
||||
|
||||
export function createFontStore(params: FontStoreParams = {}): FontStore {
|
||||
return new FontStore(params);
|
||||
export function createFontCatalogStore(params: FontStoreParams = {}): FontCatalogStore {
|
||||
return new FontCatalogStore(params);
|
||||
}
|
||||
|
||||
export const fontStore = new FontStore({ limit: 50 });
|
||||
export const fontCatalogStore = new FontCatalogStore({ limit: 50 });
|
||||
File renamed without changes.
+4
-4
@@ -17,7 +17,7 @@ import { FontBufferCache } from './utils/fontBufferCache/FontBufferCache';
|
||||
import { FontEvictionPolicy } from './utils/fontEvictionPolicy/FontEvictionPolicy';
|
||||
import { FontLoadQueue } from './utils/fontLoadQueue/FontLoadQueue';
|
||||
|
||||
interface AppliedFontsManagerDeps {
|
||||
interface FontLifecycleManagerDeps {
|
||||
cache?: FontBufferCache;
|
||||
eviction?: FontEvictionPolicy;
|
||||
queue?: FontLoadQueue;
|
||||
@@ -46,7 +46,7 @@ interface AppliedFontsManagerDeps {
|
||||
*
|
||||
* **Browser APIs Used:** `scheduler.yield()`, `isInputPending()`, `requestIdleCallback`, Cache API, Network Information API
|
||||
*/
|
||||
export class AppliedFontsManager {
|
||||
export class FontLifecycleManager {
|
||||
// Injected collaborators - each handles one concern for better testability
|
||||
readonly #cache: FontBufferCache;
|
||||
readonly #eviction: FontEvictionPolicy;
|
||||
@@ -78,7 +78,7 @@ export class AppliedFontsManager {
|
||||
// Starts periodic cleanup timer (browser-only).
|
||||
constructor(
|
||||
{ cache = new FontBufferCache(), eviction = new FontEvictionPolicy(), queue = new FontLoadQueue() }:
|
||||
AppliedFontsManagerDeps = {},
|
||||
FontLifecycleManagerDeps = {},
|
||||
) {
|
||||
// Inject collaborators - defaults provided for production, fakes for testing
|
||||
this.#cache = cache;
|
||||
@@ -396,4 +396,4 @@ export class AppliedFontsManager {
|
||||
/**
|
||||
* Singleton instance — use throughout the application for unified font loading state.
|
||||
*/
|
||||
export const appliedFontsManager = new AppliedFontsManager();
|
||||
export const fontLifecycleManager = new FontLifecycleManager();
|
||||
+8
-8
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { AppliedFontsManager } from './appliedFontsStore.svelte';
|
||||
import { FontFetchError } from './errors';
|
||||
import { FontLifecycleManager } from './fontLifecycleManager.svelte';
|
||||
import { FontEvictionPolicy } from './utils/fontEvictionPolicy/FontEvictionPolicy';
|
||||
|
||||
class FakeBufferCache {
|
||||
@@ -32,8 +32,8 @@ const makeConfig = (id: string, overrides: Partial<{ weight: number; isVariable:
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('AppliedFontsManager', () => {
|
||||
let manager: AppliedFontsManager;
|
||||
describe('FontLifecycleManager', () => {
|
||||
let manager: FontLifecycleManager;
|
||||
let eviction: FontEvictionPolicy;
|
||||
let mockFontFaceSet: { add: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> };
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('AppliedFontsManager', () => {
|
||||
});
|
||||
vi.stubGlobal('FontFace', MockFontFace);
|
||||
|
||||
manager = new AppliedFontsManager({ cache: new FakeBufferCache() as any, eviction });
|
||||
manager = new FontLifecycleManager({ cache: new FakeBufferCache() as any, eviction });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -101,7 +101,7 @@ describe('AppliedFontsManager', () => {
|
||||
|
||||
it('skips fonts that have exhausted retries', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const failManager = new AppliedFontsManager({ cache: new FailingBufferCache() as any, eviction });
|
||||
const failManager = new FontLifecycleManager({ cache: new FailingBufferCache() as any, eviction });
|
||||
|
||||
// exhaust all 3 retries
|
||||
for (let i = 0; i < 3; i++) {
|
||||
@@ -160,7 +160,7 @@ describe('AppliedFontsManager', () => {
|
||||
describe('Phase 1 — fetch', () => {
|
||||
it('sets status to error on fetch failure', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const failManager = new AppliedFontsManager({ cache: new FailingBufferCache() as any, eviction });
|
||||
const failManager = new FontLifecycleManager({ cache: new FailingBufferCache() as any, eviction });
|
||||
|
||||
failManager.touch([makeConfig('broken')]);
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
@@ -171,7 +171,7 @@ describe('AppliedFontsManager', () => {
|
||||
|
||||
it('logs a console error on fetch failure', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const failManager = new AppliedFontsManager({ cache: new FailingBufferCache() as any, eviction });
|
||||
const failManager = new FontLifecycleManager({ cache: new FailingBufferCache() as any, eviction });
|
||||
|
||||
failManager.touch([makeConfig('broken')]);
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
@@ -189,7 +189,7 @@ describe('AppliedFontsManager', () => {
|
||||
evict() {},
|
||||
clear() {},
|
||||
};
|
||||
const abortManager = new AppliedFontsManager({ cache: abortingCache as any, eviction });
|
||||
const abortManager = new FontLifecycleManager({ cache: abortingCache as any, eviction });
|
||||
|
||||
abortManager.touch([makeConfig('aborted')]);
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,9 +1,9 @@
|
||||
// Applied fonts manager
|
||||
export * from './appliedFontsStore/appliedFontsStore.svelte';
|
||||
// Font lifecycle manager (browser-side load + cache + eviction)
|
||||
export * from './fontLifecycleManager/fontLifecycleManager.svelte';
|
||||
|
||||
// Single FontStore
|
||||
// Paginated catalog
|
||||
export {
|
||||
createFontStore,
|
||||
FontStore,
|
||||
fontStore,
|
||||
} from './fontStore/fontStore.svelte';
|
||||
createFontCatalogStore,
|
||||
FontCatalogStore,
|
||||
fontCatalogStore,
|
||||
} from './fontCatalogStore/fontCatalogStore.svelte';
|
||||
@@ -23,5 +23,5 @@ export type {
|
||||
FontCollectionState,
|
||||
} from './store';
|
||||
|
||||
export * from './store/appliedFonts';
|
||||
export * from './store/fontLifecycle';
|
||||
export * from './typography';
|
||||
File renamed without changes.
@@ -39,7 +39,7 @@ const fontArialBold = mockUnifiedFont({ id: 'arial-bold', name: 'Arial' });
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Font that has never been loaded by appliedFontsManager. The component renders in its pending state: blurred, scaled down, and semi-transparent.',
|
||||
'Font that has never been loaded by fontLifecycleManager. The component renders in its pending state: blurred, scaled down, and semi-transparent.',
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -58,7 +58,7 @@ const fontArialBold = mockUnifiedFont({ id: 'arial-bold', name: 'Arial' });
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Uses Arial, a system font available in all browsers. Because appliedFontsManager has not loaded it via FontFace, the manager status may remain pending — meaning the blur/scale state may still show. In a real app the manager would load the font and transition to the revealed state.',
|
||||
'Uses Arial, a system font available in all browsers. Because fontLifecycleManager has not loaded it via FontFace, the manager status may remain pending — meaning the blur/scale state may still show. In a real app the manager would load the font and transition to the revealed state.',
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -77,7 +77,7 @@ const fontArialBold = mockUnifiedFont({ id: 'arial-bold', name: 'Arial' });
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Demonstrates passing a custom weight (700). The weight is forwarded to appliedFontsManager for font resolution; visually identical to the loaded state story until the manager confirms the font.',
|
||||
'Demonstrates passing a custom weight (700). The weight is forwarded to fontLifecycleManager for font resolution; visually identical to the loaded state story until the manager confirms the font.',
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { Snippet } from 'svelte';
|
||||
import {
|
||||
DEFAULT_FONT_WEIGHT,
|
||||
type UnifiedFont,
|
||||
appliedFontsManager,
|
||||
fontLifecycleManager,
|
||||
} from '../../model';
|
||||
|
||||
interface Props {
|
||||
@@ -46,7 +46,7 @@ let {
|
||||
}: Props = $props();
|
||||
|
||||
const status = $derived(
|
||||
appliedFontsManager.getFontStatus(
|
||||
fontLifecycleManager.getFontStatus(
|
||||
font.id,
|
||||
weight,
|
||||
font.features?.isVariable,
|
||||
|
||||
@@ -10,7 +10,7 @@ const { Story } = defineMeta({
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Virtualized font list backed by the `fontStore` singleton. Handles font loading registration (pin/touch) for visible items and triggers infinite scroll pagination via `fontStore.nextPage()`. Because the component reads directly from the `fontStore` singleton, stories render against a live (but empty/loading) store — no font data will appear unless the API is reachable from the Storybook host.',
|
||||
'Virtualized font list backed by the `fontCatalogStore` singleton. Handles font loading registration (pin/touch) for visible items and triggers infinite scroll pagination via `fontCatalogStore.nextPage()`. Because the component reads directly from the `fontCatalogStore` singleton, stories render against a live (but empty/loading) store — no font data will appear unless the API is reachable from the Storybook host.',
|
||||
},
|
||||
story: { inline: false },
|
||||
},
|
||||
@@ -33,7 +33,7 @@ import type { ComponentProps } from 'svelte';
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Skeleton state shown while `fontStore.fonts` is empty and `fontStore.isLoading` is true. In a real session the skeleton fades out once the first page loads.',
|
||||
'Skeleton state shown while `fontCatalogStore.fonts` is empty and `fontCatalogStore.isLoading` is true. In a real session the skeleton fades out once the first page loads.',
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -63,7 +63,7 @@ import type { ComponentProps } from 'svelte';
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'No `skeleton` snippet provided. When `fontStore.fonts` is empty the underlying VirtualList renders its empty state directly.',
|
||||
'No `skeleton` snippet provided. When `fontCatalogStore.fonts` is empty the underlying VirtualList renders its empty state directly.',
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -86,7 +86,7 @@ import type { ComponentProps } from 'svelte';
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Demonstrates how to configure a `children` snippet for item rendering. The list will be empty because `fontStore` is not populated in Storybook, but the template shows the expected slot shape: `{ item: UnifiedFont }`.',
|
||||
'Demonstrates how to configure a `children` snippet for item rendering. The list will be empty because `fontCatalogStore` is not populated in Storybook, but the template shows the expected slot shape: `{ item: UnifiedFont }`.',
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -18,8 +18,8 @@ import { getFontUrl } from '../../lib';
|
||||
import {
|
||||
type FontLoadRequestConfig,
|
||||
type UnifiedFont,
|
||||
appliedFontsManager,
|
||||
fontStore,
|
||||
fontCatalogStore,
|
||||
fontLifecycleManager,
|
||||
} from '../../model';
|
||||
|
||||
interface Props extends
|
||||
@@ -51,13 +51,13 @@ let {
|
||||
}: Props = $props();
|
||||
|
||||
const isLoading = $derived(
|
||||
fontStore.isFetching || fontStore.isLoading,
|
||||
fontCatalogStore.isFetching || fontCatalogStore.isLoading,
|
||||
);
|
||||
|
||||
let visibleFonts = $state<UnifiedFont[]>([]);
|
||||
let isCatchingUp = $state(false);
|
||||
|
||||
const showInitialSkeleton = $derived(!!skeleton && isLoading && fontStore.fonts.length === 0);
|
||||
const showInitialSkeleton = $derived(!!skeleton && isLoading && fontCatalogStore.fonts.length === 0);
|
||||
const showCatchupSkeleton = $derived(!!skeleton && isCatchingUp);
|
||||
|
||||
function handleInternalVisibleChange(items: UnifiedFont[]) {
|
||||
@@ -68,23 +68,23 @@ function handleInternalVisibleChange(items: UnifiedFont[]) {
|
||||
|
||||
/**
|
||||
* Handle jump scroll — batch-load all missing pages then re-enable font loading.
|
||||
* Suppresses appliedFontsManager.touch() during catch-up to avoid loading
|
||||
* Suppresses fontLifecycleManager.touch() during catch-up to avoid loading
|
||||
* font files for thousands of intermediate fonts.
|
||||
*/
|
||||
async function handleJump(targetIndex: number) {
|
||||
if (isCatchingUp || !fontStore.pagination.hasMore) {
|
||||
if (isCatchingUp || !fontCatalogStore.pagination.hasMore) {
|
||||
return;
|
||||
}
|
||||
isCatchingUp = true;
|
||||
try {
|
||||
await fontStore.fetchAllPagesTo(targetIndex);
|
||||
await fontCatalogStore.fetchAllPagesTo(targetIndex);
|
||||
} finally {
|
||||
isCatchingUp = false;
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedTouch = debounce((configs: FontLoadRequestConfig[]) => {
|
||||
appliedFontsManager.touch(configs);
|
||||
fontLifecycleManager.touch(configs);
|
||||
}, 150);
|
||||
|
||||
// Re-touch whenever visible set or weight changes — fixes weight-change gap
|
||||
@@ -111,11 +111,11 @@ $effect(() => {
|
||||
const w = weight;
|
||||
const fonts = visibleFonts;
|
||||
for (const f of fonts) {
|
||||
appliedFontsManager.pin(f.id, w, f.features?.isVariable);
|
||||
fontLifecycleManager.pin(f.id, w, f.features?.isVariable);
|
||||
}
|
||||
return () => {
|
||||
for (const f of fonts) {
|
||||
appliedFontsManager.unpin(f.id, w, f.features?.isVariable);
|
||||
fontLifecycleManager.unpin(f.id, w, f.features?.isVariable);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -125,12 +125,12 @@ $effect(() => {
|
||||
*/
|
||||
function loadMore() {
|
||||
if (
|
||||
!fontStore.pagination.hasMore
|
||||
|| fontStore.isFetching
|
||||
!fontCatalogStore.pagination.hasMore
|
||||
|| fontCatalogStore.isFetching
|
||||
) {
|
||||
return;
|
||||
}
|
||||
fontStore.nextPage();
|
||||
fontCatalogStore.nextPage();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,12 +140,12 @@ function loadMore() {
|
||||
* of the loaded items. Only fetches if there are more pages available.
|
||||
*/
|
||||
function handleNearBottom(_lastVisibleIndex: number) {
|
||||
const { hasMore } = fontStore.pagination;
|
||||
const { hasMore } = fontCatalogStore.pagination;
|
||||
|
||||
// VirtualList already checks if we're near the bottom of loaded items.
|
||||
// Guard isCatchingUp: fetchAllPagesTo bypasses TQ so isFetching stays false
|
||||
// during batch catch-up, which would otherwise let nextPage() race with it.
|
||||
if (hasMore && !fontStore.isFetching && !isCatchingUp) {
|
||||
if (hasMore && !fontCatalogStore.isFetching && !isCatchingUp) {
|
||||
loadMore();
|
||||
}
|
||||
}
|
||||
@@ -160,8 +160,8 @@ function handleNearBottom(_lastVisibleIndex: number) {
|
||||
{:else}
|
||||
<!-- VirtualList persists during pagination - no destruction/recreation -->
|
||||
<VirtualList
|
||||
items={fontStore.fonts}
|
||||
total={fontStore.pagination.total}
|
||||
items={fontCatalogStore.fonts}
|
||||
total={fontCatalogStore.pagination.total}
|
||||
isLoading={isLoading || isCatchingUp}
|
||||
onVisibleItemsChange={handleInternalVisibleChange}
|
||||
onNearBottom={handleNearBottom}
|
||||
|
||||
Reference in new issue
Block a user