From 0fa3437661e082e83727fba322be2e2bb5e9ba57 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 2 Mar 2026 22:20:29 +0300 Subject: [PATCH] refactor(SetupFont): reorganize TypographyMenu and add control tests --- .../controlManager/controlManager.svelte.ts | 75 +- .../lib/controlManager/controlManager.test.ts | 723 ++++++++++++++++++ src/features/SetupFont/model/const/const.ts | 8 +- .../SetupFont/ui/TypographyMenu.svelte | 95 --- .../ui/TypographyMenu/TypographyMenu.svelte | 193 +++++ src/features/SetupFont/ui/index.ts | 2 +- 6 files changed, 979 insertions(+), 117 deletions(-) create mode 100644 src/features/SetupFont/lib/controlManager/controlManager.test.ts delete mode 100644 src/features/SetupFont/ui/TypographyMenu.svelte create mode 100644 src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte diff --git a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts b/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts index d65516d..39b0315 100644 --- a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts +++ b/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts @@ -1,3 +1,15 @@ +/** + * 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 ControlDataModel, type ControlModel, @@ -17,14 +29,37 @@ import { 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 TypographyControlManager { + /** 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) { @@ -85,6 +120,9 @@ export class TypographyControlManager { }); } + /** + * 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; @@ -93,11 +131,17 @@ export class TypographyControlManager { return 0; } - // --- Getters / Setters --- - + /** 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; @@ -109,7 +153,10 @@ export class TypographyControlManager { } } - /** The scaled size for CSS usage */ + /** + * The scaled size for CSS usage + * Returns baseSize * multiplier for actual rendering + */ get renderedSize() { return this.#baseSize * this.#multiplier; } @@ -118,6 +165,7 @@ export class TypographyControlManager { get baseSize() { return this.#baseSize; } + set baseSize(val: number) { this.#baseSize = val; const ctrl = this.#controls.get('font_size')?.instance; @@ -162,6 +210,9 @@ export class TypographyControlManager { 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; @@ -185,21 +236,11 @@ export class TypographyControlManager { } /** - * 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. + * Creates a typography control manager * - * @param configs - Array of control configurations. - * @param storageId - Persistent storage identifier. - * @returns - Typography control manager instance. + * @param configs - Array of control configurations + * @param storageId - Persistent storage identifier + * @returns Typography control manager instance */ export function createTypographyControlManager( configs: ControlModel[], diff --git a/src/features/SetupFont/lib/controlManager/controlManager.test.ts b/src/features/SetupFont/lib/controlManager/controlManager.test.ts new file mode 100644 index 0000000..7e73e49 --- /dev/null +++ b/src/features/SetupFont/lib/controlManager/controlManager.test.ts @@ -0,0 +1,723 @@ +/** @vitest-environment jsdom */ +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import { + DEFAULT_FONT_SIZE, + DEFAULT_FONT_WEIGHT, + DEFAULT_LETTER_SPACING, + DEFAULT_LINE_HEIGHT, + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, +} from '../../model'; +import { + TypographyControlManager, + type TypographySettings, +} from './controlManager.svelte'; + +/** + * Test Strategy for TypographyControlManager + * + * This test suite validates the TypographyControlManager state management logic. + * These are unit tests for the manager logic, separate from component rendering. + * + * NOTE: Svelte 5's $effect runs in microtasks, so we need to flush effects + * after state changes to test reactive behavior. This is a limitation of unit + * testing Svelte 5 reactive code in Node.js. + * + * Test Coverage: + * 1. Initialization: Loading from storage, creating controls with correct values + * 2. Multiplier System: Changing multiplier updates font size display + * 3. Base Size Proxy: UI changes update #baseSize via the proxy effect + * 4. Storage Sync: Changes to controls sync to storage (via $effect) + * 5. Reset Functionality: Clearing storage resets all controls + * 6. Rendered Size: base * multiplier calculation + * 7. Control Getters: Return correct control instances + */ + +// Helper to flush Svelte effects (they run in microtasks) +async function flushEffects() { + await Promise.resolve(); + await Promise.resolve(); +} + +describe('TypographyControlManager - Unit Tests', () => { + let mockStorage: TypographySettings; + let mockPersistentStore: { + value: TypographySettings; + clear: () => void; + }; + + const createMockPersistentStore = (initialValue: TypographySettings) => { + let value = initialValue; + return { + get value() { + return value; + }, + set value(v: TypographySettings) { + value = v; + }, + clear() { + value = { + fontSize: DEFAULT_FONT_SIZE, + fontWeight: DEFAULT_FONT_WEIGHT, + lineHeight: DEFAULT_LINE_HEIGHT, + letterSpacing: DEFAULT_LETTER_SPACING, + }; + }, + }; + }; + + beforeEach(() => { + // Reset mock storage with default values before each test + mockStorage = { + fontSize: DEFAULT_FONT_SIZE, + fontWeight: DEFAULT_FONT_WEIGHT, + lineHeight: DEFAULT_LINE_HEIGHT, + letterSpacing: DEFAULT_LETTER_SPACING, + }; + mockPersistentStore = createMockPersistentStore(mockStorage); + }); + + describe('Initialization', () => { + it('creates manager with default values from storage', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + expect(manager.baseSize).toBe(DEFAULT_FONT_SIZE); + expect(manager.weight).toBe(DEFAULT_FONT_WEIGHT); + expect(manager.height).toBe(DEFAULT_LINE_HEIGHT); + expect(manager.spacing).toBe(DEFAULT_LETTER_SPACING); + }); + + it('creates manager with saved values from storage', () => { + mockStorage = { + fontSize: 72, + fontWeight: 700, + lineHeight: 1.8, + letterSpacing: 0.05, + }; + mockPersistentStore = createMockPersistentStore(mockStorage); + + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + expect(manager.baseSize).toBe(72); + expect(manager.weight).toBe(700); + expect(manager.height).toBe(1.8); + expect(manager.spacing).toBe(0.05); + }); + + it('initializes font size control with base size multiplied by current multiplier (1)', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + expect(manager.sizeControl?.value).toBe(DEFAULT_FONT_SIZE); + }); + + it('returns all controls via controls getter', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + const controls = manager.controls; + expect(controls).toHaveLength(4); + expect(controls.map(c => c.id)).toEqual([ + 'font_size', + 'font_weight', + 'line_height', + 'letter_spacing', + ]); + }); + + it('returns individual controls via specific getters', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + expect(manager.sizeControl).toBeDefined(); + expect(manager.weightControl).toBeDefined(); + expect(manager.heightControl).toBeDefined(); + expect(manager.spacingControl).toBeDefined(); + + // Control instances have value, min, max, step, isAtMax, isAtMin, increase, decrease + expect(manager.sizeControl).toHaveProperty('value'); + expect(manager.weightControl).toHaveProperty('value'); + expect(manager.heightControl).toHaveProperty('value'); + expect(manager.spacingControl).toHaveProperty('value'); + }); + + it('control instances have expected interface', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + const ctrl = manager.sizeControl!; + expect(typeof ctrl.value).toBe('number'); + expect(typeof ctrl.min).toBe('number'); + expect(typeof ctrl.max).toBe('number'); + expect(typeof ctrl.step).toBe('number'); + expect(typeof ctrl.isAtMax).toBe('boolean'); + expect(typeof ctrl.isAtMin).toBe('boolean'); + expect(typeof ctrl.increase).toBe('function'); + expect(typeof ctrl.decrease).toBe('function'); + }); + }); + + describe('Multiplier System', () => { + it('has default multiplier of 1', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + expect(manager.multiplier).toBe(1); + }); + + it('updates multiplier when set', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.multiplier = 0.75; + expect(manager.multiplier).toBe(0.75); + + manager.multiplier = 0.5; + expect(manager.multiplier).toBe(0.5); + }); + + it('does not update multiplier if set to same value', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + const originalSizeValue = manager.sizeControl?.value; + + manager.multiplier = 1; // Same as default + + expect(manager.sizeControl?.value).toBe(originalSizeValue); + }); + + it('updates font size control display value when multiplier changes', () => { + mockStorage = { fontSize: 48, fontWeight: 400, lineHeight: 1.5, letterSpacing: 0 }; + mockPersistentStore = createMockPersistentStore(mockStorage); + + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + // Initial state: base = 48, multiplier = 1, display = 48 + expect(manager.baseSize).toBe(48); + expect(manager.sizeControl?.value).toBe(48); + + // Change multiplier to 0.75 + manager.multiplier = 0.75; + // Display should be 48 * 0.75 = 36 + expect(manager.sizeControl?.value).toBe(36); + + // Change multiplier to 0.5 + manager.multiplier = 0.5; + // Display should be 48 * 0.5 = 24 + expect(manager.sizeControl?.value).toBe(24); + + // Base size should remain unchanged + expect(manager.baseSize).toBe(48); + }); + + it('updates font size control display value when multiplier increases', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + // Start with multiplier 0.5 + manager.multiplier = 0.5; + expect(manager.sizeControl?.value).toBe(DEFAULT_FONT_SIZE * 0.5); + + // Increase to 0.75 + manager.multiplier = 0.75; + expect(manager.sizeControl?.value).toBe(DEFAULT_FONT_SIZE * 0.75); + + // Increase to 1.0 + manager.multiplier = 1; + expect(manager.sizeControl?.value).toBe(DEFAULT_FONT_SIZE); + }); + }); + + describe('Base Size Setter', () => { + it('updates baseSize when set directly', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.baseSize = 72; + + expect(manager.baseSize).toBe(72); + }); + + it('updates size control value when baseSize is set', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.baseSize = 60; + + expect(manager.sizeControl?.value).toBe(60); + }); + + it('applies multiplier to size control when baseSize is set', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.multiplier = 0.5; + manager.baseSize = 60; + + expect(manager.sizeControl?.value).toBe(30); // 60 * 0.5 + }); + }); + + describe('Rendered Size Calculation', () => { + it('calculates renderedSize as baseSize * multiplier', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + expect(manager.renderedSize).toBe(DEFAULT_FONT_SIZE * 1); + }); + + it('updates renderedSize when multiplier changes', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.multiplier = 0.5; + expect(manager.renderedSize).toBe(DEFAULT_FONT_SIZE * 0.5); + + manager.multiplier = 0.75; + expect(manager.renderedSize).toBe(DEFAULT_FONT_SIZE * 0.75); + }); + + it('updates renderedSize when baseSize changes', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.baseSize = 72; + expect(manager.renderedSize).toBe(72); + + manager.multiplier = 0.5; + expect(manager.renderedSize).toBe(36); + }); + }); + + describe('Base Size Proxy Effect (UI -> baseSize)', () => { + // NOTE: The proxy effect that updates baseSize when the control value changes + // runs in a $effect, which is asynchronous in unit tests. We test the + // synchronous behavior here (baseSize setter) and note that the full + // proxy effect behavior should be tested in E2E tests. + + it('does NOT immediately update baseSize from control change (effect is async)', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + const originalBaseSize = manager.baseSize; + + // Change the control value directly + manager.sizeControl!.value = 60; + + // baseSize is NOT updated immediately because the effect runs in microtasks + expect(manager.baseSize).toBe(originalBaseSize); + }); + + it('updates baseSize via direct setter (synchronous)', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.baseSize = 60; + + expect(manager.baseSize).toBe(60); + expect(manager.sizeControl?.value).toBe(60); + }); + }); + + describe('Storage Sync (Controls -> Storage)', () => { + // NOTE: Storage sync happens via $effect which runs in microtasks. + // In unit tests, we verify the initial sync and test async behavior. + + it('has initial values in storage from constructor', () => { + mockStorage = { + fontSize: 60, + fontWeight: 500, + lineHeight: 1.6, + letterSpacing: 0.02, + }; + mockPersistentStore = createMockPersistentStore(mockStorage); + + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + // Initial values are loaded from storage + expect(manager.baseSize).toBe(60); + expect(manager.weight).toBe(500); + expect(manager.height).toBe(1.6); + expect(manager.spacing).toBe(0.02); + }); + + it('syncs to storage after effect flush (async)', async () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.baseSize = 72; + + // Storage is NOT updated immediately + expect(mockPersistentStore.value.fontSize).toBe(DEFAULT_FONT_SIZE); + + // After flushing effects, storage should be updated + await flushEffects(); + expect(mockPersistentStore.value.fontSize).toBe(72); + }); + + it('syncs control changes to storage after effect flush (async)', async () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.weightControl!.value = 700; + + // After flushing effects + await flushEffects(); + expect(mockPersistentStore.value.fontWeight).toBe(700); + }); + + it('syncs height control changes to storage after effect flush (async)', async () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.heightControl!.value = 1.8; + + await flushEffects(); + expect(mockPersistentStore.value.lineHeight).toBe(1.8); + }); + + it('syncs spacing control changes to storage after effect flush (async)', async () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.spacingControl!.value = 0.05; + + await flushEffects(); + expect(mockPersistentStore.value.letterSpacing).toBe(0.05); + }); + }); + + describe('Control Value Getters', () => { + it('returns current weight value', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + expect(manager.weight).toBe(DEFAULT_FONT_WEIGHT); + + manager.weightControl!.value = 700; + expect(manager.weight).toBe(700); + }); + + it('returns current height value', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + expect(manager.height).toBe(DEFAULT_LINE_HEIGHT); + + manager.heightControl!.value = 1.8; + expect(manager.height).toBe(1.8); + }); + + it('returns current spacing value', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + expect(manager.spacing).toBe(DEFAULT_LETTER_SPACING); + + manager.spacingControl!.value = 0.05; + expect(manager.spacing).toBe(0.05); + }); + + it('returns default value when control is not found', () => { + // Create a manager with empty configs (no controls) + const manager = new TypographyControlManager([], mockPersistentStore); + + expect(manager.weight).toBe(DEFAULT_FONT_WEIGHT); + expect(manager.height).toBe(DEFAULT_LINE_HEIGHT); + expect(manager.spacing).toBe(DEFAULT_LETTER_SPACING); + }); + }); + + describe('Reset Functionality', () => { + it('resets all controls to default values', () => { + mockStorage = { + fontSize: 72, + fontWeight: 700, + lineHeight: 1.8, + letterSpacing: 0.05, + }; + mockPersistentStore = createMockPersistentStore(mockStorage); + + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + // Modify values + manager.baseSize = 80; + manager.weightControl!.value = 900; + manager.heightControl!.value = 2.0; + manager.spacingControl!.value = 0.1; + + // Reset + manager.reset(); + + // Check all values are reset to defaults + expect(manager.baseSize).toBe(DEFAULT_FONT_SIZE); + expect(manager.weight).toBe(DEFAULT_FONT_WEIGHT); + expect(manager.height).toBe(DEFAULT_LINE_HEIGHT); + expect(manager.spacing).toBe(DEFAULT_LETTER_SPACING); + }); + + it('calls storage.clear() on reset', () => { + const clearSpy = vi.fn(); + mockPersistentStore = { + get value() { + return mockStorage; + }, + set value(v: TypographySettings) { + mockStorage = v; + }, + clear: clearSpy, + }; + + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.reset(); + + expect(clearSpy).toHaveBeenCalled(); + }); + + it('respects multiplier when resetting font size control', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.multiplier = 0.5; + manager.baseSize = 80; + + manager.reset(); + + // Font size control should show default * multiplier + expect(manager.sizeControl?.value).toBe(DEFAULT_FONT_SIZE * 0.5); + expect(manager.baseSize).toBe(DEFAULT_FONT_SIZE); + }); + }); + + describe('Complex Scenarios', () => { + it('handles changing multiplier then modifying baseSize', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + // Change multiplier + manager.multiplier = 0.5; + expect(manager.sizeControl?.value).toBe(DEFAULT_FONT_SIZE * 0.5); + + // Change baseSize + manager.baseSize = 60; + expect(manager.sizeControl?.value).toBe(30); // 60 * 0.5 + expect(manager.baseSize).toBe(60); + + // Change multiplier again + manager.multiplier = 1; + expect(manager.sizeControl?.value).toBe(60); // 60 * 1 + expect(manager.baseSize).toBe(60); + }); + + it('maintains correct renderedSize throughout changes', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + // Initial: 48 * 1 = 48 + expect(manager.renderedSize).toBe(48); + + // Change baseSize: 60 * 1 = 60 + manager.baseSize = 60; + expect(manager.renderedSize).toBe(60); + + // Change multiplier: 60 * 0.5 = 30 + manager.multiplier = 0.5; + expect(manager.renderedSize).toBe(30); + + // Change baseSize again: 72 * 0.5 = 36 + manager.baseSize = 72; + expect(manager.renderedSize).toBe(36); + }); + + it('handles multiple control changes in sequence', async () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + // Change multiple controls + manager.baseSize = 72; + manager.weightControl!.value = 700; + manager.heightControl!.value = 1.8; + manager.spacingControl!.value = 0.05; + + // After flushing effects, verify all are synced to storage + await flushEffects(); + expect(mockPersistentStore.value.fontSize).toBe(72); + expect(mockPersistentStore.value.fontWeight).toBe(700); + expect(mockPersistentStore.value.lineHeight).toBe(1.8); + expect(mockPersistentStore.value.letterSpacing).toBe(0.05); + }); + }); + + describe('Edge Cases', () => { + it('handles multiplier of 1 (no change)', () => { + mockStorage = { fontSize: 48, fontWeight: 400, lineHeight: 1.5, letterSpacing: 0 }; + mockPersistentStore = createMockPersistentStore(mockStorage); + + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.multiplier = 1; + + expect(manager.sizeControl?.value).toBe(48); + expect(manager.baseSize).toBe(48); + }); + + it('handles very small multiplier', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.baseSize = 100; + manager.multiplier = 0.1; + + expect(manager.sizeControl?.value).toBe(10); + expect(manager.renderedSize).toBe(10); + }); + + it('handles large base size with multiplier', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.baseSize = 100; + manager.multiplier = 0.75; + + expect(manager.sizeControl?.value).toBe(75); + expect(manager.renderedSize).toBe(75); + }); + + it('handles floating point precision in multiplier', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + manager.baseSize = 48; + manager.multiplier = 0.5; + + // 48 * 0.5 = 24 (exact, no rounding needed) + expect(manager.sizeControl?.value).toBe(24); + expect(manager.renderedSize).toBe(24); + + // 48 * 0.33 = 15.84 -> rounds to 16 (step precision is 1) + manager.multiplier = 0.33; + expect(manager.sizeControl?.value).toBe(16); + expect(manager.renderedSize).toBeCloseTo(15.84); + }); + + it('handles control methods (increase/decrease)', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + const initialWeight = manager.weight; + manager.weightControl!.increase(); + expect(manager.weight).toBe(initialWeight + 100); + + manager.weightControl!.decrease(); + expect(manager.weight).toBe(initialWeight); + }); + + it('handles control boundary conditions', () => { + const manager = new TypographyControlManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + mockPersistentStore, + ); + + const sizeCtrl = manager.sizeControl!; + + // Test min boundary + sizeCtrl.value = 5; + expect(sizeCtrl.value).toBe(sizeCtrl.min); // Should clamp to MIN_FONT_SIZE (8) + + // Test max boundary + sizeCtrl.value = 200; + expect(sizeCtrl.value).toBe(sizeCtrl.max); // Should clamp to MAX_FONT_SIZE (100) + }); + }); +}); diff --git a/src/features/SetupFont/model/const/const.ts b/src/features/SetupFont/model/const/const.ts index 51d5f89..0bebbcd 100644 --- a/src/features/SetupFont/model/const/const.ts +++ b/src/features/SetupFont/model/const/const.ts @@ -43,7 +43,7 @@ export const DEFAULT_TYPOGRAPHY_CONTROLS_DATA: ControlModel[] = [ increaseLabel: 'Increase Font Size', decreaseLabel: 'Decrease Font Size', - controlLabel: 'Font Size', + controlLabel: 'Size', }, { id: 'font_weight', @@ -54,7 +54,7 @@ export const DEFAULT_TYPOGRAPHY_CONTROLS_DATA: ControlModel[] = [ increaseLabel: 'Increase Font Weight', decreaseLabel: 'Decrease Font Weight', - controlLabel: 'Font Weight', + controlLabel: 'Weight', }, { id: 'line_height', @@ -65,7 +65,7 @@ export const DEFAULT_TYPOGRAPHY_CONTROLS_DATA: ControlModel[] = [ increaseLabel: 'Increase Line Height', decreaseLabel: 'Decrease Line Height', - controlLabel: 'Line Height', + controlLabel: 'Leading', }, { id: 'letter_spacing', @@ -76,7 +76,7 @@ export const DEFAULT_TYPOGRAPHY_CONTROLS_DATA: ControlModel[] = [ increaseLabel: 'Increase Letter Spacing', decreaseLabel: 'Decrease Letter Spacing', - controlLabel: 'Letter Spacing', + controlLabel: 'Tracking', }, ]; diff --git a/src/features/SetupFont/ui/TypographyMenu.svelte b/src/features/SetupFont/ui/TypographyMenu.svelte deleted file mode 100644 index 3150ada..0000000 --- a/src/features/SetupFont/ui/TypographyMenu.svelte +++ /dev/null @@ -1,95 +0,0 @@ - - -{#if !hidden} -
-
- -
- - -
- - - {#each controlManager.controls as control, i (control.id)} - {#if i > 0} -
- {/if} - - - {/each} -
-
-{/if} diff --git a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte new file mode 100644 index 0000000..e81bae3 --- /dev/null +++ b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte @@ -0,0 +1,193 @@ + + + +{#if !hidden} + {#if responsive.isMobile} + + + {#snippet child({ props })} + + {/snippet} + + + + + +
+
+ + + CONTROLS + +
+ + {#snippet child({ props })} + + {/snippet} + +
+ + + {#each controlManager.controls as control (control.id)} + + + + {/each} +
+
+
+ {:else} +
+
+ +
+ + +
+ + + {#each controlManager.controls as control, i (control.id)} + {#if i > 0} +
+ {/if} + + + {/each} +
+
+ {/if} +{/if} diff --git a/src/features/SetupFont/ui/index.ts b/src/features/SetupFont/ui/index.ts index ff51592..1723e59 100644 --- a/src/features/SetupFont/ui/index.ts +++ b/src/features/SetupFont/ui/index.ts @@ -1 +1 @@ -export { default as TypographyMenu } from './TypographyMenu.svelte'; +export { default as TypographyMenu } from './TypographyMenu/TypographyMenu.svelte';