feat: refactor ComparisonStore to use BatchFontStore
Replace hand-rolled async fetching (fetchFontsByIds + isRestoring flag) with BatchFontStore backed by TanStack Query. Three reactive effects handle batch sync, CSS font loading, and default-font fallback. isLoading now derives from batchStore.isLoading + fontsReady.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Font comparison store for side-by-side font comparison
|
||||
* Font comparison store — TanStack Query refactor
|
||||
*
|
||||
* Manages the state for comparing two fonts character by character.
|
||||
* Persists font selection to localStorage and handles font loading
|
||||
@@ -7,17 +7,17 @@
|
||||
*
|
||||
* Features:
|
||||
* - Persistent font selection (survives page refresh)
|
||||
* - Font loading state tracking
|
||||
* - Font loading state tracking via BatchFontStore + TanStack Query
|
||||
* - Sample text management
|
||||
* - Typography controls (size, weight, line height, spacing)
|
||||
* - Slider position for character-by-character morphing
|
||||
*/
|
||||
|
||||
import {
|
||||
BatchFontStore,
|
||||
type FontLoadRequestConfig,
|
||||
type UnifiedFont,
|
||||
appliedFontsManager,
|
||||
fetchFontsByIds,
|
||||
fontStore,
|
||||
getFontUrl,
|
||||
} from '$entities/Font';
|
||||
@@ -47,11 +47,13 @@ const storage = createPersistentStore<ComparisonState>('glyphdiff:comparison', {
|
||||
});
|
||||
|
||||
/**
|
||||
* Store for managing font comparison state
|
||||
* Store for managing font comparison state.
|
||||
*
|
||||
* Handles font selection persistence, fetching, and loading state tracking.
|
||||
* Uses the CSS Font Loading API to ensure fonts are loaded before
|
||||
* showing the comparison interface.
|
||||
* Uses BatchFontStore (TanStack Query) to fetch fonts by ID, replacing
|
||||
* the previous hand-rolled async fetch approach. Three reactive effects
|
||||
* handle: (1) syncing batch results into fontA/fontB, (2) triggering the
|
||||
* CSS Font Loading API, and (3) falling back to default fonts when
|
||||
* storage is empty.
|
||||
*/
|
||||
export class ComparisonStore {
|
||||
/** Font for side A */
|
||||
@@ -60,8 +62,6 @@ export class ComparisonStore {
|
||||
#fontB = $state<UnifiedFont | undefined>();
|
||||
/** Sample text to display */
|
||||
#sampleText = $state('The quick brown fox jumps over the lazy dog');
|
||||
/** Whether currently restoring from storage */
|
||||
#isRestoring = $state(true);
|
||||
/** Whether fonts are loaded and ready to display */
|
||||
#fontsReady = $state(false);
|
||||
/** Active side for single-font operations */
|
||||
@@ -70,13 +70,32 @@ export class ComparisonStore {
|
||||
#sliderPosition = $state(50);
|
||||
/** Typography controls for this comparison */
|
||||
#typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography');
|
||||
/** TanStack Query-backed batch font fetcher */
|
||||
#batchStore: BatchFontStore;
|
||||
|
||||
constructor() {
|
||||
this.restoreFromStorage();
|
||||
// Synchronously seed the batch store with any IDs already in storage
|
||||
const { fontAId, fontBId } = storage.value;
|
||||
this.#batchStore = new BatchFontStore(fontAId && fontBId ? [fontAId, fontBId] : []);
|
||||
|
||||
// Reactively handle font loading and default selection
|
||||
$effect.root(() => {
|
||||
// Effect 1: Trigger font loading whenever selection or weight changes
|
||||
// Effect 1: Sync batch results → fontA / fontB
|
||||
$effect(() => {
|
||||
const fonts = this.#batchStore.fonts;
|
||||
if (fonts.length === 0) return;
|
||||
|
||||
const { fontAId: aId, fontBId: bId } = storage.value;
|
||||
if (aId) {
|
||||
const fa = fonts.find(f => f.id === aId);
|
||||
if (fa) this.#fontA = fa;
|
||||
}
|
||||
if (bId) {
|
||||
const fb = fonts.find(f => f.id === bId);
|
||||
if (fb) this.#fontB = fb;
|
||||
}
|
||||
});
|
||||
|
||||
// Effect 2: Trigger font loading whenever selection or weight changes
|
||||
$effect(() => {
|
||||
const fa = this.#fontA;
|
||||
const fb = this.#fontB;
|
||||
@@ -104,25 +123,17 @@ export class ComparisonStore {
|
||||
}
|
||||
});
|
||||
|
||||
// Effect 2: Set defaults if we aren't restoring and have no selection
|
||||
// Effect 3: Set default fonts when storage is empty
|
||||
$effect(() => {
|
||||
// Wait until we are done checking storage
|
||||
if (this.#isRestoring) {
|
||||
return;
|
||||
}
|
||||
if (this.#fontA && this.#fontB) return;
|
||||
|
||||
// If we already have a selection, do nothing
|
||||
if (this.#fontA && this.#fontB) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if fonts are available to set as defaults
|
||||
const fonts = fontStore.fonts;
|
||||
if (fonts.length >= 2) {
|
||||
// We need full objects with all URLs, so we trigger a batch fetch
|
||||
// This is the "batch request" seen on initial load when storage is empty
|
||||
untrack(() => {
|
||||
this.restoreDefaults([fonts[0].id, fonts[fonts.length - 1].id]);
|
||||
const id1 = fonts[0].id;
|
||||
const id2 = fonts[fonts.length - 1].id;
|
||||
storage.value = { fontAId: id1, fontBId: id2 };
|
||||
this.#batchStore.setIds([id1, id2]);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -130,26 +141,7 @@ export class ComparisonStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default fonts by fetching full objects from the API
|
||||
*/
|
||||
private async restoreDefaults(ids: string[]) {
|
||||
this.#isRestoring = true;
|
||||
try {
|
||||
const fullFonts = await fetchFontsByIds(ids);
|
||||
if (fullFonts.length >= 2) {
|
||||
this.#fontA = fullFonts[0];
|
||||
this.#fontB = fullFonts[1];
|
||||
this.updateStorage();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[ComparisonStore] Failed to set defaults:', error);
|
||||
} finally {
|
||||
this.#isRestoring = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if fonts are actually loaded in the browser at current weight
|
||||
* Checks if fonts are actually loaded in the browser at current weight.
|
||||
*
|
||||
* Uses CSS Font Loading API to prevent FOUT. Waits for fonts to load
|
||||
* and forces a layout/paint cycle before marking as ready.
|
||||
@@ -182,71 +174,35 @@ export class ComparisonStore {
|
||||
this.#fontsReady = false;
|
||||
|
||||
try {
|
||||
// Step 1: Load fonts into memory
|
||||
await Promise.all([
|
||||
document.fonts.load(fontAString),
|
||||
document.fonts.load(fontBString),
|
||||
]);
|
||||
|
||||
// Step 2: Wait for browser to be ready to render
|
||||
await document.fonts.ready;
|
||||
|
||||
// Step 3: Force a layout/paint cycle (critical!)
|
||||
await new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(resolve); // Double rAF ensures paint completes
|
||||
requestAnimationFrame(resolve);
|
||||
});
|
||||
});
|
||||
|
||||
this.#fontsReady = true;
|
||||
} catch (error) {
|
||||
console.warn('[ComparisonStore] Font loading failed:', error);
|
||||
setTimeout(() => this.#fontsReady = true, 1000);
|
||||
setTimeout(() => (this.#fontsReady = true), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore state from persistent storage
|
||||
*
|
||||
* Fetches saved fonts from the API and restores them to the store.
|
||||
*/
|
||||
async restoreFromStorage() {
|
||||
this.#isRestoring = true;
|
||||
const { fontAId, fontBId } = storage.value;
|
||||
|
||||
if (fontAId && fontBId) {
|
||||
try {
|
||||
// Batch fetch the saved fonts
|
||||
const fonts = await fetchFontsByIds([fontAId, fontBId]);
|
||||
const loadedFontA = fonts.find((f: UnifiedFont) => f.id === fontAId);
|
||||
const loadedFontB = fonts.find((f: UnifiedFont) => f.id === fontBId);
|
||||
|
||||
if (loadedFontA && loadedFontB) {
|
||||
this.#fontA = loadedFontA;
|
||||
this.#fontB = loadedFontB;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[ComparisonStore] Failed to restore fonts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark restoration as complete (whether success or fail)
|
||||
this.#isRestoring = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update storage with current state
|
||||
* Updates persistent storage with the current font selection.
|
||||
*/
|
||||
private updateStorage() {
|
||||
// Don't save if we are currently restoring (avoid race)
|
||||
if (this.#isRestoring) return;
|
||||
|
||||
storage.value = {
|
||||
fontAId: this.#fontA?.id ?? null,
|
||||
fontBId: this.#fontB?.id ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Getters / Setters ─────────────────────────────────────────────────────
|
||||
|
||||
/** Typography control manager */
|
||||
get typography() {
|
||||
return this.#typography;
|
||||
@@ -299,33 +255,23 @@ export class ComparisonStore {
|
||||
this.#sliderPosition = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if both fonts are selected and loaded
|
||||
*/
|
||||
/** Whether both fonts are selected and loaded */
|
||||
get isReady() {
|
||||
return !!this.#fontA && !!this.#fontB && this.#fontsReady;
|
||||
}
|
||||
|
||||
/** Whether currently loading or restoring */
|
||||
/** Whether currently loading (batch fetch in flight or fonts not yet painted) */
|
||||
get isLoading() {
|
||||
return this.#isRestoring || !this.#fontsReady;
|
||||
return this.#batchStore.isLoading || !this.#fontsReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public initializer (optional, as constructor starts it)
|
||||
*/
|
||||
initialize() {
|
||||
if (!this.#isRestoring && !this.#fontA && !this.#fontB) {
|
||||
this.restoreFromStorage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all state and clear storage
|
||||
* Resets all state, clears storage, and disables the batch query.
|
||||
*/
|
||||
resetAll() {
|
||||
this.#fontA = undefined;
|
||||
this.#fontB = undefined;
|
||||
this.#batchStore.setIds([]);
|
||||
storage.clear();
|
||||
this.#typography.reset();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user