diff --git a/src/features/AdjustTypography/index.ts b/src/features/AdjustTypography/index.ts index 2ff571d..28fe87c 100644 --- a/src/features/AdjustTypography/index.ts +++ b/src/features/AdjustTypography/index.ts @@ -1,9 +1,9 @@ export { createTypographySettingsStore, + getTypographySettingsStore, MULTIPLIER_L, MULTIPLIER_M, MULTIPLIER_S, type TypographySettingsStore, - typographySettingsStore, } from './model'; export { TypographyMenu } from './ui'; diff --git a/src/features/AdjustTypography/model/index.ts b/src/features/AdjustTypography/model/index.ts index 4543ff8..6e26ac4 100644 --- a/src/features/AdjustTypography/model/index.ts +++ b/src/features/AdjustTypography/model/index.ts @@ -5,6 +5,6 @@ export { } from './const/const'; export { createTypographySettingsStore, + getTypographySettingsStore, type TypographySettingsStore, - typographySettingsStore, } from './store/typographySettingsStore/typographySettingsStore.svelte'; diff --git a/src/features/AdjustTypography/model/store/typographySettingsStore/typographySettingsStore.svelte.ts b/src/features/AdjustTypography/model/store/typographySettingsStore/typographySettingsStore.svelte.ts index 7461b22..d7b2512 100644 --- a/src/features/AdjustTypography/model/store/typographySettingsStore/typographySettingsStore.svelte.ts +++ b/src/features/AdjustTypography/model/store/typographySettingsStore/typographySettingsStore.svelte.ts @@ -94,6 +94,12 @@ export class TypographySettingsStore { * The underlying font size before responsive scaling is applied */ #baseSize = $state(DEFAULT_FONT_SIZE); + /** + * Disposes the $effect.root that backs the storage-sync effects. + * $effect.root lives outside component lifecycle, so callers must invoke + * destroy() to avoid leaking the subscriptions. + */ + #disposeEffects: () => void; constructor(configs: ControlModel[], storage: PersistentStore) { this.#storage = storage; @@ -117,7 +123,7 @@ export class TypographySettingsStore { // The Sync Effect (UI -> Storage) // We access .value explicitly to ensure Svelte 5 tracks the dependency - $effect.root(() => { + this.#disposeEffects = $effect.root(() => { $effect(() => { // EXPLICIT DEPENDENCIES: Accessing these triggers the effect const fontSize = this.#baseSize; @@ -155,6 +161,13 @@ export class TypographySettingsStore { }); } + /** + * Tears down the storage-sync effects. Call on unmount / store disposal. + */ + destroy(): void { + this.#disposeEffects(); + } + /** * Gets initial value for a control from storage or defaults */ @@ -336,10 +349,24 @@ export function createTypographySettingsStore( return new TypographySettingsStore(configs, storage); } +export type TypographySettingsStoreInstance = ReturnType; + +let _typographySettingsStore: TypographySettingsStoreInstance | undefined; + /** - * App-wide typography settings singleton, keyed for the comparison view. + * App-wide typography settings store, keyed for the comparison view. + * Created on first access so its persistent-store sync effects aren't set up + * at module load. */ -export const typographySettingsStore = createTypographySettingsStore( - DEFAULT_TYPOGRAPHY_CONTROLS_DATA, - COMPARISON_STORAGE_KEY, -); +export function getTypographySettingsStore(): TypographySettingsStoreInstance { + return (_typographySettingsStore ??= createTypographySettingsStore( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + COMPARISON_STORAGE_KEY, + )); +} + +// test-only reset, so specs don't share persisted typography state or leak effects +export function __resetTypographySettingsStore() { + _typographySettingsStore?.destroy(); + _typographySettingsStore = undefined; +} diff --git a/src/features/AdjustTypography/ui/TypographyMenu/TypographyMenu.svelte b/src/features/AdjustTypography/ui/TypographyMenu/TypographyMenu.svelte index d5b7b2e..7d8c9bd 100644 --- a/src/features/AdjustTypography/ui/TypographyMenu/TypographyMenu.svelte +++ b/src/features/AdjustTypography/ui/TypographyMenu/TypographyMenu.svelte @@ -23,7 +23,7 @@ import { MULTIPLIER_L, MULTIPLIER_M, MULTIPLIER_S, - typographySettingsStore, + getTypographySettingsStore, } from '../../model'; interface Props { @@ -46,6 +46,7 @@ interface Props { let { class: className, hidden = false, open = $bindable(false) }: Props = $props(); const responsive = getContext('responsive'); +const typographySettingsStore = getTypographySettingsStore(); /** * Sets the common font size multiplier based on the current responsive state. diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte index 84ba0f6..4d15486 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte @@ -9,7 +9,7 @@ import { type FontLoadStatus, type UnifiedFont, } from '$entities/Font'; -import { typographySettingsStore } from '$features/AdjustTypography/model'; +import { getTypographySettingsStore } from '$features/AdjustTypography/model'; import { Badge, ContentEditable, @@ -43,6 +43,8 @@ interface Props { let { font, status, text = $bindable(), index = 0 }: Props = $props(); +const typographySettingsStore = getTypographySettingsStore(); + // Extract provider badge with fallback const providerBadge = $derived( font.providerBadge diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts index b4b616a..7abfa6c 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts @@ -24,7 +24,10 @@ import { fontLifecycleManager, getFontCatalog, } from '$entities/Font/model'; -import { typographySettingsStore } from '$features/AdjustTypography/model'; +import { + type TypographySettingsStore, + getTypographySettingsStore, +} from '$features/AdjustTypography/model'; import { createPersistentStore } from '$shared/lib'; import { untrack } from 'svelte'; import { getPretextFontString } from '../../lib'; @@ -101,11 +104,14 @@ export class ComparisonStore { #fontCatalog: FontCatalogStore; + #typography: TypographySettingsStore; + constructor() { // Synchronously seed the batch store with any IDs already in storage const { fontAId, fontBId } = storage.value; this.#fontsByIdsStore = new FontsByIdsStore(fontAId && fontBId ? [fontAId, fontBId] : []); this.#fontCatalog = getFontCatalog(); + this.#typography = getTypographySettingsStore(); $effect.root(() => { // Sync batch results → fontA / fontB @@ -134,7 +140,7 @@ export class ComparisonStore { $effect(() => { const fa = this.#fontA; const fb = this.#fontB; - const weight = typographySettingsStore.weight; + const weight = this.#typography.weight; if (!fa || !fb) { return; @@ -195,7 +201,7 @@ export class ComparisonStore { $effect(() => { const fa = this.#fontA; const fb = this.#fontB; - const w = typographySettingsStore.weight; + const w = this.#typography.weight; if (fa) { fontLifecycleManager.pin(fa.id, w, fa.features?.isVariable); } @@ -226,8 +232,8 @@ export class ComparisonStore { return; } - const weight = typographySettingsStore.weight; - const size = typographySettingsStore.renderedSize; + const weight = this.#typography.weight; + const size = this.#typography.renderedSize; const fontAName = this.#fontA?.name; const fontBName = this.#fontB?.name; @@ -356,7 +362,7 @@ export class ComparisonStore { this.#fontB = undefined; this.#fontsByIdsStore.setIds([]); storage.clear(); - typographySettingsStore.reset(); + this.#typography.reset(); } } diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts index 1aa0240..495aa70 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts @@ -89,12 +89,14 @@ vi.mock('$features/AdjustTypography', () => ({ })), })); +const mockTypography = vi.hoisted(() => ({ + weight: 400, + renderedSize: 48, + reset: vi.fn(), +})); + vi.mock('$features/AdjustTypography/model', () => ({ - typographySettingsStore: { - weight: 400, - renderedSize: 48, - reset: vi.fn(), - }, + getTypographySettingsStore: () => mockTypography, })); import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts'; diff --git a/src/widgets/ComparisonView/ui/Line/Line.svelte b/src/widgets/ComparisonView/ui/Line/Line.svelte index e25c30f..96e8b6d 100644 --- a/src/widgets/ComparisonView/ui/Line/Line.svelte +++ b/src/widgets/ComparisonView/ui/Line/Line.svelte @@ -11,7 +11,7 @@ import { type ComparisonLine, computeLineRenderModel, } from '$entities/Font'; -import { typographySettingsStore } from '$features/AdjustTypography'; +import { getTypographySettingsStore } from '$features/AdjustTypography'; import { getComparisonStore } from '../../model'; import Character from '../Character/Character.svelte'; @@ -36,7 +36,7 @@ const comparisonStore = getComparisonStore(); const model = $derived(computeLineRenderModel(line, split, windowSize)); -const typography = $derived(typographySettingsStore); +const typography = getTypographySettingsStore(); const fontA = $derived(comparisonStore.fontA); const fontB = $derived(comparisonStore.fontB); diff --git a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte index b3a62d8..8b98abe 100644 --- a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte +++ b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte @@ -18,7 +18,7 @@ import { MULTIPLIER_M, MULTIPLIER_S, TypographyMenu, - typographySettingsStore, + getTypographySettingsStore, } from '$features/AdjustTypography'; import { type ResponsiveManager, @@ -81,7 +81,7 @@ const SLIDER_STEP_COARSE = 10; const fontA = $derived(comparisonStore.fontA); const fontB = $derived(comparisonStore.fontB); const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady); -const typography = $derived(typographySettingsStore); +const typography = getTypographySettingsStore(); let container = $state(); diff --git a/src/widgets/SampleList/ui/SampleList/SampleList.svelte b/src/widgets/SampleList/ui/SampleList/SampleList.svelte index fa9861f..20f7a21 100644 --- a/src/widgets/SampleList/ui/SampleList/SampleList.svelte +++ b/src/widgets/SampleList/ui/SampleList/SampleList.svelte @@ -15,7 +15,7 @@ import { } from '$entities/Font/model'; import { TypographyMenu, - typographySettingsStore, + getTypographySettingsStore, } from '$features/AdjustTypography'; import { FontSampler } from '$features/DisplayFont'; import { throttle } from '$shared/lib/utils'; @@ -23,6 +23,7 @@ import { Skeleton } from '$shared/ui'; import { layoutManager } from '../../model'; const fontCatalog = getFontCatalog(); +const typographySettingsStore = getTypographySettingsStore(); // FontSampler chrome heights — derived from Tailwind classes in FontSampler.svelte. // Header: py-3 (12+12px padding) + ~32px content row ≈ 56px.