chore: follow the general comments style
This commit is contained in:
@@ -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<BreadcrumbItem[]>([]);
|
||||
/** 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<Set<number>>(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;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @vitest-environment jsdom */
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string>[];
|
||||
}
|
||||
|
||||
@@ -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<typeof createFilter<'google' | 'fontshare'>>;
|
||||
/** Category filter (sans-serif, serif, display, etc.) */
|
||||
/**
|
||||
* Category filter (sans-serif, serif, display, etc.)
|
||||
*/
|
||||
categories: ReturnType<typeof createFilter<FontCategory>>;
|
||||
/** Subset filter (latin, latin-ext, cyrillic, etc.) */
|
||||
/**
|
||||
* Subset filter (latin, latin-ext, cyrillic, etc.)
|
||||
*/
|
||||
subsets: ReturnType<typeof createFilter<FontSubset>>;
|
||||
}
|
||||
|
||||
// FONT CATEGORIES
|
||||
|
||||
/**
|
||||
* Unified categories (combines both providers)
|
||||
*/
|
||||
@@ -71,8 +47,6 @@ export const UNIFIED_CATEGORIES: Property<FontCategory>[] = [
|
||||
{ id: 'script', name: 'Script', value: 'script' },
|
||||
];
|
||||
|
||||
// FONT SUBSETS
|
||||
|
||||
/**
|
||||
* Common font subsets
|
||||
*/
|
||||
@@ -85,8 +59,6 @@ export const FONT_SUBSETS: Property<FontSubset>[] = [
|
||||
{ id: 'devanagari', name: 'Devanagari', value: 'devanagari' },
|
||||
];
|
||||
|
||||
// FONT PROVIDERS
|
||||
|
||||
/**
|
||||
* Font providers
|
||||
*/
|
||||
@@ -95,8 +67,6 @@ export const FONT_PROVIDERS: Property<FontProvider>[] = [
|
||||
{ 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<FontProvider>({ 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: [],
|
||||
}),
|
||||
|
||||
@@ -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<FontMetadata>;
|
||||
/** Features overrides */
|
||||
/**
|
||||
* Features overrides
|
||||
*/
|
||||
features?: Partial<FontFeatures>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TData = unknown, TError = Error> {
|
||||
/**
|
||||
* 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<TData = unknown, TError = Error> {
|
||||
* Mock TanStack Query observer result
|
||||
*/
|
||||
export interface MockQueryObserverResult<TData = unknown, TError = Error> {
|
||||
/**
|
||||
* 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<TData>(data: TData): MockQueryObserverResult<
|
||||
return createMockQueryState<TData>({ 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<string, UnifiedFont>;
|
||||
/** 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<T>(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<T>(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<T>(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<UnifiedFont[]>({
|
||||
isLoading: true,
|
||||
data: undefined,
|
||||
}),
|
||||
|
||||
/** Font store with fonts loaded */
|
||||
/**
|
||||
* Successful data load state
|
||||
*/
|
||||
successFontStore: createMockStore<UnifiedFont[]>({
|
||||
data: Object.values(UNIFIED_FONTS),
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
}),
|
||||
|
||||
/** Font store with error */
|
||||
/**
|
||||
* API error state
|
||||
*/
|
||||
errorFontStore: createMockStore<UnifiedFont[]>({
|
||||
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<UnifiedFont[]>({
|
||||
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');
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<ArrayBuffer> {
|
||||
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<never> {
|
||||
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')]);
|
||||
|
||||
@@ -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<string, ArrayBuffer>();
|
||||
|
||||
// ==================== 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<void> {
|
||||
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();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @vitest-environment jsdom */
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { FontFetchError } from '../../errors';
|
||||
import { FontBufferCache } from './FontBufferCache';
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@ import { FontFetchError } from '../../errors';
|
||||
type Fetcher = (url: string, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @vitest-environment jsdom */
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { FontParseError } from '../../errors';
|
||||
import { loadFont } from './loadFont';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ProxyFontsParams, 'offset'>;
|
||||
|
||||
type FontStoreResult = InfiniteQueryObserverResult<InfiniteData<ProxyFontsResponse, PageParam>, 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<FontStoreParams>) {
|
||||
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<InfiniteData<ProxyFontsResponse, PageParam>>(
|
||||
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<InfiniteData<ProxyFontsResponse, PageParam>>(
|
||||
@@ -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<void> {
|
||||
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<string, any> = {};
|
||||
|
||||
|
||||
@@ -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<Record<UnifiedFontVariant, string>>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, UnifiedFont>;
|
||||
/** 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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user