feat: storybook cases and mocks

This commit is contained in:
Ilia Mashkov
2026-02-19 13:58:12 +03:00
parent 9d1f59d819
commit da79dd2e35
22 changed files with 3047 additions and 45 deletions

View File

@@ -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',
},
},
});
</script>
<script lang="ts">
const control = createTypographyControl({ min: 0, max: 100, step: 1, value: 50 });
const horizontalControl = createTypographyControl({ min: 0, max: 100, step: 1, value: 50 });
const verticalControl = createTypographyControl({ min: 0, max: 100, step: 1, value: 50 });
const floatControl = createTypographyControl({ min: 0, max: 1, step: 0.01, value: 0.5 });
const atMinControl = createTypographyControl({ min: 0, max: 100, step: 1, value: 0 });
const atMaxControl = createTypographyControl({ min: 0, max: 100, step: 1, value: 100 });
const largeRangeControl = createTypographyControl({ min: 0, max: 1000, step: 10, value: 500 });
</script>
<Story name="Horizontal" args={{ orientation: 'horizontal', control }}>
<ComboControlV2 control={control} orientation="horizontal" />
<Story
name="Horizontal"
args={{
control: horizontalControl,
orientation: 'horizontal',
label: 'Size',
}}
>
<ComboControlV2 control={horizontalControl} orientation="horizontal" label="Size" />
</Story>
<Story name="Vertical" args={{ orientation: 'vertical', control, class: 'h-48' }}>
<ComboControlV2 control={control} orientation="vertical" />
<Story
name="Vertical"
args={{
control: verticalControl,
orientation: 'vertical',
label: 'Size',
}}
>
<ComboControlV2 control={verticalControl} orientation="vertical" class="h-48" label="Size" />
</Story>
<Story
name="With Float Values"
args={{
control: floatControl,
orientation: 'vertical',
label: 'Opacity',
}}
>
<ComboControlV2 control={floatControl} orientation="vertical" class="h-48" label="Opacity" />
</Story>
<Story
name="At Minimum"
args={{
control: atMinControl,
orientation: 'horizontal',
label: 'Size',
}}
>
<ComboControlV2 control={atMinControl} orientation="horizontal" label="Size" />
</Story>
<Story
name="At Maximum"
args={{
control: atMaxControl,
orientation: 'horizontal',
label: 'Size',
}}
>
<ComboControlV2 control={atMaxControl} orientation="horizontal" label="Size" />
</Story>
<Story
name="Large Range"
args={{
control: largeRangeControl,
orientation: 'horizontal',
label: 'Scale',
}}
>
<ComboControlV2 control={largeRangeControl} orientation="horizontal" label="Scale" />
</Story>

View File

