From c1ac9b5bc4781a8ad45bfbf343af7a8927d463d4 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 16 Apr 2026 08:22:08 +0300 Subject: [PATCH 01/83] chore(SetupFont): rename controlManager to typographySettingsStore for better semantic --- src/features/SetupFont/lib/index.ts | 6 +- .../settingsManager.svelte.ts} | 6 +- .../settingsManager.test.ts} | 90 +++++++++---------- src/features/SetupFont/model/index.ts | 4 +- .../SetupFont/model/state/manager.svelte.ts | 6 -- .../model/state/typographySettingsStore.ts | 8 ++ .../ui/TypographyMenu/TypographyMenu.svelte | 15 ++-- 7 files changed, 68 insertions(+), 67 deletions(-) rename src/features/SetupFont/lib/{controlManager/controlManager.svelte.ts => settingsManager/settingsManager.svelte.ts} (98%) rename src/features/SetupFont/lib/{controlManager/controlManager.test.ts => settingsManager/settingsManager.test.ts} (89%) delete mode 100644 src/features/SetupFont/model/state/manager.svelte.ts create mode 100644 src/features/SetupFont/model/state/typographySettingsStore.ts diff --git a/src/features/SetupFont/lib/index.ts b/src/features/SetupFont/lib/index.ts index 223fdba..a05ed6e 100644 --- a/src/features/SetupFont/lib/index.ts +++ b/src/features/SetupFont/lib/index.ts @@ -1,4 +1,4 @@ export { - createTypographyControlManager, - type TypographyControlManager, -} from './controlManager/controlManager.svelte'; + createTypographySettingsManager, + type TypographySettingsManager, +} from './settingsManager/settingsManager.svelte'; diff --git a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts b/src/features/SetupFont/lib/settingsManager/settingsManager.svelte.ts similarity index 98% rename from src/features/SetupFont/lib/controlManager/controlManager.svelte.ts rename to src/features/SetupFont/lib/settingsManager/settingsManager.svelte.ts index 39b0315..a3c9414 100644 --- a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts +++ b/src/features/SetupFont/lib/settingsManager/settingsManager.svelte.ts @@ -52,7 +52,7 @@ export interface TypographySettings { * Manages multiple typography controls with persistent storage and * responsive scaling support for font size. */ -export class TypographyControlManager { +export class TypographySettingsManager { /** Map of controls keyed by ID */ #controls = new SvelteMap(); /** Responsive multiplier for font size display */ @@ -242,7 +242,7 @@ export class TypographyControlManager { * @param storageId - Persistent storage identifier * @returns Typography control manager instance */ -export function createTypographyControlManager( +export function createTypographySettingsManager( configs: ControlModel[], storageId: string = 'glyphdiff:typography', ) { @@ -252,5 +252,5 @@ export function createTypographyControlManager( lineHeight: DEFAULT_LINE_HEIGHT, letterSpacing: DEFAULT_LETTER_SPACING, }); - return new TypographyControlManager(configs, storage); + return new TypographySettingsManager(configs, storage); } diff --git a/src/features/SetupFont/lib/controlManager/controlManager.test.ts b/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts similarity index 89% rename from src/features/SetupFont/lib/controlManager/controlManager.test.ts rename to src/features/SetupFont/lib/settingsManager/settingsManager.test.ts index 7e73e49..a50cdb7 100644 --- a/src/features/SetupFont/lib/controlManager/controlManager.test.ts +++ b/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts @@ -15,14 +15,14 @@ import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA, } from '../../model'; import { - TypographyControlManager, type TypographySettings, -} from './controlManager.svelte'; + TypographySettingsManager, +} from './settingsManager.svelte'; /** - * Test Strategy for TypographyControlManager + * Test Strategy for TypographySettingsManager * - * This test suite validates the TypographyControlManager state management logic. + * This test suite validates the TypographySettingsManager 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 @@ -45,7 +45,7 @@ async function flushEffects() { await Promise.resolve(); } -describe('TypographyControlManager - Unit Tests', () => { +describe('TypographySettingsManager - Unit Tests', () => { let mockStorage: TypographySettings; let mockPersistentStore: { value: TypographySettings; @@ -85,7 +85,7 @@ describe('TypographyControlManager - Unit Tests', () => { describe('Initialization', () => { it('creates manager with default values from storage', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -105,7 +105,7 @@ describe('TypographyControlManager - Unit Tests', () => { }; mockPersistentStore = createMockPersistentStore(mockStorage); - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -117,7 +117,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('initializes font size control with base size multiplied by current multiplier (1)', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -126,7 +126,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('returns all controls via controls getter', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -142,7 +142,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('returns individual controls via specific getters', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -160,7 +160,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('control instances have expected interface', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -179,7 +179,7 @@ describe('TypographyControlManager - Unit Tests', () => { describe('Multiplier System', () => { it('has default multiplier of 1', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -188,7 +188,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('updates multiplier when set', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -201,7 +201,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('does not update multiplier if set to same value', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -217,7 +217,7 @@ describe('TypographyControlManager - Unit Tests', () => { mockStorage = { fontSize: 48, fontWeight: 400, lineHeight: 1.5, letterSpacing: 0 }; mockPersistentStore = createMockPersistentStore(mockStorage); - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -241,7 +241,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('updates font size control display value when multiplier increases', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -262,7 +262,7 @@ describe('TypographyControlManager - Unit Tests', () => { describe('Base Size Setter', () => { it('updates baseSize when set directly', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -273,7 +273,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('updates size control value when baseSize is set', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -284,7 +284,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('applies multiplier to size control when baseSize is set', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -298,7 +298,7 @@ describe('TypographyControlManager - Unit Tests', () => { describe('Rendered Size Calculation', () => { it('calculates renderedSize as baseSize * multiplier', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -307,7 +307,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('updates renderedSize when multiplier changes', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -320,7 +320,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('updates renderedSize when baseSize changes', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -340,7 +340,7 @@ describe('TypographyControlManager - Unit Tests', () => { // 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( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -355,7 +355,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('updates baseSize via direct setter (synchronous)', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -380,7 +380,7 @@ describe('TypographyControlManager - Unit Tests', () => { }; mockPersistentStore = createMockPersistentStore(mockStorage); - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -393,7 +393,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('syncs to storage after effect flush (async)', async () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -409,7 +409,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('syncs control changes to storage after effect flush (async)', async () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -422,7 +422,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('syncs height control changes to storage after effect flush (async)', async () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -434,7 +434,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('syncs spacing control changes to storage after effect flush (async)', async () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -448,7 +448,7 @@ describe('TypographyControlManager - Unit Tests', () => { describe('Control Value Getters', () => { it('returns current weight value', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -460,7 +460,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('returns current height value', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -472,7 +472,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('returns current spacing value', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -485,7 +485,7 @@ describe('TypographyControlManager - Unit Tests', () => { it('returns default value when control is not found', () => { // Create a manager with empty configs (no controls) - const manager = new TypographyControlManager([], mockPersistentStore); + const manager = new TypographySettingsManager([], mockPersistentStore); expect(manager.weight).toBe(DEFAULT_FONT_WEIGHT); expect(manager.height).toBe(DEFAULT_LINE_HEIGHT); @@ -503,7 +503,7 @@ describe('TypographyControlManager - Unit Tests', () => { }; mockPersistentStore = createMockPersistentStore(mockStorage); - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -536,7 +536,7 @@ describe('TypographyControlManager - Unit Tests', () => { clear: clearSpy, }; - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -547,7 +547,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('respects multiplier when resetting font size control', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -565,7 +565,7 @@ describe('TypographyControlManager - Unit Tests', () => { describe('Complex Scenarios', () => { it('handles changing multiplier then modifying baseSize', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -586,7 +586,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('maintains correct renderedSize throughout changes', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -608,7 +608,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('handles multiple control changes in sequence', async () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -633,7 +633,7 @@ describe('TypographyControlManager - Unit Tests', () => { mockStorage = { fontSize: 48, fontWeight: 400, lineHeight: 1.5, letterSpacing: 0 }; mockPersistentStore = createMockPersistentStore(mockStorage); - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -645,7 +645,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('handles very small multiplier', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -658,7 +658,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('handles large base size with multiplier', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -671,7 +671,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('handles floating point precision in multiplier', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -690,7 +690,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('handles control methods (increase/decrease)', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); @@ -704,7 +704,7 @@ describe('TypographyControlManager - Unit Tests', () => { }); it('handles control boundary conditions', () => { - const manager = new TypographyControlManager( + const manager = new TypographySettingsManager( DEFAULT_TYPOGRAPHY_CONTROLS_DATA, mockPersistentStore, ); diff --git a/src/features/SetupFont/model/index.ts b/src/features/SetupFont/model/index.ts index 891cf1f..2e7f5e0 100644 --- a/src/features/SetupFont/model/index.ts +++ b/src/features/SetupFont/model/index.ts @@ -20,5 +20,5 @@ export { export { type ControlId, - controlManager, -} from './state/manager.svelte'; + typographySettingsStore, +} from './state/typographySettingsStore'; diff --git a/src/features/SetupFont/model/state/manager.svelte.ts b/src/features/SetupFont/model/state/manager.svelte.ts deleted file mode 100644 index 33bd362..0000000 --- a/src/features/SetupFont/model/state/manager.svelte.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createTypographyControlManager } from '../../lib'; -import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '../const/const'; - -export type ControlId = 'font_size' | 'font_weight' | 'line_height' | 'letter_spacing'; - -export const controlManager = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA); diff --git a/src/features/SetupFont/model/state/typographySettingsStore.ts b/src/features/SetupFont/model/state/typographySettingsStore.ts new file mode 100644 index 0000000..65d6fb1 --- /dev/null +++ b/src/features/SetupFont/model/state/typographySettingsStore.ts @@ -0,0 +1,8 @@ +import { createTypographySettingsManager } from '../../lib'; +import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '../const/const'; + +export type ControlId = 'font_size' | 'font_weight' | 'line_height' | 'letter_spacing'; +export const typographySettingsStore = createTypographySettingsManager( + DEFAULT_TYPOGRAPHY_CONTROLS_DATA, + 'glyphdiff:comparison:typography', +); diff --git a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte index f49a28a..e4e864c 100644 --- a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte +++ b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte @@ -9,7 +9,6 @@ import type { ResponsiveManager } from '$shared/lib'; import { cn } from '$shared/shadcn/utils/shadcn-utils'; import { - Button, ComboControl, ControlGroup, Slider, @@ -24,7 +23,7 @@ import { MULTIPLIER_L, MULTIPLIER_M, MULTIPLIER_S, - controlManager, + typographySettingsStore, } from '../../model'; interface Props { @@ -52,16 +51,16 @@ $effect(() => { if (!responsive) return; switch (true) { case responsive.isMobile: - controlManager.multiplier = MULTIPLIER_S; + typographySettingsStore.multiplier = MULTIPLIER_S; break; case responsive.isTablet: - controlManager.multiplier = MULTIPLIER_M; + typographySettingsStore.multiplier = MULTIPLIER_M; break; case responsive.isDesktop: - controlManager.multiplier = MULTIPLIER_L; + typographySettingsStore.multiplier = MULTIPLIER_L; break; default: - controlManager.multiplier = MULTIPLIER_L; + typographySettingsStore.multiplier = MULTIPLIER_L; } }); @@ -133,7 +132,7 @@ $effect(() => { - {#each controlManager.controls as control (control.id)} + {#each typographySettingsStore.controls as control (control.id)} { - {#each controlManager.controls as control, i (control.id)} + {#each typographySettingsStore.controls as control, i (control.id)} {#if i > 0}
{/if} From fbeb84270b619c16dc3454e75f648da5e7e84a98 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 16 Apr 2026 08:40:16 +0300 Subject: [PATCH 02/83] feat(Layout): remove breadcrumbs --- src/app/ui/Layout.svelte | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/app/ui/Layout.svelte b/src/app/ui/Layout.svelte index c430a3f..85fa981 100644 --- a/src/app/ui/Layout.svelte +++ b/src/app/ui/Layout.svelte @@ -85,19 +85,11 @@ onDestroy(() => themeManager.destroy()); theme === 'dark' ? 'dark' : '', )} > -
- -
- - - {#if fontsReady} {@render children?.()} {/if} - -
From 8645c7dcc87d994f17b509700cc8109c13938bc7 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 16 Apr 2026 08:44:49 +0300 Subject: [PATCH 03/83] feat: use typographySettingsStore everywhere for the typography settings --- .../ui/FontSampler/FontSampler.stories.svelte | 1 - .../ui/FontSampler/FontSampler.svelte | 26 +++---- .../model/stores/comparisonStore.svelte.ts | 27 +++---- .../ui/Character/Character.svelte | 3 +- .../ui/ComparisonView/ComparisonView.svelte | 78 +++++++++---------- .../ui/FontList/FontList.svelte | 3 +- .../ComparisonView/ui/Line/Line.svelte | 4 +- .../ui/SliderArea/SliderArea.svelte | 17 ++-- .../ui/SampleList/SampleList.svelte | 10 +-- 9 files changed, 82 insertions(+), 87 deletions(-) diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.stories.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.stories.svelte index 0238a83..19c1d84 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.stories.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.stories.svelte @@ -35,7 +35,6 @@ const { Story } = defineMeta({ @@ -75,7 +69,7 @@ const stats = $derived([ min-h-60 rounded-none " - style:font-weight={fontWeight} + style:font-weight={typographySettingsStore.weight} >
- +
diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts index 28c90fe..2bf9844 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts @@ -21,10 +21,7 @@ import { fontStore, getFontUrl, } from '$entities/Font'; -import { - DEFAULT_TYPOGRAPHY_CONTROLS_DATA, - createTypographyControlManager, -} from '$features/SetupFont'; +import { typographySettingsStore } from '$features/SetupFont/model'; import { createPersistentStore } from '$shared/lib'; import { untrack } from 'svelte'; @@ -68,8 +65,8 @@ export class ComparisonStore { #side = $state('A'); /** Slider position for character morphing (0-100) */ #sliderPosition = $state(50); - /** Typography controls for this comparison */ - #typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography'); + // /** Typography controls for this comparison */ + // #typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography'); /** TanStack Query-backed batch font fetcher */ #batchStore: BatchFontStore; @@ -99,7 +96,7 @@ export class ComparisonStore { $effect(() => { const fa = this.#fontA; const fb = this.#fontB; - const weight = this.#typography.weight; + const weight = typographySettingsStore.weight; if (!fa || !fb) return; @@ -152,8 +149,8 @@ export class ComparisonStore { return; } - const weight = this.#typography.weight; - const size = this.#typography.renderedSize; + const weight = typographySettingsStore.weight; + const size = typographySettingsStore.renderedSize; const fontAName = this.#fontA?.name; const fontBName = this.#fontB?.name; @@ -201,12 +198,12 @@ export class ComparisonStore { }; } - // ── Getters / Setters ───────────────────────────────────────────────────── + // // ── Getters / Setters ───────────────────────────────────────────────────── - /** Typography control manager */ - get typography() { - return this.#typography; - } + // /** Typography control manager */ + // get typography() { + // return typographySettingsStore; + // } /** Font for side A */ get fontA() { @@ -273,7 +270,7 @@ export class ComparisonStore { this.#fontB = undefined; this.#batchStore.setIds([]); storage.clear(); - this.#typography.reset(); + typographySettingsStore.reset(); } } diff --git a/src/widgets/ComparisonView/ui/Character/Character.svelte b/src/widgets/ComparisonView/ui/Character/Character.svelte index 0269015..466c234 100644 --- a/src/widgets/ComparisonView/ui/Character/Character.svelte +++ b/src/widgets/ComparisonView/ui/Character/Character.svelte @@ -3,6 +3,7 @@ Renders a single character with morphing animation --> diff --git a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte index c6d1f45..4630937 100644 --- a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte +++ b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte @@ -8,6 +8,8 @@ - Performance optimized using offscreen canvas for measurements and transform-based animations. --> - + {#snippet rightIcon(size)} {/snippet} diff --git a/src/widgets/ComparisonView/ui/Search/Search.svelte b/src/widgets/ComparisonView/ui/Search/Search.svelte new file mode 100644 index 0000000..0f0eba4 --- /dev/null +++ b/src/widgets/ComparisonView/ui/Search/Search.svelte @@ -0,0 +1,14 @@ + + +
+ +
From 33e589f04124fc455e920197c4c97832bda275b1 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 16 Apr 2026 15:58:33 +0300 Subject: [PATCH 12/83] feat: remove widgets from page --- src/routes/Page.svelte | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/routes/Page.svelte b/src/routes/Page.svelte index 19c108d..0346bad 100644 --- a/src/routes/Page.svelte +++ b/src/routes/Page.svelte @@ -3,10 +3,7 @@ Description: The main page component of the application. --> @@ -18,8 +15,4 @@ import { fade } from 'svelte/transition';
-
- - -
From aa1379c15b92d10fe4fef656fa18b8753fee3034 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 16 Apr 2026 15:59:34 +0300 Subject: [PATCH 13/83] chore: remove unused code --- .../settingsManager/settingsManager.test.ts | 1 - .../ui/ComparisonView/ComparisonView.svelte | 54 ++----------------- .../ui/FontList/FontList.svelte | 6 +-- 3 files changed, 6 insertions(+), 55 deletions(-) diff --git a/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts b/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts index 3f66536..ecdfd89 100644 --- a/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts +++ b/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts @@ -7,7 +7,6 @@ import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA, } from '$entities/Font'; import { - afterEach, beforeEach, describe, expect, diff --git a/src/widgets/ComparisonView/ui/ComparisonView/ComparisonView.svelte b/src/widgets/ComparisonView/ui/ComparisonView/ComparisonView.svelte index 1727439..feb9f65 100644 --- a/src/widgets/ComparisonView/ui/ComparisonView/ComparisonView.svelte +++ b/src/widgets/ComparisonView/ui/ComparisonView/ComparisonView.svelte @@ -6,22 +6,18 @@
-
-
+
+
@@ -80,7 +80,7 @@ $effect(() => { data-font-list weight={DEFAULT_FONT_WEIGHT} itemHeight={45} - class="bg-transparent min-h-0 h-full scroll-stable pr-4" + class="bg-transparent min-h-0 h-full scroll-stable py-2 pl-6 pr-4" > {#snippet children({ item: font, index })} {@const isSelectedA = font.id === comparisonStore.fontA?.id} From 816d4b89ce0097aed13d204de90fc6076d4c2294 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 16 Apr 2026 16:32:41 +0300 Subject: [PATCH 14/83] =?UTF-8?q?refactor:=20tailwind=20tier=201=20?= =?UTF-8?q?=E2=80=94=20border-subtle/text-secondary/focus-ring=20utilities?= =?UTF-8?q?=20+=20Input=20config=20extraction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/styles/app.css | 15 ++++++++ .../BreadcrumbHeader/BreadcrumbHeader.svelte | 2 +- .../ui/FontSampler/FontSampler.svelte | 6 ++-- .../ui/TypographyMenu/TypographyMenu.svelte | 10 +++--- src/shared/ui/Button/Button.svelte | 8 ++--- src/shared/ui/Button/ButtonGroup.svelte | 2 +- .../ui/ComboControl/ComboControl.svelte | 8 ++--- .../ui/ControlGroup/ControlGroup.svelte | 2 +- src/shared/ui/Input/Input.svelte | 35 ++++--------------- src/shared/ui/Input/config.ts | 31 ++++++++++++++++ .../SidebarContainer/SidebarContainer.svelte | 2 +- src/shared/ui/Slider/Slider.svelte | 2 +- .../ui/FontList/FontList.svelte | 2 +- .../ComparisonView/ui/Header/Header.svelte | 2 +- .../ComparisonView/ui/Sidebar/Sidebar.svelte | 6 ++-- 15 files changed, 78 insertions(+), 55 deletions(-) create mode 100644 src/shared/ui/Input/config.ts diff --git a/src/app/styles/app.css b/src/app/styles/app.css index 60e21a4..adb7fe0 100644 --- a/src/app/styles/app.css +++ b/src/app/styles/app.css @@ -265,6 +265,21 @@ } } +@layer utilities { + /* 21× border-black/5 dark:border-white/10 → single token */ + .border-subtle { + @apply border-black/5 dark:border-white/10; + } + /* Secondary text pair */ + .text-secondary { + @apply text-neutral-500 dark:text-neutral-400; + } + /* Standard focus ring */ + .focus-ring { + @apply focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2; + } +} + /* Global utility - useful across your app */ @media (prefers-reduced-motion: reduce) { * { diff --git a/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.svelte b/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.svelte index 6eed8c5..f419995 100644 --- a/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.svelte +++ b/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.svelte @@ -44,7 +44,7 @@ function createButtonText(item: BreadcrumbItem) { flex items-center justify-between z-40 bg-surface/90 dark:bg-dark-bg/90 backdrop-blur-md - border-b border-black/5 dark:border-white/10 + border-b border-subtle " >
diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte index 3b51dd0..15e8e79 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte @@ -59,7 +59,7 @@ const stats = $derived([ group relative w-full h-full bg-paper dark:bg-dark-card - border border-black/5 dark:border-white/10 + border border-subtle hover:border-brand dark:hover:border-brand hover:shadow-brand/10 hover:shadow-[5px_5px_0px_0px] @@ -76,7 +76,7 @@ const stats = $derived([ class=" flex items-center justify-between px-4 sm:px-5 md:px-6 py-3 sm:py-4 - border-b border-black/5 dark:border-white/10 + border-b border-subtle bg-paper dark:bg-dark-card " > @@ -145,7 +145,7 @@ const stats = $derived([
-
+
{#each stats as stat, i} {stat.label}:{stat.value} diff --git a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte index 856d7d8..45540f2 100644 --- a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte +++ b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte @@ -79,7 +79,7 @@ $effect(() => { 'transition-colors duration-150', 'hover:bg-white/50 dark:hover:bg-white/5', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30', - isOpen && 'bg-paper dark:bg-dark-card border-black/5 dark:border-white/10 shadow-sm', + isOpen && 'bg-paper dark:bg-dark-card border-subtle shadow-sm', className, )} > @@ -96,7 +96,7 @@ $effect(() => { class={cn( 'z-50 w-72', 'bg-surface dark:bg-dark-card', - 'border border-black/5 dark:border-white/10', + 'border border-subtle', 'shadow-[0_20px_40px_-10px_rgba(0,0,0,0.15)]', 'rounded-none p-4', 'data-[state=open]:animate-in data-[state=closed]:animate-out', @@ -109,7 +109,7 @@ $effect(() => { escapeKeydownBehavior="close" > -
+
{ class={cn( 'flex items-center gap-1 md:gap-2 p-1.5 md:p-2', 'bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl', - 'border border-black/5 dark:border-white/10', + 'border border-subtle', 'shadow-[0_20px_40px_-10px_rgba(0,0,0,0.1)]', 'rounded-none ring-1 ring-black/5 dark:ring-white/5', )} > -
+
= { ), ghost: cn( 'bg-transparent', - 'text-neutral-500 dark:text-neutral-400', + 'text-secondary', 'border border-transparent', 'hover:bg-transparent dark:hover:bg-transparent', 'hover:text-brand dark:hover:text-brand', @@ -121,7 +121,7 @@ const variantStyles: Record = { ), icon: cn( 'bg-surface dark:bg-dark-bg', - 'text-neutral-500 dark:text-neutral-400', + 'text-secondary', 'border border-transparent', 'hover:bg-paper dark:hover:bg-paper', 'hover:text-brand', @@ -172,7 +172,7 @@ const activeStyles: Partial> = { 'bg-paper dark:bg-dark-card border-black/10 dark:border-white/10 shadow-sm text-neutral-900 dark:text-neutral-100', ghost: 'bg-transparent dark:bg-transparent text-brand dark:text-brand', outline: 'bg-surface dark:bg-paper border-brand', - icon: 'bg-paper dark:bg-paper text-brand border-black/5 dark:border-white/10', + icon: 'bg-paper dark:bg-paper text-brand border-subtle', }; const classes = $derived(cn( @@ -184,7 +184,7 @@ const classes = $derived(cn( 'select-none', 'outline-none', 'cursor-pointer', - 'focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2', + 'focus-ring', 'focus-visible:ring-offset-surface dark:focus-visible:ring-offset-dark-bg', 'disabled:cursor-not-allowed disabled:pointer-events-none', // Variant diff --git a/src/shared/ui/Button/ButtonGroup.svelte b/src/shared/ui/Button/ButtonGroup.svelte index fd06816..3e1881f 100644 --- a/src/shared/ui/Button/ButtonGroup.svelte +++ b/src/shared/ui/Button/ButtonGroup.svelte @@ -26,7 +26,7 @@ let { children, class: className, ...rest }: Props = $props(); class={cn( 'flex items-center gap-1 p-1', 'bg-surface dark:bg-dark-bg', - 'border border-black/5 dark:border-white/10', + 'border border-subtle', 'rounded-none', 'transition-colors duration-500', className, diff --git a/src/shared/ui/ComboControl/ComboControl.svelte b/src/shared/ui/ComboControl/ComboControl.svelte index 5750544..32d2276 100644 --- a/src/shared/ui/ComboControl/ComboControl.svelte +++ b/src/shared/ui/ComboControl/ComboControl.svelte @@ -93,9 +93,7 @@ const displayLabel = $derived(label ?? controlLabel ?? ''); step={control.step} orientation="horizontal" /> - + {formattedValue()}
@@ -129,7 +127,7 @@ const displayLabel = $derived(label ?? controlLabel ?? ''); 'border border-transparent', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30', open - ? 'bg-paper dark:bg-dark-card shadow-sm border-black/5 dark:border-white/10' + ? 'bg-paper dark:bg-dark-card shadow-sm border-subtle' : 'hover:bg-paper/50 dark:hover:bg-dark-card/50', )} aria-label={controlLabel} @@ -157,7 +155,7 @@ const displayLabel = $derived(label ?? controlLabel ?? ''); diff --git a/src/shared/ui/ControlGroup/ControlGroup.svelte b/src/shared/ui/ControlGroup/ControlGroup.svelte index bfcde08..abe45b7 100644 --- a/src/shared/ui/ControlGroup/ControlGroup.svelte +++ b/src/shared/ui/ControlGroup/ControlGroup.svelte @@ -24,7 +24,7 @@ interface Props { const { label, children, class: className }: Props = $props(); -
+
{label}
diff --git a/src/shared/ui/Input/Input.svelte b/src/shared/ui/Input/Input.svelte index dd6db7d..c65b185 100644 --- a/src/shared/ui/Input/Input.svelte +++ b/src/shared/ui/Input/Input.svelte @@ -9,6 +9,10 @@ import type { Snippet } from 'svelte'; import { cubicOut } from 'svelte/easing'; import type { HTMLInputAttributes } from 'svelte/elements'; import { scale } from 'svelte/transition'; +import { + inputSizeConfig, + inputVariantConfig, +} from './config'; import type { InputSize, InputVariant, @@ -80,36 +84,11 @@ let { ...rest }: Props = $props(); -const sizeConfig: Record = { - sm: { input: 'px-3 py-1.5', text: 'text-sm', height: 'h-8', clearIcon: 12 }, - md: { input: 'px-4 py-2', text: 'text-base', height: 'h-10', clearIcon: 14 }, - lg: { input: 'px-4 py-3', text: 'text-lg md:text-xl', height: 'h-12', clearIcon: 16 }, - xl: { input: 'px-4 py-3', text: 'text-xl md:text-2xl', height: 'h-14', clearIcon: 18 }, -}; - -const variantConfig: Record = { - default: { - base: 'bg-paper dark:bg-paper border border-black/5 dark:border-white/10', - focus: 'focus:border-brand focus:ring-1 focus:ring-brand/20', - error: 'border-brand ring-1 ring-brand/20', - }, - underline: { - base: 'bg-transparent border-0 border-b border-neutral-300 dark:border-neutral-700', - focus: 'focus:border-brand', - error: 'border-brand', - }, - filled: { - base: 'bg-surface dark:bg-paper border border-transparent', - focus: 'focus:border-brand focus:ring-1 focus:ring-brand/20', - error: 'border-brand ring-1 ring-brand/20', - }, -}; - const hasValue = $derived(value !== undefined && value !== ''); const showClear = $derived(showClearButton && hasValue && !!onclear); const hasRightSlot = $derived(!!rightIcon || showClearButton); -const cfg = $derived(sizeConfig[size]); -const styles = $derived(variantConfig[variant]); +const cfg = $derived(inputSizeConfig[size]); +const styles = $derived(inputVariantConfig[variant]); const inputClasses = $derived(cn( 'font-primary rounded-none outline-none transition-all duration-200', @@ -170,7 +149,7 @@ const inputClasses = $derived(cn( {helperText} diff --git a/src/shared/ui/Input/config.ts b/src/shared/ui/Input/config.ts new file mode 100644 index 0000000..30964fe --- /dev/null +++ b/src/shared/ui/Input/config.ts @@ -0,0 +1,31 @@ +import type { + InputSize, + InputVariant, +} from './types'; + +/** Size-specific layout classes: padding, text size, height, and clear-icon pixel size. */ +export const inputSizeConfig: Record = { + sm: { input: 'px-3 py-1.5', text: 'text-sm', height: 'h-8', clearIcon: 12 }, + md: { input: 'px-4 py-2', text: 'text-base', height: 'h-10', clearIcon: 14 }, + lg: { input: 'px-4 py-3', text: 'text-lg md:text-xl', height: 'h-12', clearIcon: 16 }, + xl: { input: 'px-4 py-3', text: 'text-xl md:text-2xl', height: 'h-14', clearIcon: 18 }, +}; + +/** Variant-specific classes: base background/border, focus ring, and error state. */ +export const inputVariantConfig: Record = { + default: { + base: 'bg-paper dark:bg-paper border border-subtle', + focus: 'focus:border-brand focus:ring-1 focus:ring-brand/20', + error: 'border-brand ring-1 ring-brand/20', + }, + underline: { + base: 'bg-transparent border-0 border-b border-neutral-300 dark:border-neutral-700', + focus: 'focus:border-brand', + error: 'border-brand', + }, + filled: { + base: 'bg-surface dark:bg-paper border border-transparent', + focus: 'focus:border-brand focus:ring-1 focus:ring-brand/20', + error: 'border-brand ring-1 ring-brand/20', + }, +}; diff --git a/src/shared/ui/SidebarContainer/SidebarContainer.svelte b/src/shared/ui/SidebarContainer/SidebarContainer.svelte index e83c8d2..15932e2 100644 --- a/src/shared/ui/SidebarContainer/SidebarContainer.svelte +++ b/src/shared/ui/SidebarContainer/SidebarContainer.svelte @@ -84,7 +84,7 @@ function close() { 'overflow-hidden', 'will-change-[width]', 'transition-[width] duration-300 ease-out', - 'border-r border-black/5 dark:border-white/10', + 'border-r border-subtle', 'bg-surface dark:bg-dark-bg', isOpen ? 'w-80 opacity-100' : 'w-0 opacity-0', 'transition-[width,opacity] duration-300 ease-out', diff --git a/src/shared/ui/Slider/Slider.svelte b/src/shared/ui/Slider/Slider.svelte index 1b481bc..ce43c28 100644 --- a/src/shared/ui/Slider/Slider.svelte +++ b/src/shared/ui/Slider/Slider.svelte @@ -70,7 +70,7 @@ let { const isVertical = $derived(orientation === 'vertical'); const labelClasses = `font-mono text-[0.625rem] tabular-nums shrink-0 - text-neutral-500 dark:text-neutral-400 + text-secondary group-hover:text-neutral-700 dark:group-hover:text-neutral-300 transition-colors`; diff --git a/src/widgets/ComparisonView/ui/FontList/FontList.svelte b/src/widgets/ComparisonView/ui/FontList/FontList.svelte index cdbc293..7f67562 100644 --- a/src/widgets/ComparisonView/ui/FontList/FontList.svelte +++ b/src/widgets/ComparisonView/ui/FontList/FontList.svelte @@ -71,7 +71,7 @@ $effect(() => {
-
+
diff --git a/src/widgets/ComparisonView/ui/Header/Header.svelte b/src/widgets/ComparisonView/ui/Header/Header.svelte index 70f2fcc..3b2d98e 100644 --- a/src/widgets/ComparisonView/ui/Header/Header.svelte +++ b/src/widgets/ComparisonView/ui/Header/Header.svelte @@ -53,7 +53,7 @@ const fontBName = $derived(comparisonStore.fontB?.name ?? ''); 'flex items-center justify-between', 'px-4 md:px-8 py-4 md:py-6', 'h-16 md:h-20 z-20', - 'border-b border-black/5 dark:border-white/10', + 'border-b border-subtle', 'bg-surface dark:bg-dark-bg', className, )} diff --git a/src/widgets/ComparisonView/ui/Sidebar/Sidebar.svelte b/src/widgets/ComparisonView/ui/Sidebar/Sidebar.svelte index 85b52f9..22da262 100644 --- a/src/widgets/ComparisonView/ui/Sidebar/Sidebar.svelte +++ b/src/widgets/ComparisonView/ui/Sidebar/Sidebar.svelte @@ -44,7 +44,7 @@ let { 'flex flex-col h-full', 'w-80', 'bg-surface dark:bg-dark-bg', - 'border-r border-black/5 dark:border-white/10', + 'border-r border-subtle', 'transition-colors duration-500', className, )} @@ -53,7 +53,7 @@ let {
@@ -100,7 +100,7 @@ let { class=" shrink-0 p-6 bg-surface dark:bg-dark-bg - border-t border-black/5 dark:border-white/10 + border-t border-subtle z-10 " > From ef08512986837229a43a130e51888d2738823409 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 08:53:16 +0300 Subject: [PATCH 15/83] feat(Badge): add nowrap prop to purge custom classes --- src/features/DisplayFont/ui/FontSampler/FontSampler.svelte | 4 ++-- src/shared/ui/Badge/Badge.svelte | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte index 15e8e79..5413bfd 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte @@ -94,14 +94,14 @@ const stats = $derived([ {#if fontType} - + {fontType} {/if} {#if providerBadge} - + {providerBadge} {/if} diff --git a/src/shared/ui/Badge/Badge.svelte b/src/shared/ui/Badge/Badge.svelte index aa42df9..a0bc653 100644 --- a/src/shared/ui/Badge/Badge.svelte +++ b/src/shared/ui/Badge/Badge.svelte @@ -37,6 +37,11 @@ interface Props extends HTMLAttributes { * @default false */ dot?: boolean; + /** + * Prevent text wrapping + * @default false + */ + nowrap?: boolean; /** * Content snippet */ @@ -51,6 +56,7 @@ let { variant = 'default', size = 'xs', dot = false, + nowrap = false, children, class: className, ...rest @@ -63,6 +69,7 @@ let { 'font-mono uppercase tracking-wide', labelSizeConfig[size], badgeVariantConfig[variant], + nowrap && 'text-nowrap', className, )} {...rest} From 0562b94b0391d44ab8fb4fbaee41fc89f11b46db Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 08:55:10 +0300 Subject: [PATCH 16/83] feat(Label): add font prop to purge custom classes --- src/shared/ui/Label/Label.svelte | 8 ++++++++ src/shared/ui/Label/config.ts | 2 ++ src/widgets/ComparisonView/ui/FontList/FontList.svelte | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/shared/ui/Label/Label.svelte b/src/shared/ui/Label/Label.svelte index 908af2d..beda71a 100644 --- a/src/shared/ui/Label/Label.svelte +++ b/src/shared/ui/Label/Label.svelte @@ -6,6 +6,7 @@ import { cn } from '$shared/shadcn/utils/shadcn-utils'; import type { Snippet } from 'svelte'; import { + type LabelFont, type LabelSize, type LabelVariant, labelSizeConfig, @@ -28,6 +29,11 @@ interface Props { * @default true */ uppercase?: boolean; + /** + * Font family + * @default 'mono' + */ + font?: LabelFont; /** * Bold text * @default false @@ -55,6 +61,7 @@ interface Props { let { variant = 'default', size = 'sm', + font = 'mono', uppercase = true, bold = false, icon, @@ -68,6 +75,7 @@ let { class={cn( 'font-mono tracking-widest leading-none', 'inline-flex items-center gap-1.5', + font === 'primary' && 'font-primary tracking-tight', labelSizeConfig[size], labelVariantConfig[variant], uppercase && 'uppercase', diff --git a/src/shared/ui/Label/config.ts b/src/shared/ui/Label/config.ts index d76f73b..1ee02c7 100644 --- a/src/shared/ui/Label/config.ts +++ b/src/shared/ui/Label/config.ts @@ -3,6 +3,8 @@ * Import from here in each component to keep maps DRY. */ +export type LabelFont = 'mono' | 'primary'; + export type LabelVariant = | 'default' | 'accent' diff --git a/src/widgets/ComparisonView/ui/FontList/FontList.svelte b/src/widgets/ComparisonView/ui/FontList/FontList.svelte index 7f67562..5471d8d 100644 --- a/src/widgets/ComparisonView/ui/FontList/FontList.svelte +++ b/src/widgets/ComparisonView/ui/FontList/FontList.svelte @@ -72,7 +72,7 @@ $effect(() => {
-
From 5b1a1d0b0ac9e30f15c32740a7551a407cd8d703 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 08:56:46 +0300 Subject: [PATCH 17/83] fix: use Button's size prop instead of direct font-size class --- .../ui/FiltersControl/FilterControls.svelte | 14 ++++---------- .../ComparisonView/ui/Sidebar/Sidebar.svelte | 8 +++++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/features/GetFonts/ui/FiltersControl/FilterControls.svelte b/src/features/GetFonts/ui/FiltersControl/FilterControls.svelte index 242a8bc..8ccda10 100644 --- a/src/features/GetFonts/ui/FiltersControl/FilterControls.svelte +++ b/src/features/GetFonts/ui/FiltersControl/FilterControls.svelte @@ -61,13 +61,10 @@ function handleReset() { {#each SORT_OPTIONS as option} @@ -78,12 +75,9 @@ function handleReset() {
From 7f0d2b54e08b61db571748440334f27c98157ecf Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 09:40:42 +0300 Subject: [PATCH 18/83] feat: add micro type scale and tracking-wider-mono tokens to @theme --- src/app/styles/app.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/styles/app.css b/src/app/styles/app.css index adb7fe0..dc3f313 100644 --- a/src/app/styles/app.css +++ b/src/app/styles/app.css @@ -91,7 +91,6 @@ --space-4xl: 4rem; /* Typography Scale */ - --text-2xs: 0.625rem; --text-xs: 0.75rem; --text-sm: 0.875rem; --text-base: 1rem; @@ -205,6 +204,14 @@ --font-mono: 'Space Mono', monospace; --font-primary: 'Space Grotesk', system-ui, -apple-system, 'Segoe UI', Inter, Roboto, Arial, sans-serif; --font-secondary: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; + + /* Micro typography scale — extends Tailwind's text-xs (0.75rem) downward */ + --font-size-5xs: 0.4375rem; + --font-size-4xs: 0.5rem; + --font-size-3xs: 0.5625rem; + --font-size-2xs: 0.625rem; + /* Monospace label tracking — used in Loader and Footnote */ + --tracking-wider-mono: 0.2em; } @layer base { From 64b4a65e7bf77ad08734dd95ce2913b22678be72 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 09:40:53 +0300 Subject: [PATCH 19/83] refactor: replace arbitrary sizes in labelSizeConfig with named tokens --- src/shared/ui/Label/config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/ui/Label/config.ts b/src/shared/ui/Label/config.ts index 1ee02c7..deb2a13 100644 --- a/src/shared/ui/Label/config.ts +++ b/src/shared/ui/Label/config.ts @@ -16,10 +16,10 @@ export type LabelVariant = export type LabelSize = 'xs' | 'sm' | 'md' | 'lg'; export const labelSizeConfig: Record = { - xs: 'text-[0.5rem]', - sm: 'text-[0.5625rem] md:text-[0.625rem]', - md: 'text-[0.625rem] md:text-[0.6875rem]', - lg: 'text-[0.8rem] md:text-[0.875rem]', + xs: 'text-4xs', + sm: 'text-3xs md:text-2xs', + md: 'text-2xs md:text-xs', + lg: 'text-sm', }; export const labelVariantConfig: Record = { From 0737db69a958c506df0b7bca2c0cc58729f831d7 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 09:41:14 +0300 Subject: [PATCH 20/83] refactor: replace px text sizes in Button, Loader, Footnote with named tokens --- src/shared/ui/Button/Button.svelte | 10 +++++----- src/shared/ui/Footnote/Footnote.svelte | 4 ++-- src/shared/ui/Loader/Loader.svelte | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/shared/ui/Button/Button.svelte b/src/shared/ui/Button/Button.svelte index c866689..cc5ee6f 100644 --- a/src/shared/ui/Button/Button.svelte +++ b/src/shared/ui/Button/Button.svelte @@ -150,11 +150,11 @@ const variantStyles: Record = { }; const sizeStyles: Record = { - xs: 'h-6 px-2 text-[9px] gap-1', - sm: 'h-8 px-3 text-[10px] gap-1.5', - md: 'h-10 px-4 text-[11px] gap-2', - lg: 'h-12 px-6 text-[12px] gap-2', - xl: 'h-14 px-8 text-[13px] gap-2.5', + xs: 'h-6 px-2 text-3xs gap-1', + sm: 'h-8 px-3 text-2xs gap-1.5', + md: 'h-10 px-4 text-xs gap-2', + lg: 'h-12 px-6 text-xs gap-2', + xl: 'h-14 px-8 text-sm gap-2.5', }; // Square padding for icon-only mode diff --git a/src/shared/ui/Footnote/Footnote.svelte b/src/shared/ui/Footnote/Footnote.svelte index 2b72ce9..aa48360 100644 --- a/src/shared/ui/Footnote/Footnote.svelte +++ b/src/shared/ui/Footnote/Footnote.svelte @@ -27,14 +27,14 @@ const { children, class: className, render }: Props = $props(); {#if render} {@render render({ class: cn( - 'font-mono text-[0.5625rem] sm:text-[0.625rem] lowercase tracking-[0.2em] text-text-soft', + 'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft', className, ), })} {:else if children} diff --git a/src/shared/ui/Loader/Loader.svelte b/src/shared/ui/Loader/Loader.svelte index fe304e3..86b8840 100644 --- a/src/shared/ui/Loader/Loader.svelte +++ b/src/shared/ui/Loader/Loader.svelte @@ -71,7 +71,7 @@ let { size = 20, class: className = '', message = 'analyzing_data' }: Props = $p
- + {message}
From 7b46e06f8b1e221220fea374beaa693da7a8541f Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 09:41:55 +0300 Subject: [PATCH 21/83] refactor: replace arbitrary text sizes in ComboControl, ControlGroup, Input, Slider, SectionHeader --- src/shared/ui/ComboControl/ComboControl.svelte | 4 ++-- src/shared/ui/ControlGroup/ControlGroup.svelte | 2 +- src/shared/ui/Input/Input.svelte | 2 +- src/shared/ui/Section/SectionHeader/SectionHeader.svelte | 2 +- src/shared/ui/Slider/Slider.svelte | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/shared/ui/ComboControl/ComboControl.svelte b/src/shared/ui/ComboControl/ComboControl.svelte index 32d2276..9782d36 100644 --- a/src/shared/ui/ComboControl/ComboControl.svelte +++ b/src/shared/ui/ComboControl/ComboControl.svelte @@ -93,7 +93,7 @@ const displayLabel = $derived(label ?? controlLabel ?? ''); step={control.step} orientation="horizontal" /> - + {formattedValue()}
@@ -136,7 +136,7 @@ const displayLabel = $derived(label ?? controlLabel ?? ''); {#if displayLabel}
-
+
{label}
{@render children?.()} diff --git a/src/shared/ui/Input/Input.svelte b/src/shared/ui/Input/Input.svelte index c65b185..b8a33ad 100644 --- a/src/shared/ui/Input/Input.svelte +++ b/src/shared/ui/Input/Input.svelte @@ -148,7 +148,7 @@ const inputClasses = $derived(cn( {#if helperText} diff --git a/src/shared/ui/Section/SectionHeader/SectionHeader.svelte b/src/shared/ui/Section/SectionHeader/SectionHeader.svelte index 9e76d42..89e6be5 100644 --- a/src/shared/ui/Section/SectionHeader/SectionHeader.svelte +++ b/src/shared/ui/Section/SectionHeader/SectionHeader.svelte @@ -53,7 +53,7 @@ const indexStr = $derived(String(index).padStart(2, '0'));
{#if subtitle} - + {/if}
diff --git a/src/shared/ui/Slider/Slider.svelte b/src/shared/ui/Slider/Slider.svelte index ce43c28..2b2de67 100644 --- a/src/shared/ui/Slider/Slider.svelte +++ b/src/shared/ui/Slider/Slider.svelte @@ -69,7 +69,7 @@ let { const isVertical = $derived(orientation === 'vertical'); -const labelClasses = `font-mono text-[0.625rem] tabular-nums shrink-0 +const labelClasses = `font-mono text-2xs tabular-nums shrink-0 text-secondary group-hover:text-neutral-700 dark:group-hover:text-neutral-300 transition-colors`; From 0ebf75b24ea317143ec33d6266b4846882069415 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 09:42:24 +0300 Subject: [PATCH 22/83] refactor: replace arbitrary text sizes in FontSampler, TypographyMenu; fix font token in SectionTitle --- src/features/DisplayFont/ui/FontSampler/FontSampler.svelte | 4 ++-- .../SetupFont/ui/TypographyMenu/TypographyMenu.svelte | 4 ++-- src/shared/ui/Section/SectionTitle/SectionTitle.svelte | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte index 5413bfd..79b4e9c 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.svelte @@ -82,7 +82,7 @@ const stats = $derived([ >
- + {String(index + 1).padStart(2, '0')} @@ -147,7 +147,7 @@ const stats = $derived([
{#each stats as stat, i} - + {stat.label}:{stat.value} {#if i < stats.length - 1} diff --git a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte index 45540f2..654845c 100644 --- a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte +++ b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte @@ -113,7 +113,7 @@ $effect(() => {
CONTROLS @@ -166,7 +166,7 @@ $effect(() => { class="text-swiss-red" /> diff --git a/src/shared/ui/Section/SectionTitle/SectionTitle.svelte b/src/shared/ui/Section/SectionTitle/SectionTitle.svelte index 53325c0..aa266d1 100644 --- a/src/shared/ui/Section/SectionTitle/SectionTitle.svelte +++ b/src/shared/ui/Section/SectionTitle/SectionTitle.svelte @@ -13,7 +13,7 @@ interface Props { const { text }: Props = $props(); {#if text} -

+

{text}

{/if} From cfaff46d59677c9143f4ee1e122fd42a5281448e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 12:14:55 +0300 Subject: [PATCH 23/83] chore: follow the general comments style --- .../store/scrollBreadcrumbsStore.svelte.ts | 56 ++- .../store/scrollBreadcrumbsStore.test.ts | 4 +- src/entities/Font/api/proxy/proxyFonts.ts | 16 +- .../Font/lib/getFontUrl/getFontUrl.ts | 4 +- src/entities/Font/lib/mocks/filters.mock.ts | 82 ++-- src/entities/Font/lib/mocks/fonts.mock.ts | 36 +- src/entities/Font/lib/mocks/stores.mock.ts | 350 ++++++++++++++++-- .../sizeResolver/createFontRowSizeResolver.ts | 28 +- .../appliedFontStore.test.ts | 26 +- .../appliedFontsStore.svelte.ts | 34 +- .../fontBufferCache/FontBufferCache.test.ts | 4 +- .../utils/fontBufferCache/FontBufferCache.ts | 16 +- .../fontEvictionPolicy/FontEvictionPolicy.ts | 24 +- .../utils/fontLoadQueue/FontLoadQueue.ts | 16 +- .../utils/loadFont/loadFont.test.ts | 4 +- .../store/fontStore/fontStore.svelte.spec.ts | 19 - .../model/store/fontStore/fontStore.svelte.ts | 117 ++++-- src/entities/Font/model/types/font.ts | 129 +++++-- src/entities/Font/model/types/index.ts | 9 - src/entities/Font/model/types/store.ts | 48 ++- .../store/ThemeManager/ThemeManager.svelte.ts | 36 +- .../store/ThemeManager/ThemeManager.test.ts | 8 +- src/features/GetFonts/api/filters/filters.ts | 40 +- src/features/GetFonts/model/index.ts | 47 ++- .../GetFonts/model/state/filters.svelte.ts | 12 +- .../GetFonts/model/store/sortStore.svelte.ts | 12 +- src/features/GetFonts/model/types/filter.ts | 15 + .../settingsManager/settingsManager.svelte.ts | 73 +++- .../settingsManager/settingsManager.test.ts | 4 +- src/main.ts | 6 + src/shared/api/api.ts | 8 +- src/shared/api/queryClient.ts | 20 +- src/shared/api/queryKeys.ts | 28 +- .../CharacterComparisonEngine.svelte.ts | 39 +- .../CharacterComparisonEngine.test.ts | 4 - .../TextLayoutEngine.svelte.ts | 35 +- .../createDebouncedState.svelte.ts | 8 +- .../createEntityStore.svelte.ts | 8 +- .../createFilter/createFilter.svelte.ts | 20 +- .../createPersistentStore.test.ts | 4 +- .../createPerspectiveManager.svelte.ts | 28 +- .../createResponsiveManager.svelte.ts | 80 +++- .../createTypographyControl.svelte.ts | 55 ++- .../createVirtualizer.svelte.ts | 39 +- .../createVirtualizer.test.ts | 4 +- src/shared/lib/helpers/index.ts | 123 +++++- .../utils/smoothScroll/smoothScroll.test.ts | 4 +- src/shared/types/common.ts | 8 +- src/shared/ui/Input/config.ts | 8 +- src/shared/ui/Input/types.ts | 4 +- src/shared/ui/index.ts | 166 +++++++-- .../model/stores/comparisonStore.svelte.ts | 73 ++-- .../model/stores/comparisonStore.test.ts | 26 +- .../ComparisonView/ui/Search/Search.svelte | 5 + .../stores/layoutStore/layoutStore.svelte.ts | 23 +- .../stores/layoutStore/layoutStore.test.ts | 4 +- 56 files changed, 1600 insertions(+), 499 deletions(-) diff --git a/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts b/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts index d67e6d0..3817fb8 100644 --- a/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts +++ b/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.svelte.ts @@ -34,11 +34,17 @@ * A breadcrumb item representing a tracked section */ export interface BreadcrumbItem { - /** Unique index for ordering */ + /** + * Unique index for ordering + */ index: number; - /** Display title for the breadcrumb */ + /** + * Display title for the breadcrumb + */ title: string; - /** DOM element to track */ + /** + * DOM element to track + */ element: HTMLElement; } @@ -50,21 +56,37 @@ export interface BreadcrumbItem { * past while moving down the page. */ class ScrollBreadcrumbsStore { - /** All tracked breadcrumb items */ + /** + * All tracked breadcrumb items + */ #items = $state([]); - /** Set of indices that have scrolled past (exited viewport while scrolling down) */ + /** + * Set of indices that have scrolled past (exited viewport while scrolling down) + */ #scrolledPast = $state>(new Set()); - /** Intersection Observer instance */ + /** + * Intersection Observer instance + */ #observer: IntersectionObserver | null = null; - /** Offset for smooth scrolling (sticky header height) */ + /** + * Offset for smooth scrolling (sticky header height) + */ #scrollOffset = 0; - /** Current scroll direction */ + /** + * Current scroll direction + */ #isScrollingDown = $state(false); - /** Previous scroll Y position to determine direction */ + /** + * Previous scroll Y position to determine direction + */ #prevScrollY = 0; - /** Throttled scroll handler */ + /** + * Throttled scroll handler + */ #handleScroll: (() => void) | null = null; - /** Listener count for cleanup */ + /** + * Listener count for cleanup + */ #listenerCount = 0; /** @@ -141,17 +163,23 @@ class ScrollBreadcrumbsStore { this.#detachScrollListener(); } - /** All tracked items sorted by index */ + /** + * All tracked items sorted by index + */ get items(): BreadcrumbItem[] { return this.#items.slice().sort((a, b) => a.index - b.index); } - /** Items that have scrolled past viewport top (visible in breadcrumbs) */ + /** + * Items that have scrolled past viewport top (visible in breadcrumbs) + */ get scrolledPastItems(): BreadcrumbItem[] { return this.items.filter(item => this.#scrolledPast.has(item.index)); } - /** Index of the most recently scrolled item (active section) */ + /** + * Index of the most recently scrolled item (active section) + */ get activeIndex(): number | null { const past = this.scrolledPastItems; return past.length > 0 ? past[past.length - 1].index : null; diff --git a/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.test.ts b/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.test.ts index 7f6744b..4a9d4c8 100644 --- a/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.test.ts +++ b/src/entities/Breadcrumb/model/store/scrollBreadcrumbsStore.test.ts @@ -1,4 +1,6 @@ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ import { afterEach, beforeEach, diff --git a/src/entities/Font/api/proxy/proxyFonts.ts b/src/entities/Font/api/proxy/proxyFonts.ts index f7699d5..7aa3eb1 100644 --- a/src/entities/Font/api/proxy/proxyFonts.ts +++ b/src/entities/Font/api/proxy/proxyFonts.ts @@ -97,16 +97,24 @@ export interface ProxyFontsParams extends QueryParams { * Includes pagination metadata alongside font data */ export interface ProxyFontsResponse { - /** Array of unified font objects */ + /** + * List of font objects returned by the proxy + */ fonts: UnifiedFont[]; - /** Total number of fonts matching the query */ + /** + * Total number of matching fonts (ignoring limit/offset) + */ total: number; - /** Limit used for this request */ + /** + * Page size used for the request + */ limit: number; - /** Offset used for this request */ + /** + * Start index for the result set + */ offset: number; } diff --git a/src/entities/Font/lib/getFontUrl/getFontUrl.ts b/src/entities/Font/lib/getFontUrl/getFontUrl.ts index 13f697e..da4433d 100644 --- a/src/entities/Font/lib/getFontUrl/getFontUrl.ts +++ b/src/entities/Font/lib/getFontUrl/getFontUrl.ts @@ -3,7 +3,9 @@ import type { UnifiedFont, } from '../../model'; -/** Valid font weight values (100-900 in increments of 100) */ +/** + * Valid font weight values (100-900 in increments of 100) + */ const SIZES = [100, 200, 300, 400, 500, 600, 700, 800, 900]; /** diff --git a/src/entities/Font/lib/mocks/filters.mock.ts b/src/entities/Font/lib/mocks/filters.mock.ts index 1b32531..d4e6432 100644 --- a/src/entities/Font/lib/mocks/filters.mock.ts +++ b/src/entities/Font/lib/mocks/filters.mock.ts @@ -1,31 +1,3 @@ -/** - * Mock font filter data - * - * Factory functions and preset mock data for font-related filters. - * Used in Storybook stories for font filtering components. - * - * ## Usage - * - * ```ts - * import { - * createMockFilter, - * MOCK_FILTERS, - * } from '$entities/Font/lib/mocks'; - * - * // Create a custom filter - * const customFilter = createMockFilter({ - * properties: [ - * { id: 'option1', name: 'Option 1', value: 'option1' }, - * { id: 'option2', name: 'Option 2', value: 'option2', selected: true }, - * ], - * }); - * - * // Use preset filters - * const categoriesFilter = MOCK_FILTERS.categories; - * const subsetsFilter = MOCK_FILTERS.subsets; - * ``` - */ - import type { FontCategory, FontProvider, @@ -34,13 +6,13 @@ import type { import type { Property } from '$shared/lib'; import { createFilter } from '$shared/lib'; -// TYPE DEFINITIONS - /** * Options for creating a mock filter */ export interface MockFilterOptions { - /** Filter properties */ + /** + * Initial set of properties for the mock filter + */ properties: Property[]; } @@ -48,16 +20,20 @@ export interface MockFilterOptions { * Preset mock filters for font filtering */ export interface MockFilters { - /** Provider filter (Google, Fontshare) */ + /** + * Provider filter (Google, Fontshare) + */ providers: ReturnType>; - /** Category filter (sans-serif, serif, display, etc.) */ + /** + * Category filter (sans-serif, serif, display, etc.) + */ categories: ReturnType>; - /** Subset filter (latin, latin-ext, cyrillic, etc.) */ + /** + * Subset filter (latin, latin-ext, cyrillic, etc.) + */ subsets: ReturnType>; } -// FONT CATEGORIES - /** * Unified categories (combines both providers) */ @@ -71,8 +47,6 @@ export const UNIFIED_CATEGORIES: Property[] = [ { id: 'script', name: 'Script', value: 'script' }, ]; -// FONT SUBSETS - /** * Common font subsets */ @@ -85,8 +59,6 @@ export const FONT_SUBSETS: Property[] = [ { id: 'devanagari', name: 'Devanagari', value: 'devanagari' }, ]; -// FONT PROVIDERS - /** * Font providers */ @@ -95,8 +67,6 @@ export const FONT_PROVIDERS: Property[] = [ { id: 'fontshare', name: 'Fontshare', value: 'fontshare' }, ]; -// FILTER FACTORIES - /** * Create a mock filter from properties */ @@ -139,8 +109,6 @@ export function createProvidersFilter(options?: { selected?: FontProvider[] }) { return createFilter({ properties }); } -// PRESET FILTERS - /** * Preset mock filters - use these directly in stories */ @@ -216,8 +184,6 @@ export const MOCK_FILTERS_ALL_SELECTED: MockFilters = { }), }; -// GENERIC FILTER MOCKS - /** * Create a mock filter with generic string properties * Useful for testing generic filter components @@ -239,7 +205,9 @@ export function createGenericFilter( * Preset generic filters for testing */ export const GENERIC_FILTERS = { - /** Small filter with 3 items */ + /** + * Small filter with 3 items + */ small: createFilter({ properties: [ { id: 'option-1', name: 'Option 1', value: 'option-1' }, @@ -247,7 +215,9 @@ export const GENERIC_FILTERS = { { id: 'option-3', name: 'Option 3', value: 'option-3' }, ], }), - /** Medium filter with 6 items */ + /** + * Medium filter with 6 items + */ medium: createFilter({ properties: [ { id: 'alpha', name: 'Alpha', value: 'alpha' }, @@ -258,7 +228,9 @@ export const GENERIC_FILTERS = { { id: 'zeta', name: 'Zeta', value: 'zeta' }, ], }), - /** Large filter with 12 items */ + /** + * Large filter with 12 items + */ large: createFilter({ properties: [ { id: 'jan', name: 'January', value: 'jan' }, @@ -275,7 +247,9 @@ export const GENERIC_FILTERS = { { id: 'dec', name: 'December', value: 'dec' }, ], }), - /** Filter with some pre-selected items */ + /** + * Filter with some pre-selected items + */ partial: createFilter({ properties: [ { id: 'red', name: 'Red', value: 'red', selected: true }, @@ -284,7 +258,9 @@ export const GENERIC_FILTERS = { { id: 'yellow', name: 'Yellow', value: 'yellow', selected: false }, ], }), - /** Filter with all items selected */ + /** + * Filter with all items selected + */ allSelected: createFilter({ properties: [ { id: 'cat', name: 'Cat', value: 'cat', selected: true }, @@ -292,7 +268,9 @@ export const GENERIC_FILTERS = { { id: 'bird', name: 'Bird', value: 'bird', selected: true }, ], }), - /** Empty filter (no items) */ + /** + * Empty filter (no items) + */ empty: createFilter({ properties: [], }), diff --git a/src/entities/Font/lib/mocks/fonts.mock.ts b/src/entities/Font/lib/mocks/fonts.mock.ts index 309701a..a691dc7 100644 --- a/src/entities/Font/lib/mocks/fonts.mock.ts +++ b/src/entities/Font/lib/mocks/fonts.mock.ts @@ -51,23 +51,41 @@ import type { * Options for creating a mock UnifiedFont */ export interface MockUnifiedFontOptions { - /** Unique identifier (default: derived from name) */ + /** + * Unique identifier (default: derived from name) + */ id?: string; - /** Font display name (default: 'Mock Font') */ + /** + * Font display name (default: 'Mock Font') + */ name?: string; - /** Font provider (default: 'google') */ + /** + * Font provider (default: 'google') + */ provider?: FontProvider; - /** Font category (default: 'sans-serif') */ + /** + * Font category (default: 'sans-serif') + */ category?: FontCategory; - /** Font subsets (default: ['latin']) */ + /** + * Font subsets (default: ['latin']) + */ subsets?: FontSubset[]; - /** Font variants (default: ['regular', '700', 'italic', '700italic']) */ + /** + * Font variants (default: ['regular', '700', 'italic', '700italic']) + */ variants?: FontVariant[]; - /** Style URLs (if not provided, mock URLs are generated) */ + /** + * Style URLs (if not provided, mock URLs are generated) + */ styles?: FontStyleUrls; - /** Metadata overrides */ + /** + * Metadata overrides + */ metadata?: Partial; - /** Features overrides */ + /** + * Features overrides + */ features?: Partial; } diff --git a/src/entities/Font/lib/mocks/stores.mock.ts b/src/entities/Font/lib/mocks/stores.mock.ts index 53f3dcb..ec6b689 100644 --- a/src/entities/Font/lib/mocks/stores.mock.ts +++ b/src/entities/Font/lib/mocks/stores.mock.ts @@ -1,8 +1,4 @@ /** - * ============================================================================ - * MOCK FONT STORE HELPERS - * ============================================================================ - * * Factory functions and preset mock data for TanStack Query stores and state management. * Used in Storybook stories for components that use reactive stores. * @@ -35,27 +31,73 @@ import { generateMockFonts, } from './fonts.mock'; -// TANSTACK QUERY MOCK TYPES - /** * Mock TanStack Query state */ export interface MockQueryState { + /** + * Primary query status (pending, success, error) + */ status: QueryStatus; + /** + * Payload data (present on success) + */ data?: TData; + /** + * Caught error object (present on error) + */ error?: TError; + /** + * True if initial load is in progress + */ isLoading?: boolean; + /** + * True if background fetch is in progress + */ isFetching?: boolean; + /** + * True if query resolved successfully + */ isSuccess?: boolean; + /** + * True if query failed + */ isError?: boolean; + /** + * True if query is waiting to be executed + */ isPending?: boolean; + /** + * Timestamp of last successful data retrieval + */ dataUpdatedAt?: number; + /** + * Timestamp of last recorded error + */ errorUpdatedAt?: number; + /** + * Total number of consecutive failures + */ failureCount?: number; + /** + * Detailed reason for the last failure + */ failureReason?: TError; + /** + * Number of times an error has been caught + */ errorUpdateCount?: number; + /** + * True if currently refetching in background + */ isRefetching?: boolean; + /** + * True if refetch attempt failed + */ isRefetchError?: boolean; + /** + * True if query is paused (e.g. offline) + */ isPaused?: boolean; } @@ -63,26 +105,72 @@ export interface MockQueryState { * Mock TanStack Query observer result */ export interface MockQueryObserverResult { + /** + * Current observer status + */ status?: QueryStatus; + /** + * Cached or active data payload + */ data?: TData; + /** + * Caught error from the observer + */ error?: TError; + /** + * Loading flag for the observer + */ isLoading?: boolean; + /** + * Fetching flag for the observer + */ isFetching?: boolean; + /** + * Success flag for the observer + */ isSuccess?: boolean; + /** + * Error flag for the observer + */ isError?: boolean; + /** + * Pending flag for the observer + */ isPending?: boolean; + /** + * Last update time for data + */ dataUpdatedAt?: number; + /** + * Last update time for error + */ errorUpdatedAt?: number; + /** + * Consecutive failure count + */ failureCount?: number; + /** + * Failure reason object + */ failureReason?: TError; + /** + * Error count for the observer + */ errorUpdateCount?: number; + /** + * Refetching flag + */ isRefetching?: boolean; + /** + * Refetch error flag + */ isRefetchError?: boolean; + /** + * Paused flag + */ isPaused?: boolean; } -// TANSTACK QUERY MOCK FACTORIES - /** * Create a mock query state for TanStack Query */ @@ -138,33 +226,53 @@ export function createSuccessState(data: TData): MockQueryObserverResult< return createMockQueryState({ status: 'success', data, error: undefined }); } -// FONT STORE MOCKS - /** * Mock UnifiedFontStore state */ export interface MockFontStoreState { - /** All cached fonts */ + /** + * Map of mock fonts indexed by ID + */ fonts: Record; - /** Current page */ + /** + * Currently active page number + */ page: number; - /** Total pages available */ + /** + * Total number of pages calculated from limit + */ totalPages: number; - /** Items per page */ + /** + * Number of items per page + */ limit: number; - /** Total font count */ + /** + * Total number of available fonts + */ total: number; - /** Loading state */ + /** + * Store-level loading status + */ isLoading: boolean; - /** Error state */ + /** + * Caught error object + */ error: Error | null; - /** Search query */ + /** + * Mock search filter string + */ searchQuery: string; - /** Selected provider */ + /** + * Mock provider filter selection + */ provider: 'google' | 'fontshare' | 'all'; - /** Selected category */ + /** + * Mock category filter selection + */ category: string | null; - /** Selected subset */ + /** + * Mock subset filter selection + */ subset: string | null; } @@ -210,10 +318,12 @@ export function createMockFontStoreState( } /** - * Preset font store states + * Preset font store states for UI testing */ export const MOCK_FONT_STORE_STATES = { - /** Initial loading state */ + /** + * Initial loading state with no data + */ loading: createMockFontStoreState({ isLoading: true, fonts: {}, @@ -221,7 +331,9 @@ export const MOCK_FONT_STORE_STATES = { page: 1, }), - /** Empty state (no fonts found) */ + /** + * State with no fonts matching filters + */ empty: createMockFontStoreState({ fonts: {}, total: 0, @@ -229,7 +341,9 @@ export const MOCK_FONT_STORE_STATES = { isLoading: false, }), - /** First page with fonts */ + /** + * First page of results (10 items) + */ firstPage: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS).slice(0, 10).map(font => [font.id, font]), @@ -241,7 +355,9 @@ export const MOCK_FONT_STORE_STATES = { isLoading: false, }), - /** Second page with fonts */ + /** + * Second page of results (10 items) + */ secondPage: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS).slice(10, 20).map(font => [font.id, font]), @@ -253,7 +369,9 @@ export const MOCK_FONT_STORE_STATES = { isLoading: false, }), - /** Last page with fonts */ + /** + * Final page of results (5 items) + */ lastPage: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS).slice(0, 5).map(font => [font.id, font]), @@ -265,7 +383,9 @@ export const MOCK_FONT_STORE_STATES = { isLoading: false, }), - /** Error state */ + /** + * Terminal failure state + */ error: createMockFontStoreState({ fonts: {}, error: new Error('Failed to load fonts'), @@ -274,7 +394,9 @@ export const MOCK_FONT_STORE_STATES = { isLoading: false, }), - /** With search query */ + /** + * State with active search query + */ withSearch: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS).slice(0, 3).map(font => [font.id, font]), @@ -285,7 +407,9 @@ export const MOCK_FONT_STORE_STATES = { searchQuery: 'Roboto', }), - /** Filtered by category */ + /** + * State with active category filter + */ filteredByCategory: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS) @@ -299,7 +423,9 @@ export const MOCK_FONT_STORE_STATES = { category: 'serif', }), - /** Filtered by provider */ + /** + * State with active provider filter + */ filteredByProvider: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS) @@ -313,7 +439,9 @@ export const MOCK_FONT_STORE_STATES = { provider: 'google', }), - /** Large dataset */ + /** + * Large collection for performance testing (50 items) + */ largeDataset: createMockFontStoreState({ fonts: Object.fromEntries( generateMockFonts(50).map(font => [font.id, font]), @@ -326,17 +454,30 @@ export const MOCK_FONT_STORE_STATES = { }), }; -// MOCK STORE OBJECT - /** * Create a mock store object that mimics TanStack Query behavior * Useful for components that subscribe to store properties */ export function createMockStore(config: { + /** + * Reactive data payload + */ data?: T; + /** + * Loading status flag + */ isLoading?: boolean; + /** + * Error status flag + */ isError?: boolean; + /** + * Catch-all error object + */ error?: Error; + /** + * Background fetching flag + */ isFetching?: boolean; }) { const { @@ -348,24 +489,45 @@ export function createMockStore(config: { } = config; return { + /** + * Returns the active data payload + */ get data() { return data; }, + /** + * True if initially loading + */ get isLoading() { return isLoading; }, + /** + * True if last request failed + */ get isError() { return isError; }, + /** + * Returns the caught error object + */ get error() { return error; }, + /** + * True if fetching in background + */ get isFetching() { return isFetching; }, + /** + * True if query is stable and has data + */ get isSuccess() { return !isLoading && !isError && data !== undefined; }, + /** + * Returns semantic status string + */ get status() { if (isLoading) return 'pending'; if (isError) return 'error'; @@ -375,23 +537,29 @@ export function createMockStore(config: { } /** - * Preset mock stores + * Preset mock stores for common UI states */ export const MOCK_STORES = { - /** Font store in loading state */ + /** + * Initial loading state + */ loadingFontStore: createMockStore({ isLoading: true, data: undefined, }), - /** Font store with fonts loaded */ + /** + * Successful data load state + */ successFontStore: createMockStore({ data: Object.values(UNIFIED_FONTS), isLoading: false, isError: false, }), - /** Font store with error */ + /** + * API error state + */ errorFontStore: createMockStore({ data: undefined, isLoading: false, @@ -399,7 +567,9 @@ export const MOCK_STORES = { error: new Error('Failed to load fonts'), }), - /** Font store with empty results */ + /** + * Empty result set state + */ emptyFontStore: createMockStore({ data: [], isLoading: false, @@ -414,36 +584,69 @@ export const MOCK_STORES = { const mockState = createMockFontStoreState(state); return { // State properties + /** + * Collection of mock fonts + */ get fonts() { return mockState.fonts; }, + /** + * Current mock page + */ get page() { return mockState.page; }, + /** + * Total mock pages + */ get totalPages() { return mockState.totalPages; }, + /** + * Mock items per page + */ get limit() { return mockState.limit; }, + /** + * Total mock items + */ get total() { return mockState.total; }, + /** + * Mock loading status + */ get isLoading() { return mockState.isLoading; }, + /** + * Mock error status + */ get error() { return mockState.error; }, + /** + * Mock search string + */ get searchQuery() { return mockState.searchQuery; }, + /** + * Mock provider filter + */ get provider() { return mockState.provider; }, + /** + * Mock category filter + */ get category() { return mockState.category; }, + /** + * Mock subset filter + */ get subset() { return mockState.subset; }, @@ -464,15 +667,45 @@ export const MOCK_STORES = { * Matches FontStore's public API for Storybook use */ fontStore: (config: { + /** + * Preset font list + */ fonts?: UnifiedFont[]; + /** + * Total item count + */ total?: number; + /** + * Items per page + */ limit?: number; + /** + * Pagination offset + */ offset?: number; + /** + * Loading flag + */ isLoading?: boolean; + /** + * Fetching flag + */ isFetching?: boolean; + /** + * Error flag + */ isError?: boolean; + /** + * Catch-all error object + */ error?: Error | null; + /** + * Has more pages flag + */ hasMore?: boolean; + /** + * Current page number + */ page?: number; } = {}) => { const { @@ -495,27 +728,51 @@ export const MOCK_STORES = { return { // State getters + /** + * Current mock parameters + */ get params() { return state.params; }, + /** + * Mock font list + */ get fonts() { return mockFonts; }, + /** + * Mock loading state + */ get isLoading() { return isLoading; }, + /** + * Mock fetching state + */ get isFetching() { return isFetching; }, + /** + * Mock error state + */ get isError() { return isError; }, + /** + * Mock error object + */ get error() { return error; }, + /** + * Mock empty state check + */ get isEmpty() { return !isLoading && !isFetching && mockFonts.length === 0; }, + /** + * Mock pagination metadata + */ get pagination() { return { total: mockTotal, @@ -527,18 +784,33 @@ export const MOCK_STORES = { }; }, // Category getters + /** + * Derived sans-serif filter + */ get sansSerifFonts() { return mockFonts.filter(f => f.category === 'sans-serif'); }, + /** + * Derived serif filter + */ get serifFonts() { return mockFonts.filter(f => f.category === 'serif'); }, + /** + * Derived display filter + */ get displayFonts() { return mockFonts.filter(f => f.category === 'display'); }, + /** + * Derived handwriting filter + */ get handwritingFonts() { return mockFonts.filter(f => f.category === 'handwriting'); }, + /** + * Derived monospace filter + */ get monospaceFonts() { return mockFonts.filter(f => f.category === 'monospace'); }, diff --git a/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.ts b/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.ts index fe053c3..d7a3875 100644 --- a/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.ts +++ b/src/entities/Font/lib/sizeResolver/createFontRowSizeResolver.ts @@ -13,15 +13,25 @@ import type { * (e.g. `SvelteMap.get()`) is automatically tracked as a dependency. */ export interface FontRowSizeResolverOptions { - /** Returns the current fonts array. Index `i` corresponds to row `i`. */ + /** + * Returns the current fonts array. Index `i` corresponds to row `i`. + */ getFonts: () => UnifiedFont[]; - /** Returns the active font weight (e.g. 400). */ + /** + * Returns the active font weight (e.g. 400). + */ getWeight: () => number; - /** Returns the preview text string. */ + /** + * Returns the preview text string. + */ getPreviewText: () => string; - /** Returns the scroll container's inner width in pixels. Returns 0 before mount. */ + /** + * Returns the scroll container's inner width in pixels. Returns 0 before mount. + */ getContainerWidth: () => number; - /** Returns the font size in pixels (e.g. `controlManager.renderedSize`). */ + /** + * Returns the font size in pixels (e.g. `controlManager.renderedSize`). + */ getFontSizePx: () => number; /** * Returns the computed line height in pixels. @@ -44,9 +54,13 @@ export interface FontRowSizeResolverOptions { * the content width is never over-estimated, keeping the height estimate safe. */ contentHorizontalPadding: number; - /** Fixed height in pixels of chrome that is not text content (header bar, etc.). */ + /** + * Fixed height in pixels of chrome that is not text content (header bar, etc.). + */ chromeHeight: number; - /** Height in pixels to return when the font is not loaded or container width is 0. */ + /** + * Height in pixels to return when the font is not loaded or container width is 0. + */ fallbackHeight: number; } diff --git a/src/entities/Font/model/store/appliedFontsStore/appliedFontStore.test.ts b/src/entities/Font/model/store/appliedFontsStore/appliedFontStore.test.ts index bac5059..b1183ba 100644 --- a/src/entities/Font/model/store/appliedFontsStore/appliedFontStore.test.ts +++ b/src/entities/Font/model/store/appliedFontsStore/appliedFontStore.test.ts @@ -1,10 +1,10 @@ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ import { AppliedFontsManager } from './appliedFontsStore.svelte'; import { FontFetchError } from './errors'; import { FontEvictionPolicy } from './utils/fontEvictionPolicy/FontEvictionPolicy'; -// ── Fake collaborators ──────────────────────────────────────────────────────── - class FakeBufferCache { async get(_url: string): Promise { return new ArrayBuffer(8); @@ -13,7 +13,9 @@ class FakeBufferCache { clear(): void {} } -/** Throws {@link FontFetchError} on every `get()` — simulates network/HTTP failure. */ +/** + * Throws {@link FontFetchError} on every `get()` — simulates network/HTTP failure. + */ class FailingBufferCache { async get(url: string): Promise { throw new FontFetchError(url, new Error('network error'), 500); @@ -22,8 +24,6 @@ class FailingBufferCache { clear(): void {} } -// ── Helpers ─────────────────────────────────────────────────────────────────── - const makeConfig = (id: string, overrides: Partial<{ weight: number; isVariable: boolean }> = {}) => ({ id, name: id, @@ -32,8 +32,6 @@ const makeConfig = (id: string, overrides: Partial<{ weight: number; isVariable: ...overrides, }); -// ── Suite ───────────────────────────────────────────────────────────────────── - describe('AppliedFontsManager', () => { let manager: AppliedFontsManager; let eviction: FontEvictionPolicy; @@ -66,8 +64,6 @@ describe('AppliedFontsManager', () => { vi.unstubAllGlobals(); }); - // ── touch() ─────────────────────────────────────────────────────────────── - describe('touch()', () => { it('queues and loads a new font', async () => { manager.touch([makeConfig('roboto')]); @@ -131,8 +127,6 @@ describe('AppliedFontsManager', () => { }); }); - // ── queue processing ────────────────────────────────────────────────────── - describe('queue processing', () => { it('filters non-critical weights in data-saver mode', async () => { (navigator as any).connection = { saveData: true }; @@ -163,8 +157,6 @@ describe('AppliedFontsManager', () => { }); }); - // ── Phase 1: fetch ──────────────────────────────────────────────────────── - describe('Phase 1 — fetch', () => { it('sets status to error on fetch failure', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); @@ -209,8 +201,6 @@ describe('AppliedFontsManager', () => { }); }); - // ── Phase 2: parse ──────────────────────────────────────────────────────── - describe('Phase 2 — parse', () => { it('sets status to error on parse failure', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); @@ -241,8 +231,6 @@ describe('AppliedFontsManager', () => { }); }); - // ── #purgeUnused ────────────────────────────────────────────────────────── - describe('#purgeUnused', () => { it('evicts fonts after TTL expires', async () => { manager.touch([makeConfig('ephemeral')]); @@ -300,8 +288,6 @@ describe('AppliedFontsManager', () => { }); }); - // ── destroy() ───────────────────────────────────────────────────────────── - describe('destroy()', () => { it('clears all statuses', async () => { manager.touch([makeConfig('roboto')]); diff --git a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts index f0ff3cd..11bce4a 100644 --- a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts +++ b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts @@ -156,7 +156,9 @@ export class AppliedFontsManager { } } - /** Returns true if data-saver mode is enabled (defers non-critical weights). */ + /** + * Returns true if data-saver mode is enabled (defers non-critical weights). + */ #shouldDeferNonCritical(): boolean { return (navigator as any).connection?.saveData === true; } @@ -188,13 +190,11 @@ export class AppliedFontsManager { const concurrency = getEffectiveConcurrency(); const buffers = new Map(); - // ==================== PHASE 1: Concurrent Fetching ==================== // Fetch multiple font files in parallel since network I/O is non-blocking for (let i = 0; i < entries.length; i += concurrency) { await this.#fetchChunk(entries.slice(i, i + concurrency), buffers); } - // ==================== PHASE 2: Sequential Parsing ==================== // Parse buffers one at a time with periodic yields to avoid blocking UI const hasInputPending = !!(navigator as any).scheduling?.isInputPending; let lastYield = performance.now(); @@ -279,7 +279,9 @@ export class AppliedFontsManager { } } - /** Removes fonts unused within TTL (LRU-style cleanup). Runs every PURGE_INTERVAL. Pinned fonts are never evicted. */ + /** + * Removes fonts unused within TTL (LRU-style cleanup). Runs every PURGE_INTERVAL. Pinned fonts are never evicted. + */ #purgeUnused() { const now = Date.now(); // Iterate through all tracked font keys @@ -307,7 +309,9 @@ export class AppliedFontsManager { } } - /** Returns current loading status for a font, or undefined if never requested. */ + /** + * Returns current loading status for a font, or undefined if never requested. + */ getFontStatus(id: string, weight: number, isVariable = false) { try { return this.statuses.get(generateFontKey({ id, weight, isVariable })); @@ -316,17 +320,23 @@ export class AppliedFontsManager { } } - /** Pins a font so it is never evicted by #purgeUnused(), regardless of TTL. */ + /** + * Pins a font so it is never evicted by #purgeUnused(), regardless of TTL. + */ pin(id: string, weight: number, isVariable = false): void { this.#eviction.pin(generateFontKey({ id, weight, isVariable })); } - /** Unpins a font, allowing it to be evicted by #purgeUnused() once its TTL expires. */ + /** + * Unpins a font, allowing it to be evicted by #purgeUnused() once its TTL expires. + */ unpin(id: string, weight: number, isVariable = false): void { this.#eviction.unpin(generateFontKey({ id, weight, isVariable })); } - /** Waits for all fonts to finish loading using document.fonts.ready. */ + /** + * Waits for all fonts to finish loading using document.fonts.ready. + */ async ready(): Promise { if (typeof document === 'undefined') { return; @@ -336,7 +346,9 @@ export class AppliedFontsManager { } catch { /* document unloaded */ } } - /** Aborts all operations, removes fonts from document, and clears state. Manager cannot be reused after. */ + /** + * Aborts all operations, removes fonts from document, and clears state. Manager cannot be reused after. + */ destroy() { // Abort all in-flight network requests this.#abortController.abort(); @@ -375,5 +387,7 @@ export class AppliedFontsManager { } } -/** Singleton instance — use throughout the application for unified font loading state. */ +/** + * Singleton instance — use throughout the application for unified font loading state. + */ export const appliedFontsManager = new AppliedFontsManager(); diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/fontBufferCache/FontBufferCache.test.ts b/src/entities/Font/model/store/appliedFontsStore/utils/fontBufferCache/FontBufferCache.test.ts index 3ae0884..c8b52f0 100644 --- a/src/entities/Font/model/store/appliedFontsStore/utils/fontBufferCache/FontBufferCache.test.ts +++ b/src/entities/Font/model/store/appliedFontsStore/utils/fontBufferCache/FontBufferCache.test.ts @@ -1,4 +1,6 @@ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ import { FontFetchError } from '../../errors'; import { FontBufferCache } from './FontBufferCache'; diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/fontBufferCache/FontBufferCache.ts b/src/entities/Font/model/store/appliedFontsStore/utils/fontBufferCache/FontBufferCache.ts index a2e6ace..6f5eb6a 100644 --- a/src/entities/Font/model/store/appliedFontsStore/utils/fontBufferCache/FontBufferCache.ts +++ b/src/entities/Font/model/store/appliedFontsStore/utils/fontBufferCache/FontBufferCache.ts @@ -3,9 +3,13 @@ import { FontFetchError } from '../../errors'; type Fetcher = (url: string, init?: RequestInit) => Promise; interface FontBufferCacheOptions { - /** Custom fetch implementation. Defaults to `globalThis.fetch`. Inject in tests for isolation. */ + /** + * Custom fetch implementation. Defaults to `globalThis.fetch`. Inject in tests for isolation. + */ fetcher?: Fetcher; - /** Cache API cache name. Defaults to `'font-cache-v1'`. */ + /** + * Cache API cache name. Defaults to `'font-cache-v1'`. + */ cacheName?: string; } @@ -85,12 +89,16 @@ export class FontBufferCache { return buffer; } - /** Removes a URL from the in-memory cache. Next call to `get()` will re-fetch. */ + /** + * Removes a URL from the in-memory cache. Next call to `get()` will re-fetch. + */ evict(url: string): void { this.#buffersByUrl.delete(url); } - /** Clears all in-memory cached buffers. */ + /** + * Clears all in-memory cached buffers. + */ clear(): void { this.#buffersByUrl.clear(); } diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/fontEvictionPolicy/FontEvictionPolicy.ts b/src/entities/Font/model/store/appliedFontsStore/utils/fontEvictionPolicy/FontEvictionPolicy.ts index e99abde..2a64cc6 100644 --- a/src/entities/Font/model/store/appliedFontsStore/utils/fontEvictionPolicy/FontEvictionPolicy.ts +++ b/src/entities/Font/model/store/appliedFontsStore/utils/fontEvictionPolicy/FontEvictionPolicy.ts @@ -1,5 +1,7 @@ interface FontEvictionPolicyOptions { - /** TTL in milliseconds. Defaults to 5 minutes. */ + /** + * TTL in milliseconds. Defaults to 5 minutes. + */ ttl?: number; } @@ -28,12 +30,16 @@ export class FontEvictionPolicy { this.#usageTracker.set(key, now); } - /** Pins a font key so it is never evicted regardless of TTL. */ + /** + * Pins a font key so it is never evicted regardless of TTL. + */ pin(key: string): void { this.#pinnedFonts.add(key); } - /** Unpins a font key, allowing it to be evicted once its TTL expires. */ + /** + * Unpins a font key, allowing it to be evicted once its TTL expires. + */ unpin(key: string): void { this.#pinnedFonts.delete(key); } @@ -57,18 +63,24 @@ export class FontEvictionPolicy { return now - lastUsed >= this.#TTL; } - /** Returns an iterator over all tracked font keys. */ + /** + * Returns an iterator over all tracked font keys. + */ keys(): IterableIterator { return this.#usageTracker.keys(); } - /** Removes a font key from tracking. Called by the orchestrator after eviction. */ + /** + * Removes a font key from tracking. Called by the orchestrator after eviction. + */ remove(key: string): void { this.#usageTracker.delete(key); this.#pinnedFonts.delete(key); } - /** Clears all usage timestamps and pinned keys. */ + /** + * Clears all usage timestamps and pinned keys. + */ clear(): void { this.#usageTracker.clear(); this.#pinnedFonts.clear(); diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/fontLoadQueue/FontLoadQueue.ts b/src/entities/Font/model/store/appliedFontsStore/utils/fontLoadQueue/FontLoadQueue.ts index 5e6de9f..e921eb9 100644 --- a/src/entities/Font/model/store/appliedFontsStore/utils/fontLoadQueue/FontLoadQueue.ts +++ b/src/entities/Font/model/store/appliedFontsStore/utils/fontLoadQueue/FontLoadQueue.ts @@ -34,22 +34,30 @@ export class FontLoadQueue { return entries; } - /** Returns `true` if the key is currently in the queue. */ + /** + * Returns `true` if the key is currently in the queue. + */ has(key: string): boolean { return this.#queue.has(key); } - /** Increments the retry count for a font key. */ + /** + * Increments the retry count for a font key. + */ incrementRetry(key: string): void { this.#retryCounts.set(key, (this.#retryCounts.get(key) ?? 0) + 1); } - /** Returns `true` if the font has reached or exceeded the maximum retry limit. */ + /** + * Returns `true` if the font has reached or exceeded the maximum retry limit. + */ isMaxRetriesReached(key: string): boolean { return (this.#retryCounts.get(key) ?? 0) >= this.#MAX_RETRIES; } - /** Clears all queued fonts and resets all retry counts. */ + /** + * Clears all queued fonts and resets all retry counts. + */ clear(): void { this.#queue.clear(); this.#retryCounts.clear(); diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.test.ts b/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.test.ts index 4b7a667..f44b289 100644 --- a/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.test.ts +++ b/src/entities/Font/model/store/appliedFontsStore/utils/loadFont/loadFont.test.ts @@ -1,4 +1,6 @@ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ import { FontParseError } from '../../errors'; import { loadFont } from './loadFont'; diff --git a/src/entities/Font/model/store/fontStore/fontStore.svelte.spec.ts b/src/entities/Font/model/store/fontStore/fontStore.svelte.spec.ts index 2b6f181..8caadf2 100644 --- a/src/entities/Font/model/store/fontStore/fontStore.svelte.spec.ts +++ b/src/entities/Font/model/store/fontStore/fontStore.svelte.spec.ts @@ -61,7 +61,6 @@ describe('FontStore', () => { vi.resetAllMocks(); }); - // ----------------------------------------------------------------------- describe('construction', () => { it('stores initial params', () => { const store = makeStore({ limit: 20 }); @@ -90,7 +89,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('state after fetch', () => { it('exposes loaded fonts', async () => { const store = await fetchedStore({}, generateMockFonts(7)); @@ -129,7 +127,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('error states', () => { it('isError is false before any fetch', () => { const store = makeStore(); @@ -178,7 +175,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('font accumulation', () => { it('replaces fonts when refetching the first page', async () => { const store = makeStore(); @@ -212,7 +208,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('pagination state', () => { it('returns zero-value defaults before any fetch', () => { const store = makeStore(); @@ -248,7 +243,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('setParams', () => { it('merges updates into existing params', () => { const store = makeStore({ limit: 10 }); @@ -266,7 +260,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('filter change resets', () => { it('clears accumulated fonts when a filter changes', async () => { const store = await fetchedStore({}, generateMockFonts(5)); @@ -302,7 +295,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('staleTime in buildOptions', () => { it('is 5 minutes with no active filters', () => { const store = makeStore(); @@ -331,7 +323,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('buildQueryKey', () => { it('omits empty-string params', () => { const store = makeStore(); @@ -366,7 +357,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('destroy', () => { it('does not throw', () => { const store = makeStore(); @@ -380,7 +370,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('refetch', () => { it('triggers a fetch', async () => { const store = makeStore(); @@ -400,7 +389,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('nextPage', () => { let store: FontStore; @@ -437,7 +425,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('prevPage and goToPage', () => { it('prevPage is a no-op — infinite scroll does not support backward navigation', async () => { const store = await fetchedStore({}, generateMockFonts(5)); @@ -454,7 +441,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('prefetch', () => { it('triggers a fetch for the provided params', async () => { const store = makeStore(); @@ -465,7 +451,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('getCachedData / setQueryData', () => { it('getCachedData returns undefined before any fetch', () => { queryClient.clear(); @@ -497,7 +482,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('invalidate', () => { it('calls invalidateQueries', async () => { const store = await fetchedStore(); @@ -508,7 +492,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('setLimit', () => { it('updates the limit param', () => { const store = makeStore({ limit: 10 }); @@ -518,7 +501,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('filter shortcut methods', () => { let store: FontStore; @@ -561,7 +543,6 @@ describe('FontStore', () => { }); }); - // ----------------------------------------------------------------------- describe('category getters', () => { it('each getter returns only fonts of that category', async () => { const fonts = generateMixedCategoryFonts(2); // 2 of each category = 10 total diff --git a/src/entities/Font/model/store/fontStore/fontStore.svelte.ts b/src/entities/Font/model/store/fontStore/fontStore.svelte.ts index 95edee0..ca01ac4 100644 --- a/src/entities/Font/model/store/fontStore/fontStore.svelte.ts +++ b/src/entities/Font/model/store/fontStore/fontStore.svelte.ts @@ -18,7 +18,9 @@ import type { UnifiedFont } from '../../types'; type PageParam = { offset: number }; -/** Filter params + limit — offset is managed by TQ as a page param, not a user param. */ +/** + * Filter params + limit — offset is managed by TQ as a page param, not a user param. + */ type FontStoreParams = Omit; type FontStoreResult = InfiniteQueryObserverResult, Error>; @@ -44,34 +46,53 @@ export class FontStore { }); } - // -- Public state -- - + /** + * Current filter and limit configuration + */ get params(): FontStoreParams { return this.#params; } + /** + * Flattened list of all fonts loaded across all pages (reactive) + */ get fonts(): UnifiedFont[] { return this.#result.data?.pages.flatMap((p: ProxyFontsResponse) => p.fonts) ?? []; } + /** + * True if the first page is currently being fetched + */ get isLoading(): boolean { return this.#result.isLoading; } + /** + * True if any background fetch is in progress (initial or pagination) + */ get isFetching(): boolean { return this.#result.isFetching; } + /** + * True if the last fetch attempt resulted in an error + */ get isError(): boolean { return this.#result.isError; } + /** + * Last caught error from the query observer + */ get error(): Error | null { return this.#result.error ?? null; } - // isEmpty is false during loading/fetching so the UI never flashes "no results" - // while a fetch is in progress. The !isFetching guard is specifically for the filter-change - // transition: fonts clear synchronously → isFetching becomes true → isEmpty stays false. + /** + * True if no fonts were found for the current filter criteria + */ get isEmpty(): boolean { return !this.isLoading && !this.isFetching && this.fonts.length === 0; } + /** + * Pagination metadata derived from the last loaded page + */ get pagination() { const pages = this.#result.data?.pages; const last = pages?.at(-1); @@ -95,37 +116,52 @@ export class FontStore { }; } - // -- Lifecycle -- - + /** + * Cleans up subscriptions and destroys the observer + */ destroy() { this.#unsubscribe(); this.#observer.destroy(); } - // -- Param management -- - + /** + * Merge new parameters into existing state and trigger a refetch + */ setParams(updates: Partial) { this.#params = { ...this.#params, ...updates }; this.#observer.setOptions(this.buildOptions()); } + /** + * Forcefully invalidate and refetch the current query from the network + */ invalidate() { this.#qc.invalidateQueries({ queryKey: this.buildQueryKey(this.#params) }); } - // -- Async operations -- - + /** + * Manually trigger a query refetch + */ async refetch() { await this.#observer.refetch(); } + /** + * Prime the cache with data for a specific parameter set + */ async prefetch(params: FontStoreParams) { await this.#qc.prefetchInfiniteQuery(this.buildOptions(params)); } + /** + * Abort any active network requests for this store + */ cancel() { this.#qc.cancelQueries({ queryKey: this.buildQueryKey(this.#params) }); } + /** + * Retrieve current font list from cache without triggering a fetch + */ getCachedData(): UnifiedFont[] | undefined { const data = this.#qc.getQueryData>( this.buildQueryKey(this.#params), @@ -134,6 +170,9 @@ export class FontStore { return data.pages.flatMap(p => p.fonts); } + /** + * Manually update the cached font data (useful for optimistic updates) + */ setQueryData(updater: (old: UnifiedFont[] | undefined) => UnifiedFont[]) { const key = this.buildQueryKey(this.#params); this.#qc.setQueryData>( @@ -164,56 +203,90 @@ export class FontStore { ); } - // -- Filter shortcuts -- - + /** + * Shortcut to update provider filters + */ setProviders(v: ProxyFontsParams['providers']) { this.setParams({ providers: v }); } + /** + * Shortcut to update category filters + */ setCategories(v: ProxyFontsParams['categories']) { this.setParams({ categories: v }); } + /** + * Shortcut to update subset filters + */ setSubsets(v: ProxyFontsParams['subsets']) { this.setParams({ subsets: v }); } + /** + * Shortcut to update search query + */ setSearch(v: string) { this.setParams({ q: v || undefined }); } + /** + * Shortcut to update sort order + */ setSort(v: ProxyFontsParams['sort']) { this.setParams({ sort: v }); } - // -- Pagination navigation -- - + /** + * Fetch the next page of results if available + */ async nextPage(): Promise { await this.#observer.fetchNextPage(); } - prevPage(): void {} // no-op: infinite scroll accumulates forward only; method kept for API compatibility - goToPage(_page: number): void {} // no-op + /** + * Backward pagination (no-op: infinite scroll accumulates forward only) + */ + prevPage(): void {} + /** + * Jump to specific page (no-op for infinite scroll) + */ + goToPage(_page: number): void {} + /** + * Update the number of items fetched per page + */ setLimit(limit: number) { this.setParams({ limit }); } - // -- Category views -- - + /** + * Derived list of sans-serif fonts in the current set + */ get sansSerifFonts() { return this.fonts.filter(f => f.category === 'sans-serif'); } + /** + * Derived list of serif fonts in the current set + */ get serifFonts() { return this.fonts.filter(f => f.category === 'serif'); } + /** + * Derived list of display fonts in the current set + */ get displayFonts() { return this.fonts.filter(f => f.category === 'display'); } + /** + * Derived list of handwriting fonts in the current set + */ get handwritingFonts() { return this.fonts.filter(f => f.category === 'handwriting'); } + /** + * Derived list of monospace fonts in the current set + */ get monospaceFonts() { return this.fonts.filter(f => f.category === 'monospace'); } - // -- Private helpers (TypeScript-private so tests can spy via `as any`) -- - private buildQueryKey(params: FontStoreParams): readonly unknown[] { const filtered: Record = {}; diff --git a/src/entities/Font/model/types/font.ts b/src/entities/Font/model/types/font.ts index b35d213..9cf368f 100644 --- a/src/entities/Font/model/types/font.ts +++ b/src/entities/Font/model/types/font.ts @@ -31,18 +31,28 @@ export type FontSubset = 'latin' | 'latin-ext' | 'cyrillic' | 'greek' | 'arabic' * Combined filter state for font queries */ export interface FontFilters { - /** Selected font providers */ + /** + * Active font providers to fetch from + */ providers: FontProvider[]; - /** Selected font categories */ + /** + * Visual classifications (sans, serif, etc.) + */ categories: FontCategory[]; - /** Selected character subsets */ + /** + * Character sets required for the sample text + */ subsets: FontSubset[]; } -/** Filter group identifier */ +/** + * Filter group identifier + */ export type FilterGroup = 'providers' | 'categories' | 'subsets'; -/** Filter type including search query */ +/** + * Filter type including search query + */ export type FilterType = FilterGroup | 'searchQuery'; /** @@ -80,15 +90,25 @@ export type UnifiedFontVariant = FontVariant; * Font style URLs */ export interface FontStyleUrls { - /** Regular weight URL */ + /** + * URL for the regular (400) weight + */ regular?: string; - /** Italic URL */ + /** + * URL for the italic (400) style + */ italic?: string; - /** Bold weight URL */ + /** + * URL for the bold (700) weight + */ bold?: string; - /** Bold italic URL */ + /** + * URL for the bold-italic (700) style + */ boldItalic?: string; - /** Additional variant mapping */ + /** + * Mapping for all other numeric/custom variants + */ variants?: Partial>; } @@ -96,19 +116,24 @@ export interface FontStyleUrls { * Font metadata */ export interface FontMetadata { - /** Timestamp when font was cached */ + /** + * Epoch timestamp of last successful fetch + */ cachedAt: number; - /** Font version from provider */ + /** + * Semantic version string from upstream + */ version?: string; - /** Last modified date from provider */ + /** + * ISO date string of last remote update + */ lastModified?: string; - /** Popularity rank (if available from provider) */ + /** + * Raw ranking integer from provider + */ popularity?: number; /** - * Normalized popularity score (0-100) - * - * Normalized across all fonts for consistent ranking - * Higher values indicate more popular fonts + * Normalized score (0-100) used for global sorting */ popularityScore?: number; } @@ -117,17 +142,38 @@ export interface FontMetadata { * Font features (variable fonts, axes, tags) */ export interface FontFeatures { - /** Whether this is a variable font */ + /** + * Whether the font supports fluid weight/width axes + */ isVariable?: boolean; - /** Variable font axes (for Fontshare) */ + /** + * Definable axes for variable font interpolation + */ axes?: Array<{ + /** + * Human-readable axis name (e.g., 'Weight') + */ name: string; + /** + * CSS property name (e.g., 'wght') + */ property: string; + /** + * Default numeric value for the axis + */ default: number; + /** + * Minimum inclusive bound + */ min: number; + /** + * Maximum inclusive bound + */ max: number; }>; - /** Usage tags (for Fontshare) */ + /** + * Descriptive keywords for search indexing + */ tags?: string[]; } @@ -138,29 +184,44 @@ export interface FontFeatures { * for consistent font handling across the application. */ export interface UnifiedFont { - /** Unique identifier (Google: family name, Fontshare: slug) */ + /** + * Unique ID (family name for Google, slug for Fontshare) + */ id: string; - /** Font display name */ + /** + * Canonical family name for CSS font-family + */ name: string; - /** Font provider (google | fontshare) */ + /** + * Upstream data source + */ provider: FontProvider; /** - * Provider badge display name - * - * Human-readable provider name for UI display - * e.g., "Google Fonts" or "Fontshare" + * Display label for provider badges */ providerBadge?: string; - /** Font category classification */ + /** + * Primary typographic category + */ category: FontCategory; - /** Supported character subsets */ + /** + * All supported character sets + */ subsets: FontSubset[]; - /** Available font variants (weights, styles) */ + /** + * List of available weights and styles + */ variants: UnifiedFontVariant[]; - /** URL mapping for font file downloads */ + /** + * Remote assets for font loading + */ styles: FontStyleUrls; - /** Additional metadata */ + /** + * Technical metadata and rankings + */ metadata: FontMetadata; - /** Advanced font features */ + /** + * Variable font details and tags + */ features: FontFeatures; } diff --git a/src/entities/Font/model/types/index.ts b/src/entities/Font/model/types/index.ts index 28ef772..f4edb26 100644 --- a/src/entities/Font/model/types/index.ts +++ b/src/entities/Font/model/types/index.ts @@ -1,12 +1,3 @@ -/** - * ============================================================================ - * SINGLE EXPORT POINT - * ============================================================================ - * - * This is the single export point for all Font types. - * All imports should use: `import { X } from '$entities/Font/model/types'` - */ - // Font domain and model types export type { FilterGroup, diff --git a/src/entities/Font/model/types/store.ts b/src/entities/Font/model/types/store.ts index 894adcc..dde1165 100644 --- a/src/entities/Font/model/types/store.ts +++ b/src/entities/Font/model/types/store.ts @@ -1,9 +1,3 @@ -/** - * ============================================================================ - * STORE TYPES - * ============================================================================ - */ - import type { FontCategory, FontProvider, @@ -12,37 +6,55 @@ import type { } from './font'; /** - * Font collection state + * Global state for the local font collection */ export interface FontCollectionState { - /** All cached fonts */ + /** + * Map of cached fonts indexed by their unique family ID + */ fonts: Record; - /** Active filters */ + /** + * Set of active user-defined filters + */ filters: FontCollectionFilters; - /** Sort configuration */ + /** + * Current sorting parameters for the display list + */ sort: FontCollectionSort; } /** - * Font collection filters + * Filter configuration for narrow collections */ export interface FontCollectionFilters { - /** Search query */ + /** + * Partial family name to match against + */ searchQuery: string; - /** Filter by providers */ + /** + * Data sources (Google, Fontshare) to include + */ providers?: FontProvider[]; - /** Filter by categories */ + /** + * Typographic categories (Serif, Sans, etc.) to include + */ categories?: FontCategory[]; - /** Filter by subsets */ + /** + * Character sets (Latin, Cyrillic, etc.) to include + */ subsets?: FontSubset[]; } /** - * Font collection sort configuration + * Ordering configuration for the font list */ export interface FontCollectionSort { - /** Sort field */ + /** + * The font property to order by + */ field: 'name' | 'popularity' | 'category'; - /** Sort direction */ + /** + * The sort order (Ascending or Descending) + */ direction: 'asc' | 'desc'; } diff --git a/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts b/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts index 5a999d4..540f0e2 100644 --- a/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts +++ b/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.svelte.ts @@ -41,15 +41,25 @@ type ThemeSource = 'system' | 'user'; */ class ThemeManager { // Private reactive state - /** Current theme value ('light' or 'dark') */ + /** + * Current theme value ('light' or 'dark') + */ #theme = $state('light'); - /** Whether theme is controlled by user or follows system */ + /** + * Whether theme is controlled by user or follows system + */ #source = $state('system'); - /** MediaQueryList for detecting system theme changes */ + /** + * MediaQueryList for detecting system theme changes + */ #mediaQuery: MediaQueryList | null = null; - /** Persistent storage for user's theme preference */ + /** + * Persistent storage for user's theme preference + */ #store = createPersistentStore('glyphdiff:theme', null); - /** Bound handler for system theme change events */ + /** + * Bound handler for system theme change events + */ #systemChangeHandler = this.#onSystemChange.bind(this); constructor() { @@ -64,22 +74,30 @@ class ThemeManager { } } - /** Current theme value */ + /** + * Current theme value + */ get value(): Theme { return this.#theme; } - /** Source of current theme ('system' or 'user') */ + /** + * Source of current theme ('system' or 'user') + */ get source(): ThemeSource { return this.#source; } - /** Whether dark theme is active */ + /** + * Whether dark theme is active + */ get isDark(): boolean { return this.#theme === 'dark'; } - /** Whether theme is controlled by user (not following system) */ + /** + * Whether theme is controlled by user (not following system) + */ get isUserControlled(): boolean { return this.#source === 'user'; } diff --git a/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.test.ts b/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.test.ts index 702883a..2018e0f 100644 --- a/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.test.ts +++ b/src/features/ChangeAppTheme/model/store/ThemeManager/ThemeManager.test.ts @@ -1,9 +1,9 @@ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ -// ============================================================ // Mock MediaQueryListEvent for system theme change simulations // Note: Other mocks (ResizeObserver, localStorage, matchMedia) are set up in vitest.setup.unit.ts -// ============================================================ class MockMediaQueryListEvent extends Event { matches: boolean; @@ -16,9 +16,7 @@ class MockMediaQueryListEvent extends Event { } } -// ============================================================ // NOW IT'S SAFE TO IMPORT -// ============================================================ import { afterEach, diff --git a/src/features/GetFonts/api/filters/filters.ts b/src/features/GetFonts/api/filters/filters.ts index a576a1b..822c968 100644 --- a/src/features/GetFonts/api/filters/filters.ts +++ b/src/features/GetFonts/api/filters/filters.ts @@ -15,19 +15,29 @@ const PROXY_API_URL = 'https://api.glyphdiff.com/api/v1/filters' as const; * Filter metadata type from backend */ export interface FilterMetadata { - /** Filter ID (e.g., "providers", "categories", "subsets") */ + /** + * Filter ID (e.g., "providers", "categories", "subsets") + */ id: string; - /** Display name (e.g., "Font Providers", "Categories", "Character Subsets") */ + /** + * Display name (e.g., "Font Providers", "Categories", "Character Subsets") + */ name: string; - /** Filter description */ + /** + * Filter description + */ description: string; - /** Filter type */ + /** + * Filter type + */ type: 'enum' | 'string' | 'array'; - /** Available filter options */ + /** + * Available filter options + */ options: FilterOption[]; } @@ -35,16 +45,24 @@ export interface FilterMetadata { * Filter option type */ export interface FilterOption { - /** Option ID (e.g., "google", "serif", "latin") */ + /** + * Option ID (e.g., "google", "serif", "latin") + */ id: string; - /** Display name (e.g., "Google Fonts", "Serif", "Latin") */ + /** + * Display name (e.g., "Google Fonts", "Serif", "Latin") + */ name: string; - /** Option value (e.g., "google", "serif", "latin") */ + /** + * Option value (e.g., "google", "serif", "latin") + */ value: string; - /** Number of fonts with this value */ + /** + * Number of fonts with this value + */ count: number; } @@ -52,7 +70,9 @@ export interface FilterOption { * Proxy filters API response */ export interface ProxyFiltersResponse { - /** Array of filter metadata */ + /** + * Array of filter metadata + */ filters: FilterMetadata[]; } diff --git a/src/features/GetFonts/model/index.ts b/src/features/GetFonts/model/index.ts index 36844c9..2c8f3c8 100644 --- a/src/features/GetFonts/model/index.ts +++ b/src/features/GetFonts/model/index.ts @@ -1,15 +1,56 @@ export type { + /** + * Top-level configuration for all filters + */ FilterConfig, + /** + * Configuration for a single grouping of filter properties + */ FilterGroupConfig, } from './types/filter'; -export { filtersStore } from './state/filters.svelte'; -export { filterManager } from './state/manager.svelte'; - +/** + * Global reactive filter state + */ export { + /** + * Low-level property selection store + */ + filtersStore, +} from './state/filters.svelte'; + +/** + * Main filter controller + */ +export { + /** + * High-level manager for syncing search and filters + */ + filterManager, +} from './state/manager.svelte'; + +/** + * Sorting logic + */ +export { + /** + * Map of human-readable labels to API sort keys + */ SORT_MAP, + /** + * List of all available sort options for the UI + */ SORT_OPTIONS, + /** + * Valid sort key values + */ type SortApiValue, + /** + * UI model for a single sort option + */ type SortOption, + /** + * Reactive store for the current sort selection + */ sortStore, } from './store/sortStore.svelte'; diff --git a/src/features/GetFonts/model/state/filters.svelte.ts b/src/features/GetFonts/model/state/filters.svelte.ts index 055e0d0..27e4aba 100644 --- a/src/features/GetFonts/model/state/filters.svelte.ts +++ b/src/features/GetFonts/model/state/filters.svelte.ts @@ -32,13 +32,19 @@ import { * Provides reactive access to filter data */ class FiltersStore { - /** TanStack Query result state */ + /** + * TanStack Query result state + */ protected result = $state>({} as any); - /** TanStack Query observer instance */ + /** + * TanStack Query observer instance + */ protected observer: QueryObserver; - /** Shared query client */ + /** + * Shared query client + */ protected qc = queryClient; /** diff --git a/src/features/GetFonts/model/store/sortStore.svelte.ts b/src/features/GetFonts/model/store/sortStore.svelte.ts index a886339..0a090c5 100644 --- a/src/features/GetFonts/model/store/sortStore.svelte.ts +++ b/src/features/GetFonts/model/store/sortStore.svelte.ts @@ -21,17 +21,23 @@ function createSortStore(initial: SortOption = 'Popularity') { let current = $state(initial); return { - /** Current display label (e.g. 'Popularity') */ + /** + * Current display label (e.g. 'Popularity') + */ get value() { return current; }, - /** Mapped API value (e.g. 'popularity') */ + /** + * Mapped API value (e.g. 'popularity') + */ get apiValue(): SortApiValue { return SORT_MAP[current]; }, - /** Set the active sort option by its display label */ + /** + * Set the active sort option by its display label + */ set(option: SortOption) { current = option; }, diff --git a/src/features/GetFonts/model/types/filter.ts b/src/features/GetFonts/model/types/filter.ts index 41f3193..047c86a 100644 --- a/src/features/GetFonts/model/types/filter.ts +++ b/src/features/GetFonts/model/types/filter.ts @@ -1,12 +1,27 @@ import type { Property } from '$shared/lib'; export interface FilterGroupConfig { + /** + * Unique identifier for the filter group (e.g. 'categories') + */ id: string; + /** + * Human-readable label displayed in the UI header + */ label: string; + /** + * List of toggleable properties within this group + */ properties: Property[]; } export interface FilterConfig { + /** + * Optional string to filter results by name + */ queryValue?: string; + /** + * Collection of filter groups to display + */ groups: FilterGroupConfig[]; } diff --git a/src/features/SetupFont/lib/settingsManager/settingsManager.svelte.ts b/src/features/SetupFont/lib/settingsManager/settingsManager.svelte.ts index f096ddb..31ea444 100644 --- a/src/features/SetupFont/lib/settingsManager/settingsManager.svelte.ts +++ b/src/features/SetupFont/lib/settingsManager/settingsManager.svelte.ts @@ -30,9 +30,12 @@ import { SvelteMap } from 'svelte/reactivity'; type ControlOnlyFields = Omit, keyof ControlDataModel>; /** - * A control with its instance + * A control with its associated instance */ export interface Control extends ControlOnlyFields { + /** + * The reactive typography control instance + */ instance: TypographyControl; } @@ -40,9 +43,21 @@ export interface Control extends ControlOnlyFields { * Storage schema for typography settings */ export interface TypographySettings { + /** + * Base font size (User preference, unscaled) + */ fontSize: number; + /** + * Numeric font weight (100-900) + */ fontWeight: number; + /** + * Line height multiplier (e.g. 1.5) + */ lineHeight: number; + /** + * Letter spacing in em/px + */ letterSpacing: number; } @@ -53,13 +68,21 @@ export interface TypographySettings { * responsive scaling support for font size. */ export class TypographySettingsManager { - /** Map of controls keyed by ID */ + /** + * Internal map of reactive controls keyed by their identifier + */ #controls = new SvelteMap(); - /** Responsive multiplier for font size display */ + /** + * Global multiplier for responsive font size scaling + */ #multiplier = $state(1); - /** Persistent storage for settings */ + /** + * LocalStorage-backed storage for persistence + */ #storage: PersistentStore; - /** Base font size (user preference, unscaled) */ + /** + * The underlying font size before responsive scaling is applied + */ #baseSize = $state(DEFAULT_FONT_SIZE); constructor(configs: ControlModel[], storage: PersistentStore) { @@ -131,16 +154,15 @@ export class TypographySettingsManager { return 0; } - /** Current multiplier for responsive scaling */ + /** + * Active scaling factor for the rendered font size + */ 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. + * Updates the multiplier and recalculates dependent control values */ set multiplier(value: number) { if (this.#multiplier === value) return; @@ -154,14 +176,15 @@ export class TypographySettingsManager { } /** - * The scaled size for CSS usage - * Returns baseSize * multiplier for actual rendering + * The actual pixel value for CSS font-size (baseSize * multiplier) */ get renderedSize() { return this.#baseSize * this.#multiplier; } - /** The base size (User Preference) */ + /** + * The raw font size preference before scaling + */ get baseSize() { return this.#baseSize; } @@ -173,45 +196,63 @@ export class TypographySettingsManager { } /** - * Getters for controls + * List of all managed typography controls */ get controls() { return Array.from(this.#controls.values()); } + /** + * Reactive instance for weight manipulation + */ get weightControl() { return this.#controls.get('font_weight')?.instance; } + /** + * Reactive instance for size manipulation + */ get sizeControl() { return this.#controls.get('font_size')?.instance; } + /** + * Reactive instance for line-height manipulation + */ get heightControl() { return this.#controls.get('line_height')?.instance; } + /** + * Reactive instance for letter-spacing manipulation + */ get spacingControl() { return this.#controls.get('letter_spacing')?.instance; } /** - * Getters for values (besides font-size) + * Current numeric font weight (reactive) */ get weight() { return this.#controls.get('font_weight')?.instance.value ?? DEFAULT_FONT_WEIGHT; } + /** + * Current numeric line height (reactive) + */ get height() { return this.#controls.get('line_height')?.instance.value ?? DEFAULT_LINE_HEIGHT; } + /** + * Current numeric letter spacing (reactive) + */ get spacing() { return this.#controls.get('letter_spacing')?.instance.value ?? DEFAULT_LETTER_SPACING; } /** - * Reset all controls to default values + * Reset all controls to project-defined defaults */ reset() { this.#storage.clear(); diff --git a/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts b/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts index ecdfd89..31d4c8d 100644 --- a/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts +++ b/src/features/SetupFont/lib/settingsManager/settingsManager.test.ts @@ -1,4 +1,6 @@ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ import { DEFAULT_FONT_SIZE, DEFAULT_FONT_WEIGHT, diff --git a/src/main.ts b/src/main.ts index c353493..fc2eaa3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,9 @@ +/** + * Application entry point + * + * Mounts the main App component to the DOM and initializes + * global styles. + */ import App from '$app/App.svelte'; import { mount } from 'svelte'; import '$app/styles/app.css'; diff --git a/src/shared/api/api.ts b/src/shared/api/api.ts index 40e12a8..3b17ea4 100644 --- a/src/shared/api/api.ts +++ b/src/shared/api/api.ts @@ -41,10 +41,14 @@ export class ApiError extends Error { * @param response - Original fetch Response object */ constructor( - /** HTTP status code */ + /** + * HTTP status code + */ public status: number, message: string, - /** Original Response object for inspection */ + /** + * Original Response object for inspection + */ public response?: Response, ) { super(message); diff --git a/src/shared/api/queryClient.ts b/src/shared/api/queryClient.ts index f6a25aa..cf16c92 100644 --- a/src/shared/api/queryClient.ts +++ b/src/shared/api/queryClient.ts @@ -15,15 +15,25 @@ import { QueryClient } from '@tanstack/query-core'; export const queryClient = new QueryClient({ defaultOptions: { queries: { - /** Data remains fresh for 5 minutes after fetch */ + /** + * Data remains fresh for 5 minutes after fetch + */ staleTime: 5 * 60 * 1000, - /** Unused cache entries are removed after 10 minutes */ + /** + * Unused cache entries are removed after 10 minutes + */ gcTime: 10 * 60 * 1000, - /** Don't refetch when window regains focus */ + /** + * Don't refetch when window regains focus + */ refetchOnWindowFocus: false, - /** Refetch on mount if data is stale */ + /** + * Refetch on mount if data is stale + */ refetchOnMount: true, - /** Retry failed requests up to 3 times */ + /** + * Retry failed requests up to 3 times + */ retry: 3, /** * Exponential backoff for retries diff --git a/src/shared/api/queryKeys.ts b/src/shared/api/queryKeys.ts index 93e5661..e9a4460 100644 --- a/src/shared/api/queryKeys.ts +++ b/src/shared/api/queryKeys.ts @@ -3,21 +3,35 @@ * Ensures consistent serialization for batch requests by sorting IDs. */ export const fontKeys = { - /** Base key for all font queries */ + /** + * Base key for all font queries + */ all: ['fonts'] as const, - /** Keys for font list queries */ + /** + * Keys for font list queries + */ lists: () => [...fontKeys.all, 'list'] as const, - /** Specific font list key with filter parameters */ + /** + * Specific font list key with filter parameters + */ list: (params: object) => [...fontKeys.lists(), params] as const, - /** Keys for font batch queries */ + /** + * Keys for font batch queries + */ batches: () => [...fontKeys.all, 'batch'] as const, - /** Specific batch key, sorted for stability */ + /** + * Specific batch key, sorted for stability + */ batch: (ids: string[]) => [...fontKeys.batches(), [...ids].sort()] as const, - /** Keys for font detail queries */ + /** + * Keys for font detail queries + */ details: () => [...fontKeys.all, 'detail'] as const, - /** Specific font detail key by ID */ + /** + * Specific font detail key by ID + */ detail: (id: string) => [...fontKeys.details(), id] as const, } as const; diff --git a/src/shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte.ts b/src/shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte.ts index 2f36bf1..5bcb186 100644 --- a/src/shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte.ts +++ b/src/shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte.ts @@ -12,20 +12,37 @@ import { * each font's actual advance widths independently. */ export interface ComparisonLine { - /** Full text of this line as returned by pretext. */ + /** + * Full text of this line as returned by pretext. + */ text: string; - /** Rendered width of this line in pixels — maximum across font A and font B. */ + /** + * Rendered width of this line in pixels — maximum across font A and font B. + */ width: number; + /** + * Individual character metadata for both fonts in this line + */ chars: Array<{ - /** The grapheme cluster string (may be >1 code unit for emoji, etc.). */ + /** + * The grapheme cluster string (may be >1 code unit for emoji, etc.). + */ char: string; - /** X offset from the start of the line in font A, in pixels. */ + /** + * X offset from the start of the line in font A, in pixels. + */ xA: number; - /** Advance width of this grapheme in font A, in pixels. */ + /** + * Advance width of this grapheme in font A, in pixels. + */ widthA: number; - /** X offset from the start of the line in font B, in pixels. */ + /** + * X offset from the start of the line in font B, in pixels. + */ xB: number; - /** Advance width of this grapheme in font B, in pixels. */ + /** + * Advance width of this grapheme in font B, in pixels. + */ widthB: number; }>; } @@ -34,9 +51,13 @@ export interface ComparisonLine { * Aggregated output of a dual-font layout pass. */ export interface ComparisonResult { - /** Per-line grapheme data for both fonts. Empty when input text is empty. */ + /** + * Per-line grapheme data for both fonts. Empty when input text is empty. + */ lines: ComparisonLine[]; - /** Total height in pixels. Equals `lines.length * lineHeight` (pretext guarantee). */ + /** + * Total height in pixels. Equals `lines.length * lineHeight` (pretext guarantee). + */ totalHeight: number; } diff --git a/src/shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.test.ts b/src/shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.test.ts index 28c6c4c..d7f9fe7 100644 --- a/src/shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.test.ts +++ b/src/shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.test.ts @@ -28,8 +28,6 @@ describe('CharacterComparisonEngine', () => { engine = new CharacterComparisonEngine(); }); - // --- layout() --- - it('returns empty result for empty string', () => { const result = engine.layout('', '400 16px "FontA"', '400 16px "FontB"', 500, 20); expect(result.lines).toHaveLength(0); @@ -111,8 +109,6 @@ describe('CharacterComparisonEngine', () => { expect(r2).not.toBe(r1); }); - // --- getCharState() --- - it('getCharState returns proximity 1 when slider is exactly over char center', () => { // 'A' only: FontA width=10. Container=500px. Line centered. // lineXOffset = (500 - maxWidth) / 2. maxWidth = max(10, 15) = 15 (FontB is wider). diff --git a/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts b/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts index 048bf0f..41739a9 100644 --- a/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts +++ b/src/shared/lib/helpers/TextLayoutEngine/TextLayoutEngine.svelte.ts @@ -10,16 +10,29 @@ import { * sequences and combining characters each produce exactly one entry. */ export interface LayoutLine { - /** Full text of this line as returned by pretext. */ + /** + * Full text of this line as returned by pretext. + */ text: string; - /** Rendered width of this line in pixels. */ + /** + * Rendered width of this line in pixels. + */ width: number; + /** + * Individual character metadata for this line + */ chars: Array<{ - /** The grapheme cluster string (may be >1 code unit for emoji, etc.). */ + /** + * The grapheme cluster string (may be >1 code unit for emoji, etc.). + */ char: string; - /** X offset from the start of the line, in pixels. */ + /** + * X offset from the start of the line, in pixels. + */ x: number; - /** Advance width of this grapheme, in pixels. */ + /** + * Advance width of this grapheme, in pixels. + */ width: number; }>; } @@ -28,9 +41,13 @@ export interface LayoutLine { * Aggregated output of a single-font layout pass. */ export interface LayoutResult { - /** Per-line grapheme data. Empty when input text is empty. */ + /** + * Per-line grapheme data. Empty when input text is empty. + */ lines: LayoutLine[]; - /** Total height in pixels. Equals `lines.length * lineHeight` (pretext guarantee). */ + /** + * Total height in pixels. Equals `lines.length * lineHeight` (pretext guarantee). + */ totalHeight: number; } @@ -65,7 +82,9 @@ export class TextLayoutEngine { */ #segmenter: Intl.Segmenter; - /** @param locale BCP 47 language tag passed to Intl.Segmenter. Defaults to the runtime locale. */ + /** + * @param locale BCP 47 language tag passed to Intl.Segmenter. Defaults to the runtime locale. + */ constructor(locale?: string) { this.#segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' }); } diff --git a/src/shared/lib/helpers/createDebouncedState/createDebouncedState.svelte.ts b/src/shared/lib/helpers/createDebouncedState/createDebouncedState.svelte.ts index 93840cc..7115d31 100644 --- a/src/shared/lib/helpers/createDebouncedState/createDebouncedState.svelte.ts +++ b/src/shared/lib/helpers/createDebouncedState/createDebouncedState.svelte.ts @@ -32,7 +32,9 @@ export function createDebouncedState(initialValue: T, wait: number = 300) { }, wait); return { - /** Current value with immediate updates (for UI binding) */ + /** + * Current value with immediate updates (for UI binding) + */ get immediate() { return immediate; }, @@ -41,7 +43,9 @@ export function createDebouncedState(initialValue: T, wait: number = 300) { // Manually trigger the debounce on write updateDebounced(value); }, - /** Current value with debounced updates (for logic/operations) */ + /** + * Current value with debounced updates (for logic/operations) + */ get debounced() { return debounced; }, diff --git a/src/shared/lib/helpers/createEntityStore/createEntityStore.svelte.ts b/src/shared/lib/helpers/createEntityStore/createEntityStore.svelte.ts index c304ab2..3977f7f 100644 --- a/src/shared/lib/helpers/createEntityStore/createEntityStore.svelte.ts +++ b/src/shared/lib/helpers/createEntityStore/createEntityStore.svelte.ts @@ -28,7 +28,9 @@ import { SvelteMap } from 'svelte/reactivity'; * Base entity interface requiring an ID field */ export interface Entity { - /** Unique identifier for the entity */ + /** + * Unique identifier for the entity + */ id: string; } @@ -39,7 +41,9 @@ export interface Entity { * triggers updates when entities are added, removed, or modified. */ export class EntityStore { - /** Reactive map of entities keyed by ID */ + /** + * Reactive map of entities keyed by ID + */ #entities = new SvelteMap(); /** diff --git a/src/shared/lib/helpers/createFilter/createFilter.svelte.ts b/src/shared/lib/helpers/createFilter/createFilter.svelte.ts index 2787482..d9e523d 100644 --- a/src/shared/lib/helpers/createFilter/createFilter.svelte.ts +++ b/src/shared/lib/helpers/createFilter/createFilter.svelte.ts @@ -29,13 +29,21 @@ * @template TValue - The type of the property value (typically string) */ export interface Property { - /** Unique identifier for the property */ + /** + * Unique string identifier for the filterable property + */ id: string; - /** Human-readable display name */ + /** + * Human-readable label for UI display + */ name: string; - /** Underlying value for filtering logic */ + /** + * Underlying machine-readable value used for filtering logic + */ value: TValue; - /** Whether the property is currently selected */ + /** + * Current selection status (reactive) + */ selected?: boolean; } @@ -45,7 +53,9 @@ export interface Property { * @template TValue - The type of property values */ export interface FilterModel { - /** Array of filterable properties */ + /** + * Collection of properties that can be toggled in this filter + */ properties: Property[]; } diff --git a/src/shared/lib/helpers/createPersistentStore/createPersistentStore.test.ts b/src/shared/lib/helpers/createPersistentStore/createPersistentStore.test.ts index 9cbdac3..7435a2a 100644 --- a/src/shared/lib/helpers/createPersistentStore/createPersistentStore.test.ts +++ b/src/shared/lib/helpers/createPersistentStore/createPersistentStore.test.ts @@ -1,4 +1,6 @@ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ import { afterEach, beforeEach, diff --git a/src/shared/lib/helpers/createPerspectiveManager/createPerspectiveManager.svelte.ts b/src/shared/lib/helpers/createPerspectiveManager/createPerspectiveManager.svelte.ts index 4ed4416..5c15c7a 100644 --- a/src/shared/lib/helpers/createPerspectiveManager/createPerspectiveManager.svelte.ts +++ b/src/shared/lib/helpers/createPerspectiveManager/createPerspectiveManager.svelte.ts @@ -32,19 +32,33 @@ import { Spring } from 'svelte/motion'; * Configuration options for perspective effects */ export interface PerspectiveConfig { - /** Z-axis translation per level in pixels */ + /** + * Z-axis translation per level in pixels + */ depthStep?: number; - /** Scale reduction per level (0-1) */ + /** + * Scale reduction per level (0-1) + */ scaleStep?: number; - /** Blur amount per level in pixels */ + /** + * Blur amount per level in pixels + */ blurStep?: number; - /** Opacity reduction per level (0-1) */ + /** + * Opacity reduction per level (0-1) + */ opacityStep?: number; - /** Parallax movement intensity per level */ + /** + * Parallax movement intensity per level + */ parallaxIntensity?: number; - /** Horizontal offset - positive for right, negative for left */ + /** + * Horizontal offset - positive for right, negative for left + */ horizontalOffset?: number; - /** Layout mode: 'center' for centered, 'split' for side-by-side */ + /** + * Layout mode: 'center' for centered, 'split' for side-by-side + */ layoutMode?: 'center' | 'split'; } diff --git a/src/shared/lib/helpers/createResponsiveManager/createResponsiveManager.svelte.ts b/src/shared/lib/helpers/createResponsiveManager/createResponsiveManager.svelte.ts index 413b848..8502228 100644 --- a/src/shared/lib/helpers/createResponsiveManager/createResponsiveManager.svelte.ts +++ b/src/shared/lib/helpers/createResponsiveManager/createResponsiveManager.svelte.ts @@ -39,15 +39,25 @@ * Customize to match your design system's breakpoints. */ export interface Breakpoints { - /** Mobile devices - default 640px */ + /** + * Mobile devices - default 640px + */ mobile: number; - /** Tablet portrait - default 768px */ + /** + * Tablet portrait - default 768px + */ tabletPortrait: number; - /** Tablet landscape - default 1024px */ + /** + * Tablet landscape - default 1024px + */ tablet: number; - /** Desktop - default 1280px */ + /** + * Desktop - default 1280px + */ desktop: number; - /** Large desktop - default 1536px */ + /** + * Large desktop - default 1536px + */ desktopLarge: number; } @@ -206,66 +216,108 @@ export function createResponsiveManager(customBreakpoints?: Partial ); return { - /** Viewport width in pixels */ + /** + * Current viewport width in pixels (reactive) + */ get width() { return width; }, - /** Viewport height in pixels */ + /** + * Current viewport height in pixels (reactive) + */ get height() { return height; }, - // Standard breakpoints + /** + * True if viewport width is below the mobile threshold + */ get isMobile() { return isMobile; }, + /** + * True if viewport width is between mobile and tablet portrait thresholds + */ get isTabletPortrait() { return isTabletPortrait; }, + /** + * True if viewport width is between tablet portrait and desktop thresholds + */ get isTablet() { return isTablet; }, + /** + * True if viewport width is between desktop and large desktop thresholds + */ get isDesktop() { return isDesktop; }, + /** + * True if viewport width is at or above the large desktop threshold + */ get isDesktopLarge() { return isDesktopLarge; }, - // Convenience groupings + /** + * True if viewport width is below the desktop threshold + */ get isMobileOrTablet() { return isMobileOrTablet; }, + /** + * True if viewport width is at or above the tablet portrait threshold + */ get isTabletOrDesktop() { return isTabletOrDesktop; }, - // Orientation + /** + * Current screen orientation (portrait | landscape) + */ get orientation() { return orientation; }, + /** + * True if screen height is greater than width + */ get isPortrait() { return isPortrait; }, + /** + * True if screen width is greater than height + */ get isLandscape() { return isLandscape; }, - // Device capabilities + /** + * True if the device supports touch interaction + */ get isTouchDevice() { return isTouchDevice; }, - // Current breakpoint + /** + * Name of the currently active breakpoint (reactive) + */ get currentBreakpoint() { return currentBreakpoint; }, - // Methods + /** + * Initialization function to start event listeners + */ init, + /** + * Helper to check for custom width ranges + */ matches, - // Breakpoint values (for custom logic) + /** + * Underlying breakpoint pixel values + */ breakpoints, }; } diff --git a/src/shared/lib/helpers/createTypographyControl/createTypographyControl.svelte.ts b/src/shared/lib/helpers/createTypographyControl/createTypographyControl.svelte.ts index 10df8a7..8d225ce 100644 --- a/src/shared/lib/helpers/createTypographyControl/createTypographyControl.svelte.ts +++ b/src/shared/lib/helpers/createTypographyControl/createTypographyControl.svelte.ts @@ -34,13 +34,21 @@ import { * Defines the bounds and stepping behavior for a control */ export interface ControlDataModel { - /** Current numeric value */ + /** + * Initial or current numeric value + */ value: number; - /** Minimum allowed value (inclusive) */ + /** + * Lower inclusive bound + */ min: number; - /** Maximum allowed value (inclusive) */ + /** + * Upper inclusive bound + */ max: number; - /** Step size for increment/decrement operations */ + /** + * Precision for increment/decrement operations + */ step: number; } @@ -50,13 +58,21 @@ export interface ControlDataModel { * @template T - Type for the control identifier */ export interface ControlModel extends ControlDataModel { - /** Unique identifier for the control */ + /** + * Unique string identifier for the control + */ id: T; - /** ARIA label for the increase button */ + /** + * Label used by screen readers for the increase button + */ increaseLabel?: string; - /** ARIA label for the decrease button */ + /** + * Label used by screen readers for the decrease button + */ decreaseLabel?: string; - /** ARIA label for the control area */ + /** + * Overall label describing the control's purpose + */ controlLabel?: string; } @@ -109,8 +125,7 @@ export function createTypographyControl( return { /** - * Current control value (getter/setter) - * Setting automatically clamps to bounds and rounds to step precision + * Clamped and rounded control value (reactive) */ get value() { return value; @@ -122,27 +137,37 @@ export function createTypographyControl( } }, - /** Maximum allowed value */ + /** + * Upper limit for the control value + */ get max() { return max; }, - /** Minimum allowed value */ + /** + * Lower limit for the control value + */ get min() { return min; }, - /** Step increment size */ + /** + * Configured step increment + */ get step() { return step; }, - /** Whether the value is at or exceeds the maximum */ + /** + * True if current value is equal to or greater than max + */ get isAtMax() { return isAtMax; }, - /** Whether the value is at or below the minimum */ + /** + * True if current value is equal to or less than min + */ get isAtMin() { return isAtMin; }, diff --git a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts index 301f1d6..fb0657c 100644 --- a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts +++ b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.svelte.ts @@ -45,7 +45,9 @@ export interface VirtualItem { * Options are reactive - pass them through a function getter to enable updates. */ export interface VirtualizerOptions { - /** Total number of items in the data array */ + /** + * Total number of items in the underlying data array + */ count: number; /** * Function to estimate the size of an item at a given index. @@ -60,7 +62,10 @@ export interface VirtualizerOptions { * as fonts finish loading, eliminating the DOM-measurement snap on load. */ estimateSize: (index: number) => number; - /** Number of extra items to render outside viewport for smoother scrolling (default: 5) */ + /** + * Number of extra items to render outside viewport for smoother scrolling + * @default 5 + */ overscan?: number; /** * Function to get the key of an item at a given index. @@ -464,27 +469,45 @@ export function createVirtualizer( } return { + /** + * Current vertical scroll position in pixels (reactive) + */ get scrollOffset() { return scrollOffset; }, + /** + * Measured height of the visible container area (reactive) + */ get containerHeight() { return containerHeight; }, - /** Computed array of visible items to render (reactive) */ + /** + * Computed array of visible items to render (reactive) + */ get items() { return items; }, - /** Total height of all items in pixels (reactive) */ + /** + * Total height of all items in pixels (reactive) + */ get totalSize() { return totalSize; }, - /** Svelte action for the scrollable container element */ + /** + * Svelte action for the scrollable container element + */ container, - /** Svelte action for measuring individual item elements */ + /** + * Svelte action for measuring individual item elements + */ measureElement, - /** Programmatic scroll method to scroll to a specific item */ + /** + * Programmatic scroll method to scroll to a specific item + */ scrollToIndex, - /** Programmatic scroll method to scroll to a specific pixel offset */ + /** + * Programmatic scroll method to scroll to a specific pixel offset + */ scrollToOffset, }; } diff --git a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.test.ts b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.test.ts index d78b551..faee1cd 100644 --- a/src/shared/lib/helpers/createVirtualizer/createVirtualizer.test.ts +++ b/src/shared/lib/helpers/createVirtualizer/createVirtualizer.test.ts @@ -1,4 +1,6 @@ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ import { afterEach, describe, diff --git a/src/shared/lib/helpers/index.ts b/src/shared/lib/helpers/index.ts index 1580dcd..ba2bb77 100644 --- a/src/shared/lib/helpers/index.ts +++ b/src/shared/lib/helpers/index.ts @@ -22,59 +22,178 @@ * ``` */ +/** + * Filter management + */ export { + /** + * Reactive filter factory + */ createFilter, + /** + * Filter instance type + */ type Filter, + /** + * Initial state model + */ type FilterModel, + /** + * Filterable property definition + */ type Property, } from './createFilter/createFilter.svelte'; +/** + * Bounded numeric controls + */ export { + /** + * Base numeric configuration + */ type ControlDataModel, + /** + * Extended model with labels + */ type ControlModel, + /** + * Reactive control factory + */ createTypographyControl, + /** + * Control instance type + */ type TypographyControl, } from './createTypographyControl/createTypographyControl.svelte'; +/** + * List virtualization + */ export { + /** + * Reactive virtualizer factory + */ createVirtualizer, + /** + * Rendered item layout data + */ type VirtualItem, + /** + * Virtualizer instance type + */ type Virtualizer, + /** + * Configuration options + */ type VirtualizerOptions, } from './createVirtualizer/createVirtualizer.svelte'; -export { createDebouncedState } from './createDebouncedState/createDebouncedState.svelte'; - +/** + * UI State + */ export { + /** + * Immediate/debounced state factory + */ + createDebouncedState, +} from './createDebouncedState/createDebouncedState.svelte'; + +/** + * Entity collections + */ +export { + /** + * Reactive entity store factory + */ createEntityStore, + /** + * Base entity requirement + */ type Entity, + /** + * Entity store instance type + */ type EntityStore, } from './createEntityStore/createEntityStore.svelte'; +/** + * Comparison logic + */ export { + /** + * Character-by-character comparison utility + */ CharacterComparisonEngine, + /** + * Single line of comparison results + */ type ComparisonLine, + /** + * Full comparison output + */ type ComparisonResult, } from './CharacterComparisonEngine/CharacterComparisonEngine.svelte'; +/** + * Text layout + */ export { + /** + * Single line layout information + */ type LayoutLine as TextLayoutLine, + /** + * Full multi-line layout information + */ type LayoutResult as TextLayoutResult, + /** + * High-level text measurement engine + */ TextLayoutEngine, } from './TextLayoutEngine/TextLayoutEngine.svelte'; +/** + * Persistence + */ export { + /** + * LocalStorage-backed reactive store factory + */ createPersistentStore, + /** + * Persistent store instance type + */ type PersistentStore, } from './createPersistentStore/createPersistentStore.svelte'; +/** + * Responsive design + */ export { + /** + * Breakpoint tracking factory + */ createResponsiveManager, + /** + * Responsive manager instance type + */ type ResponsiveManager, + /** + * Singleton manager for global usage + */ responsiveManager, } from './createResponsiveManager/createResponsiveManager.svelte'; +/** + * 3D Perspectives + */ export { + /** + * Motion-aware perspective factory + */ createPerspectiveManager, + /** + * Perspective manager instance type + */ type PerspectiveManager, } from './createPerspectiveManager/createPerspectiveManager.svelte'; diff --git a/src/shared/lib/utils/smoothScroll/smoothScroll.test.ts b/src/shared/lib/utils/smoothScroll/smoothScroll.test.ts index 39d6044..37dba65 100644 --- a/src/shared/lib/utils/smoothScroll/smoothScroll.test.ts +++ b/src/shared/lib/utils/smoothScroll/smoothScroll.test.ts @@ -1,4 +1,6 @@ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ import { afterEach, beforeEach, diff --git a/src/shared/types/common.ts b/src/shared/types/common.ts index 3dfe594..5911a63 100644 --- a/src/shared/types/common.ts +++ b/src/shared/types/common.ts @@ -7,8 +7,12 @@ * @template T - Type of the response data */ export interface ApiResponse { - /** Response payload data */ + /** + * Primary data payload returned by the server + */ data: T; - /** HTTP status code */ + /** + * HTTP status code (e.g. 200, 404, 500) + */ status: number; } diff --git a/src/shared/ui/Input/config.ts b/src/shared/ui/Input/config.ts index 30964fe..77d3499 100644 --- a/src/shared/ui/Input/config.ts +++ b/src/shared/ui/Input/config.ts @@ -3,7 +3,9 @@ import type { InputVariant, } from './types'; -/** Size-specific layout classes: padding, text size, height, and clear-icon pixel size. */ +/** + * Size-specific layout classes: padding, text size, height, and clear-icon pixel size. + */ export const inputSizeConfig: Record = { sm: { input: 'px-3 py-1.5', text: 'text-sm', height: 'h-8', clearIcon: 12 }, md: { input: 'px-4 py-2', text: 'text-base', height: 'h-10', clearIcon: 14 }, @@ -11,7 +13,9 @@ export const inputSizeConfig: Record = { default: { base: 'bg-paper dark:bg-paper border border-subtle', diff --git a/src/shared/ui/Input/types.ts b/src/shared/ui/Input/types.ts index 9947652..6432603 100644 --- a/src/shared/ui/Input/types.ts +++ b/src/shared/ui/Input/types.ts @@ -1,6 +1,8 @@ export type InputVariant = 'default' | 'underline' | 'filled'; export type InputSize = 'sm' | 'md' | 'lg' | 'xl'; -/** Convenience map for consumers sizing icons to match the input. */ +/** + * Convenience map for consumers sizing icons to match the input. + */ export const inputIconSize: Record = { sm: 14, md: 16, diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index b4aa258..7be032d 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,28 +1,150 @@ -export { default as Badge } from './Badge/Badge.svelte'; export { + /** + * Pill-shaped status indicator + */ + default as Badge, +} from './Badge/Badge.svelte'; +export { + /** + * Main action trigger + */ Button, + /** + * Horizontal layout for related buttons + */ ButtonGroup, + /** + * Button optimized for single-icon display + */ IconButton, + /** + * State-aware toggle switch + */ ToggleButton, } from './Button'; -export { default as ComboControl } from './ComboControl/ComboControl.svelte'; -export { default as ContentEditable } from './ContentEditable/ContentEditable.svelte'; -export { default as ControlGroup } from './ControlGroup/ControlGroup.svelte'; -export { default as Divider } from './Divider/Divider.svelte'; -export { default as FilterGroup } from './FilterGroup/FilterGroup.svelte'; -export { default as Footnote } from './Footnote/Footnote.svelte'; -export { default as Input } from './Input/Input.svelte'; -export { default as Label } from './Label/Label.svelte'; -export { default as Loader } from './Loader/Loader.svelte'; -export { default as Logo } from './Logo/Logo.svelte'; -export { default as PerspectivePlan } from './PerspectivePlan/PerspectivePlan.svelte'; -export { default as SearchBar } from './SearchBar/SearchBar.svelte'; -export { default as Section } from './Section/Section.svelte'; -export type { TitleStatusChangeHandler } from './Section/types'; -export { default as SidebarContainer } from './SidebarContainer/SidebarContainer.svelte'; -export { default as Skeleton } from './Skeleton/Skeleton.svelte'; -export { default as Slider } from './Slider/Slider.svelte'; -export { default as Stat } from './Stat/Stat.svelte'; -export { default as StatGroup } from './Stat/StatGroup.svelte'; -export { default as TechText } from './TechText/TechText.svelte'; -export { default as VirtualList } from './VirtualList/VirtualList.svelte'; +export { + /** + * Input with associated increment/decrement controls + */ + default as ComboControl, +} from './ComboControl/ComboControl.svelte'; +export { + /** + * Rich text input using contenteditable attribute + */ + default as ContentEditable, +} from './ContentEditable/ContentEditable.svelte'; +export { + /** + * Semantic grouping for related UI controls + */ + default as ControlGroup, +} from './ControlGroup/ControlGroup.svelte'; +export { + /** + * Simple horizontal line separator + */ + default as Divider, +} from './Divider/Divider.svelte'; +export { + /** + * Filterable property set with selection logic + */ + default as FilterGroup, +} from './FilterGroup/FilterGroup.svelte'; +export { + /** + * Small text for secondary meta-information + */ + default as Footnote, +} from './Footnote/Footnote.svelte'; +export { + /** + * Design-system standard text input + */ + default as Input, +} from './Input/Input.svelte'; +export { + /** + * Text label for input fields + */ + default as Label, +} from './Label/Label.svelte'; +export { + /** + * Full-page or component-level progress spinner + */ + default as Loader, +} from './Loader/Loader.svelte'; +export { + /** + * Main application logo + */ + default as Logo, +} from './Logo/Logo.svelte'; +export { + /** + * 3D perspective background/container + */ + default as PerspectivePlan, +} from './PerspectivePlan/PerspectivePlan.svelte'; +export { + /** + * Specialized input with search icon and clear state + */ + default as SearchBar, +} from './SearchBar/SearchBar.svelte'; +export { + /** + * Content section with header and optional title tracking + */ + default as Section, +} from './Section/Section.svelte'; +export { + /** + * Callback for section visibility status changes + */ + type TitleStatusChangeHandler, +} from './Section/types'; +export { + /** + * Structural sidebar component + */ + default as SidebarContainer, +} from './SidebarContainer/SidebarContainer.svelte'; +export { + /** + * Loading placeholder with pulsing animation + */ + default as Skeleton, +} from './Skeleton/Skeleton.svelte'; +export { + /** + * Range selector with numeric feedback + */ + default as Slider, +} from './Slider/Slider.svelte'; +export { + /** + * Individual numeric statistic display + */ + default as Stat, +} from './Stat/Stat.svelte'; +export { + /** + * Grouping for multiple statistics + */ + default as StatGroup, +} from './Stat/StatGroup.svelte'; +export { + /** + * Mono-spaced technical/metadata text + */ + default as TechText, +} from './TechText/TechText.svelte'; +export { + /** + * High-performance list renderer for large datasets + */ + default as VirtualList, +} from './VirtualList/VirtualList.svelte'; diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts index aef2e02..c1e80b9 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts @@ -29,9 +29,13 @@ import { untrack } from 'svelte'; * Storage schema for comparison state */ interface ComparisonState { - /** Font ID for side A (left/top) */ + /** + * Unique identifier for the primary font being compared (Side A) + */ fontAId: string | null; - /** Font ID for side B (right/bottom) */ + /** + * Unique identifier for the secondary font being compared (Side B) + */ fontBId: string | null; } @@ -53,21 +57,33 @@ const storage = createPersistentStore('glyphdiff:comparison', { * storage is empty. */ export class ComparisonStore { - /** Font for side A */ + /** + * The primary font model for Side A (left/top) + */ #fontA = $state(); - /** Font for side B */ + /** + * The secondary font model for Side B (right/bottom) + */ #fontB = $state(); - /** Sample text to display */ + /** + * The preview text string displayed in the comparison area + */ #sampleText = $state('The quick brown fox jumps over the lazy dog'); - /** Whether fonts are loaded and ready to display */ + /** + * Flag indicating if both fonts are successfully loaded and ready for rendering + */ #fontsReady = $state(false); - /** Active side for single-font operations */ + /** + * Currently active side (A or B) for single-font adjustments + */ #side = $state('A'); - /** Slider position for character morphing (0-100) */ + /** + * Interactive slider position (0-100) used for morphing/layout transitions + */ #sliderPosition = $state(50); - // /** Typography controls for this comparison */ - // #typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography'); - /** TanStack Query-backed batch font fetcher */ + /** + * TanStack Query-backed store for efficient batch font retrieval + */ #batchStore: BatchFontStore; constructor() { @@ -211,14 +227,9 @@ export class ComparisonStore { }; } - // // ── Getters / Setters ───────────────────────────────────────────────────── - - // /** Typography control manager */ - // get typography() { - // return typographySettingsStore; - // } - - /** Font for side A */ + /** + * Primary font for comparison (reactive) + */ get fontA() { return this.#fontA; } @@ -228,7 +239,9 @@ export class ComparisonStore { this.updateStorage(); } - /** Font for side B */ + /** + * Secondary font for comparison (reactive) + */ get fontB() { return this.#fontB; } @@ -238,7 +251,9 @@ export class ComparisonStore { this.updateStorage(); } - /** Sample text to display */ + /** + * Shared preview text string (reactive) + */ get text() { return this.#sampleText; } @@ -247,7 +262,9 @@ export class ComparisonStore { this.#sampleText = value; } - /** Active side for single-font operations */ + /** + * Side currently selected for focused manipulation (reactive) + */ get side() { return this.#side; } @@ -256,7 +273,9 @@ export class ComparisonStore { this.#side = value; } - /** Slider position (0-100) for character morphing */ + /** + * Morphing slider position (0-100) used by Character components (reactive) + */ get sliderPosition() { return this.#sliderPosition; } @@ -265,12 +284,16 @@ export class ComparisonStore { this.#sliderPosition = value; } - /** Whether both fonts are selected and loaded */ + /** + * True if both fonts are ready for side-by-side display (reactive) + */ get isReady() { return !!this.#fontA && !!this.#fontB && this.#fontsReady; } - /** Whether currently loading (batch fetch in flight or fonts not yet painted) */ + /** + * True if any font is currently being fetched or loaded (reactive) + */ get isLoading() { return this.#batchStore.isLoading || !this.#fontsReady; } diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts index 049bd65..775f922 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts @@ -5,7 +5,9 @@ * Controls network behaviour via vi.spyOn on the proxyFonts API layer. */ -/** @vitest-environment jsdom */ +/** + * @vitest-environment jsdom + */ import type { UnifiedFont } from '$entities/Font'; import { UNIFIED_FONTS } from '$entities/Font/lib/mocks'; @@ -18,8 +20,6 @@ import { vi, } from 'vitest'; -// ── Persistent-store mock ───────────────────────────────────────────────────── - const mockStorage = vi.hoisted(() => { const storage: any = {}; storage._value = { fontAId: null, fontBId: null }; @@ -51,8 +51,6 @@ vi.mock('$shared/lib/helpers/createPersistentStore/createPersistentStore.svelte' createPersistentStore: vi.fn(() => mockStorage), })); -// ── $entities/Font mock — keep real BatchFontStore, stub singletons ─────────── - vi.mock('$entities/Font', async importOriginal => { const actual = await importOriginal(); const { BatchFontStore } = await import( @@ -73,8 +71,6 @@ vi.mock('$entities/Font', async importOriginal => { }; }); -// ── $features/SetupFont mock ────────────────────────────────────────────────── - vi.mock('$features/SetupFont', () => ({ DEFAULT_TYPOGRAPHY_CONTROLS_DATA: [], createTypographyControlManager: vi.fn(() => ({ @@ -92,8 +88,6 @@ vi.mock('$features/SetupFont/model', () => ({ }, })); -// ── Imports (after mocks) ───────────────────────────────────────────────────── - import { appliedFontsManager, fontStore, @@ -101,8 +95,6 @@ import { import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts'; import { ComparisonStore } from './comparisonStore.svelte'; -// ── Tests ───────────────────────────────────────────────────────────────────── - describe('ComparisonStore', () => { const mockFontA: UnifiedFont = UNIFIED_FONTS.roboto; // id: 'roboto' const mockFontB: UnifiedFont = UNIFIED_FONTS.openSans; // id: 'open-sans' @@ -129,8 +121,6 @@ describe('ComparisonStore', () => { }); }); - // ── Initialization ──────────────────────────────────────────────────────── - describe('Initialization', () => { it('should create store with initial empty state', () => { const store = new ComparisonStore(); @@ -139,8 +129,6 @@ describe('ComparisonStore', () => { }); }); - // ── Restoration from Storage ────────────────────────────────────────────── - describe('Restoration from Storage (via BatchFontStore)', () => { it('should restore fontA and fontB from stored IDs', async () => { mockStorage._value.fontAId = mockFontA.id; @@ -169,8 +157,6 @@ describe('ComparisonStore', () => { }); }); - // ── Default Fallbacks ───────────────────────────────────────────────────── - describe('Default Fallbacks', () => { it('should update storage with default IDs when storage is empty', async () => { (fontStore as any).fonts = [mockFontA, mockFontB]; @@ -185,8 +171,6 @@ describe('ComparisonStore', () => { }); }); - // ── Loading State ───────────────────────────────────────────────────────── - describe('Aggregate Loading State', () => { it('should be loading initially when storage has IDs', async () => { mockStorage._value.fontAId = mockFontA.id; @@ -202,8 +186,6 @@ describe('ComparisonStore', () => { }); }); - // ── Reset ───────────────────────────────────────────────────────────────── - describe('Reset Functionality', () => { it('should reset all state and clear storage', () => { const store = new ComparisonStore(); @@ -225,8 +207,6 @@ describe('ComparisonStore', () => { }); }); - // ── Pin / Unpin ─────────────────────────────────────────────────────────── - describe('Pin / Unpin (eviction guard)', () => { it('pins fontA and fontB when they are loaded', async () => { mockStorage._value.fontAId = mockFontA.id; diff --git a/src/widgets/ComparisonView/ui/Search/Search.svelte b/src/widgets/ComparisonView/ui/Search/Search.svelte index 0f0eba4..d221c80 100644 --- a/src/widgets/ComparisonView/ui/Search/Search.svelte +++ b/src/widgets/ComparisonView/ui/Search/Search.svelte @@ -1,3 +1,8 @@ + - - {@render children()} - +{@render children()} diff --git a/src/app/ui/Layout.svelte b/src/app/ui/Layout.svelte index 85fa981..9feb665 100644 --- a/src/app/ui/Layout.svelte +++ b/src/app/ui/Layout.svelte @@ -14,11 +14,9 @@ * * - Footer area (currently empty, reserved for future use) */ -import { BreadcrumbHeader } from '$entities/Breadcrumb'; import { themeManager } from '$features/ChangeAppTheme'; import GD from '$shared/assets/GD.svg'; import { ResponsiveProvider } from '$shared/lib'; -import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip'; import { cn } from '$shared/shadcn/utils/shadcn-utils'; import { type Snippet, @@ -85,11 +83,9 @@ onDestroy(() => themeManager.destroy()); theme === 'dark' ? 'dark' : '', )} > - - {#if fontsReady} - {@render children?.()} - {/if} - + {#if fontsReady} + {@render children?.()} + {/if}
diff --git a/src/shared/lib/storybook/Providers.svelte b/src/shared/lib/storybook/Providers.svelte index 2fe10e9..24fda3a 100644 --- a/src/shared/lib/storybook/Providers.svelte +++ b/src/shared/lib/storybook/Providers.svelte @@ -10,7 +10,6 @@
- - {@render children()} - + {@render children()}
From 0004b81e40dc3643811aa70bdc63b780a4b2c18a Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 13:20:47 +0300 Subject: [PATCH 26/83] chore(ComboControl): replace shadcn tooltip with the one from bits-ui --- src/shared/ui/ComboControl/ComboControl.svelte | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/shared/ui/ComboControl/ComboControl.svelte b/src/shared/ui/ComboControl/ComboControl.svelte index 41142e7..b261147 100644 --- a/src/shared/ui/ComboControl/ComboControl.svelte +++ b/src/shared/ui/ComboControl/ComboControl.svelte @@ -5,16 +5,12 @@ -->
{#snippet icon()} diff --git a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte index 6f517e9..05bd94e 100644 --- a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte +++ b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte @@ -12,7 +12,6 @@ import { MULTIPLIER_S, } from '$entities/Font'; import type { ResponsiveManager } from '$shared/lib'; -import { cn } from '$shared/shadcn/utils/shadcn-utils'; import { ComboControl, ControlGroup, @@ -21,6 +20,7 @@ import { import Settings2Icon from '@lucide/svelte/icons/settings-2'; import XIcon from '@lucide/svelte/icons/x'; import { Popover } from 'bits-ui'; +import clsx from 'clsx'; import { getContext } from 'svelte'; import { cubicOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; @@ -74,7 +74,7 @@ $effect(() => { {#snippet child({ props })} + {/snippet} From bb5c3667b41af747d597ee6753ed4455e134628a Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 14:39:25 +0300 Subject: [PATCH 31/83] feat(SliderArea): utilize responsive breakpoints for TypographyMenu positioning --- src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte index 13dbd10..c083d8c 100644 --- a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte +++ b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte @@ -254,7 +254,10 @@ const scaleClass = $derived(
From 5eb9584797af8fadf186e8823093b009b96be985 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 16:30:41 +0300 Subject: [PATCH 32/83] feat(TypographyMenu): add bindable "open" prop to close popover from outside --- .../ui/TypographyMenu/TypographyMenu.svelte | 14 ++++++++------ .../ComparisonView/ui/SliderArea/SliderArea.svelte | 6 +++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte index e9e320a..0bd8555 100644 --- a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte +++ b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.svelte @@ -1,7 +1,6 @@ @@ -37,14 +36,17 @@ interface Props { * @default false */ hidden?: boolean; + /** + * Bindable popover open state + * @default false + */ + open?: boolean; } -const { class: className, hidden = false }: Props = $props(); +let { class: className, hidden = false, open = $bindable(false) }: Props = $props(); const responsive = getContext('responsive'); -let isOpen = $state(false); - /** * Sets the common font size multiplier based on the current responsive state. */ @@ -70,7 +72,7 @@ $effect(() => { {#if !hidden} {#if responsive.isMobileOrTablet} - + {#snippet child({ props })}
Date: Fri, 17 Apr 2026 18:01:24 +0300 Subject: [PATCH 33/83] feat: add missing storybook files and type template arguments properly --- .../BreadcrumbHeader.stories.svelte | 65 ++++++++++ .../BreadcrumbHeaderSeeded.svelte | 49 ++++++++ .../NavigationWrapper.stories.svelte | 109 +++++++++++++++++ .../FontApplicator.stories.svelte | 91 ++++++++++++++ .../FontVirtualList.stories.svelte | 114 ++++++++++++++++++ .../ui/FontSampler/FontSampler.stories.svelte | 5 +- .../ui/Filters/Filters.stories.svelte | 26 ++++ .../FilterControls.stories.svelte | 39 ++++++ .../TypographyMenu.stories.svelte | 54 +++++++++ src/shared/ui/Button/Button.stories.svelte | 13 +- .../ui/Button/ButtonGroup.stories.svelte | 11 +- .../ui/Button/IconButton.stories.svelte | 9 +- .../ui/Button/ToggleButton.stories.svelte | 19 ++- .../ComboControl/ComboControl.stories.svelte | 3 +- .../ContentEditable.stories.svelte | 12 +- .../ControlGroup/ControlGroup.stories.svelte | 53 ++++++++ .../ui/FilterGroup/FilterGroup.stories.svelte | 19 ++- .../ui/Footnote/Footnote.stories.svelte | 8 +- src/shared/ui/Input/Input.stories.svelte | 35 +++--- src/shared/ui/Label/Label.stories.svelte | 25 ++-- src/shared/ui/Loader/Loader.stories.svelte | 6 +- src/shared/ui/Logo/Logo.stories.svelte | 6 +- .../PerspectivePlan.stories.svelte | 91 ++++++++++++++ .../ui/SearchBar/SearchBar.stories.svelte | 7 +- .../SectionSeparator.stories.svelte | 45 +++++++ .../SectionTitle/SectionTitle.stories.svelte | 45 +++++++ .../SidebarContainer.stories.svelte | 102 ++++++++++++++++ .../ui/Skeleton/Skeleton.stories.svelte | 6 +- src/shared/ui/Slider/Slider.stories.svelte | 66 ++++++++-- src/shared/ui/Stat/StatGroup.stories.svelte | 52 ++++++++ .../ui/VirtualList/VirtualList.stories.svelte | 29 +++-- 31 files changed, 1124 insertions(+), 90 deletions(-) create mode 100644 src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.stories.svelte create mode 100644 src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeaderSeeded.svelte create mode 100644 src/entities/Breadcrumb/ui/NavigationWrapper/NavigationWrapper.stories.svelte create mode 100644 src/entities/Font/ui/FontApplicator/FontApplicator.stories.svelte create mode 100644 src/entities/Font/ui/FontVirtualList/FontVirtualList.stories.svelte create mode 100644 src/features/GetFonts/ui/Filters/Filters.stories.svelte create mode 100644 src/features/GetFonts/ui/FiltersControl/FilterControls.stories.svelte create mode 100644 src/features/SetupFont/ui/TypographyMenu/TypographyMenu.stories.svelte create mode 100644 src/shared/ui/ControlGroup/ControlGroup.stories.svelte create mode 100644 src/shared/ui/PerspectivePlan/PerspectivePlan.stories.svelte create mode 100644 src/shared/ui/Section/SectionSeparator/SectionSeparator.stories.svelte create mode 100644 src/shared/ui/Section/SectionTitle/SectionTitle.stories.svelte create mode 100644 src/shared/ui/SidebarContainer/SidebarContainer.stories.svelte create mode 100644 src/shared/ui/Stat/StatGroup.stories.svelte diff --git a/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.stories.svelte b/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.stories.svelte new file mode 100644 index 0000000..bdec72a --- /dev/null +++ b/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeader.stories.svelte @@ -0,0 +1,65 @@ + + + + + + {#snippet template()} + + + + {/snippet} + + + + {#snippet template()} + +
+ BreadcrumbHeader renders nothing when scrolledPastItems is empty. +
+ +
+ {/snippet} +
diff --git a/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeaderSeeded.svelte b/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeaderSeeded.svelte new file mode 100644 index 0000000..792d86c --- /dev/null +++ b/src/entities/Breadcrumb/ui/BreadcrumbHeader/BreadcrumbHeaderSeeded.svelte @@ -0,0 +1,49 @@ + + +
+ {#each sections as section} +
+ {section.title} — scroll up to see the breadcrumb header +
+ {/each} +
+ + diff --git a/src/entities/Breadcrumb/ui/NavigationWrapper/NavigationWrapper.stories.svelte b/src/entities/Breadcrumb/ui/NavigationWrapper/NavigationWrapper.stories.svelte new file mode 100644 index 0000000..d3adb47 --- /dev/null +++ b/src/entities/Breadcrumb/ui/NavigationWrapper/NavigationWrapper.stories.svelte @@ -0,0 +1,109 @@ + + + + + + {#snippet template(args: ComponentProps)} + + {#snippet content(register)} +
+

+ Section registered as {args.title} at index {args.index}. Scroll past this + section to see it appear in the breadcrumb header. +

+
+ {/snippet} +
+ {/snippet} +
+ + + {#snippet template()} +
+ + {#snippet content(register)} +
+

Introduction

+

+ Registered as section 0. Scroll down to build the breadcrumb trail. +

+
+ {/snippet} +
+ + + {#snippet content(register)} +
+

Typography

+

Registered as section 1.

+
+ {/snippet} +
+ + + {#snippet content(register)} +
+

Spacing

+

Registered as section 2.

+
+ {/snippet} +
+
+ {/snippet} +
diff --git a/src/entities/Font/ui/FontApplicator/FontApplicator.stories.svelte b/src/entities/Font/ui/FontApplicator/FontApplicator.stories.svelte new file mode 100644 index 0000000..ff14ba7 --- /dev/null +++ b/src/entities/Font/ui/FontApplicator/FontApplicator.stories.svelte @@ -0,0 +1,91 @@ + + + + + + {#snippet template(args: ComponentProps)} + +

The quick brown fox jumps over the lazy dog

+
+ {/snippet} +
+ + + {#snippet template(args: ComponentProps)} + +

The quick brown fox jumps over the lazy dog

+
+ {/snippet} +
+ + + {#snippet template(args: ComponentProps)} + +

The quick brown fox jumps over the lazy dog

+
+ {/snippet} +
diff --git a/src/entities/Font/ui/FontVirtualList/FontVirtualList.stories.svelte b/src/entities/Font/ui/FontVirtualList/FontVirtualList.stories.svelte new file mode 100644 index 0000000..caf0107 --- /dev/null +++ b/src/entities/Font/ui/FontVirtualList/FontVirtualList.stories.svelte @@ -0,0 +1,114 @@ + + + + + + {#snippet template(args: ComponentProps)} +
+ + {#snippet skeleton()} +
+ {#each Array(6) as _} +
+ {/each} +
+ {/snippet} + {#snippet children({ item })} +
{item.name}
+ {/snippet} +
+
+ {/snippet} +
+ + + {#snippet template(args: ComponentProps)} +
+ + {#snippet children({ item })} +
{item.name}
+ {/snippet} +
+
+ {/snippet} +
+ + + {#snippet template(args: ComponentProps)} +
+ + {#snippet skeleton()} +
+ {#each Array(6) as _} +
+ {/each} +
+ {/snippet} + {#snippet children({ item })} +
+ {item.name} + {item.category} +
+ {/snippet} +
+
+ {/snippet} +
diff --git a/src/features/DisplayFont/ui/FontSampler/FontSampler.stories.svelte b/src/features/DisplayFont/ui/FontSampler/FontSampler.stories.svelte index 19c1d84..ffc2a3f 100644 --- a/src/features/DisplayFont/ui/FontSampler/FontSampler.stories.svelte +++ b/src/features/DisplayFont/ui/FontSampler/FontSampler.stories.svelte @@ -35,6 +35,7 @@ const { Story } = defineMeta({ + + + {#snippet template()} + + {/snippet} + diff --git a/src/features/GetFonts/ui/FiltersControl/FilterControls.stories.svelte b/src/features/GetFonts/ui/FiltersControl/FilterControls.stories.svelte new file mode 100644 index 0000000..9f935a9 --- /dev/null +++ b/src/features/GetFonts/ui/FiltersControl/FilterControls.stories.svelte @@ -0,0 +1,39 @@ + + + + {#snippet template()} + + + + {/snippet} + + + + {#snippet template()} + +
+ +
+
+ {/snippet} +
diff --git a/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.stories.svelte b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.stories.svelte new file mode 100644 index 0000000..20dde39 --- /dev/null +++ b/src/features/SetupFont/ui/TypographyMenu/TypographyMenu.stories.svelte @@ -0,0 +1,54 @@ + + + + {#snippet template()} + +
+ +
+
+ {/snippet} +
+ + + {#snippet template()} + +
+ +
+
+ {/snippet} +
+ + + {#snippet template()} + +
+
+
+ {/snippet} +
diff --git a/src/shared/ui/Button/Button.stories.svelte b/src/shared/ui/Button/Button.stories.svelte index 990177c..acb1748 100644 --- a/src/shared/ui/Button/Button.stories.svelte +++ b/src/shared/ui/Button/Button.stories.svelte @@ -45,6 +45,7 @@ const { Story } = defineMeta({ @@ -52,7 +53,7 @@ import ButtonGroup from './ButtonGroup.svelte'; name="Default/Basic" parameters={{ docs: { description: { story: 'Standard text button at all sizes' } } }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} @@ -67,7 +68,7 @@ import ButtonGroup from './ButtonGroup.svelte'; name="Default/With Icon" args={{ variant: 'secondary', size: 'md', iconPosition: 'left', active: false, animate: true }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} @@ -90,7 +91,7 @@ import ButtonGroup from './ButtonGroup.svelte'; name="Secondary" args={{ variant: 'secondary', size: 'md', iconPosition: 'left', active: false, animate: true }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} @@ -99,7 +100,7 @@ import ButtonGroup from './ButtonGroup.svelte'; name="Icon" args={{ variant: 'icon', size: 'md', iconPosition: 'left', active: false, animate: true }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} diff --git a/src/shared/ui/Button/ButtonGroup.stories.svelte b/src/shared/ui/Button/ButtonGroup.stories.svelte index 2babde6..b379c3f 100644 --- a/src/shared/ui/Button/ButtonGroup.stories.svelte +++ b/src/shared/ui/Button/ButtonGroup.stories.svelte @@ -27,10 +27,11 @@ const { Story } = defineMeta({ - {#snippet template(args)} + {#snippet template(args: ComponentProps)} @@ -40,7 +41,7 @@ import { Button } from '$shared/ui/Button'; - {#snippet template(args)} + {#snippet template(args: ComponentProps)} @@ -51,7 +52,7 @@ import { Button } from '$shared/ui/Button'; - {#snippet template(args)} + {#snippet template(args: ComponentProps)} @@ -61,7 +62,7 @@ import { Button } from '$shared/ui/Button'; - {#snippet template(args)} + {#snippet template(args: ComponentProps)} @@ -78,7 +79,7 @@ import { Button } from '$shared/ui/Button'; }, }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)}

Dark Mode

diff --git a/src/shared/ui/Button/IconButton.stories.svelte b/src/shared/ui/Button/IconButton.stories.svelte index b097d6f..7b5793c 100644 --- a/src/shared/ui/Button/IconButton.stories.svelte +++ b/src/shared/ui/Button/IconButton.stories.svelte @@ -43,13 +43,14 @@ const { Story } = defineMeta({ import MoonIcon from '@lucide/svelte/icons/moon'; import SearchIcon from '@lucide/svelte/icons/search'; import TrashIcon from '@lucide/svelte/icons/trash-2'; +import type { ComponentProps } from 'svelte'; - {#snippet template(args)} + {#snippet template(args: ComponentProps)}
{#snippet icon()} @@ -74,7 +75,7 @@ import TrashIcon from '@lucide/svelte/icons/trash-2'; name="Variants" args={{ size: 'md', active: false, animate: true }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)}
{#snippet icon()} @@ -99,7 +100,7 @@ import TrashIcon from '@lucide/svelte/icons/trash-2'; name="Active State" args={{ size: 'md', animate: true }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)}
{#snippet icon()} @@ -123,7 +124,7 @@ import TrashIcon from '@lucide/svelte/icons/trash-2'; }, }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)}

Dark Mode

diff --git a/src/shared/ui/Button/ToggleButton.stories.svelte b/src/shared/ui/Button/ToggleButton.stories.svelte index 73b115e..86d45b6 100644 --- a/src/shared/ui/Button/ToggleButton.stories.svelte +++ b/src/shared/ui/Button/ToggleButton.stories.svelte @@ -44,6 +44,7 @@ const { Story } = defineMeta({ @@ -51,7 +52,7 @@ let selected = $state(false); name="Default" args={{ variant: 'tertiary', size: 'md', selected: false, animate: true }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} Toggle Me {/snippet} @@ -60,7 +61,7 @@ let selected = $state(false); name="Selected/Unselected" args={{ variant: 'tertiary', size: 'md', animate: true }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)}
Unselected @@ -76,7 +77,7 @@ let selected = $state(false); name="Variants" args={{ size: 'md', selected: true, animate: true }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)}
Primary @@ -101,9 +102,15 @@ let selected = $state(false); name="Interactive" args={{ variant: 'tertiary', size: 'md', animate: true }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)}
- selected = !selected}> + { + selected = !selected; + }} + > Click to toggle Currently: {selected ? 'selected' : 'unselected'} @@ -119,7 +126,7 @@ let selected = $state(false); }, }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)}

Dark Mode

diff --git a/src/shared/ui/ComboControl/ComboControl.stories.svelte b/src/shared/ui/ComboControl/ComboControl.stories.svelte index 776669f..bdaf27f 100644 --- a/src/shared/ui/ComboControl/ComboControl.stories.svelte +++ b/src/shared/ui/ComboControl/ComboControl.stories.svelte @@ -30,6 +30,7 @@ const { Story } = defineMeta({ @@ -40,7 +41,7 @@ const horizontalControl = createTypographyControl({ min: 0, max: 100, step: 1, v label: 'Size', }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} diff --git a/src/shared/ui/ContentEditable/ContentEditable.stories.svelte b/src/shared/ui/ContentEditable/ContentEditable.stories.svelte index a71fd6d..f46335a 100644 --- a/src/shared/ui/ContentEditable/ContentEditable.stories.svelte +++ b/src/shared/ui/ContentEditable/ContentEditable.stories.svelte @@ -37,6 +37,8 @@ const { Story } = defineMeta({ + + + {#snippet template()} +
+ +
Control content here
+
+
+ {/snippet} +
+ + + {#snippet template()} +
+ +
+ + + +
+
+
+ {/snippet} +
diff --git a/src/shared/ui/FilterGroup/FilterGroup.stories.svelte b/src/shared/ui/FilterGroup/FilterGroup.stories.svelte index d9d8ada..dff21e0 100644 --- a/src/shared/ui/FilterGroup/FilterGroup.stories.svelte +++ b/src/shared/ui/FilterGroup/FilterGroup.stories.svelte @@ -29,6 +29,7 @@ const { Story } = defineMeta({ - - {#snippet template(args)} - + + {#snippet template(args: ComponentProps)} + {/snippet} - - {#snippet template(args)} - + + {#snippet template(args: ComponentProps)} + {/snippet} diff --git a/src/shared/ui/Footnote/Footnote.stories.svelte b/src/shared/ui/Footnote/Footnote.stories.svelte index c3f065b..f33bcc7 100644 --- a/src/shared/ui/Footnote/Footnote.stories.svelte +++ b/src/shared/ui/Footnote/Footnote.stories.svelte @@ -16,8 +16,12 @@ const { Story } = defineMeta({ }); + + - {#snippet template(args)} + {#snippet template(args: ComponentProps)} Footnote @@ -25,7 +29,7 @@ const { Story } = defineMeta({ - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {#snippet render({ class: className })} Footnote diff --git a/src/shared/ui/Input/Input.stories.svelte b/src/shared/ui/Input/Input.stories.svelte index 4a58da8..090c2fc 100644 --- a/src/shared/ui/Input/Input.stories.svelte +++ b/src/shared/ui/Input/Input.stories.svelte @@ -61,42 +61,43 @@ const { Story } = defineMeta({ - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} - {#snippet template(args)} + {#snippet template(args: ComponentProps)}
- - - - + + + +
{/snippet}
- - {#snippet template(args)} - + + {#snippet template(args: ComponentProps)} + {/snippet} - - {#snippet template(args)} - + + {#snippet template(args: ComponentProps)} + {/snippet} - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {#snippet rightIcon()} @@ -106,7 +107,7 @@ const placeholder = 'Enter text'; - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {#snippet leftIcon()} @@ -115,9 +116,9 @@ const placeholder = 'Enter text'; {/snippet} - - {#snippet template(args)} - + + {#snippet template(args: ComponentProps)} + {#snippet rightIcon()} {/snippet} diff --git a/src/shared/ui/Label/Label.stories.svelte b/src/shared/ui/Label/Label.stories.svelte index a72556e..dc9b81f 100644 --- a/src/shared/ui/Label/Label.stories.svelte +++ b/src/shared/ui/Label/Label.stories.svelte @@ -47,13 +47,14 @@ const { Story } = defineMeta({ import AlertTriangleIcon from '@lucide/svelte/icons/alert-triangle'; import CheckIcon from '@lucide/svelte/icons/check'; import CircleIcon from '@lucide/svelte/icons/circle'; +import type { ComponentProps } from 'svelte'; - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} @@ -72,7 +73,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="Default variant" args={{ variant: 'default', size: 'sm' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} @@ -81,7 +82,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="Accent variant" args={{ variant: 'accent', size: 'sm' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} @@ -90,7 +91,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="Muted variant" args={{ variant: 'muted', size: 'sm' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} @@ -99,7 +100,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="Success variant" args={{ variant: 'success', size: 'sm' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} @@ -108,7 +109,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="Warning variant" args={{ variant: 'warning', size: 'sm' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet}
@@ -117,7 +118,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="Error variant" args={{ variant: 'error', size: 'sm' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet}
@@ -139,7 +140,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="Uppercase" args={{ uppercase: true, size: 'sm' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet}
@@ -148,7 +149,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="Lowercase" args={{ uppercase: false, size: 'sm' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} @@ -157,7 +158,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="Bold" args={{ bold: true, size: 'sm' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)} {/snippet} @@ -166,7 +167,7 @@ import CircleIcon from '@lucide/svelte/icons/circle'; name="With icon (left)" args={{ variant: 'default', size: 'sm', iconPosition: 'left' }} > - {#snippet template(args)} + {#snippet template(args: ComponentProps)}