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"