@@ -0,0 +1,101 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import IconButton from './IconButton.svelte';
const { Story } = defineMeta({
title: 'Shared/IconButton',
component: IconButton,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Icon button with rotation animation on click. Features clockwise/counterclockwise rotation options and icon snippet support for flexible icon rendering.',
},
story: { inline: false },
},
layout: 'centered',
},
argTypes: {
rotation: {
control: 'select',
options: ['clockwise', 'counterclockwise'],
description: 'Direction of rotation animation on click',
},
icon: {
control: 'object',
description: 'Icon snippet to render (required)',
},
disabled: {
control: 'boolean',
description: 'Disable the button',
},
onclick: {
action: 'clicked',
description: 'Click handler',
},
},
});
</script>
<script lang="ts">
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import MinusIcon from '@lucide/svelte/icons/minus';
import PlusIcon from '@lucide/svelte/icons/plus';
import SettingsIcon from '@lucide/svelte/icons/settings';
import XIcon from '@lucide/svelte/icons/x';
</script>
{#snippet chevronRightIcon({ className }: { className: string })}
<ChevronRight class={className} />
{/snippet}
{#snippet chevronLeftIcon({ className }: { className: string })}
<ChevronLeft class={className} />
{/snippet}
{#snippet plusIcon({ className }: { className: string })}
<PlusIcon class={className} />
{/snippet}
{#snippet minusIcon({ className }: { className: string })}
<MinusIcon class={className} />
{/snippet}
{#snippet settingsIcon({ className }: { className: string })}
<SettingsIcon class={className} />
{/snippet}
{#snippet xIcon({ className }: { className: string })}
<XIcon class={className} />
{/snippet}
<Story
name="Default"
args={{
icon: chevronRightIcon,
}}
>
<IconButton onclick={() => console.log('Default clicked')}>
{#snippet icon({ className })}
<ChevronRight class={className} />
{/snippet}
</IconButton>
</Story>
<Story
name="Disabled"
args={{
icon: chevronRightIcon,
disabled: true,
}}
>
<div class="flex flex-col gap-4 items-center">
<IconButton disabled>
{#snippet icon({ className })}
<ChevronRight class={className} />
{/snippet}
</IconButton>
</div>
</Story>

View File

@@ -0,0 +1,475 @@
<script module>
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { defineMeta } from '@storybook/addon-svelte-csf';
import Section from './Section.svelte';
const { Story } = defineMeta({
title: 'Shared/Section',
component: Section,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Page layout component with optional sticky title feature. Provides a container for page widgets with title, icon, description snippets. The title can remain fixed while scrolling through content.',
},
story: { inline: false },
},
layout: 'fullscreen',
},
argTypes: {
id: {
control: 'text',
description: 'ID of the section',
},
index: {
control: 'number',
description: 'Index of the section (used for default description)',
},
stickyTitle: {
control: 'boolean',
description: 'When true, title stays fixed while scrolling through content',
},
stickyOffset: {
control: 'text',
description: 'Top offset for sticky title (e.g. "60px")',
},
class: {
control: 'text',
description: 'Additional CSS classes',
},
onTitleStatusChange: {
action: 'titleStatusChanged',
description: 'Callback when title visibility status changes',
},
},
});
</script>
<script lang="ts">
import ListIcon from '@lucide/svelte/icons/list';
import SearchIcon from '@lucide/svelte/icons/search';
import SettingsIcon from '@lucide/svelte/icons/settings';
</script>
{#snippet searchIcon({ className }: { className?: string })}
<SearchIcon class={className} />
{/snippet}
{#snippet welcomeTitle({ className }: { className?: string })}
<h2 class={className}>Welcome</h2>
{/snippet}
{#snippet welcomeContent({ className }: { className?: string })}
<div class={cn(className, 'min-w-128')}>
<p class="text-lg text-text-muted">
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.
</p>
</div>
{/snippet}
{#snippet stickyTitle({ className }: { className?: string })}
<h2 class={className}>Sticky Title</h2>
{/snippet}
{#snippet stickyContent({ className }: { className?: string })}
<div class={cn(className, 'min-w-128')}>
<p class="text-lg text-text-muted mb-4">
This section has a sticky title that stays fixed while you scroll through the content. Try scrolling down to
see the effect.
</p>
<div class="space-y-4">
<p class="text-text-muted">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua.
</p>
<p class="text-text-muted">
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</p>
<p class="text-text-muted">
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
</p>
<p class="text-text-muted">
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollim anim id est
laborum.
</p>
<p class="text-text-muted">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
</p>
</div>
</div>
{/snippet}
{#snippet searchFontsTitle({ className }: { className?: string })}
<h2 class={className}>Search Fonts</h2>
{/snippet}
{#snippet searchFontsDescription({ className }: { className?: string })}
<span class={className}>Find your perfect typeface</span>
{/snippet}
{#snippet searchFontsContent({ className }: { className?: string })}
<div class={cn(className, 'min-w-128')}>
<p class="text-lg text-text-muted">
Use the search bar to find fonts by name, or use the filters to browse by category, subset, or provider.
</p>
</div>
{/snippet}
{#snippet longContentTitle({ className }: { className?: string })}
<h2 class={className}>Long Content</h2>
{/snippet}
{#snippet longContent({ className }: { className?: string })}
<div class={cn(className, 'min-w-128')}>
<div class="space-y-6">
<p class="text-lg text-text-muted">
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.
</p>
<div class="h-64 bg-background-40 rounded-lg flex items-center justify-center">
<span class="text-text-muted">Content block 1</span>
</div>
<p class="text-text-muted">
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.
</p>
<div class="h-64 bg-background-40 rounded-lg flex items-center justify-center">
<span class="text-text-muted">Content block 2</span>
</div>
<p class="text-text-muted">
The Intersection Observer API is used to detect when the section title scrolls out of view, triggering
the optional onTitleStatusChange callback.
</p>
<div class="h-64 bg-background-40 rounded-lg flex items-center justify-center">
<span class="text-text-muted">Content block 3</span>
</div>
</div>
</div>
{/snippet}
{#snippet minimalTitle({ className }: { className?: string })}
<h2 class={className}>Minimal Section</h2>
{/snippet}
{#snippet minimalContent({ className }: { className?: string })}
<div class={cn(className, 'min-w-128')}>
<p class="text-text-muted">
A minimal section without index, icon, or description. Just the essentials.
</p>
</div>
{/snippet}
{#snippet customTitle({ className }: { className?: string })}
<h2 class={className}>Custom Content</h2>
{/snippet}
{#snippet customDescription({ className }: { className?: string })}
<span class={className}>With interactive elements</span>
{/snippet}
{#snippet customContent({ className }: { className?: string })}
<div class={cn(className, 'min-w-128')}>
<div class="grid grid-cols-2 gap-4">
<div class="p-4 bg-background-40 rounded-lg">
<h3 class="font-semibold mb-2">Card 1</h3>
<p class="text-sm text-text-muted">Some content here</p>
</div>
<div class="p-4 bg-background-40 rounded-lg">
<h3 class="font-semibold mb-2">Card 2</h3>
<p class="text-sm text-text-muted">More content here</p>
</div>
</div>
</div>
{/snippet}
<Story
name="Default"
args={{
title: welcomeTitle,
content: welcomeContent,
}}
>
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-x-6 sm:gap-x-8 md:gap-x-10 lg:gap-x-12 p-8 max-w-6xl mx-auto">
<Section index={1}>
{#snippet title({ className })}
<h2 class={className}>Welcome</h2>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, 'min-w-128')}>
<p class="text-lg text-text-muted">
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.
</p>
</div>
{/snippet}
</Section>
</div>
</Story>
<Story
name="With Sticky Title"
args={{
title: stickyTitle,
content: stickyContent,
}}
>
<div class="h-[200vh]">
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-x-6 sm:gap-x-8 md:gap-x-10 lg:gap-x-12 p-8 max-w-6xl mx-auto">
<Section
id="sticky-section"
index={1}
stickyTitle={true}
stickyOffset="20px"
>
{#snippet title({ className })}
<h2 class={className}>Sticky Title</h2>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, 'min-w-128')}>
<p class="text-lg text-text-muted mb-4">
This section has a sticky title that stays fixed while you scroll through the content. Try
scrolling down to see the effect.
</p>
<div class="space-y-4">
<p class="text-text-muted">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
</p>
<p class="text-text-muted">
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
</p>
<p class="text-text-muted">
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
nulla pariatur.
</p>
<p class="text-text-muted">
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollim anim id est laborum.
</p>
<p class="text-text-muted">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
laudantium.
</p>
</div>
</div>
{/snippet}
</Section>
</div>
</div>
</Story>
<Story
name="With Icon and Description"
args={{
icon: searchIcon,
title: searchFontsTitle,
description: searchFontsDescription,
content: searchFontsContent,
}}
>
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-x-6 sm:gap-x-8 md:gap-x-10 lg:gap-x-12 p-8 max-w-6xl mx-auto">
<Section index={1}>
{#snippet title({ className })}
<h2 class={className}>Search Fonts</h2>
{/snippet}
{#snippet icon({ className })}
<SearchIcon class={className} />
{/snippet}
{#snippet description({ className })}
<span class={className}>Find your perfect typeface</span>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, 'min-w-128')}>
<p class="text-lg text-text-muted">
Use the search bar to find fonts by name, or use the filters to browse by category, subset, or
provider.
</p>
</div>
{/snippet}
</Section>
</div>
</Story>
<Story name="Multiple Sections" tags={['!autodocs']}>
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-x-6 sm:gap-x-8 md:gap-x-10 lg:gap-x-12 p-8 max-w-6xl mx-auto">
<Section
id="section-1"
index={1}
stickyTitle={true}
>
{#snippet title({ className })}
<h2 class={className}>Typography</h2>
{/snippet}
{#snippet icon({ className })}
<SettingsIcon class={className} />
{/snippet}
{#snippet description({ className })}
<span class={className}>Adjust text appearance</span>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, 'min-w-128')}>
<p class="text-lg text-text-muted">
Control the size, weight, and line height of your text. These settings apply across the
comparison view.
</p>
</div>
{/snippet}
</Section>
<Section
id="section-2"
index={2}
stickyTitle={true}
>
{#snippet title({ className })}
<h2 class={className}>Font Search</h2>
{/snippet}
{#snippet icon({ className })}
<SearchIcon class={className} />
{/snippet}
{#snippet description({ className })}
<span class={className}>Browse available typefaces</span>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, 'min-w-128')}>
<p class="text-lg text-text-muted">
Search through our collection of fonts from Google Fonts and Fontshare. Use filters to narrow
down your selection.
</p>
</div>
{/snippet}
</Section>
<Section
id="section-3"
index={3}
stickyTitle={true}
>
{#snippet title({ className })}
<h2 class={className}>Sample List</h2>
{/snippet}
{#snippet icon({ className })}
<ListIcon class={className} />
{/snippet}
{#snippet description({ className })}
<span class={className}>Preview font samples</span>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, 'min-w-128')}>
<p class="text-lg text-text-muted">
Browse through font samples with your custom text. The list is virtualized for optimal
performance.
</p>
</div>
{/snippet}
</Section>
</div>
</Story>
<Story
name="With Long Content"
args={{
title: longContentTitle,
content: longContent,
}}
>
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-x-6 sm:gap-x-8 md:gap-x-10 lg:gap-x-12 p-8 max-w-6xl mx-auto">
<Section
index={1}
stickyTitle={true}
stickyOffset="0px"
>
{#snippet title({ className })}
<h2 class={className}>Long Content</h2>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, 'min-w-128')}>
<div class="space-y-6">
<p class="text-lg text-text-muted">
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.
</p>
<div class="h-64 bg-background-40 rounded-lg flex items-center justify-center">
<span class="text-text-muted">Content block 1</span>
</div>
<p class="text-text-muted">
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.
</p>
<div class="h-64 bg-background-40 rounded-lg flex items-center justify-center">
<span class="text-text-muted">Content block 2</span>
</div>
<p class="text-text-muted">
The Intersection Observer API is used to detect when the section title scrolls out of view,
triggering the optional onTitleStatusChange callback.
</p>
<div class="h-64 bg-background-40 rounded-lg flex items-center justify-center">
<span class="text-text-muted">Content block 3</span>
</div>
</div>
</div>
{/snippet}
</Section>
</div>
</Story>
<Story
name="Minimal"
args={{
title: minimalTitle,
content: minimalContent,
}}
>
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-x-6 sm:gap-x-8 md:gap-x-10 lg:gap-x-12 p-8 max-w-6xl mx-auto">
<Section>
{#snippet title({ className })}
<h2 class={className}>Minimal Section</h2>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, 'min-w-128')}>
<p class="text-text-muted">
A minimal section without index, icon, or description. Just the essentials.
</p>
</div>
{/snippet}
</Section>
</div>
</Story>
<Story
name="Custom Content"
args={{
title: customTitle,
description: customDescription,
content: customContent,
}}
>
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-x-6 sm:gap-x-8 md:gap-x-10 lg:gap-x-12 p-8 max-w-6xl mx-auto">
<Section index={42}>
{#snippet title({ className })}
<h2 class={className}>Custom Content</h2>
{/snippet}
{#snippet description({ className })}
<span class={className}>With interactive elements</span>
{/snippet}
{#snippet content({ className })}
<div class={cn(className, 'min-w-128')}>
<div class="grid grid-cols-2 gap-4">
<div class="p-4 bg-background-40 rounded-lg">
<h3 class="font-semibold mb-2">Card 1</h3>
<p class="text-sm text-text-muted">Some content here</p>
</div>
<div class="p-4 bg-background-40 rounded-lg">
<h3 class="font-semibold mb-2">Card 2</h3>
<p class="text-sm text-text-muted">More content here</p>
</div>
</div>
</div>
{/snippet}
</Section>
</div>
</Story>

View File

@@ -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',
},
},
});
</script>
<script lang="ts">
let minValue = 0;
let maxValue = 100;
let stepValue = 1;
let value = $state(50);
</script>
<Story name="Horizontal" args={{ orientation: 'horizontal', min: minValue, max: maxValue, step: stepValue, value }}>
<Slider bind:value min={minValue} max={maxValue} step={stepValue} />
<Story name="Horizontal" args={{ orientation: 'horizontal', min: 0, max: 100, step: 1, value }}>
<Slider bind:value />
</Story>
<Story name="Vertical" args={{ orientation: 'vertical', min: minValue, max: maxValue, step: stepValue, value }}>
<Slider bind:value min={minValue} max={maxValue} step={stepValue} orientation="vertical" />
<Story name="Vertical" args={{ orientation: 'vertical', min: 0, max: 100, step: 1, value }}>
<Slider bind:value />
</Story>
<Story name="With Label" args={{ orientation: 'horizontal', min: 0, max: 100, step: 1, value, label: 'SIZE' }}>
<Slider bind:value />
</Story>

View File

@@ -38,27 +38,31 @@ const { Story } = defineMeta({
<script lang="ts">
const smallDataSet = Array.from({ length: 20 }, (_, i) => `${i + 1}) I will not waste chalk.`);
const largeDataSet = Array.from(
{ length: 10000 },
const mediumDataSet = Array.from(
{ length: 200 },
(_, i) => `${i + 1}) I will not skateboard in the halls.`,
);
const emptyDataSet: string[] = [];
</script>
<Story name="Small Dataset">
<VirtualList items={smallDataSet} itemHeight={40}>
{#snippet children({ item })}
<div class="p-2 m-0.5 rounded-sm hover:bg-accent">{item}</div>
{/snippet}
</VirtualList>
<div class="h-[400px]">
<VirtualList items={smallDataSet} itemHeight={40}>
{#snippet children({ item })}
<div class="p-2 m-0.5 rounded-sm hover:bg-accent">{item}</div>
{/snippet}
</VirtualList>
</div>
</Story>
<Story name="Large Dataset">
<VirtualList items={largeDataSet} itemHeight={40}>
{#snippet children({ item })}
<div class="p-2 m-0.5 rounded-sm hover:bg-accent">{item}</div>
{/snippet}
</VirtualList>
<Story name="Medium Dataset (200 items)">
<div class="h-[400px]">
<VirtualList items={mediumDataSet} itemHeight={40}>
{#snippet children({ item })}
<div class="p-2 m-0.5 rounded-sm hover:bg-accent">{item}</div>
{/snippet}
</VirtualList>
</div>
</Story>
<Story name="Empty Dataset">