refactor(features, widgets): update ThemeManager, FontSampler, FontSearch, and SampleList

This commit is contained in:
Ilia Mashkov
2026-03-02 22:20:48 +03:00
parent 0fa3437661
commit 55e2efc222
14 changed files with 1512 additions and 115 deletions

View File

@@ -14,7 +14,74 @@ const { Story } = defineMeta({
},
story: { inline: false },
},
layout: 'centered',
viewport: {
viewports: {
mobile1: {
name: 'iPhone 5/SE',
styles: {
width: '320px',
height: '568px',
},
},
mobile2: {
name: 'iPhone 14 Pro Max',
styles: {
width: '414px',
height: '896px',
},
},
tablet: {
name: 'iPad (Portrait)',
styles: {
width: '834px',
height: '1112px',
},
},
desktop: {
name: 'Desktop (Small)',
styles: {
width: '1024px',
height: '1280px',
},
},
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',
},
},
fullWidth: {
name: 'Full Width',
styles: {
width: '100%',
height: '800px',
},
},
fullScreen: {
name: 'Full Screen',
styles: {
width: '100%',
height: '100%',
},
},
},
},
layout: 'padded',
},
argTypes: {
showFilters: {
@@ -31,14 +98,14 @@ let showFiltersClosed = $state(false);
let showFiltersOpen = $state(true);
</script>
<Story name="Default">
<div class="w-full max-w-2xl">
<Story name="Default" parameters={{ globals: { viewport: 'widgetWide' } }}>
<div class="w-full max-w-3xl">
<FontSearch bind:showFilters={showFiltersDefault} />
</div>
</Story>
<Story name="Filters Open">
<div class="w-full max-w-2xl">
<Story name="Filters Open" parameters={{ globals: { viewport: 'widgetWide' } }}>
<div class="w-full max-w-3xl">
<FontSearch bind:showFilters={showFiltersOpen} />
<div class="mt-8 text-center">
<p class="text-text-muted text-sm">Filters panel is open and visible</p>
@@ -46,8 +113,8 @@ let showFiltersOpen = $state(true);
</div>
</Story>
<Story name="Filters Closed">
<div class="w-full max-w-2xl">
<Story name="Filters Closed" parameters={{ globals: { viewport: 'widgetWide' } }}>
<div class="w-full max-w-3xl">
<FontSearch bind:showFilters={showFiltersClosed} />
<div class="mt-8 text-center">
<p class="text-text-muted text-sm">Filters panel is closed - click the slider icon to open</p>
@@ -55,13 +122,13 @@ let showFiltersOpen = $state(true);
</div>
</Story>
<Story name="Full Width">
<Story name="Full Width" parameters={{ globals: { viewport: 'fullWidth' } }}>
<div class="w-full px-8">
<FontSearch />
</div>
</Story>
<Story name="In Context" tags={['!autodocs']}>
<Story name="In Context" tags={['!autodocs']} parameters={{ globals: { viewport: 'widgetWide' } }}>
<div class="w-full max-w-3xl p-8 space-y-6">
<div class="text-center mb-8">
<h1 class="text-4xl font-bold mb-2">Font Browser</h1>
@@ -78,8 +145,8 @@ let showFiltersOpen = $state(true);
</div>
</Story>
<Story name="With Filters Demo">
<div class="w-full max-w-2xl">
<Story name="With Filters Demo" parameters={{ globals: { viewport: 'widgetWide' } }}>
<div class="w-full max-w-3xl">
<div class="mb-4 p-4 bg-background-40 rounded-lg">
<p class="text-sm text-text-muted">
<strong class="text-foreground">Demo Note:</strong> Click the slider icon to toggle filters. Use the
@@ -90,7 +157,7 @@ let showFiltersOpen = $state(true);
</div>
</Story>
<Story name="Responsive Behavior">
<Story name="Responsive Behavior" parameters={{ globals: { viewport: 'fullWidth' } }}>
<div class="w-full">
<div class="mb-4 text-center">
<p class="text-text-muted text-sm">Resize browser to see responsive layout</p>

View File

@@ -11,15 +11,12 @@ import {
mapManagerToParams,
} from '$features/GetFonts';
import { springySlideFade } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import {
Button,
Footnote,
IconButton,
SearchBar,
} from '$shared/ui';
import SlidersHorizontalIcon from '@lucide/svelte/icons/sliders-horizontal';
import { onMount } from 'svelte';
import { untrack } from 'svelte';
import { cubicOut } from 'svelte/easing';
import {
Tween,
@@ -29,22 +26,17 @@ import { type SlideParams } from 'svelte/transition';
interface Props {
/**
* Controllable flag to show/hide filters (bindable)
* Show filters flag
* @default true
*/
showFilters?: boolean;
}
let { showFilters = $bindable(true) }: Props = $props();
onMount(() => {
/**
* The Pairing:
* We "plug" this manager into the global store.
* addBinding returns a function that removes this binding when the component unmounts.
*/
const unbind = unifiedFontStore.addBinding(() => mapManagerToParams(filterManager));
return unbind;
$effect(() => {
const params = mapManagerToParams(filterManager);
untrack(() => unifiedFontStore.setParams(params));
});
const transform = new Tween(
@@ -67,8 +59,8 @@ function toggleFilters() {
}
</script>
<div class="flex flex-col gap-3 border-b border-t border-[#1a1a1a]/5 dark:border-white/10">
<div class="relative w-full flex border-b border-[#1a1a1a]/5 dark:border-white/10 py-4 md:py-6">
<div class="flex flex-col gap-3 border-b border-t border-swiss-black/5 dark:border-white/10">
<div class="relative w-full flex flex-col md:flex-row gap-y-4 border-b border-swiss-black/5 dark:border-white/10 py-4 md:py-6">
<SearchBar
id="font-search"
class="w-full"

View File

@@ -3,29 +3,45 @@
Wraps FontSearch with a Section component
-->
<script lang="ts">
import { handleTitleStatusChanged } from '$entities/Breadcrumb';
import { NavigationWrapper } from '$entities/Breadcrumb';
import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Section } from '$shared/ui';
import { getContext } from 'svelte';
import {
getContext,
untrack,
} from 'svelte';
import FontSearch from '../FontSearch/FontSearch.svelte';
let isExpanded = $state(true);
const responsive = getContext<ResponsiveManager>('responsive');
const isMobileOrTabletPortrait = $derived(responsive.isMobile || responsive.isTabletPortrait);
let isExpanded = $state(!isMobileOrTabletPortrait);
$effect(() => {
if (isMobileOrTabletPortrait) {
untrack(() => {
isExpanded = false;
});
}
});
</script>
<Section
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-x-12 sm:gap-y-8"
index={2}
id="query_module"
onTitleStatusChange={handleTitleStatusChanged}
title="Query Module"
headerTitle="query_matrix"
headerSubtitle="active_nodes:"
>
{#snippet content({ className })}
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
<FontSearch bind:showFilters={isExpanded} />
</div>
<NavigationWrapper index={1} title="Query">
{#snippet content(registerAction)}
<Section
class="pt-16 md:pt-24 gap-6 sm:gap-x-12 sm:gap-y-8"
index={0}
id="query_module"
title="Query Module"
headerTitle="query_matrix"
headerSubtitle="active_nodes:"
headerAction={registerAction}
>
{#snippet content({ className })}
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
<FontSearch bind:showFilters={isExpanded} />
</div>
{/snippet}
</Section>
{/snippet}
</Section>
</NavigationWrapper>

View File

@@ -1,3 +1,21 @@
/**
* Layout mode manager for SampleList widget
*
* Manages the display layout (list vs grid) for the sample list widget.
* Persists user preference and provides responsive column calculations.
*
* Layout modes:
* - List: Single column, full-width items
* - Grid: Multi-column with responsive breakpoints
*
* Responsive grid columns:
* - Mobile (< 640px): 1 column
* - Tablet Portrait (640-767px): 1 column
* - Tablet (768-1023px): 2 columns
* - Desktop (1024-1279px): 3 columns
* - Desktop Large (>= 1280px): 4 columns
*/
import { createPersistentStore } from '$shared/lib';
import { responsiveManager } from '$shared/lib';
@@ -16,12 +34,15 @@ const DEFAULT_CONFIG: LayoutConfig = {
};
/**
* LayoutManager manages the layout configuration for SampleList widget.
* Handles mode switching between list/grid and responsive column calculation.
* Layout manager for SampleList widget
*
* Handles mode switching between list/grid and responsive column
* calculation. Persists user preference to localStorage.
*/
class LayoutManager {
// Private reactive state
/** Current layout mode */
#mode = $state<LayoutMode>(DEFAULT_CONFIG.mode);
/** Persistent storage for layout preference */
#store = createPersistentStore<LayoutConfig>(STORAGE_KEY, DEFAULT_CONFIG);
constructor() {
@@ -32,31 +53,34 @@ class LayoutManager {
}
}
/** Current layout mode ('list' or 'grid') */
get mode(): LayoutMode {
return this.#mode;
}
/**
* Gap between items in pixels
* Responsive: 16px on mobile, 24px on tablet+
*/
get gap(): number {
return responsiveManager.isMobile || responsiveManager.isTabletPortrait ? SM_GAP_PX : MD_GAP_PX;
}
/** Whether currently in list mode */
get isListMode(): boolean {
return this.#mode === 'list';
}
/** Whether currently in grid mode */
get isGridMode(): boolean {
return this.#mode === 'grid';
}
/**
* Get current number of columns based on mode and screen size.
* Current number of columns based on mode and screen size
*
* List mode always uses 1 column.
* Grid mode uses responsive column counts:
* - Mobile: 1 column
* - Tablet Portrait: 1 column
* - Tablet: 2 columns
* - Desktop: 3 columns
* - Desktop Large: 4 columns
* Grid mode uses responsive column counts.
*/
get columns(): number {
if (this.#mode === 'list') {
@@ -81,7 +105,7 @@ class LayoutManager {
}
/**
* Set the layout mode.
* Set the layout mode
* @param mode - The new layout mode ('list' or 'grid')
*/
setMode(mode: LayoutMode): void {
@@ -94,14 +118,14 @@ class LayoutManager {
}
/**
* Toggle between list and grid modes.
* Toggle between list and grid modes
*/
toggleMode(): void {
this.setMode(this.#mode === 'list' ? 'grid' : 'list');
}
/**
* Reset to default layout mode.
* Reset to default layout mode
*/
reset(): void {
this.#mode = DEFAULT_CONFIG.mode;
@@ -109,5 +133,10 @@ class LayoutManager {
}
}
// Export a singleton — one instance for the whole app
/**
* Singleton layout manager instance
*/
export const layoutManager = new LayoutManager();
// Export class for testing purposes
export { LayoutManager };

View File

@@ -0,0 +1,381 @@
/** @vitest-environment jsdom */
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
// Helper to flush Svelte effects (they run in microtasks)
async function flushEffects() {
await Promise.resolve();
}
// Storage key used by LayoutManager
const STORAGE_KEY = 'glyphdiff:sample-list-layout';
describe('layoutStore', () => {
// Default viewport for most tests (desktop large - >= 1536px)
const DEFAULT_WIDTH = 1600;
beforeEach(() => {
// Clear localStorage before each test
localStorage.clear();
// Mock window.innerWidth for responsive manager
// Default to desktop large (>= 1536px)
Object.defineProperty(window, 'innerWidth', {
writable: true,
configurable: true,
value: DEFAULT_WIDTH,
});
// Trigger a resize event to update responsiveManager
window.dispatchEvent(new Event('resize'));
});
describe('Initialization', () => {
it('should initialize with default list mode when no saved value', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.mode).toBe('list');
expect(manager.isListMode).toBe(true);
expect(manager.isGridMode).toBe(false);
});
it('should load saved grid mode from localStorage', async () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ mode: 'grid' }));
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.mode).toBe('grid');
expect(manager.isListMode).toBe(false);
expect(manager.isGridMode).toBe(true);
});
it('should load saved list mode from localStorage', async () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ mode: 'list' }));
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.mode).toBe('list');
expect(manager.isListMode).toBe(true);
expect(manager.isGridMode).toBe(false);
});
it('should default to list mode when localStorage has invalid data', async () => {
localStorage.setItem(STORAGE_KEY, 'invalid json');
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.mode).toBe('list');
});
it('should default to list mode when localStorage has empty object', async () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify({}));
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.mode).toBe('list');
});
});
describe('columns', () => {
it('should return 1 column in list mode regardless of screen size', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('list');
// At default viewport (1600px - desktop large)
expect(manager.columns).toBe(1);
});
describe('grid mode', () => {
it('should return 1 column on mobile (< 640px)', async () => {
Object.defineProperty(window, 'innerWidth', { value: 320, configurable: true, writable: true });
window.dispatchEvent(new Event('resize'));
await flushEffects();
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
await flushEffects();
expect(manager.columns).toBe(1);
});
it('should return 1 column on tablet portrait (640-767px)', async () => {
Object.defineProperty(window, 'innerWidth', { value: 700, configurable: true, writable: true });
window.dispatchEvent(new Event('resize'));
await flushEffects();
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
await flushEffects();
expect(manager.columns).toBe(1);
});
it('should return 2 columns on tablet (768-1279px)', async () => {
Object.defineProperty(window, 'innerWidth', { value: 900, configurable: true, writable: true });
window.dispatchEvent(new Event('resize'));
await flushEffects();
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
await flushEffects();
expect(manager.columns).toBe(2);
});
it('should return 3 columns on desktop (1280-1535px)', async () => {
Object.defineProperty(window, 'innerWidth', { value: 1400, configurable: true, writable: true });
window.dispatchEvent(new Event('resize'));
await flushEffects();
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
await flushEffects();
expect(manager.columns).toBe(3);
});
it('should return 4 columns on desktop large (>= 1536px)', async () => {
Object.defineProperty(window, 'innerWidth', {
value: DEFAULT_WIDTH,
configurable: true,
writable: true,
});
window.dispatchEvent(new Event('resize'));
await flushEffects();
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
await flushEffects();
expect(manager.columns).toBe(4);
});
});
});
describe('gap', () => {
it('should return 16px on mobile (< 640px)', async () => {
Object.defineProperty(window, 'innerWidth', { value: 320, configurable: true, writable: true });
window.dispatchEvent(new Event('resize'));
await flushEffects();
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.gap).toBe(16);
});
it('should return 16px on tablet portrait (640-767px)', async () => {
Object.defineProperty(window, 'innerWidth', { value: 700, configurable: true, writable: true });
window.dispatchEvent(new Event('resize'));
await flushEffects();
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.gap).toBe(16);
});
it('should return 24px on tablet and larger', async () => {
Object.defineProperty(window, 'innerWidth', { value: 900, configurable: true, writable: true });
window.dispatchEvent(new Event('resize'));
await flushEffects();
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.gap).toBe(24);
Object.defineProperty(window, 'innerWidth', { value: 1400, configurable: true, writable: true });
window.dispatchEvent(new Event('resize'));
await flushEffects();
expect(manager.gap).toBe(24);
Object.defineProperty(window, 'innerWidth', { value: DEFAULT_WIDTH, configurable: true, writable: true });
window.dispatchEvent(new Event('resize'));
await flushEffects();
expect(manager.gap).toBe(24);
});
});
describe('setMode', () => {
it('should change mode from list to grid', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.mode).toBe('list');
manager.setMode('grid');
expect(manager.mode).toBe('grid');
expect(manager.isListMode).toBe(false);
expect(manager.isGridMode).toBe(true);
});
it('should change mode from grid to list', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
expect(manager.mode).toBe('grid');
manager.setMode('list');
expect(manager.mode).toBe('list');
expect(manager.isListMode).toBe(true);
expect(manager.isGridMode).toBe(false);
});
it('should persist mode to localStorage', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
await flushEffects();
const stored = localStorage.getItem(STORAGE_KEY);
expect(stored).toBe(JSON.stringify({ mode: 'grid' }));
});
it('should not do anything if setting the same mode', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
// Store the current localStorage value
const storedBefore = localStorage.getItem(STORAGE_KEY);
manager.setMode('grid');
// Mode should still be grid
expect(manager.mode).toBe('grid');
// localStorage should have the same value (no re-write)
expect(localStorage.getItem(STORAGE_KEY)).toBe(storedBefore);
});
});
describe('toggleMode', () => {
it('should toggle from list to grid', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.mode).toBe('list');
manager.toggleMode();
expect(manager.mode).toBe('grid');
});
it('should toggle from grid to list', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
expect(manager.mode).toBe('grid');
manager.toggleMode();
expect(manager.mode).toBe('list');
});
it('should persist toggled mode to localStorage', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.toggleMode();
await flushEffects();
const stored = localStorage.getItem(STORAGE_KEY);
expect(stored).toBe(JSON.stringify({ mode: 'grid' }));
});
});
describe('reset', () => {
it('should reset to default list mode', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
expect(manager.mode).toBe('grid');
manager.reset();
expect(manager.mode).toBe('list');
});
it('should clear localStorage', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
// Wait for the effect to write to localStorage
await flushEffects();
expect(localStorage.getItem(STORAGE_KEY)).toBe(JSON.stringify({ mode: 'grid' }));
manager.reset();
expect(localStorage.getItem(STORAGE_KEY)).toBe(null);
});
it('should work when already at default mode', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.mode).toBe('list');
manager.reset();
expect(manager.mode).toBe('list');
expect(localStorage.getItem(STORAGE_KEY)).toBe(null);
});
});
describe('isListMode and isGridMode', () => {
it('should return correct boolean states for list mode', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.isListMode).toBe(true);
expect(manager.isGridMode).toBe(false);
});
it('should return correct boolean states for grid mode', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
manager.setMode('grid');
expect(manager.isListMode).toBe(false);
expect(manager.isGridMode).toBe(true);
});
it('should update boolean states when mode changes', async () => {
const { LayoutManager } = await import('./layoutStore.svelte');
const manager = new LayoutManager();
expect(manager.isListMode).toBe(true);
expect(manager.isGridMode).toBe(false);
manager.toggleMode();
expect(manager.isListMode).toBe(false);
expect(manager.isGridMode).toBe(true);
manager.setMode('list');
expect(manager.isListMode).toBe(true);
expect(manager.isGridMode).toBe(false);
});
});
});

