+
diff --git a/.storybook/main.ts b/.storybook/main.ts
index 78cd878..f0a4d6d 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -21,7 +21,8 @@ const config: StorybookConfig = {
{
name: '@storybook/addon-svelte-csf',
options: {
- legacyTemplate: true, // Enables the legacy template syntax
+ // Use modern template syntax for better performance
+ legacyTemplate: false,
},
},
'@chromatic-com/storybook',
diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html
new file mode 100644
index 0000000..6f1d1e0
--- /dev/null
+++ b/.storybook/preview-head.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/.storybook/preview.ts b/.storybook/preview.ts
index 2734a23..281bb4e 100644
--- a/.storybook/preview.ts
+++ b/.storybook/preview.ts
@@ -1,4 +1,5 @@
import type { Preview } from '@storybook/svelte-vite';
+import Decorator from './Decorator.svelte';
import StoryStage from './StoryStage.svelte';
import '../src/app/styles/app.css';
@@ -23,25 +24,41 @@ const preview: Preview = {
story: {
// This sets the default height for the iframe in Autodocs
iframeHeight: '400px',
- // Ensure the story isn't forced into a tiny inline box
- // inline: true,
},
},
+
+ head: `
+
+
+
+
+
+
+ `,
},
+
decorators: [
- (storyFn, { parameters }) => {
- const { Component, props } = storyFn();
- return {
- Component: StoryStage,
- // We pass the actual story component into the Stage via a snippet/slot
- // Svelte 5 Storybook handles this mapping internally when you return this structure
- props: {
- children: Component,
- width: parameters.stageWidth || 'max-w-3xl',
- ...props,
- },
- };
- },
+ // Wrap with providers (TooltipProvider, ResponsiveManager)
+ story => ({
+ Component: Decorator,
+ props: {
+ children: story(),
+ },
+ }),
+ // Wrap with StoryStage for presentation styling
+ story => ({
+ Component: StoryStage,
+ props: {
+ children: story(),
+ },
+ }),
],
};
diff --git a/src/entities/Font/index.ts b/src/entities/Font/index.ts
index f0f07a5..4c3340d 100644
--- a/src/entities/Font/index.ts
+++ b/src/entities/Font/index.ts
@@ -78,6 +78,56 @@ export {
unifiedFontStore,
} from './model';
+// Mock data helpers for Storybook and testing
+export {
+ createCategoriesFilter,
+ createErrorState,
+ createGenericFilter,
+ createLoadingState,
+ createMockComparisonStore,
+ // Filter mocks
+ createMockFilter,
+ createMockFontApiResponse,
+ createMockFontStoreState,
+ // Store mocks
+ createMockQueryState,
+ createMockReactiveState,
+ createMockStore,
+ createProvidersFilter,
+ createSubsetsFilter,
+ createSuccessState,
+ FONTHARE_FONTS,
+ generateMixedCategoryFonts,
+ generateMockFonts,
+ generatePaginatedFonts,
+ generateSequentialFilter,
+ GENERIC_FILTERS,
+ getAllMockFonts,
+ getFontsByCategory,
+ getFontsByProvider,
+ GOOGLE_FONTS,
+ MOCK_FILTERS,
+ MOCK_FILTERS_ALL_SELECTED,
+ MOCK_FILTERS_EMPTY,
+ MOCK_FILTERS_SELECTED,
+ MOCK_FONT_STORE_STATES,
+ MOCK_STORES,
+ type MockFilterOptions,
+ type MockFilters,
+ mockFontshareFont,
+ type MockFontshareFontOptions,
+ type MockFontStoreState,
+ // Font mocks
+ mockGoogleFont,
+ // Types
+ type MockGoogleFontOptions,
+ type MockQueryObserverResult,
+ type MockQueryState,
+ mockUnifiedFont,
+ type MockUnifiedFontOptions,
+ UNIFIED_FONTS,
+} from './lib/mocks';
+
// UI elements
export {
FontApplicator,
diff --git a/src/entities/Font/lib/index.ts b/src/entities/Font/lib/index.ts
index 8d7bad8..532d5b8 100644
--- a/src/entities/Font/lib/index.ts
+++ b/src/entities/Font/lib/index.ts
@@ -6,3 +6,53 @@ export {
} from './normalize/normalize';
export { getFontUrl } from './getFontUrl/getFontUrl';
+
+// Mock data helpers for Storybook and testing
+export {
+ createCategoriesFilter,
+ createErrorState,
+ createGenericFilter,
+ createLoadingState,
+ createMockComparisonStore,
+ // Filter mocks
+ createMockFilter,
+ createMockFontApiResponse,
+ createMockFontStoreState,
+ // Store mocks
+ createMockQueryState,
+ createMockReactiveState,
+ createMockStore,
+ createProvidersFilter,
+ createSubsetsFilter,
+ createSuccessState,
+ FONTHARE_FONTS,
+ generateMixedCategoryFonts,
+ generateMockFonts,
+ generatePaginatedFonts,
+ generateSequentialFilter,
+ GENERIC_FILTERS,
+ getAllMockFonts,
+ getFontsByCategory,
+ getFontsByProvider,
+ GOOGLE_FONTS,
+ MOCK_FILTERS,
+ MOCK_FILTERS_ALL_SELECTED,
+ MOCK_FILTERS_EMPTY,
+ MOCK_FILTERS_SELECTED,
+ MOCK_FONT_STORE_STATES,
+ MOCK_STORES,
+ type MockFilterOptions,
+ type MockFilters,
+ mockFontshareFont,
+ type MockFontshareFontOptions,
+ type MockFontStoreState,
+ // Font mocks
+ mockGoogleFont,
+ // Types
+ type MockGoogleFontOptions,
+ type MockQueryObserverResult,
+ type MockQueryState,
+ mockUnifiedFont,
+ type MockUnifiedFontOptions,
+ UNIFIED_FONTS,
+} from './mocks';
diff --git a/src/entities/Font/lib/mocks/filters.mock.ts b/src/entities/Font/lib/mocks/filters.mock.ts
new file mode 100644
index 0000000..35f1e31
--- /dev/null
+++ b/src/entities/Font/lib/mocks/filters.mock.ts
@@ -0,0 +1,348 @@
+/**
+ * ============================================================================
+ * 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,
+ FontSubset,
+} from '$entities/Font/model/types';
+import type { Property } from '$shared/lib';
+import { createFilter } from '$shared/lib';
+
+// ============================================================================
+// TYPE DEFINITIONS
+// ============================================================================
+
+/**
+ * Options for creating a mock filter
+ */
+export interface MockFilterOptions {
+ /** Filter properties */
+ properties: Property
[];
+}
+
+/**
+ * Preset mock filters for font filtering
+ */
+export interface MockFilters {
+ /** Provider filter (Google, Fontshare) */
+ providers: ReturnType>;
+ /** Category filter (sans-serif, serif, display, etc.) */
+ categories: ReturnType>;
+ /** Subset filter (latin, latin-ext, cyrillic, etc.) */
+ subsets: ReturnType>;
+}
+
+// ============================================================================
+// FONT CATEGORIES
+// ============================================================================
+
+/**
+ * Google Fonts categories
+ */
+export const GOOGLE_CATEGORIES: Property<'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace'>[] = [
+ { id: 'sans-serif', name: 'Sans Serif', value: 'sans-serif' },
+ { id: 'serif', name: 'Serif', value: 'serif' },
+ { id: 'display', name: 'Display', value: 'display' },
+ { id: 'handwriting', name: 'Handwriting', value: 'handwriting' },
+ { id: 'monospace', name: 'Monospace', value: 'monospace' },
+];
+
+/**
+ * Fontshare categories (mapped to common naming)
+ */
+export const FONTHARE_CATEGORIES: Property<'sans' | 'serif' | 'slab' | 'display' | 'handwritten' | 'script'>[] = [
+ { id: 'sans', name: 'Sans', value: 'sans' },
+ { id: 'serif', name: 'Serif', value: 'serif' },
+ { id: 'slab', name: 'Slab', value: 'slab' },
+ { id: 'display', name: 'Display', value: 'display' },
+ { id: 'handwritten', name: 'Handwritten', value: 'handwritten' },
+ { id: 'script', name: 'Script', value: 'script' },
+];
+
+/**
+ * Unified categories (combines both providers)
+ */
+export const UNIFIED_CATEGORIES: Property[] = [
+ { id: 'sans-serif', name: 'Sans Serif', value: 'sans-serif' },
+ { id: 'serif', name: 'Serif', value: 'serif' },
+ { id: 'display', name: 'Display', value: 'display' },
+ { id: 'handwriting', name: 'Handwriting', value: 'handwriting' },
+ { id: 'monospace', name: 'Monospace', value: 'monospace' },
+];
+
+// ============================================================================
+// FONT SUBSETS
+// ============================================================================
+
+/**
+ * Common font subsets
+ */
+export const FONT_SUBSETS: Property[] = [
+ { id: 'latin', name: 'Latin', value: 'latin' },
+ { id: 'latin-ext', name: 'Latin Extended', value: 'latin-ext' },
+ { id: 'cyrillic', name: 'Cyrillic', value: 'cyrillic' },
+ { id: 'greek', name: 'Greek', value: 'greek' },
+ { id: 'arabic', name: 'Arabic', value: 'arabic' },
+ { id: 'devanagari', name: 'Devanagari', value: 'devanagari' },
+];
+
+// ============================================================================
+// FONT PROVIDERS
+// ============================================================================
+
+/**
+ * Font providers
+ */
+export const FONT_PROVIDERS: Property[] = [
+ { id: 'google', name: 'Google Fonts', value: 'google' },
+ { id: 'fontshare', name: 'Fontshare', value: 'fontshare' },
+];
+
+// ============================================================================
+// FILTER FACTORIES
+// ============================================================================
+
+/**
+ * Create a mock filter from properties
+ */
+export function createMockFilter(
+ options: MockFilterOptions & { properties: Property[] },
+) {
+ return createFilter(options);
+}
+
+/**
+ * Create a mock filter for categories
+ */
+export function createCategoriesFilter(options?: { selected?: FontCategory[] }) {
+ const properties = UNIFIED_CATEGORIES.map(cat => ({
+ ...cat,
+ selected: options?.selected?.includes(cat.value) ?? false,
+ }));
+ return createFilter({ properties });
+}
+
+/**
+ * Create a mock filter for subsets
+ */
+export function createSubsetsFilter(options?: { selected?: FontSubset[] }) {
+ const properties = FONT_SUBSETS.map(subset => ({
+ ...subset,
+ selected: options?.selected?.includes(subset.value) ?? false,
+ }));
+ return createFilter({ properties });
+}
+
+/**
+ * Create a mock filter for providers
+ */
+export function createProvidersFilter(options?: { selected?: FontProvider[] }) {
+ const properties = FONT_PROVIDERS.map(provider => ({
+ ...provider,
+ selected: options?.selected?.includes(provider.value) ?? false,
+ }));
+ return createFilter({ properties });
+}
+
+// ============================================================================
+// PRESET FILTERS
+// ============================================================================
+
+/**
+ * Preset mock filters - use these directly in stories
+ */
+export const MOCK_FILTERS: MockFilters = {
+ providers: createFilter({
+ properties: FONT_PROVIDERS,
+ }),
+ categories: createFilter({
+ properties: UNIFIED_CATEGORIES,
+ }),
+ subsets: createFilter({
+ properties: FONT_SUBSETS,
+ }),
+};
+
+/**
+ * Preset filters with some items selected
+ */
+export const MOCK_FILTERS_SELECTED: MockFilters = {
+ providers: createFilter({
+ properties: [
+ { ...FONT_PROVIDERS[0], selected: true },
+ { ...FONT_PROVIDERS[1] },
+ ],
+ }),
+ categories: createFilter({
+ properties: [
+ { ...UNIFIED_CATEGORIES[0], selected: true },
+ { ...UNIFIED_CATEGORIES[1], selected: true },
+ { ...UNIFIED_CATEGORIES[2] },
+ { ...UNIFIED_CATEGORIES[3] },
+ { ...UNIFIED_CATEGORIES[4] },
+ ],
+ }),
+ subsets: createFilter({
+ properties: [
+ { ...FONT_SUBSETS[0], selected: true },
+ { ...FONT_SUBSETS[1] },
+ { ...FONT_SUBSETS[2] },
+ { ...FONT_SUBSETS[3] },
+ { ...FONT_SUBSETS[4] },
+ ],
+ }),
+};
+
+/**
+ * Empty filters (all properties, none selected)
+ */
+export const MOCK_FILTERS_EMPTY: MockFilters = {
+ providers: createFilter({
+ properties: FONT_PROVIDERS.map(p => ({ ...p, selected: false })),
+ }),
+ categories: createFilter({
+ properties: UNIFIED_CATEGORIES.map(c => ({ ...c, selected: false })),
+ }),
+ subsets: createFilter({
+ properties: FONT_SUBSETS.map(s => ({ ...s, selected: false })),
+ }),
+};
+
+/**
+ * All selected filters
+ */
+export const MOCK_FILTERS_ALL_SELECTED: MockFilters = {
+ providers: createFilter({
+ properties: FONT_PROVIDERS.map(p => ({ ...p, selected: true })),
+ }),
+ categories: createFilter({
+ properties: UNIFIED_CATEGORIES.map(c => ({ ...c, selected: true })),
+ }),
+ subsets: createFilter({
+ properties: FONT_SUBSETS.map(s => ({ ...s, selected: true })),
+ }),
+};
+
+// ============================================================================
+// GENERIC FILTER MOCKS
+// ============================================================================
+
+/**
+ * Create a mock filter with generic string properties
+ * Useful for testing generic filter components
+ */
+export function createGenericFilter(
+ items: Array<{ id: string; name: string; selected?: boolean }>,
+ options?: { selected?: string[] },
+) {
+ const properties = items.map(item => ({
+ id: item.id,
+ name: item.name,
+ value: item.id,
+ selected: options?.selected?.includes(item.id) ?? item.selected ?? false,
+ }));
+ return createFilter({ properties });
+}
+
+/**
+ * Preset generic filters for testing
+ */
+export const GENERIC_FILTERS = {
+ /** Small filter with 3 items */
+ small: createFilter({
+ properties: [
+ { id: 'option-1', name: 'Option 1', value: 'option-1' },
+ { id: 'option-2', name: 'Option 2', value: 'option-2' },
+ { id: 'option-3', name: 'Option 3', value: 'option-3' },
+ ],
+ }),
+ /** Medium filter with 6 items */
+ medium: createFilter({
+ properties: [
+ { id: 'alpha', name: 'Alpha', value: 'alpha' },
+ { id: 'beta', name: 'Beta', value: 'beta' },
+ { id: 'gamma', name: 'Gamma', value: 'gamma' },
+ { id: 'delta', name: 'Delta', value: 'delta' },
+ { id: 'epsilon', name: 'Epsilon', value: 'epsilon' },
+ { id: 'zeta', name: 'Zeta', value: 'zeta' },
+ ],
+ }),
+ /** Large filter with 12 items */
+ large: createFilter({
+ properties: [
+ { id: 'jan', name: 'January', value: 'jan' },
+ { id: 'feb', name: 'February', value: 'feb' },
+ { id: 'mar', name: 'March', value: 'mar' },
+ { id: 'apr', name: 'April', value: 'apr' },
+ { id: 'may', name: 'May', value: 'may' },
+ { id: 'jun', name: 'June', value: 'jun' },
+ { id: 'jul', name: 'July', value: 'jul' },
+ { id: 'aug', name: 'August', value: 'aug' },
+ { id: 'sep', name: 'September', value: 'sep' },
+ { id: 'oct', name: 'October', value: 'oct' },
+ { id: 'nov', name: 'November', value: 'nov' },
+ { id: 'dec', name: 'December', value: 'dec' },
+ ],
+ }),
+ /** Filter with some pre-selected items */
+ partial: createFilter({
+ properties: [
+ { id: 'red', name: 'Red', value: 'red', selected: true },
+ { id: 'blue', name: 'Blue', value: 'blue', selected: false },
+ { id: 'green', name: 'Green', value: 'green', selected: true },
+ { id: 'yellow', name: 'Yellow', value: 'yellow', selected: false },
+ ],
+ }),
+ /** Filter with all items selected */
+ allSelected: createFilter({
+ properties: [
+ { id: 'cat', name: 'Cat', value: 'cat', selected: true },
+ { id: 'dog', name: 'Dog', value: 'dog', selected: true },
+ { id: 'bird', name: 'Bird', value: 'bird', selected: true },
+ ],
+ }),
+ /** Empty filter (no items) */
+ empty: createFilter({
+ properties: [],
+ }),
+};
+
+/**
+ * Generate a filter with sequential items
+ */
+export function generateSequentialFilter(count: number, prefix = 'Item ') {
+ const properties = Array.from({ length: count }, (_, i) => ({
+ id: `item-${i + 1}`,
+ name: `${prefix}${i + 1}`,
+ value: `item-${i + 1}`,
+ }));
+ return createFilter({ properties });
+}
diff --git a/src/entities/Font/lib/mocks/fonts.mock.ts b/src/entities/Font/lib/mocks/fonts.mock.ts
new file mode 100644
index 0000000..2ee8868
--- /dev/null
+++ b/src/entities/Font/lib/mocks/fonts.mock.ts
@@ -0,0 +1,630 @@
+/**
+ * ============================================================================
+ * MOCK FONT DATA
+ * ============================================================================
+ *
+ * Factory functions and preset mock data for fonts.
+ * Used in Storybook stories, tests, and development.
+ *
+ * ## Usage
+ *
+ * ```ts
+ * import {
+ * mockGoogleFont,
+ * mockFontshareFont,
+ * mockUnifiedFont,
+ * GOOGLE_FONTS,
+ * FONTHARE_FONTS,
+ * UNIFIED_FONTS,
+ * } from '$entities/Font/lib/mocks';
+ *
+ * // Create a mock Google Font
+ * const roboto = mockGoogleFont({ family: 'Roboto', category: 'sans-serif' });
+ *
+ * // Create a mock Fontshare font
+ * const satoshi = mockFontshareFont({ name: 'Satoshi', slug: 'satoshi' });
+ *
+ * // Create a mock UnifiedFont
+ * const font = mockUnifiedFont({ id: 'roboto', name: 'Roboto' });
+ *
+ * // Use preset fonts
+ * import { UNIFIED_FONTS } from '$entities/Font/lib/mocks';
+ * ```
+ */
+
+import type {
+ FontCategory,
+ FontProvider,
+ FontSubset,
+ FontVariant,
+} from '$entities/Font/model/types';
+import type {
+ FontItem,
+ FontshareFont,
+ GoogleFontItem,
+} from '$entities/Font/model/types';
+import type {
+ FontFeatures,
+ FontMetadata,
+ FontStyleUrls,
+ UnifiedFont,
+} from '$entities/Font/model/types';
+
+// ============================================================================
+// GOOGLE FONTS MOCKS
+// ============================================================================
+
+/**
+ * Options for creating a mock Google Font
+ */
+export interface MockGoogleFontOptions {
+ /** Font family name (default: 'Mock Font') */
+ family?: string;
+ /** Font category (default: 'sans-serif') */
+ category?: 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace';
+ /** Font variants (default: ['regular', '700', 'italic', '700italic']) */
+ variants?: FontVariant[];
+ /** Font subsets (default: ['latin']) */
+ subsets?: string[];
+ /** Font version (default: 'v30') */
+ version?: string;
+ /** Last modified date (default: current ISO date) */
+ lastModified?: string;
+ /** Custom file URLs (if not provided, mock URLs are generated) */
+ files?: Partial>;
+ /** Popularity rank (1 = most popular) */
+ popularity?: number;
+}
+
+/**
+ * Default mock Google Font
+ */
+export function mockGoogleFont(options: MockGoogleFontOptions = {}): GoogleFontItem {
+ const {
+ family = 'Mock Font',
+ category = 'sans-serif',
+ variants = ['regular', '700', 'italic', '700italic'],
+ subsets = ['latin'],
+ version = 'v30',
+ lastModified = new Date().toISOString().split('T')[0],
+ files,
+ popularity = 1,
+ } = options;
+
+ const baseUrl = `https://fonts.gstatic.com/s/${family.toLowerCase().replace(/\s+/g, '')}/${version}`;
+
+ return {
+ family,
+ category,
+ variants: variants as FontVariant[],
+ subsets,
+ version,
+ lastModified,
+ files: files ?? {
+ regular: `${baseUrl}/KFOmCnqEu92Fr1Me4W.woff2`,
+ '700': `${baseUrl}/KFOlCnqEu92Fr1MmWUlfBBc9.woff2`,
+ italic: `${baseUrl}/KFOkCnqEu92Fr1Mu51xIIzI.woff2`,
+ '700italic': `${baseUrl}/KFOjCnqEu92Fr1Mu51TzBic6CsQ.woff2`,
+ },
+ menu: `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}`,
+ };
+}
+
+/**
+ * Preset Google Font mocks
+ */
+export const GOOGLE_FONTS: Record = {
+ roboto: mockGoogleFont({
+ family: 'Roboto',
+ category: 'sans-serif',
+ variants: ['100', '300', '400', '500', '700', '900', 'italic', '700italic'],
+ subsets: ['latin', 'latin-ext', 'cyrillic', 'greek'],
+ popularity: 1,
+ }),
+ openSans: mockGoogleFont({
+ family: 'Open Sans',
+ category: 'sans-serif',
+ variants: ['300', '400', '500', '600', '700', '800', 'italic', '700italic'],
+ subsets: ['latin', 'latin-ext', 'cyrillic', 'greek'],
+ popularity: 2,
+ }),
+ lato: mockGoogleFont({
+ family: 'Lato',
+ category: 'sans-serif',
+ variants: ['100', '300', '400', '700', '900', 'italic', '700italic'],
+ subsets: ['latin', 'latin-ext'],
+ popularity: 3,
+ }),
+ playfairDisplay: mockGoogleFont({
+ family: 'Playfair Display',
+ category: 'serif',
+ variants: ['400', '500', '600', '700', '800', '900', 'italic', '700italic'],
+ subsets: ['latin', 'latin-ext', 'cyrillic'],
+ popularity: 10,
+ }),
+ montserrat: mockGoogleFont({
+ family: 'Montserrat',
+ category: 'sans-serif',
+ variants: ['100', '200', '300', '400', '500', '600', '700', '800', '900', 'italic', '700italic'],
+ subsets: ['latin', 'latin-ext', 'cyrillic', 'vietnamese'],
+ popularity: 4,
+ }),
+ sourceSansPro: mockGoogleFont({
+ family: 'Source Sans Pro',
+ category: 'sans-serif',
+ variants: ['200', '300', '400', '600', '700', '900', 'italic', '700italic'],
+ subsets: ['latin', 'latin-ext', 'cyrillic', 'greek', 'vietnamese'],
+ popularity: 5,
+ }),
+ merriweather: mockGoogleFont({
+ family: 'Merriweather',
+ category: 'serif',
+ variants: ['300', '400', '700', '900', 'italic', '700italic'],
+ subsets: ['latin', 'latin-ext', 'cyrillic', 'vietnamese'],
+ popularity: 15,
+ }),
+ robotoSlab: mockGoogleFont({
+ family: 'Roboto Slab',
+ category: 'serif',
+ variants: ['100', '300', '400', '500', '700', '900'],
+ subsets: ['latin', 'latin-ext', 'cyrillic', 'greek', 'vietnamese'],
+ popularity: 8,
+ }),
+ oswald: mockGoogleFont({
+ family: 'Oswald',
+ category: 'sans-serif',
+ variants: ['200', '300', '400', '500', '600', '700'],
+ subsets: ['latin', 'latin-ext', 'vietnamese'],
+ popularity: 6,
+ }),
+ raleway: mockGoogleFont({
+ family: 'Raleway',
+ category: 'sans-serif',
+ variants: ['100', '200', '300', '400', '500', '600', '700', '800', '900', 'italic'],
+ subsets: ['latin', 'latin-ext', 'cyrillic', 'vietnamese'],
+ popularity: 7,
+ }),
+};
+
+// ============================================================================
+// FONTHARE MOCKS
+// ============================================================================
+
+/**
+ * Options for creating a mock Fontshare font
+ */
+export interface MockFontshareFontOptions {
+ /** Font name (default: 'Mock Font') */
+ name?: string;
+ /** URL-friendly slug (default: derived from name) */
+ slug?: string;
+ /** Font category (default: 'sans') */
+ category?: 'sans' | 'serif' | 'slab' | 'display' | 'handwritten' | 'script' | 'mono';
+ /** Script (default: 'latin') */
+ script?: string;
+ /** Whether this is a variable font (default: false) */
+ isVariable?: boolean;
+ /** Font version (default: '1.0') */
+ version?: string;
+ /** Popularity/views count (default: 1000) */
+ views?: number;
+ /** Usage tags */
+ tags?: string[];
+ /** Font weights available */
+ weights?: number[];
+ /** Publisher name */
+ publisher?: string;
+ /** Designer name */
+ designer?: string;
+}
+
+/**
+ * Create a mock Fontshare style
+ */
+function mockFontshareStyle(
+ weight: number,
+ isItalic: boolean,
+ isVariable: boolean,
+ slug: string,
+): FontshareFont['styles'][number] {
+ const weightLabel = weight === 400 ? 'Regular' : weight === 700 ? 'Bold' : weight.toString();
+ const suffix = isItalic ? 'italic' : '';
+ const variablePrefix = isVariable ? 'variable-' : '';
+
+ return {
+ id: `style-${weight}${isItalic ? '-italic' : ''}`,
+ default: weight === 400 && !isItalic,
+ file: `//cdn.fontshare.com/wf/${slug}-${variablePrefix}${weight}${suffix}.woff2`,
+ is_italic: isItalic,
+ is_variable: isVariable,
+ properties: {},
+ weight: {
+ label: isVariable ? 'Variable' + (isItalic ? ' Italic' : '') : weightLabel,
+ name: isVariable ? 'Variable' + (isItalic ? 'Italic' : '') : weightLabel,
+ native_name: null,
+ number: isVariable ? 0 : weight,
+ weight: isVariable ? 0 : weight,
+ },
+ };
+}
+
+/**
+ * Default mock Fontshare font
+ */
+export function mockFontshareFont(options: MockFontshareFontOptions = {}): FontshareFont {
+ const {
+ name = 'Mock Font',
+ slug = name.toLowerCase().replace(/\s+/g, '-'),
+ category = 'sans',
+ script = 'latin',
+ isVariable = false,
+ version = '1.0',
+ views = 1000,
+ tags = [],
+ weights = [400, 700],
+ publisher = 'Mock Foundry',
+ designer = 'Mock Designer',
+ } = options;
+
+ // Generate styles based on weights and variable setting
+ const styles: FontshareFont['styles'] = isVariable
+ ? [
+ mockFontshareStyle(0, false, true, slug),
+ mockFontshareStyle(0, true, true, slug),
+ ]
+ : weights.flatMap(weight => [
+ mockFontshareStyle(weight, false, false, slug),
+ mockFontshareStyle(weight, true, false, slug),
+ ]);
+
+ return {
+ id: `mock-${slug}`,
+ name,
+ native_name: null,
+ slug,
+ category,
+ script,
+ publisher: {
+ bio: `Mock publisher bio for ${publisher}`,
+ email: null,
+ id: `pub-${slug}`,
+ links: [],
+ name: publisher,
+ },
+ designers: [
+ {
+ bio: `Mock designer bio for ${designer}`,
+ links: [],
+ name: designer,
+ },
+ ],
+ related_families: null,
+ display_publisher_as_designer: false,
+ trials_enabled: true,
+ show_latin_metrics: false,
+ license_type: 'ofl',
+ languages: 'English, Spanish, French, German',
+ inserted_at: '2021-03-12T20:49:05Z',
+ story: `A mock font story for ${name}.
`,
+ version,
+ views,
+ views_recent: Math.floor(views * 0.1),
+ is_hot: views > 5000,
+ is_new: views < 500,
+ is_shortlisted: null,
+ is_top: views > 10000,
+ axes: isVariable
+ ? [
+ {
+ name: 'Weight',
+ property: 'wght',
+ range_default: 400,
+ range_left: 300,
+ range_right: 700,
+ },
+ ]
+ : [],
+ font_tags: tags.map(name => ({ name })),
+ features: [],
+ styles,
+ };
+}
+
+/**
+ * Preset Fontshare font mocks
+ */
+export const FONTHARE_FONTS: Record = {
+ satoshi: mockFontshareFont({
+ name: 'Satoshi',
+ slug: 'satoshi',
+ category: 'sans',
+ isVariable: true,
+ views: 15000,
+ tags: ['Branding', 'Logos', 'Editorial'],
+ publisher: 'Indian Type Foundry',
+ designer: 'Denis Shelabovets',
+ }),
+ generalSans: mockFontshareFont({
+ name: 'General Sans',
+ slug: 'general-sans',
+ category: 'sans',
+ isVariable: true,
+ views: 12000,
+ tags: ['UI', 'Branding', 'Display'],
+ publisher: 'Indestructible Type',
+ designer: 'Eugene Tantsur',
+ }),
+ clashDisplay: mockFontshareFont({
+ name: 'Clash Display',
+ slug: 'clash-display',
+ category: 'display',
+ isVariable: false,
+ views: 8000,
+ tags: ['Headlines', 'Posters', 'Branding'],
+ weights: [400, 500, 600, 700],
+ publisher: 'Letterogika',
+ designer: 'Matěj Trnka',
+ }),
+ fonta: mockFontshareFont({
+ name: 'Fonta',
+ slug: 'fonta',
+ category: 'serif',
+ isVariable: false,
+ views: 5000,
+ tags: ['Editorial', 'Books', 'Magazines'],
+ weights: [300, 400, 500, 600, 700],
+ publisher: 'Fonta',
+ designer: 'Alexei Vanyashin',
+ }),
+ aileron: mockFontshareFont({
+ name: 'Aileron',
+ slug: 'aileron',
+ category: 'sans',
+ isVariable: false,
+ views: 3000,
+ tags: ['Display', 'Headlines'],
+ weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],
+ publisher: 'Sorkin Type',
+ designer: 'Sorkin Type',
+ }),
+ beVietnamPro: mockFontshareFont({
+ name: 'Be Vietnam Pro',
+ slug: 'be-vietnam-pro',
+ category: 'sans',
+ isVariable: true,
+ views: 20000,
+ tags: ['UI', 'App', 'Web'],
+ publisher: 'ildefox',
+ designer: 'Manh Nguyen',
+ }),
+};
+
+// ============================================================================
+// UNIFIED FONT MOCKS
+// ============================================================================
+
+/**
+ * Options for creating a mock UnifiedFont
+ */
+export interface MockUnifiedFontOptions {
+ /** Unique identifier (default: derived from name) */
+ id?: string;
+ /** Font display name (default: 'Mock Font') */
+ name?: string;
+ /** Font provider (default: 'google') */
+ provider?: FontProvider;
+ /** Font category (default: 'sans-serif') */
+ category?: FontCategory;
+ /** Font subsets (default: ['latin']) */
+ subsets?: FontSubset[];
+ /** Font variants (default: ['regular', '700', 'italic', '700italic']) */
+ variants?: FontVariant[];
+ /** Style URLs (if not provided, mock URLs are generated) */
+ styles?: FontStyleUrls;
+ /** Metadata overrides */
+ metadata?: Partial;
+ /** Features overrides */
+ features?: Partial;
+}
+
+/**
+ * Default mock UnifiedFont
+ */
+export function mockUnifiedFont(options: MockUnifiedFontOptions = {}): UnifiedFont {
+ const {
+ id,
+ name = 'Mock Font',
+ provider = 'google',
+ category = 'sans-serif',
+ subsets = ['latin'],
+ variants = ['regular', '700', 'italic', '700italic'],
+ styles,
+ metadata,
+ features,
+ } = options;
+
+ const fontId = id ?? name.toLowerCase().replace(/\s+/g, '');
+ const baseUrl = provider === 'google'
+ ? `https://fonts.gstatic.com/s/${fontId}/v30`
+ : `//cdn.fontshare.com/wf/${fontId}`;
+
+ return {
+ id: fontId,
+ name,
+ provider,
+ category,
+ subsets,
+ variants: variants as FontVariant[],
+ styles: styles ?? {
+ regular: `${baseUrl}/regular.woff2`,
+ bold: `${baseUrl}/bold.woff2`,
+ italic: `${baseUrl}/italic.woff2`,
+ boldItalic: `${baseUrl}/bolditalic.woff2`,
+ },
+ metadata: {
+ cachedAt: Date.now(),
+ version: '1.0',
+ lastModified: new Date().toISOString().split('T')[0],
+ popularity: 1,
+ ...metadata,
+ },
+ features: {
+ isVariable: false,
+ ...features,
+ },
+ };
+}
+
+/**
+ * Preset UnifiedFont mocks
+ */
+export const UNIFIED_FONTS: Record = {
+ roboto: mockUnifiedFont({
+ id: 'roboto',
+ name: 'Roboto',
+ provider: 'google',
+ category: 'sans-serif',
+ subsets: ['latin', 'latin-ext'],
+ variants: ['100', '300', '400', '500', '700', '900'],
+ metadata: { popularity: 1 },
+ }),
+ openSans: mockUnifiedFont({
+ id: 'open-sans',
+ name: 'Open Sans',
+ provider: 'google',
+ category: 'sans-serif',
+ subsets: ['latin', 'latin-ext'],
+ variants: ['300', '400', '500', '600', '700', '800'],
+ metadata: { popularity: 2 },
+ }),
+ lato: mockUnifiedFont({
+ id: 'lato',
+ name: 'Lato',
+ provider: 'google',
+ category: 'sans-serif',
+ subsets: ['latin', 'latin-ext'],
+ variants: ['100', '300', '400', '700', '900'],
+ metadata: { popularity: 3 },
+ }),
+ playfairDisplay: mockUnifiedFont({
+ id: 'playfair-display',
+ name: 'Playfair Display',
+ provider: 'google',
+ category: 'serif',
+ subsets: ['latin'],
+ variants: ['400', '700', '900'],
+ metadata: { popularity: 10 },
+ }),
+ montserrat: mockUnifiedFont({
+ id: 'montserrat',
+ name: 'Montserrat',
+ provider: 'google',
+ category: 'sans-serif',
+ subsets: ['latin', 'latin-ext'],
+ variants: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
+ metadata: { popularity: 4 },
+ }),
+ satoshi: mockUnifiedFont({
+ id: 'satoshi',
+ name: 'Satoshi',
+ provider: 'fontshare',
+ category: 'sans-serif',
+ subsets: ['latin'],
+ variants: ['regular', 'bold', 'italic', 'bolditalic'] as FontVariant[],
+ features: { isVariable: true, axes: [{ name: 'wght', property: 'wght', default: 400, min: 300, max: 700 }] },
+ metadata: { popularity: 15000 },
+ }),
+ generalSans: mockUnifiedFont({
+ id: 'general-sans',
+ name: 'General Sans',
+ provider: 'fontshare',
+ category: 'sans-serif',
+ subsets: ['latin'],
+ variants: ['regular', 'bold', 'italic', 'bolditalic'] as FontVariant[],
+ features: { isVariable: true },
+ metadata: { popularity: 12000 },
+ }),
+ clashDisplay: mockUnifiedFont({
+ id: 'clash-display',
+ name: 'Clash Display',
+ provider: 'fontshare',
+ category: 'display',
+ subsets: ['latin'],
+ variants: ['regular', '500', '600', 'bold'] as FontVariant[],
+ features: { tags: ['Headlines', 'Posters', 'Branding'] },
+ metadata: { popularity: 8000 },
+ }),
+ oswald: mockUnifiedFont({
+ id: 'oswald',
+ name: 'Oswald',
+ provider: 'google',
+ category: 'sans-serif',
+ subsets: ['latin'],
+ variants: ['200', '300', '400', '500', '600', '700'],
+ metadata: { popularity: 6 },
+ }),
+ raleway: mockUnifiedFont({
+ id: 'raleway',
+ name: 'Raleway',
+ provider: 'google',
+ category: 'sans-serif',
+ subsets: ['latin'],
+ variants: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
+ metadata: { popularity: 7 },
+ }),
+};
+
+/**
+ * Get an array of all preset UnifiedFonts
+ */
+export function getAllMockFonts(): UnifiedFont[] {
+ return Object.values(UNIFIED_FONTS);
+}
+
+/**
+ * Get fonts by provider
+ */
+export function getFontsByProvider(provider: FontProvider): UnifiedFont[] {
+ return getAllMockFonts().filter(font => font.provider === provider);
+}
+
+/**
+ * Get fonts by category
+ */
+export function getFontsByCategory(category: FontCategory): UnifiedFont[] {
+ return getAllMockFonts().filter(font => font.category === category);
+}
+
+/**
+ * Generate an array of mock fonts with sequential naming
+ */
+export function generateMockFonts(count: number, options?: Omit): UnifiedFont[] {
+ return Array.from({ length: count }, (_, i) =>
+ mockUnifiedFont({
+ ...options,
+ id: `mock-font-${i + 1}`,
+ name: `Mock Font ${i + 1}`,
+ }));
+}
+
+/**
+ * Generate an array of mock fonts with different categories
+ */
+export function generateMixedCategoryFonts(countPerCategory: number = 2): UnifiedFont[] {
+ const categories: FontCategory[] = ['sans-serif', 'serif', 'display', 'handwriting', 'monospace'];
+ const fonts: UnifiedFont[] = [];
+
+ categories.forEach(category => {
+ for (let i = 0; i < countPerCategory; i++) {
+ fonts.push(
+ mockUnifiedFont({
+ id: `${category}-${i + 1}`,
+ name: `${category.replace('-', ' ')} ${i + 1}`,
+ category,
+ }),
+ );
+ }
+ });
+
+ return fonts;
+}
diff --git a/src/entities/Font/lib/mocks/index.ts b/src/entities/Font/lib/mocks/index.ts
new file mode 100644
index 0000000..e2042e3
--- /dev/null
+++ b/src/entities/Font/lib/mocks/index.ts
@@ -0,0 +1,84 @@
+/**
+ * ============================================================================
+ * MOCK DATA HELPERS - MAIN EXPORT
+ * ============================================================================
+ *
+ * Comprehensive mock data for Storybook stories, tests, and development.
+ *
+ * ## Quick Start
+ *
+ * ```ts
+ * import {
+ * mockUnifiedFont,
+ * UNIFIED_FONTS,
+ * MOCK_FILTERS,
+ * createMockFontStoreState,
+ * } from '$entities/Font/lib/mocks';
+ *
+ * // Use in stories
+ * const font = mockUnifiedFont({ name: 'My Font', category: 'serif' });
+ * const presets = UNIFIED_FONTS;
+ * const filter = MOCK_FILTERS.categories;
+ * ```
+ *
+ * @module
+ */
+
+// Font mocks
+export {
+ FONTHARE_FONTS,
+ generateMixedCategoryFonts,
+ generateMockFonts,
+ getAllMockFonts,
+ getFontsByCategory,
+ getFontsByProvider,
+ GOOGLE_FONTS,
+ mockFontshareFont,
+ type MockFontshareFontOptions,
+ mockGoogleFont,
+ type MockGoogleFontOptions,
+ mockUnifiedFont,
+ type MockUnifiedFontOptions,
+ UNIFIED_FONTS,
+} from './fonts.mock';
+
+// Filter mocks
+export {
+ createCategoriesFilter,
+ createGenericFilter,
+ createMockFilter,
+ createProvidersFilter,
+ createSubsetsFilter,
+ FONT_PROVIDERS,
+ FONT_SUBSETS,
+ FONTHARE_CATEGORIES,
+ generateSequentialFilter,
+ GENERIC_FILTERS,
+ GOOGLE_CATEGORIES,
+ MOCK_FILTERS,
+ MOCK_FILTERS_ALL_SELECTED,
+ MOCK_FILTERS_EMPTY,
+ MOCK_FILTERS_SELECTED,
+ type MockFilterOptions,
+ type MockFilters,
+ UNIFIED_CATEGORIES,
+} from './filters.mock';
+
+// Store mocks
+export {
+ createErrorState,
+ createLoadingState,
+ createMockComparisonStore,
+ createMockFontApiResponse,
+ createMockFontStoreState,
+ createMockQueryState,
+ createMockReactiveState,
+ createMockStore,
+ createSuccessState,
+ generatePaginatedFonts,
+ MOCK_FONT_STORE_STATES,
+ MOCK_STORES,
+ type MockFontStoreState,
+ type MockQueryObserverResult,
+ type MockQueryState,
+} from './stores.mock';
diff --git a/src/entities/Font/lib/mocks/stores.mock.ts b/src/entities/Font/lib/mocks/stores.mock.ts
new file mode 100644
index 0000000..f6610c7
--- /dev/null
+++ b/src/entities/Font/lib/mocks/stores.mock.ts
@@ -0,0 +1,590 @@
+/**
+ * ============================================================================
+ * 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.
+ *
+ * ## Usage
+ *
+ * ```ts
+ * import {
+ * createMockQueryState,
+ * MOCK_STORES,
+ * } from '$entities/Font/lib/mocks';
+ *
+ * // Create a mock query state
+ * const loadingState = createMockQueryState({ status: 'pending' });
+ * const errorState = createMockQueryState({ status: 'error', error: 'Failed to load' });
+ * const successState = createMockQueryState({ status: 'success', data: mockFonts });
+ *
+ * // Use preset stores
+ * const mockFontStore = MOCK_STORES.unifiedFontStore();
+ * ```
+ */
+
+import type { UnifiedFont } from '$entities/Font/model/types';
+import type {
+ QueryKey,
+ QueryObserverResult,
+ QueryStatus,
+} from '@tanstack/svelte-query';
+import {
+ UNIFIED_FONTS,
+ generateMockFonts,
+} from './fonts.mock';
+
+// ============================================================================
+// TANSTACK QUERY MOCK TYPES
+// ============================================================================
+
+/**
+ * Mock TanStack Query state
+ */
+export interface MockQueryState {
+ status: QueryStatus;
+ data?: TData;
+ error?: TError;
+ isLoading?: boolean;
+ isFetching?: boolean;
+ isSuccess?: boolean;
+ isError?: boolean;
+ isPending?: boolean;
+ dataUpdatedAt?: number;
+ errorUpdatedAt?: number;
+ failureCount?: number;
+ failureReason?: TError;
+ errorUpdateCount?: number;
+ isRefetching?: boolean;
+ isRefetchError?: boolean;
+ isPaused?: boolean;
+}
+
+/**
+ * Mock TanStack Query observer result
+ */
+export interface MockQueryObserverResult {
+ status?: QueryStatus;
+ data?: TData;
+ error?: TError;
+ isLoading?: boolean;
+ isFetching?: boolean;
+ isSuccess?: boolean;
+ isError?: boolean;
+ isPending?: boolean;
+ dataUpdatedAt?: number;
+ errorUpdatedAt?: number;
+ failureCount?: number;
+ failureReason?: TError;
+ errorUpdateCount?: number;
+ isRefetching?: boolean;
+ isRefetchError?: boolean;
+ isPaused?: boolean;
+}
+
+// ============================================================================
+// TANSTACK QUERY MOCK FACTORIES
+// ============================================================================
+
+/**
+ * Create a mock query state for TanStack Query
+ */
+export function createMockQueryState(
+ options: MockQueryState,
+): MockQueryObserverResult {
+ const {
+ status,
+ data,
+ error,
+ } = options;
+
+ return {
+ status: status ?? 'success',
+ data,
+ error,
+ isLoading: status === 'pending' ? true : false,
+ isFetching: status === 'pending' ? true : false,
+ isSuccess: status === 'success',
+ isError: status === 'error',
+ isPending: status === 'pending',
+ dataUpdatedAt: status === 'success' ? Date.now() : undefined,
+ errorUpdatedAt: status === 'error' ? Date.now() : undefined,
+ failureCount: status === 'error' ? 1 : 0,
+ failureReason: status === 'error' ? error : undefined,
+ errorUpdateCount: status === 'error' ? 1 : 0,
+ isRefetching: false,
+ isRefetchError: false,
+ isPaused: false,
+ };
+}
+
+/**
+ * Create a loading query state
+ */
+export function createLoadingState(): MockQueryObserverResult {
+ return createMockQueryState({ status: 'pending', data: undefined, error: undefined });
+}
+
+/**
+ * Create an error query state
+ */
+export function createErrorState(
+ error: TError,
+): MockQueryObserverResult {
+ return createMockQueryState({ status: 'error', data: undefined, error });
+}
+
+/**
+ * Create a success query state
+ */
+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 */
+ fonts: Record;
+ /** Current page */
+ page: number;
+ /** Total pages available */
+ totalPages: number;
+ /** Items per page */
+ limit: number;
+ /** Total font count */
+ total: number;
+ /** Loading state */
+ isLoading: boolean;
+ /** Error state */
+ error: Error | null;
+ /** Search query */
+ searchQuery: string;
+ /** Selected provider */
+ provider: 'google' | 'fontshare' | 'all';
+ /** Selected category */
+ category: string | null;
+ /** Selected subset */
+ subset: string | null;
+}
+
+/**
+ * Create a mock font store state
+ */
+export function createMockFontStoreState(
+ options: Partial = {},
+): MockFontStoreState {
+ const {
+ page = 1,
+ limit = 24,
+ isLoading = false,
+ error = null,
+ searchQuery = '',
+ provider = 'all',
+ category = null,
+ subset = null,
+ } = options;
+
+ // Generate mock fonts if not provided
+ const mockFonts = options.fonts ?? Object.fromEntries(
+ Object.values(UNIFIED_FONTS).map(font => [font.id, font]),
+ );
+
+ const fontArray = Object.values(mockFonts);
+ const total = options.total ?? fontArray.length;
+ const totalPages = options.totalPages ?? Math.ceil(total / limit);
+
+ return {
+ fonts: mockFonts,
+ page,
+ totalPages,
+ limit,
+ total,
+ isLoading,
+ error,
+ searchQuery,
+ provider,
+ category,
+ subset,
+ };
+}
+
+/**
+ * Preset font store states
+ */
+export const MOCK_FONT_STORE_STATES = {
+ /** Initial loading state */
+ loading: createMockFontStoreState({
+ isLoading: true,
+ fonts: {},
+ total: 0,
+ page: 1,
+ }),
+
+ /** Empty state (no fonts found) */
+ empty: createMockFontStoreState({
+ fonts: {},
+ total: 0,
+ page: 1,
+ isLoading: false,
+ }),
+
+ /** First page with fonts */
+ firstPage: createMockFontStoreState({
+ fonts: Object.fromEntries(
+ Object.values(UNIFIED_FONTS).slice(0, 10).map(font => [font.id, font]),
+ ),
+ total: 50,
+ page: 1,
+ limit: 10,
+ totalPages: 5,
+ isLoading: false,
+ }),
+
+ /** Second page with fonts */
+ secondPage: createMockFontStoreState({
+ fonts: Object.fromEntries(
+ Object.values(UNIFIED_FONTS).slice(10, 20).map(font => [font.id, font]),
+ ),
+ total: 50,
+ page: 2,
+ limit: 10,
+ totalPages: 5,
+ isLoading: false,
+ }),
+
+ /** Last page with fonts */
+ lastPage: createMockFontStoreState({
+ fonts: Object.fromEntries(
+ Object.values(UNIFIED_FONTS).slice(0, 5).map(font => [font.id, font]),
+ ),
+ total: 25,
+ page: 3,
+ limit: 10,
+ totalPages: 3,
+ isLoading: false,
+ }),
+
+ /** Error state */
+ error: createMockFontStoreState({
+ fonts: {},
+ error: new Error('Failed to load fonts'),
+ total: 0,
+ page: 1,
+ isLoading: false,
+ }),
+
+ /** With search query */
+ withSearch: createMockFontStoreState({
+ fonts: Object.fromEntries(
+ Object.values(UNIFIED_FONTS).slice(0, 3).map(font => [font.id, font]),
+ ),
+ total: 3,
+ page: 1,
+ isLoading: false,
+ searchQuery: 'Roboto',
+ }),
+
+ /** Filtered by category */
+ filteredByCategory: createMockFontStoreState({
+ fonts: Object.fromEntries(
+ Object.values(UNIFIED_FONTS)
+ .filter(f => f.category === 'serif')
+ .slice(0, 5)
+ .map(font => [font.id, font]),
+ ),
+ total: 5,
+ page: 1,
+ isLoading: false,
+ category: 'serif',
+ }),
+
+ /** Filtered by provider */
+ filteredByProvider: createMockFontStoreState({
+ fonts: Object.fromEntries(
+ Object.values(UNIFIED_FONTS)
+ .filter(f => f.provider === 'google')
+ .slice(0, 5)
+ .map(font => [font.id, font]),
+ ),
+ total: 5,
+ page: 1,
+ isLoading: false,
+ provider: 'google',
+ }),
+
+ /** Large dataset */
+ largeDataset: createMockFontStoreState({
+ fonts: Object.fromEntries(
+ generateMockFonts(50).map(font => [font.id, font]),
+ ),
+ total: 500,
+ page: 1,
+ limit: 50,
+ totalPages: 10,
+ isLoading: false,
+ }),
+};
+
+// ============================================================================
+// 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: {
+ data?: T;
+ isLoading?: boolean;
+ isError?: boolean;
+ error?: Error;
+ isFetching?: boolean;
+}) {
+ const {
+ data,
+ isLoading = false,
+ isError = false,
+ error,
+ isFetching = false,
+ } = config;
+
+ return {
+ get data() {
+ return data;
+ },
+ get isLoading() {
+ return isLoading;
+ },
+ get isError() {
+ return isError;
+ },
+ get error() {
+ return error;
+ },
+ get isFetching() {
+ return isFetching;
+ },
+ get isSuccess() {
+ return !isLoading && !isError && data !== undefined;
+ },
+ get status() {
+ if (isLoading) return 'pending';
+ if (isError) return 'error';
+ return 'success';
+ },
+ };
+}
+
+/**
+ * Preset mock stores
+ */
+export const MOCK_STORES = {
+ /** Font store in loading state */
+ loadingFontStore: createMockStore({
+ isLoading: true,
+ data: undefined,
+ }),
+
+ /** Font store with fonts loaded */
+ successFontStore: createMockStore({
+ data: Object.values(UNIFIED_FONTS),
+ isLoading: false,
+ isError: false,
+ }),
+
+ /** Font store with error */
+ errorFontStore: createMockStore({
+ data: undefined,
+ isLoading: false,
+ isError: true,
+ error: new Error('Failed to load fonts'),
+ }),
+
+ /** Font store with empty results */
+ emptyFontStore: createMockStore({
+ data: [],
+ isLoading: false,
+ isError: false,
+ }),
+
+ /**
+ * Create a mock UnifiedFontStore-like object
+ * Note: This is a simplified mock for Storybook use
+ */
+ unifiedFontStore: (state: Partial = {}) => {
+ const mockState = createMockFontStoreState(state);
+ return {
+ // State properties
+ get fonts() {
+ return mockState.fonts;
+ },
+ get page() {
+ return mockState.page;
+ },
+ get totalPages() {
+ return mockState.totalPages;
+ },
+ get limit() {
+ return mockState.limit;
+ },
+ get total() {
+ return mockState.total;
+ },
+ get isLoading() {
+ return mockState.isLoading;
+ },
+ get error() {
+ return mockState.error;
+ },
+ get searchQuery() {
+ return mockState.searchQuery;
+ },
+ get provider() {
+ return mockState.provider;
+ },
+ get category() {
+ return mockState.category;
+ },
+ get subset() {
+ return mockState.subset;
+ },
+ // Methods (no-op for Storybook)
+ nextPage: () => {},
+ prevPage: () => {},
+ goToPage: (_page: number) => {},
+ setLimit: (_limit: number) => {},
+ setProvider: (_provider: typeof mockState.provider) => {},
+ setCategory: (_category: string | null) => {},
+ setSubset: (_subset: string | null) => {},
+ setSearch: (_query: string) => {},
+ resetFilters: () => {},
+ };
+ },
+};
+
+// ============================================================================
+// REACTIVE STATE MOCKS
+// ============================================================================
+
+/**
+ * Create a reactive state object using Svelte 5 runes pattern
+ * Useful for stories that need reactive state
+ *
+ * Note: This uses plain JavaScript objects since Svelte runes
+ * only work in .svelte files. For Storybook, this provides
+ * a similar API for testing.
+ */
+export function createMockReactiveState(initialValue: T) {
+ let value = initialValue;
+
+ return {
+ get value() {
+ return value;
+ },
+ set value(newValue: T) {
+ value = newValue;
+ },
+ update(fn: (current: T) => T) {
+ value = fn(value);
+ },
+ };
+}
+
+/**
+ * Mock comparison store for ComparisonSlider component
+ */
+export function createMockComparisonStore(config: {
+ fontA?: UnifiedFont;
+ fontB?: UnifiedFont;
+ text?: string;
+} = {}) {
+ const { fontA, fontB, text = 'The quick brown fox jumps over the lazy dog.' } = config;
+
+ return {
+ get fontA() {
+ return fontA ?? UNIFIED_FONTS.roboto;
+ },
+ get fontB() {
+ return fontB ?? UNIFIED_FONTS.openSans;
+ },
+ get text() {
+ return text;
+ },
+ // Methods (no-op for Storybook)
+ setFontA: (_font: UnifiedFont | undefined) => {},
+ setFontB: (_font: UnifiedFont | undefined) => {},
+ setText: (_text: string) => {},
+ swapFonts: () => {},
+ };
+}
+
+// ============================================================================
+// MOCK DATA GENERATORS
+// ============================================================================
+
+/**
+ * Generate paginated font data
+ */
+export function generatePaginatedFonts(
+ totalCount: number,
+ page: number,
+ limit: number,
+): {
+ fonts: UnifiedFont[];
+ page: number;
+ totalPages: number;
+ total: number;
+ hasNextPage: boolean;
+ hasPrevPage: boolean;
+} {
+ const totalPages = Math.ceil(totalCount / limit);
+ const startIndex = (page - 1) * limit;
+ const endIndex = Math.min(startIndex + limit, totalCount);
+
+ return {
+ fonts: generateMockFonts(endIndex - startIndex).map((font, i) => ({
+ ...font,
+ id: `font-${startIndex + i + 1}`,
+ name: `Font ${startIndex + i + 1}`,
+ })),
+ page,
+ totalPages,
+ total: totalCount,
+ hasNextPage: page < totalPages,
+ hasPrevPage: page > 1,
+ };
+}
+
+/**
+ * Create mock API response for fonts
+ */
+export function createMockFontApiResponse(config: {
+ fonts?: UnifiedFont[];
+ total?: number;
+ page?: number;
+ limit?: number;
+} = {}) {
+ const fonts = config.fonts ?? Object.values(UNIFIED_FONTS);
+ const total = config.total ?? fonts.length;
+ const page = config.page ?? 1;
+ const limit = config.limit ?? fonts.length;
+
+ return {
+ data: fonts,
+ meta: {
+ total,
+ page,
+ limit,
+ totalPages: Math.ceil(total / limit),
+ hasNextPage: page < Math.ceil(total / limit),
+ hasPrevPage: page > 1,
+ },
+ };
+}
diff --git a/src/shared/lib/storybook/MockIcon.svelte b/src/shared/lib/storybook/MockIcon.svelte
new file mode 100644
index 0000000..82c91ee
--- /dev/null
+++ b/src/shared/lib/storybook/MockIcon.svelte
@@ -0,0 +1,41 @@
+
+
+
+{#if Icon}
+ {@const __iconClass__ = cn('size-4', className)}
+
+
+{/if}
diff --git a/src/shared/lib/storybook/Providers.svelte b/src/shared/lib/storybook/Providers.svelte
new file mode 100644
index 0000000..3f26917
--- /dev/null
+++ b/src/shared/lib/storybook/Providers.svelte
@@ -0,0 +1,64 @@
+
+
+
+
+
+ {@render children()}
+
+
diff --git a/src/shared/lib/storybook/index.ts b/src/shared/lib/storybook/index.ts
new file mode 100644
index 0000000..dc0137f
--- /dev/null
+++ b/src/shared/lib/storybook/index.ts
@@ -0,0 +1,24 @@
+/**
+ * ============================================================================
+ * STORYBOOK HELPERS
+ * ============================================================================
+ *
+ * Helper components and utilities for Storybook stories.
+ *
+ * ## Usage
+ *
+ * ```svelte
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * @module
+ */
+
+export { default as MockIcon } from './MockIcon.svelte';
+export { default as Providers } from './Providers.svelte';
diff --git a/src/shared/ui/ComboControlV2/ComboControlV2.stories.svelte b/src/shared/ui/ComboControlV2/ComboControlV2.stories.svelte
index f1ac0c6..232fadc 100644
--- a/src/shared/ui/ComboControlV2/ComboControlV2.stories.svelte
+++ b/src/shared/ui/ComboControlV2/ComboControlV2.stories.svelte
@@ -10,7 +10,8 @@ const { Story } = defineMeta({
parameters: {
docs: {
description: {
- component: 'ComboControl with input field and slider',
+ component:
+ 'ComboControl with input field and slider. Simplified version without increase/decrease buttons.',
},
story: { inline: false }, // Render stories in iframe for state isolation
},
@@ -26,18 +27,85 @@ const { Story } = defineMeta({
control: 'text',
description: 'Label for the ComboControl',
},
+ control: {
+ control: 'object',
+ description: 'TypographyControl instance managing the value and bounds',
+ },
},
});
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/shared/ui/IconButton/IconButton.stories.svelte b/src/shared/ui/IconButton/IconButton.stories.svelte
new file mode 100644
index 0000000..b485dde
--- /dev/null
+++ b/src/shared/ui/IconButton/IconButton.stories.svelte
@@ -0,0 +1,101 @@
+
+
+
+
+{#snippet chevronRightIcon({ className }: { className: string })}
+
+{/snippet}
+
+{#snippet chevronLeftIcon({ className }: { className: string })}
+
+{/snippet}
+
+{#snippet plusIcon({ className }: { className: string })}
+
+{/snippet}
+
+{#snippet minusIcon({ className }: { className: string })}
+
+{/snippet}
+
+{#snippet settingsIcon({ className }: { className: string })}
+
+{/snippet}
+
+{#snippet xIcon({ className }: { className: string })}
+
+{/snippet}
+
+
+ console.log('Default clicked')}>
+ {#snippet icon({ className })}
+
+ {/snippet}
+
+
+
+
+
+
+ {#snippet icon({ className })}
+
+ {/snippet}
+
+
+
diff --git a/src/shared/ui/Section/Section.stories.svelte b/src/shared/ui/Section/Section.stories.svelte
new file mode 100644
index 0000000..9e58126
--- /dev/null
+++ b/src/shared/ui/Section/Section.stories.svelte
@@ -0,0 +1,475 @@
+
+
+
+
+{#snippet searchIcon({ className }: { className?: string })}
+
+{/snippet}
+
+{#snippet welcomeTitle({ className }: { className?: string })}
+ Welcome
+{/snippet}
+
+{#snippet welcomeContent({ className }: { className?: string })}
+
+
+ This is the default section layout with a title and content area. The section uses a 2-column grid layout
+ with the title on the left and content on the right.
+
+
+{/snippet}
+
+{#snippet stickyTitle({ className }: { className?: string })}
+ Sticky Title
+{/snippet}
+
+{#snippet stickyContent({ className }: { className?: string })}
+
+
+ This section has a sticky title that stays fixed while you scroll through the content. Try scrolling down to
+ see the effect.
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et
+ dolore magna aliqua.
+
+
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+ consequat.
+
+
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+
+
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollim anim id est
+ laborum.
+
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
+
+
+
+{/snippet}
+
+{#snippet searchFontsTitle({ className }: { className?: string })}
+ Search Fonts
+{/snippet}
+
+{#snippet searchFontsDescription({ className }: { className?: string })}
+ Find your perfect typeface
+{/snippet}
+
+{#snippet searchFontsContent({ className }: { className?: string })}
+
+
+ Use the search bar to find fonts by name, or use the filters to browse by category, subset, or provider.
+
+
+{/snippet}
+
+{#snippet longContentTitle({ className }: { className?: string })}
+ Long Content
+{/snippet}
+
+{#snippet longContent({ className }: { className?: string })}
+
+
+
+ This section demonstrates how the sticky title behaves with longer content. As you scroll through this
+ content, the title remains visible at the top of the viewport.
+
+
+ Content block 1
+
+
+ The sticky position is achieved using CSS position: sticky with a configurable top offset. This is
+ useful for long sections where you want to maintain context while scrolling.
+
+
+ Content block 2
+
+
+ The Intersection Observer API is used to detect when the section title scrolls out of view, triggering
+ the optional onTitleStatusChange callback.
+
+
+ Content block 3
+
+
+
+{/snippet}
+
+{#snippet minimalTitle({ className }: { className?: string })}
+ Minimal Section
+{/snippet}
+
+{#snippet minimalContent({ className }: { className?: string })}
+
+
+ A minimal section without index, icon, or description. Just the essentials.
+
+
+{/snippet}
+
+{#snippet customTitle({ className }: { className?: string })}
+ Custom Content
+{/snippet}
+
+{#snippet customDescription({ className }: { className?: string })}
+ With interactive elements
+{/snippet}
+
+{#snippet customContent({ className }: { className?: string })}
+
+
+
+
Card 1
+
Some content here
+
+
+
Card 2
+
More content here
+
+
+
+{/snippet}
+
+
+
+
+ {#snippet title({ className })}
+ Welcome
+ {/snippet}
+ {#snippet content({ className })}
+
+
+ This is the default section layout with a title and content area. The section uses a 2-column
+ grid layout with the title on the left and content on the right.
+
+
+ {/snippet}
+
+
+
+
+
+
+
+
+ {#snippet title({ className })}
+ Sticky Title
+ {/snippet}
+ {#snippet content({ className })}
+
+
+ This section has a sticky title that stays fixed while you scroll through the content. Try
+ scrolling down to see the effect.
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
+ incididunt ut labore et dolore magna aliqua.
+
+
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+ commodo consequat.
+
+
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
+ nulla pariatur.
+
+
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
+ mollim anim id est laborum.
+
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
+ laudantium.
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
+
+ {#snippet title({ className })}
+ Search Fonts
+ {/snippet}
+ {#snippet icon({ className })}
+
+ {/snippet}
+ {#snippet description({ className })}
+ Find your perfect typeface
+ {/snippet}
+ {#snippet content({ className })}
+
+
+ Use the search bar to find fonts by name, or use the filters to browse by category, subset, or
+ provider.
+
+
+ {/snippet}
+
+
+
+
+
+
+
+ {#snippet title({ className })}
+ Typography
+ {/snippet}
+ {#snippet icon({ className })}
+
+ {/snippet}
+ {#snippet description({ className })}
+ Adjust text appearance
+ {/snippet}
+ {#snippet content({ className })}
+
+
+ Control the size, weight, and line height of your text. These settings apply across the
+ comparison view.
+
+
+ {/snippet}
+
+
+
+ {#snippet title({ className })}
+ Font Search
+ {/snippet}
+ {#snippet icon({ className })}
+
+ {/snippet}
+ {#snippet description({ className })}
+ Browse available typefaces
+ {/snippet}
+ {#snippet content({ className })}
+
+
+ Search through our collection of fonts from Google Fonts and Fontshare. Use filters to narrow
+ down your selection.
+
+
+ {/snippet}
+
+
+
+ {#snippet title({ className })}
+ Sample List
+ {/snippet}
+ {#snippet icon({ className })}
+
+ {/snippet}
+ {#snippet description({ className })}
+ Preview font samples
+ {/snippet}
+ {#snippet content({ className })}
+
+
+ Browse through font samples with your custom text. The list is virtualized for optimal
+ performance.
+
+
+ {/snippet}
+
+
+
+
+
+
+
+ {#snippet title({ className })}
+ Long Content
+ {/snippet}
+ {#snippet content({ className })}
+
+
+
+ This section demonstrates how the sticky title behaves with longer content. As you scroll
+ through this content, the title remains visible at the top of the viewport.
+
+
+ Content block 1
+
+
+ The sticky position is achieved using CSS position: sticky with a configurable top offset.
+ This is useful for long sections where you want to maintain context while scrolling.
+
+
+ Content block 2
+
+
+ The Intersection Observer API is used to detect when the section title scrolls out of view,
+ triggering the optional onTitleStatusChange callback.
+
+
+ Content block 3
+
+
+
+ {/snippet}
+
+
+
+
+
+
+
+ {#snippet title({ className })}
+ Minimal Section
+ {/snippet}
+ {#snippet content({ className })}
+
+
+ A minimal section without index, icon, or description. Just the essentials.
+
+
+ {/snippet}
+
+
+
+
+
+
+
+ {#snippet title({ className })}
+ Custom Content
+ {/snippet}
+ {#snippet description({ className })}
+ With interactive elements
+ {/snippet}
+ {#snippet content({ className })}
+
+
+
+
Card 1
+
Some content here
+
+
+
Card 2
+
More content here
+
+
+
+ {/snippet}
+
+
+
diff --git a/src/shared/ui/Slider/Slider.stories.svelte b/src/shared/ui/Slider/Slider.stories.svelte
index 9198125..3495c3f 100644
--- a/src/shared/ui/Slider/Slider.stories.svelte
+++ b/src/shared/ui/Slider/Slider.stories.svelte
@@ -31,21 +31,26 @@ const { Story } = defineMeta({
control: 'number',
description: 'Step size for value increments',
},
+ label: {
+ control: 'text',
+ description: 'Optional label displayed inline on the track',
+ },
},
});
-
-
+
+
-
-
+
+
+
+
+
+
diff --git a/src/shared/ui/VirtualList/VirtualList.stories.svelte b/src/shared/ui/VirtualList/VirtualList.stories.svelte
index 1e3064e..cb835d6 100644
--- a/src/shared/ui/VirtualList/VirtualList.stories.svelte
+++ b/src/shared/ui/VirtualList/VirtualList.stories.svelte
@@ -38,27 +38,31 @@ const { Story } = defineMeta({
-
- {#snippet children({ item })}
- {item}
- {/snippet}
-
+
+
+ {#snippet children({ item })}
+ {item}
+ {/snippet}
+
+
-
-
- {#snippet children({ item })}
- {item}
- {/snippet}
-
+
+
+
+ {#snippet children({ item })}
+ {item}
+ {/snippet}
+
+
diff --git a/src/widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.stories.svelte b/src/widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.stories.svelte
new file mode 100644
index 0000000..78b671a
--- /dev/null
+++ b/src/widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.stories.svelte
@@ -0,0 +1,217 @@
+
+
+
+
+
+ {@const _ = (comparisonStore.fontA = mockArial, comparisonStore.fontB = mockGeorgia)}
+
+
+
+
+
+
+ {@const _ = (comparisonStore.fontA = mockCourier, comparisonStore.fontB = mockVerdana)}
+
+
+
+
+
+
+ {@const _ = (comparisonStore.fontA = undefined, comparisonStore.fontB = undefined)}
+
+
+
+
+
+
+ {@const _ = (
+ comparisonStore.fontA = mockArial,
+ comparisonStore.fontB = mockVerdana,
+ comparisonStore.text = 'Typography is the art and technique of arranging type.'
+)}
+
+
+
+
+
+
+ {@const _ = (comparisonStore.fontA = mockGeorgia, comparisonStore.fontB = mockCourier, comparisonStore.text = 'Hello')}
+
+
+
+
+
+
+ {@const _ = (
+ comparisonStore.fontA = mockArial,
+ comparisonStore.fontB = mockGeorgia,
+ comparisonStore.text =
+ 'The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs. How vexingly quick daft zebras jump!'
+)}
+
+
+
+
+
+
+ {@const _ = (comparisonStore.fontA = mockVerdana, comparisonStore.fontB = mockArial)}
+
+
+
+
+
Click the settings icon to toggle settings mode
+
+
+
+
+
+
+
+
+ {@const _ = (comparisonStore.fontA = mockGeorgia, comparisonStore.fontB = mockVerdana)}
+
+
+
+
+
Resize the browser to see responsive behavior
+
+
+
+
+
+
diff --git a/src/widgets/FontSearch/ui/FontSearch/FontSearch.stories.svelte b/src/widgets/FontSearch/ui/FontSearch/FontSearch.stories.svelte
new file mode 100644
index 0000000..c950054
--- /dev/null
+++ b/src/widgets/FontSearch/ui/FontSearch/FontSearch.stories.svelte
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Filters panel is open and visible
+
+
+
+
+
+
+
+
+
Filters panel is closed - click the slider icon to open
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Font Browser
+
Search and filter through our collection of fonts
+
+
+
+
+
+
+
+
Font results will appear here...
+
+
+
+
+
+
+
+
+ Demo Note: Click the slider icon to toggle filters. Use the
+ filter categories to select options. Use the filter controls to reset or apply your selections.
+
+
+
+
+
+
+
+
+
+
Resize browser to see responsive layout
+
+
+
+
+
+
diff --git a/src/widgets/SampleList/ui/SampleList/SampleList.stories.svelte b/src/widgets/SampleList/ui/SampleList/SampleList.stories.svelte
new file mode 100644
index 0000000..14a354a
--- /dev/null
+++ b/src/widgets/SampleList/ui/SampleList/SampleList.stories.svelte
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
Font Samples
+
Scroll to see more fonts and load additional pages
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Typography Controls
+
Scroll down to see the typography menu appear
+
+
+
+
+
+
+
+
+
+
+
Custom Sample Text
+
Edit the text in any card to change all samples
+
+
+
+
+
+
+
+
+
+
+
Paginated List
+
Fonts load automatically as you scroll
+
+
+
+
+
+
+
+
+
+
+
Responsive Sample List
+
Resize browser to see responsive behavior
+
+
+
+
+