Files
frontend-svelte/src/features/SetupFont/lib/settingsManager/settingsManager.svelte.ts
T

298 lines
8.8 KiB
TypeScript
Raw Normal View History

/**
* 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<T extends string = string> = Omit<ControlModel<T>, keyof ControlDataModel>;
/**
2026-04-17 12:14:55 +03:00
* A control with its associated instance
*/
export interface Control extends ControlOnlyFields<ControlId> {
2026-04-17 12:14:55 +03:00
/**
* The reactive typography control instance
*/
instance: TypographyControl;
}
/**
* Storage schema for typography settings
*/
export interface TypographySettings {
2026-04-17 12:14:55 +03:00
/**
* Base font size (User preference, unscaled)
*/
fontSize: number;
2026-04-17 12:14:55 +03:00
/**
* Numeric font weight (100-900)
*/
fontWeight: number;
2026-04-17 12:14:55 +03:00
/**
* Line height multiplier (e.g. 1.5)
*/
lineHeight: number;
2026-04-17 12:14:55 +03:00
/**
* Letter spacing in em/px
*/
letterSpacing: number;
}
/**
* Typography control manager class
*
* Manages multiple typography controls with persistent storage and
* responsive scaling support for font size.
*/
export class TypographySettingsManager {
2026-04-17 12:14:55 +03:00
/**
* Internal map of reactive controls keyed by their identifier
*/
#controls = new SvelteMap<string, Control>();
2026-04-17 12:14:55 +03:00
/**
* Global multiplier for responsive font size scaling
*/
#multiplier = $state(1);
2026-04-17 12:14:55 +03:00
/**
* LocalStorage-backed storage for persistence
*/
#storage: PersistentStore<TypographySettings>;
2026-04-17 12:14:55 +03:00
/**
* The underlying font size before responsive scaling is applied
*/
#baseSize = $state(DEFAULT_FONT_SIZE);
constructor(configs: ControlModel<ControlId>[], storage: PersistentStore<TypographySettings>) {
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;
}
2026-04-17 12:14:55 +03:00
/**
* Active scaling factor for the rendered font size
*/
get multiplier() {
return this.#multiplier;
}
/**
2026-04-17 12:14:55 +03:00
* Updates the multiplier and recalculates dependent control values
*/
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;
}
}
/**
2026-04-17 12:14:55 +03:00
* The actual pixel value for CSS font-size (baseSize * multiplier)
*/
get renderedSize() {
return this.#baseSize * this.#multiplier;
}
2026-04-17 12:14:55 +03:00
/**
* The raw font size preference before scaling
*/
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;
}
/**
2026-04-17 12:14:55 +03:00
* List of all managed typography controls
*/
get controls() {
return Array.from(this.#controls.values());
}
2026-04-17 12:14:55 +03:00
/**
* Reactive instance for weight manipulation
*/
get weightControl() {
return this.#controls.get('font_weight')?.instance;
}
2026-04-17 12:14:55 +03:00
/**
* Reactive instance for size manipulation
*/
get sizeControl() {
return this.#controls.get('font_size')?.instance;
}
2026-04-17 12:14:55 +03:00
/**
* Reactive instance for line-height manipulation
*/
get heightControl() {
return this.#controls.get('line_height')?.instance;
}
2026-04-17 12:14:55 +03:00
/**
* Reactive instance for letter-spacing manipulation
*/
get spacingControl() {
return this.#controls.get('letter_spacing')?.instance;
}
/**
2026-04-17 12:14:55 +03:00
* Current numeric font weight (reactive)
*/
get weight() {
return this.#controls.get('font_weight')?.instance.value ?? DEFAULT_FONT_WEIGHT;
}
2026-04-17 12:14:55 +03:00
/**
* Current numeric line height (reactive)
*/
get height() {
return this.#controls.get('line_height')?.instance.value ?? DEFAULT_LINE_HEIGHT;
}
2026-04-17 12:14:55 +03:00
/**
* Current numeric letter spacing (reactive)
*/
get spacing() {
return this.#controls.get('letter_spacing')?.instance.value ?? DEFAULT_LETTER_SPACING;
}
/**
2026-04-17 12:14:55 +03:00
* Reset all controls to project-defined defaults
*/
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<ControlId>[],
storageId: string = 'glyphdiff:typography',
) {
const storage = createPersistentStore<TypographySettings>(storageId, {
fontSize: DEFAULT_FONT_SIZE,
fontWeight: DEFAULT_FONT_WEIGHT,
lineHeight: DEFAULT_LINE_HEIGHT,
letterSpacing: DEFAULT_LETTER_SPACING,
});
return new TypographySettingsManager(configs, storage);
}