From cfaff46d59677c9143f4ee1e122fd42a5281448e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 17 Apr 2026 12:14:55 +0300 Subject: [PATCH] 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 @@ +