From 49f5564cc9be4c03ac5bfa2ce0b5777d9118e4c9 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sat, 7 Feb 2026 11:28:13 +0300 Subject: [PATCH] feat(controlManager): integrate persistent storage into control manager to keep typography settings between sessions --- .../ui/FontSampler/FontSampler.svelte | 2 +- .../controlManager/controlManager.svelte.ts | 176 +++++++++++++++--- 2 files changed, 150 insertions(+), 28 deletions(-) diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte index c4bc81f..64274e5 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte @@ -45,7 +45,7 @@ let { }: Props = $props(); const fontWeight = $derived(controlManager.weight); -const fontSize = $derived(controlManager.size); +const fontSize = $derived(controlManager.renderedSize); const lineHeight = $derived(controlManager.height); const letterSpacing = $derived(controlManager.spacing); diff --git a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts b/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts index d8dcded..5b28718 100644 --- a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts +++ b/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts @@ -1,66 +1,188 @@ +import type { ControlId } from '$features/SetupFont/model/state/manager.svelte'; import { + type ControlDataModel, type ControlModel, + type PersistentStore, type TypographyControl, + createPersistentStore, createTypographyControl, } from '$shared/lib'; import { SvelteMap } from 'svelte/reactivity'; +import { + DEFAULT_FONT_SIZE, + DEFAULT_FONT_WEIGHT, + DEFAULT_LETTER_SPACING, + DEFAULT_LINE_HEIGHT, +} from '../../model'; -export interface Control { - id: string; - increaseLabel?: string; - decreaseLabel?: string; - controlLabel?: string; +type ControlOnlyFields = Omit, keyof ControlDataModel>; +export interface Control extends ControlOnlyFields { instance: TypographyControl; } export class TypographyControlManager { #controls = new SvelteMap(); - #sizeMultiplier = $state(1); + #multiplier = $state(1); + #storage: PersistentStore; + #baseSize = $state(DEFAULT_FONT_SIZE); - constructor(configs: ControlModel[]) { - configs.forEach(({ id, increaseLabel, decreaseLabel, controlLabel, ...config }) => { - this.#controls.set(id, { - id, - increaseLabel, - decreaseLabel, - controlLabel, - instance: createTypographyControl(config), + constructor(configs: ControlModel[], storage: PersistentStore) { + this.#storage = storage; + + // 1. Initial Load + const saved = storage.value; + this.#baseSize = saved.fontSize; + + // 2. Setup Controls + configs.forEach(config => { + const initialValue = this.#getInitialValue(config.id, saved); + + this.#controls.set(config.id, { + ...config, + instance: createTypographyControl({ + ...config, + value: initialValue, + }), + }); + }); + + // 3. The Sync Effect (UI -> Storage) + // We access .value explicitly to ensure Svelte 5 tracks the dependency + $effect.root(() => { + $effect(() => { + // EXPLICIT DEPENDENCIES: Accessing these triggers the effect + const fontSize = this.#baseSize; + const fontWeight = this.#controls.get('font_weight')?.instance.value ?? DEFAULT_FONT_WEIGHT; + const lineHeight = this.#controls.get('line_height')?.instance.value ?? DEFAULT_LINE_HEIGHT; + const letterSpacing = this.#controls.get('letter_spacing')?.instance.value ?? DEFAULT_LETTER_SPACING; + + // Syncing back to storage + this.#storage.value = { + fontSize, + fontWeight, + lineHeight, + letterSpacing, + }; + }); + + // 4. The Font Size Proxy Effect + // This handles the "Multiplier" logic specifically for the Font Size Control + $effect(() => { + const ctrl = this.#controls.get('font_size')?.instance; + if (!ctrl) return; + + // If the user moves the slider/clicks buttons in the UI: + // We update the baseSize (User Intent) + const currentDisplayValue = ctrl.value; + const calculatedBase = currentDisplayValue / this.#multiplier; + + // Only update if the difference is significant (prevents rounding jitter) + if (Math.abs(this.#baseSize - calculatedBase) > 0.01) { + this.#baseSize = calculatedBase; + } }); }); } + #getInitialValue(id: string, saved: TypographySettings): number { + if (id === 'font_size') return saved.fontSize * this.#multiplier; + if (id === 'font_weight') return saved.fontWeight; + if (id === 'line_height') return saved.lineHeight; + if (id === 'letter_spacing') return saved.letterSpacing; + return 0; + } + + // --- Getters / Setters --- + + get multiplier() { + return this.#multiplier; + } + set multiplier(value: number) { + if (this.#multiplier === value) return; + this.#multiplier = value; + + // When multiplier changes, we must update the Font Size Control's display value + const ctrl = this.#controls.get('font_size')?.instance; + if (ctrl) { + ctrl.value = this.#baseSize * this.#multiplier; + } + } + + /** The scaled size for CSS usage */ + get renderedSize() { + return this.#baseSize * this.#multiplier; + } + + /** The base size (User Preference) */ + get baseSize() { + return this.#baseSize; + } + set baseSize(val: number) { + this.#baseSize = val; + const ctrl = this.#controls.get('font_size')?.instance; + if (ctrl) ctrl.value = val * this.#multiplier; + } + get controls() { - return this.#controls.values(); + return Array.from(this.#controls.values()); } get weight() { - return this.#controls.get('font_weight')?.instance.value ?? 400; - } - - get size() { - const size = this.#controls.get('font_size')?.instance.value; - return size === undefined ? undefined : size * this.#sizeMultiplier; + return this.#controls.get('font_weight')?.instance.value ?? DEFAULT_FONT_WEIGHT; } get height() { - return this.#controls.get('line_height')?.instance.value; + return this.#controls.get('line_height')?.instance.value ?? DEFAULT_LINE_HEIGHT; } get spacing() { - return this.#controls.get('letter_spacing')?.instance.value; + return this.#controls.get('letter_spacing')?.instance.value ?? DEFAULT_LETTER_SPACING; } - set multiplier(value: number) { - this.#sizeMultiplier = value; + reset() { + this.#storage.clear(); + const defaults = this.#storage.value; + + this.#baseSize = defaults.fontSize; + + // Reset all control instances + this.#controls.forEach(c => { + if (c.id === 'font_size') { + c.instance.value = defaults.fontSize * this.#multiplier; + } else { + // Map storage key to control id + const key = c.id.replace('_', '') as keyof TypographySettings; + // Simplified for brevity, you'd map these properly: + if (c.id === 'font_weight') c.instance.value = defaults.fontWeight; + if (c.id === 'line_height') c.instance.value = defaults.lineHeight; + if (c.id === 'letter_spacing') c.instance.value = defaults.letterSpacing; + } + }); } } +/** + * Storage schema for typography settings + */ +export interface TypographySettings { + fontSize: number; + fontWeight: number; + lineHeight: number; + letterSpacing: number; +} + /** * Creates a typography control manager that handles a collection of typography controls. * * @param configs - Array of control configurations. * @returns - Typography control manager instance. */ -export function createTypographyControlManager(configs: ControlModel[]) { - return new TypographyControlManager(configs); +export function createTypographyControlManager(configs: ControlModel[]) { + const storage = createPersistentStore('glyphdiff:typography', { + fontSize: DEFAULT_FONT_SIZE, + fontWeight: DEFAULT_FONT_WEIGHT, + lineHeight: DEFAULT_LINE_HEIGHT, + letterSpacing: DEFAULT_LETTER_SPACING, + }); + return new TypographyControlManager(configs, storage); }