feat(fonts): implement Phase 2 - Unified Font Store

- Implemented UnifiedFontStore extending BaseFontStore
- Added pagination support with derived metadata
- Added provider-specific shortcuts (setProvider, setCategory, etc.)
- Added pagination methods (nextPage, prevPage, goToPage)
- Added category getter shortcuts (sansSerifFonts, serifFonts, etc.)
- Updated store exports to include unified store
- Fixed typo in googleFontsStore.svelte.ts (createGoogleFontsStore)

Phase 2/7: Proxy API Integration for GlyphDiff
This commit is contained in:
Ilia Mashkov
2026-01-29 14:38:07 +03:00
parent 7078cb6f8c
commit 7fbeef68e2
3 changed files with 287 additions and 26 deletions

View File

@@ -20,7 +20,7 @@ export class GoogleFontsStore extends BaseFontStore<GoogleFontsParams> {
}
}
export function createFontshareStore(params: GoogleFontsParams = {}) {
export function createGoogleFontsStore(params: GoogleFontsParams = {}) {
return new GoogleFontsStore(params);
}

View File

@@ -6,18 +6,29 @@
* Single export point for the unified font store infrastructure.
*/
// export {
// createUnifiedFontStore,
// UNIFIED_FONT_STORE_KEY,
// type UnifiedFontStore,
// } from './unifiedFontStore.svelte';
// Primary store (unified)
export {
createUnifiedFontStore,
type UnifiedFontStore,
unifiedFontStore,
} from './unifiedFontStore.svelte';
// Applied fonts manager (CSS loading - unchanged)
export { appliedFontsManager } from './appliedFontsStore/appliedFontsStore.svelte';
// Selected fonts store (user selection - unchanged)
export { selectedFontsStore } from './selectedFontsStore/selectedFontsStore.svelte';
// DEPRECATED: Fontshare store (will be removed in Phase 6)
export {
createFontshareStore,
type FontshareStore,
fontshareStore,
} from './fontshareStore.svelte';
export { appliedFontsManager } from './appliedFontsStore/appliedFontsStore.svelte';
export { selectedFontsStore } from './selectedFontsStore/selectedFontsStore.svelte';
// DEPRECATED: Google Fonts store (will be removed in Phase 6)
export {
createGoogleFontsStore,
type GoogleFontsStore,
googleFontsStore,
} from './googleFontsStore.svelte';

View File

@@ -1,25 +1,275 @@
import { type Filter } from '$shared/lib';
import { SvelteMap } from 'svelte/reactivity';
import type { FontProvider } from '../types';
import type { CheckboxFilter } from '../types/common';
import type { BaseFontStore } from './baseFontStore.svelte';
import { createFontshareStore } from './fontshareStore.svelte';
import type { ProviderParams } from './types';
/**
* Unified font store
*
* Single source of truth for font data, powered by the proxy API.
* Extends BaseFontStore for TanStack Query integration and reactivity.
*
* Key features:
* - Provider-agnostic (proxy API handles provider logic)
* - Reactive to filter changes
* - Optimistic updates via TanStack Query
* - Pagination support
* - Provider-specific shortcuts for common operations
*/
export class UnitedFontStore {
private sources: Partial<Record<FontProvider, BaseFontStore<ProviderParams>>>;
import type { ProxyFontsParams } from '../../api';
import { fetchProxyFonts } from '../../api';
import type { UnifiedFont } from '../types';
import type { FontCategory } from '../types';
import { BaseFontStore } from './baseFontStore.svelte';
filters: SvelteMap<CheckboxFilter, Filter>;
queryValue = $state('');
/**
* Unified font store wrapping TanStack Query with Svelte 5 runes
*
* Extends BaseFontStore to provide:
* - Reactive state management
* - TanStack Query integration for caching
* - Dynamic parameter binding for filters
* - Pagination support
*
* @example
* ```ts
* const store = new UnifiedFontStore({
* provider: 'google',
* category: 'sans-serif',
* limit: 50
* });
*
* // Access reactive state
* $effect(() => {
* console.log(store.fonts);
* console.log(store.isLoading);
* console.log(store.pagination);
* });
*
* // Update parameters
* store.setCategory('serif');
* store.nextPage();
* ```
*/
export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
/**
* Store pagination metadata separately from fonts
* This is a workaround for TanStack Query's type system
*/
#paginationMetadata = $state<
{
total: number;
limit: number;
offset: number;
} | null
>(null);
constructor(initialConfig: Partial<Record<FontProvider, ProviderParams>> = {}) {
this.sources = {
fontshare: createFontshareStore(initialConfig?.fontshare),
/**
* Pagination metadata (derived from proxy API response)
*/
readonly pagination = $derived.by(() => {
if (this.#paginationMetadata) {
const { total, limit, offset } = this.#paginationMetadata;
return {
total,
limit,
offset,
hasMore: offset + limit < total,
page: Math.floor(offset / limit) + 1,
totalPages: Math.ceil(total / limit),
};
this.filters = new SvelteMap();
}
return {
total: 0,
limit: this.params.limit || 50,
offset: this.params.offset || 0,
hasMore: false,
page: 1,
totalPages: 0,
};
});
constructor(initialParams: ProxyFontsParams = {}) {
super(initialParams);
}
get fonts() {
return Object.values(this.sources).map(store => store.fonts).flat();
/**
* Query key for TanStack Query caching
* Normalizes params to treat empty arrays/strings as undefined
*/
protected getQueryKey(params: ProxyFontsParams) {
// Normalize params to treat empty arrays/strings as undefined
const normalized = Object.entries(params).reduce((acc, [key, value]) => {
if (value === '' || (Array.isArray(value) && value.length === 0)) {
return acc;
}
return { ...acc, [key]: value };
}, {});
return ['unifiedFonts', normalized] as const;
}
/**
* Fetch function that calls the proxy API
* Returns the full response including pagination metadata
*/
protected async fetchFn(params: ProxyFontsParams): Promise<UnifiedFont[]> {
const response = await fetchProxyFonts(params);
// Store pagination metadata separately for derived values
this.#paginationMetadata = {
total: response.total,
limit: response.limit,
offset: response.offset,
};
return response.fonts;
}
// --- Getters (proxied from BaseFontStore) ---
/**
* Get all fonts from current query result
*/
get fonts(): UnifiedFont[] {
// The result.data is UnifiedFont[] (from TanStack Query)
return (this.result.data as UnifiedFont[] | undefined) ?? [];
}
/**
* Check if loading initial data
*/
get isLoading(): boolean {
return this.result.isLoading;
}
/**
* Check if fetching (including background refetches)
*/
get isFetching(): boolean {
return this.result.isFetching;
}
/**
* Check if error occurred
*/
get isError(): boolean {
return this.result.isError;
}
/**
* Check if result is empty (not loading and no fonts)
*/
get isEmpty(): boolean {
return !this.isLoading && this.fonts.length === 0;
}
// --- Provider-specific shortcuts ---
/**
* Set provider filter
*/
setProvider(provider: 'google' | 'fontshare' | undefined) {
this.setParams({ provider });
}
/**
* Set category filter
*/
setCategory(category: ProxyFontsParams['category']) {
this.setParams({ category });
}
/**
* Set subset filter
*/
setSubset(subset: ProxyFontsParams['subset']) {
this.setParams({ subset });
}
/**
* Set search query
*/
setSearch(search: string) {
this.setParams({ q: search || undefined });
}
/**
* Set sort order
*/
setSort(sort: ProxyFontsParams['sort']) {
this.setParams({ sort });
}
// --- Pagination methods ---
/**
* Go to next page
*/
nextPage() {
if (this.pagination.hasMore) {
this.setParams({
offset: this.pagination.offset + this.pagination.limit,
});
}
}
/**
* Go to previous page
*/
prevPage() {
if (this.pagination.page > 1) {
this.setParams({
offset: this.pagination.offset - this.pagination.limit,
});
}
}
/**
* Go to specific page
*/
goToPage(page: number) {
if (page >= 1 && page <= this.pagination.totalPages) {
this.setParams({
offset: (page - 1) * this.pagination.limit,
});
}
}
/**
* Set limit (items per page)
*/
setLimit(limit: number) {
this.setParams({ limit });
}
// --- Category shortcuts (for convenience) ---
get sansSerifFonts() {
return this.fonts.filter(f => f.category === 'sans-serif');
}
get serifFonts() {
return this.fonts.filter(f => f.category === 'serif');
}
get displayFonts() {
return this.fonts.filter(f => f.category === 'display');
}
get handwritingFonts() {
return this.fonts.filter(f => f.category === 'handwriting');
}
get monospaceFonts() {
return this.fonts.filter(f => f.category === 'monospace');
}
}
/**
* Factory function to create unified font store
*/
export function createUnifiedFontStore(params: ProxyFontsParams = {}) {
return new UnifiedFontStore(params);
}
/**
* Singleton instance for global use
*/
export const unifiedFontStore = new UnifiedFontStore();