/** * Typography control manager * * Manages a collection of typography controls (font size, weight, line height, * letter spacing) with persistent storage. Supports responsive scaling * through a multiplier system. * * The font size control uses a multiplier system to allow responsive scaling * while preserving the user's base size preference. The multiplier is applied * when displaying/editing, but the base size is what's stored. */ import { type ControlId, DEFAULT_FONT_SIZE, DEFAULT_FONT_WEIGHT, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, } from '$entities/Font'; import { type ControlDataModel, type ControlModel, type PersistentStore, type TypographyControl, createPersistentStore, createTypographyControl, } from '$shared/lib'; import { SvelteMap } from 'svelte/reactivity'; type ControlOnlyFields = Omit, keyof ControlDataModel>; /** * A control with its instance */ export interface Control extends ControlOnlyFields { instance: TypographyControl; } /** * Storage schema for typography settings */ export interface TypographySettings { fontSize: number; fontWeight: number; lineHeight: number; letterSpacing: number; } /** * Typography control manager class * * Manages multiple typography controls with persistent storage and * responsive scaling support for font size. */ export class TypographySettingsManager { /** Map of controls keyed by ID */ #controls = new SvelteMap(); /** Responsive multiplier for font size display */ #multiplier = $state(1); /** Persistent storage for settings */ #storage: PersistentStore; /** Base font size (user preference, unscaled) */ #baseSize = $state(DEFAULT_FONT_SIZE); constructor(configs: ControlModel[], storage: PersistentStore) { this.#storage = storage; // Initial Load const saved = storage.value; this.#baseSize = saved.fontSize; // Setup Controls configs.forEach(config => { const initialValue = this.#getInitialValue(config.id, saved); this.#controls.set(config.id, { ...config, instance: createTypographyControl({ ...config, value: initialValue, }), }); }); // 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, }; }); // 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; } }); }); } /** * Gets initial value for a control from storage or defaults */ #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; } /** Current multiplier for responsive scaling */ get multiplier() { return this.#multiplier; } /** * Set the multiplier and update font size display * * When multiplier changes, the font size control's display value * is updated to reflect the new scale while preserving base size. */ 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 * Returns baseSize * multiplier for actual rendering */ 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; } /** * Getters for controls */ get controls() { return Array.from(this.#controls.values()); } get weightControl() { return this.#controls.get('font_weight')?.instance; } get sizeControl() { return this.#controls.get('font_size')?.instance; } get heightControl() { return this.#controls.get('line_height')?.instance; } get spacingControl() { return this.#controls.get('letter_spacing')?.instance; } /** * Getters for values (besides font-size) */ get weight() { return this.#controls.get('font_weight')?.instance.value ?? DEFAULT_FONT_WEIGHT; } get height() { return this.#controls.get('line_height')?.instance.value ?? DEFAULT_LINE_HEIGHT; } get spacing() { return this.#controls.get('letter_spacing')?.instance.value ?? DEFAULT_LETTER_SPACING; } /** * Reset all controls to default values */ 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; } }); } } /** * Creates a typography control manager * * @param configs - Array of control configurations * @param storageId - Persistent storage identifier * @returns Typography control manager instance */ export function createTypographySettingsManager( configs: ControlModel[], storageId: string = 'glyphdiff:typography', ) { const storage = createPersistentStore(storageId, { fontSize: DEFAULT_FONT_SIZE, fontWeight: DEFAULT_FONT_WEIGHT, lineHeight: DEFAULT_LINE_HEIGHT, letterSpacing: DEFAULT_LETTER_SPACING, }); return new TypographySettingsManager(configs, storage); }