From da79dd2e359c9645adbd9830452db05b359d0b52 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 19 Feb 2026 13:58:12 +0300 Subject: [PATCH] feat: storybook cases and mocks --- .storybook/Decorator.svelte | 29 + .storybook/StoryStage.svelte | 6 +- .storybook/main.ts | 3 +- .storybook/preview-head.html | 13 + .storybook/preview.ts | 47 +- src/entities/Font/index.ts | 50 ++ src/entities/Font/lib/index.ts | 50 ++ src/entities/Font/lib/mocks/filters.mock.ts | 348 ++++++++++ src/entities/Font/lib/mocks/fonts.mock.ts | 630 ++++++++++++++++++ src/entities/Font/lib/mocks/index.ts | 84 +++ src/entities/Font/lib/mocks/stores.mock.ts | 590 ++++++++++++++++ src/shared/lib/storybook/MockIcon.svelte | 41 ++ src/shared/lib/storybook/Providers.svelte | 64 ++ src/shared/lib/storybook/index.ts | 24 + .../ComboControlV2.stories.svelte | 80 ++- .../ui/IconButton/IconButton.stories.svelte | 101 +++ src/shared/ui/Section/Section.stories.svelte | 475 +++++++++++++ src/shared/ui/Slider/Slider.stories.svelte | 19 +- .../ui/VirtualList/VirtualList.stories.svelte | 30 +- .../ComparisonSlider.stories.svelte | 217 ++++++ .../ui/FontSearch/FontSearch.stories.svelte | 102 +++ .../ui/SampleList/SampleList.stories.svelte | 89 +++ 22 files changed, 3047 insertions(+), 45 deletions(-) create mode 100644 .storybook/Decorator.svelte create mode 100644 .storybook/preview-head.html create mode 100644 src/entities/Font/lib/mocks/filters.mock.ts create mode 100644 src/entities/Font/lib/mocks/fonts.mock.ts create mode 100644 src/entities/Font/lib/mocks/index.ts create mode 100644 src/entities/Font/lib/mocks/stores.mock.ts create mode 100644 src/shared/lib/storybook/MockIcon.svelte create mode 100644 src/shared/lib/storybook/Providers.svelte create mode 100644 src/shared/lib/storybook/index.ts create mode 100644 src/shared/ui/IconButton/IconButton.stories.svelte create mode 100644 src/shared/ui/Section/Section.stories.svelte create mode 100644 src/widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.stories.svelte create mode 100644 src/widgets/FontSearch/ui/FontSearch/FontSearch.stories.svelte create mode 100644 src/widgets/SampleList/ui/SampleList/SampleList.stories.svelte diff --git a/.storybook/Decorator.svelte b/.storybook/Decorator.svelte new file mode 100644 index 0000000..e2abefd --- /dev/null +++ b/.storybook/Decorator.svelte @@ -0,0 +1,29 @@ + + + + + {@render children()} + diff --git a/.storybook/StoryStage.svelte b/.storybook/StoryStage.svelte index d0510dc..5a9a9c7 100644 --- a/.storybook/StoryStage.svelte +++ b/.storybook/StoryStage.svelte @@ -7,9 +7,9 @@ interface Props { let { children, width = 'max-w-3xl' }: Props = $props(); -
-
-
+
+
+
{@render children()}
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

+
+ +
+
+