From 87bba388dc7c206494be34e2116496ff7f8ecc05 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 2 Mar 2026 22:21:19 +0300 Subject: [PATCH] chore(app): update config, dependencies, storybook, and app shell --- .storybook/ThemeDecorator.svelte | 49 ++++++++ .storybook/preview.ts | 146 +++++++++++++++++++++++- package.json | 1 - src/app/App.svelte | 4 + src/app/providers/QueryProvider.svelte | 3 + src/app/styles/app.css | 121 ++++++++++---------- src/app/ui/Layout.svelte | 57 +++------- src/routes/Page.svelte | 149 ++----------------------- vitest.config.ts | 2 +- vitest.setup.unit.ts | 83 ++++++++++++++ yarn.lock | 37 ------ 11 files changed, 369 insertions(+), 283 deletions(-) create mode 100644 vitest.setup.unit.ts diff --git a/.storybook/ThemeDecorator.svelte b/.storybook/ThemeDecorator.svelte index 0df30cb..bcf6731 100644 --- a/.storybook/ThemeDecorator.svelte +++ b/.storybook/ThemeDecorator.svelte @@ -2,9 +2,15 @@ Component: ThemeDecorator Storybook decorator that initializes ThemeManager for theme-related stories. Ensures theme management works correctly in Storybook's iframe environment. + Includes a floating theme toggle for universal theme switching across all stories. --> + +
+ Theme: {themeLabel} + themeManager.toggle()} + size={responsive?.isMobile ? 'sm' : 'md'} + variant="ghost" + title="Toggle theme" + > + {#snippet icon()} + {#if theme === 'light'} + + {:else} + + {/if} + {/snippet} + +
+ + {@render children()} diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 281bb4e..4d8f256 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,11 +1,74 @@ import type { Preview } from '@storybook/svelte-vite'; import Decorator from './Decorator.svelte'; import StoryStage from './StoryStage.svelte'; +import ThemeDecorator from './ThemeDecorator.svelte'; import '../src/app/styles/app.css'; const preview: Preview = { + globalTypes: { + viewport: { + description: 'Viewport size for responsive design', + defaultValue: 'widgetWide', + toolbar: { + icon: 'view', + items: [ + { + value: 'reset', + icon: 'refresh', + title: 'Reset viewport', + }, + { + value: 'mobile1', + icon: 'mobile', + title: 'iPhone 5/SE', + }, + { + value: 'mobile2', + icon: 'mobile', + title: 'iPhone 14 Pro Max', + }, + { + value: 'tablet', + icon: 'tablet', + title: 'iPad (Portrait)', + }, + { + value: 'desktop', + icon: 'desktop', + title: 'Desktop (Small)', + }, + { + value: 'widgetMedium', + icon: 'view', + title: 'Widget Medium', + }, + { + value: 'widgetWide', + icon: 'view', + title: 'Widget Wide', + }, + { + value: 'widgetExtraWide', + icon: 'view', + title: 'Widget Extra Wide', + }, + { + value: 'fullWidth', + icon: 'view', + title: 'Full Width', + }, + { + value: 'fullScreen', + icon: 'expand', + title: 'Full Screen', + }, + ], + dynamicTitle: true, + }, + }, + }, parameters: { - layout: 'fullscreen', + layout: 'padded', controls: { matchers: { color: /(background|color)$/i, @@ -23,7 +86,79 @@ const preview: Preview = { docs: { story: { // This sets the default height for the iframe in Autodocs - iframeHeight: '400px', + iframeHeight: '600px', + }, + }, + + viewport: { + viewports: { + // Mobile devices + mobile1: { + name: 'iPhone 5/SE', + styles: { + width: '320px', + height: '568px', + }, + }, + mobile2: { + name: 'iPhone 14 Pro Max', + styles: { + width: '414px', + height: '896px', + }, + }, + // Tablet + tablet: { + name: 'iPad (Portrait)', + styles: { + width: '834px', + height: '1112px', + }, + }, + desktop: { + name: 'Desktop (Small)', + styles: { + width: '1024px', + height: '1280px', + }, + }, + // Widget-specific viewports + widgetMedium: { + name: 'Widget Medium', + styles: { + width: '768px', + height: '800px', + }, + }, + widgetWide: { + name: 'Widget Wide', + styles: { + width: '1024px', + height: '800px', + }, + }, + widgetExtraWide: { + name: 'Widget Extra Wide', + styles: { + width: '1280px', + height: '800px', + }, + }, + // Full-width viewports + fullWidth: { + name: 'Full Width', + styles: { + width: '100%', + height: '800px', + }, + }, + fullScreen: { + name: 'Full Screen', + styles: { + width: '100%', + height: '100%', + }, + }, }, }, @@ -45,6 +180,13 @@ const preview: Preview = { }, decorators: [ + // Outermost: initialize ThemeManager for all stories + story => ({ + Component: ThemeDecorator, + props: { + children: story(), + }, + }), // Wrap with providers (TooltipProvider, ResponsiveManager) story => ({ Component: Decorator, diff --git a/package.json b/package.json index c98f54b..3bdf137 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "tailwindcss": "^4.1.18", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", - "vaul-svelte": "^1.0.0-next.7", "vite": "^7.2.6", "vitest": "^4.0.16", "vitest-browser-svelte": "^2.0.1" diff --git a/src/app/App.svelte b/src/app/App.svelte index a4027c8..38a8bce 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -1,3 +1,7 @@ + @@ -94,30 +74,29 @@ onDestroy(() => themeManager.destroy()); href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Syne:wght@800&display=swap" /> - Compare Typography & Typefaces | GlyphDiff + GlyphDiff | Typography & Typefaces
-
-
- - {#if fontsReady} - {@render children?.()} - {/if} - -
+ + + {#if fontsReady} + {@render children?.()} + {/if} + +
diff --git a/src/routes/Page.svelte b/src/routes/Page.svelte index 2343abf..19c108d 100644 --- a/src/routes/Page.svelte +++ b/src/routes/Page.svelte @@ -4,149 +4,22 @@ --> -
-
- {#snippet icon({ className })} - - {/snippet} - {#snippet description({ className })} - Project_Codename - {/snippet} - {#snippet content({ className })} -
- -
- {/snippet} -
- -
- {#snippet icon({ className })} - - {/snippet} - {#snippet title({ className })} -

- Optical
Comparator -

- {/snippet} - {#snippet content({ className })} -
- -
- {/snippet} -
- -
- {#snippet icon({ className })} - - {/snippet} - {#snippet title({ className })} -

- Query
Module -

- {/snippet} - {#snippet content({ className })} -
- -
- {/snippet} -
- -
- {#snippet icon({ className })} - - {/snippet} - {#snippet title({ className })} -

- Sample
Set -

- {/snippet} - {#snippet content({ className })} -
- -
- {/snippet} -
+
+ +
+
+ + +
- - diff --git a/vitest.config.ts b/vitest.config.ts index d65fb1a..b14ba67 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -48,7 +48,7 @@ export default defineConfig({ statements: 70, }, }, - setupFiles: [], + setupFiles: ['./vitest.setup.unit.ts'], globals: false, }, diff --git a/vitest.setup.unit.ts b/vitest.setup.unit.ts new file mode 100644 index 0000000..7a3966f --- /dev/null +++ b/vitest.setup.unit.ts @@ -0,0 +1,83 @@ +/** + * Setup file for unit tests + * + * This file runs before all unit tests to set up global mocks + * that are needed before any module imports. + * + * IMPORTANT: This runs in Node environment BEFORE jsdom is initialized + * for test files that use @vitest-environment jsdom. + */ + +import { vi } from 'vitest'; + +// Create a storage map that persists through the test session +// This is used for the localStorage mock +// We make it global so tests can clear it +(globalThis as any).__testStorageMap = new Map(); + +// Mock ResizeObserver for tests that import modules using responsiveManager +// This must be done at setup time because the responsiveManager singleton +// is instantiated when the module is first imported +globalThis.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} +} as any; + +// Mock MediaQueryListEvent for tests that need to simulate system theme changes +// @ts-expect-error - Mocking a DOM API +globalThis.MediaQueryListEvent = class MediaQueryListEvent extends Event { + matches: boolean; + media: string; + + constructor(type: string, eventInitDict: { matches: boolean; media: string }) { + super(type); + this.matches = eventInitDict.matches; + this.media = eventInitDict.media; + } +}; + +// Mock window.matchMedia for tests that import modules using media queries +// Some modules (like createPerspectiveManager) use matchMedia during import +Object.defineProperty(globalThis, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +// Mock localStorage for tests that use it during module import +// Some modules (like ThemeManager via createPersistentStore) access localStorage during initialization +// This MUST be a fully functional mock since it's used during module load +const getStorageMap = () => (globalThis as any).__testStorageMap; + +Object.defineProperty(globalThis, 'localStorage', { + writable: true, + value: { + get length() { + return getStorageMap().size; + }, + clear() { + getStorageMap().clear(); + }, + getItem(key: string) { + return getStorageMap().get(key) ?? null; + }, + setItem(key: string, value: string) { + getStorageMap().set(key, value); + }, + removeItem(key: string) { + getStorageMap().delete(key); + }, + key(index: number) { + return Array.from(getStorageMap().keys())[index] ?? null; + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 84894cd..e1f0e72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2470,7 +2470,6 @@ __metadata: tailwindcss: "npm:^4.1.18" tw-animate-css: "npm:^1.4.0" typescript: "npm:^5.9.3" - vaul-svelte: "npm:^1.0.0-next.7" vite: "npm:^7.2.6" vitest: "npm:^4.0.16" vitest-browser-svelte: "npm:^2.0.1" @@ -3626,17 +3625,6 @@ __metadata: languageName: node linkType: hard -"runed@npm:^0.23.2": - version: 0.23.4 - resolution: "runed@npm:0.23.4" - dependencies: - esm-env: "npm:^1.0.0" - peerDependencies: - svelte: ^5.7.0 - checksum: 10c0/e27400af9e69b966dca449b851e82e09b3d2ddde4095ba72237599aa80fc248a23d0737c0286f751ca6c12721a5e09eb21b9d8cc872cbd70e7b161442818eece - languageName: node - linkType: hard - "runed@npm:^0.35.1": version: 0.35.1 resolution: "runed@npm:0.35.1" @@ -3920,19 +3908,6 @@ __metadata: languageName: node linkType: hard -"svelte-toolbelt@npm:^0.7.1": - version: 0.7.1 - resolution: "svelte-toolbelt@npm:0.7.1" - dependencies: - clsx: "npm:^2.1.1" - runed: "npm:^0.23.2" - style-to-object: "npm:^1.0.8" - peerDependencies: - svelte: ^5.0.0 - checksum: 10c0/a50db97c851fa65af7fbf77007bd76730a179ac0239c0121301bd26682c1078a4ffea77835492550b133849a42d3dffee0714ae076154d86be8d0b3a84c9a9bf - languageName: node - linkType: hard - "svelte2tsx@npm:^0.7.44, svelte2tsx@npm:~0.7.46": version: 0.7.46 resolution: "svelte2tsx@npm:0.7.46" @@ -4257,18 +4232,6 @@ __metadata: languageName: node linkType: hard -"vaul-svelte@npm:^1.0.0-next.7": - version: 1.0.0-next.7 - resolution: "vaul-svelte@npm:1.0.0-next.7" - dependencies: - runed: "npm:^0.23.2" - svelte-toolbelt: "npm:^0.7.1" - peerDependencies: - svelte: ^5.0.0 - checksum: 10c0/7a459122b39c9ef6bd830b525d5f6acbc07575491e05c758d9dfdb993cc98ab4dee4a9c022e475760faaf1d7bd8460a1434965431d36885a3ee48315ffa54eb3 - languageName: node - linkType: hard - "vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.2.6": version: 7.3.0 resolution: "vite@npm:7.3.0"