View File

@@ -1,3 +1,7 @@
<!--
Component: LayoutSwitch
Toggles between list and grid layout modes
-->
<script lang="ts">
import { ButtonGroup } from '$shared/ui';
import { IconButton } from '$shared/ui';
@@ -6,6 +10,9 @@ import ListIcon from '@lucide/svelte/icons/stretch-horizontal';
import { layoutManager } from '../../model';
interface Props {
/**
* CSS classes
*/
class?: string;
}

View File

@@ -15,6 +15,73 @@ const { Story } = defineMeta({
story: { inline: false },
},
layout: 'fullscreen',
viewport: {
viewports: {
mobile1: {
name: 'iPhone 5/SE',
styles: {
width: '320px',
height: '568px',
},
},
mobile2: {
name: 'iPhone 14 Pro Max',
styles: {
width: '414px',
height: '896px',
},
},
tablet: {
name: 'iPad (Portrait)',
styles: {
width: '834px',
height: '1112px',
},
},
desktop: {
name: 'Desktop (Small)',
styles: {
width: '1024px',
height: '1280px',
},
},
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',
},
},
fullWidth: {
name: 'Full Width',
styles: {
width: '100%',
height: '800px',
},
},
fullScreen: {
name: 'Full Screen',
styles: {
width: '100%',
height: '100%',
},
},
},
},
},
argTypes: {
// This component uses internal stores, so no direct props to document
@@ -22,7 +89,7 @@ const { Story } = defineMeta({
});
</script>
<Story name="Default">
<Story name="Default" parameters={{ globals: { viewport: 'fullScreen' } }}>
<div class="min-h-screen bg-background">
<div class="max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="mb-8 text-center">
@@ -34,13 +101,13 @@ const { Story } = defineMeta({
</div>
</Story>
<Story name="Full Page">
<Story name="Full Page" parameters={{ globals: { viewport: 'fullScreen' } }}>
<div class="min-h-screen bg-background">
<SampleList />
</div>
</Story>
<Story name="With Typography Controls">
<Story name="With Typography Controls" parameters={{ globals: { viewport: 'fullScreen' } }}>
<div class="min-h-screen bg-background">
<div class="max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="mb-8 text-center">
@@ -52,7 +119,7 @@ const { Story } = defineMeta({
</div>
</Story>
<Story name="Custom Text">
<Story name="Custom Text" parameters={{ globals: { viewport: 'fullScreen' } }}>
<div class="min-h-screen bg-background">
<div class="max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="mb-8 text-center">
@@ -64,7 +131,7 @@ const { Story } = defineMeta({
</div>
</Story>
<Story name="Pagination Info">
<Story name="Pagination Info" parameters={{ globals: { viewport: 'fullScreen' } }}>
<div class="min-h-screen bg-background">
<div class="max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="mb-8 text-center">
@@ -76,7 +143,7 @@ const { Story } = defineMeta({
</div>
</Story>
<Story name="Responsive Layout">
<Story name="Responsive Layout" parameters={{ globals: { viewport: 'fullScreen' } }}>
<div class="min-h-screen bg-background">
<div class="max-w-6xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="mb-8 text-center">

View File

@@ -3,36 +3,57 @@
Wraps SampleList with a Section component
-->
<script lang="ts">
import { handleTitleStatusChanged } from '$entities/Breadcrumb';
import { NavigationWrapper } from '$entities/Breadcrumb';
import { unifiedFontStore } from '$entities/Font';
import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Section } from '$shared/ui';
import {
type Snippet,
getContext,
} from 'svelte';
Label,
Section,
} from '$shared/ui';
import { getContext } from 'svelte';
import { layoutManager } from '../../model';
import LayoutSwitch from '../LayoutSwitch/LayoutSwitch.svelte';
import SampleList from '../SampleList/SampleList.svelte';
interface Props {
/**
* Section index
*/
index: number;
}
const { index }: Props = $props();
const responsive = getContext<ResponsiveManager>('responsive');
</script>
<Section
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-x-12 sm:gap-y-8"
index={3}
id="sample_set"
onTitleStatusChange={handleTitleStatusChanged}
title="Sample Set"
headerTitle="visual_output"
headerSubtitle="render_engine:"
>
{#snippet headerContent()}
<LayoutSwitch />
{/snippet}
<NavigationWrapper index={2} title="Samples">
{#snippet content(registerAction)}
<Section
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-x-12 sm:gap-y-8"
{index}
id="sample_set"
title="Sample Set"
headerTitle="visual_output"
headerSubtitle="items_total: {unifiedFontStore.pagination.total ?? 0}"
headerAction={registerAction}
>
{#snippet headerContent()}
<div class="flex items-center gap-3 md:gap-4">
<div class="hidden md:flex items-center gap-2 mr-4">
<Label variant="muted" size="sm">view_mode: </Label>
<Label variant="default" size="sm" bold>{layoutManager.mode}</Label>
</div>
<LayoutSwitch />
</div>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
<SampleList />
</div>
{#snippet content({ className })}
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
<SampleList />
</div>
{/snippet}
</Section>
{/snippet}
</Section>
</NavigationWrapper>

View File

@@ -1,5 +1,15 @@
export { ComparisonSlider } from './ComparisonSlider';
export { FontSearch } from './FontSearch';
/**
* Widgets layer
*
* Composed UI blocks that combine features and entities into complete
* user-facing components.
*/
export { ComparisonView } from './ComparisonView';
export {
FontSearch,
FontSearchSection,
} from './FontSearch';
export {
SampleList,
SampleListSection,