feat(controlManager): integrate persistent storage into control manager to keep typography settings between sessions
This commit is contained in:
@@ -45,7 +45,7 @@ let {
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const fontWeight = $derived(controlManager.weight);
|
const fontWeight = $derived(controlManager.weight);
|
||||||
const fontSize = $derived(controlManager.size);
|
const fontSize = $derived(controlManager.renderedSize);
|
||||||
const lineHeight = $derived(controlManager.height);
|
const lineHeight = $derived(controlManager.height);
|
||||||
const letterSpacing = $derived(controlManager.spacing);
|
const letterSpacing = $derived(controlManager.spacing);
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +1,174 @@
|
|||||||
|
import type { ControlId } from '$features/SetupFont/model/state/manager.svelte';
|
||||||
import {
|
import {
|
||||||
|
type ControlDataModel,
|
||||||
type ControlModel,
|
type ControlModel,
|
||||||
|
type PersistentStore,
|
||||||
type TypographyControl,
|
type TypographyControl,
|
||||||
|
createPersistentStore,
|
||||||
createTypographyControl,
|
createTypographyControl,
|
||||||
} from '$shared/lib';
|
} from '$shared/lib';
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
import {
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
DEFAULT_FONT_WEIGHT,
|
||||||
|
DEFAULT_LETTER_SPACING,
|
||||||
|
DEFAULT_LINE_HEIGHT,
|
||||||
|
} from '../../model';
|
||||||
|
|
||||||
export interface Control {
|
type ControlOnlyFields<T extends string = string> = Omit<ControlModel<T>, keyof ControlDataModel>;
|
||||||
id: string;
|
export interface Control extends ControlOnlyFields<ControlId> {
|
||||||
increaseLabel?: string;
|
|
||||||
decreaseLabel?: string;
|
|
||||||
controlLabel?: string;
|
|
||||||
instance: TypographyControl;
|
instance: TypographyControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TypographyControlManager {
|
export class TypographyControlManager {
|
||||||
#controls = new SvelteMap<string, Control>();
|
#controls = new SvelteMap<string, Control>();
|
||||||
#sizeMultiplier = $state(1);
|
#multiplier = $state(1);
|
||||||
|
#storage: PersistentStore<TypographySettings>;
|
||||||
|
#baseSize = $state(DEFAULT_FONT_SIZE);
|
||||||
|
|
||||||
constructor(configs: ControlModel[]) {
|
constructor(configs: ControlModel<ControlId>[], storage: PersistentStore<TypographySettings>) {
|
||||||
configs.forEach(({ id, increaseLabel, decreaseLabel, controlLabel, ...config }) => {
|
this.#storage = storage;
|
||||||
this.#controls.set(id, {
|
|
||||||
id,
|
// 1. Initial Load
|
||||||
increaseLabel,
|
const saved = storage.value;
|
||||||
decreaseLabel,
|
this.#baseSize = saved.fontSize;
|
||||||
controlLabel,
|
|
||||||
instance: createTypographyControl(config),
|
// 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() {
|
get controls() {
|
||||||
return this.#controls.values();
|
return Array.from(this.#controls.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
get weight() {
|
get weight() {
|
||||||
return this.#controls.get('font_weight')?.instance.value ?? 400;
|
return this.#controls.get('font_weight')?.instance.value ?? DEFAULT_FONT_WEIGHT;
|
||||||
}
|
|
||||||
|
|
||||||
get size() {
|
|
||||||
const size = this.#controls.get('font_size')?.instance.value;
|
|
||||||
return size === undefined ? undefined : size * this.#sizeMultiplier;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get height() {
|
get height() {
|
||||||
return this.#controls.get('line_height')?.instance.value;
|
return this.#controls.get('line_height')?.instance.value ?? DEFAULT_LINE_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
get spacing() {
|
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) {
|
reset() {
|
||||||
this.#sizeMultiplier = value;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,6 +177,12 @@ export class TypographyControlManager {
|
|||||||
* @param configs - Array of control configurations.
|
* @param configs - Array of control configurations.
|
||||||
* @returns - Typography control manager instance.
|
* @returns - Typography control manager instance.
|
||||||
*/
|
*/
|
||||||
export function createTypographyControlManager(configs: ControlModel[]) {
|
export function createTypographyControlManager(configs: ControlModel<ControlId>[]) {
|
||||||
return new TypographyControlManager(configs);
|
const storage = createPersistentStore<TypographySettings>('glyphdiff:typography', {
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
fontWeight: DEFAULT_FONT_WEIGHT,
|
||||||
|
lineHeight: DEFAULT_LINE_HEIGHT,
|
||||||
|
letterSpacing: DEFAULT_LETTER_SPACING,
|
||||||
|
});
|
||||||
|
return new TypographyControlManager(configs, storage);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user