feat: add missing storybook files and type template arguments properly

This commit is contained in:
Ilia Mashkov
2026-04-17 18:01:24 +03:00
parent 5eb9584797
commit bb65f1c8d6
31 changed files with 1124 additions and 90 deletions

View File

@@ -0,0 +1,65 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import BreadcrumbHeader from './BreadcrumbHeader.svelte';
const { Story } = defineMeta({
title: 'Entities/BreadcrumbHeader',
component: BreadcrumbHeader,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Fixed header that slides in when the user scrolls past tracked page sections. Reads `scrollBreadcrumbsStore.scrolledPastItems` — renders nothing when the list is empty. Requires the `responsive` context provided by `Providers`.',
},
story: { inline: false },
},
layout: 'fullscreen',
},
argTypes: {},
});
</script>
<script>
import Providers from '$shared/lib/storybook/Providers.svelte';
import BreadcrumbHeaderSeeded from './BreadcrumbHeaderSeeded.svelte';
</script>
<Story
name="With Breadcrumbs"
parameters={{
docs: {
description: {
story:
'Three sections are registered with the breadcrumb store. The story scrolls the iframe so the IntersectionObserver marks them as scrolled-past, revealing the fixed header.',
},
},
}}
>
{#snippet template()}
<Providers>
<BreadcrumbHeaderSeeded />
</Providers>
{/snippet}
</Story>
<Story
name="Empty"
parameters={{
docs: {
description: {
story:
'No sections registered — BreadcrumbHeader renders nothing. This is the initial state before the user scrolls past any tracked section.',
},
},
}}
>
{#snippet template()}
<Providers>
<div style="padding: 2rem; color: #888; font-size: 0.875rem;">
BreadcrumbHeader renders nothing when scrolledPastItems is empty.
</div>
<BreadcrumbHeader />
</Providers>
{/snippet}
</Story>

View File

@@ -0,0 +1,49 @@
<script>
import { onMount } from 'svelte';
import { scrollBreadcrumbsStore } from '../../model';
import BreadcrumbHeader from './BreadcrumbHeader.svelte';
const sections = [
{ index: 100, title: 'Introduction' },
{ index: 101, title: 'Typography' },
{ index: 102, title: 'Spacing' },
];
/** @type {HTMLDivElement} */
let container;
onMount(() => {
for (const section of sections) {
const el = /** @type {HTMLElement} */ (container.querySelector(`[data-story-index="${section.index}"]`));
scrollBreadcrumbsStore.add({ index: section.index, title: section.title, element: el }, 96);
}
/*
* Scroll past the sections so IntersectionObserver marks them as
* scrolled-past, making scrolledPastItems non-empty and the header visible.
*/
setTimeout(() => {
window.scrollTo({ top: 2000, behavior: 'instant' });
}, 100);
return () => {
for (const { index } of sections) {
scrollBreadcrumbsStore.remove(index);
}
window.scrollTo({ top: 0, behavior: 'instant' });
};
});
</script>
<div bind:this={container} style="height: 2400px; padding-top: 900px;">
{#each sections as section}
<div
data-story-index={section.index}
style="height: 400px; padding: 2rem; background: #f5f5f5; margin-bottom: 1rem;"
>
{section.title} — scroll up to see the breadcrumb header
</div>
{/each}
</div>
<BreadcrumbHeader />

View File

@@ -0,0 +1,109 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import NavigationWrapper from './NavigationWrapper.svelte';
const { Story } = defineMeta({
title: 'Entities/NavigationWrapper',
component: NavigationWrapper,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Thin wrapper that registers an HTML section with `scrollBreadcrumbsStore` via a Svelte use-directive action. Has no visual output of its own — renders `{@render content(registerBreadcrumb)}` where `registerBreadcrumb` is the action to attach with `use:`. On destroy the section is automatically removed from the store.',
},
story: { inline: false },
},
layout: 'fullscreen',
},
argTypes: {
index: {
control: { type: 'number', min: 0 },
description: 'Unique index used for ordering in the breadcrumb trail',
},
title: {
control: 'text',
description: 'Display title shown in the breadcrumb header',
},
offset: {
control: { type: 'number', min: 0 },
description: 'Scroll offset in pixels to account for sticky headers',
},
},
});
</script>
<script lang="ts">
import type { ComponentProps } from 'svelte';
</script>
<Story
name="Single Section"
args={{ index: 0, title: 'Introduction', offset: 96 }}
parameters={{
docs: {
description: {
story:
'A single section registered with NavigationWrapper. The `content` snippet receives the `register` action and applies it via `use:register`.',
},
},
}}
>
{#snippet template(args: ComponentProps<typeof NavigationWrapper>)}
<NavigationWrapper {...args}>
{#snippet content(register)}
<section use:register style="padding: 2rem; background: #f5f5f5; min-height: 200px;">
<p style="font-size: 0.875rem; color: #555;">
Section registered as <strong>{args.title}</strong> at index {args.index}. Scroll past this
section to see it appear in the breadcrumb header.
</p>
</section>
{/snippet}
</NavigationWrapper>
{/snippet}
</Story>
<Story
name="Multiple Sections"
parameters={{
docs: {
description: {
story:
'Three sequential sections each wrapped in NavigationWrapper with distinct indices and titles. Demonstrates how the breadcrumb trail builds as the user scrolls.',
},
},
}}
>
{#snippet template()}
<div style="display: flex; flex-direction: column; gap: 0;">
<NavigationWrapper index={0} title="Introduction" offset={96}>
{#snippet content(register)}
<section use:register style="padding: 2rem; background: #f5f5f5; min-height: 300px;">
<h2 style="margin: 0 0 0.5rem;">Introduction</h2>
<p style="font-size: 0.875rem; color: #555;">
Registered as section 0. Scroll down to build the breadcrumb trail.
</p>
</section>
{/snippet}
</NavigationWrapper>
<NavigationWrapper index={1} title="Typography" offset={96}>
{#snippet content(register)}
<section use:register style="padding: 2rem; background: #ebebeb; min-height: 300px;">
<h2 style="margin: 0 0 0.5rem;">Typography</h2>
<p style="font-size: 0.875rem; color: #555;">Registered as section 1.</p>
</section>
{/snippet}
</NavigationWrapper>
<NavigationWrapper index={2} title="Spacing" offset={96}>
{#snippet content(register)}
<section use:register style="padding: 2rem; background: #e0e0e0; min-height: 300px;">
<h2 style="margin: 0 0 0.5rem;">Spacing</h2>
<p style="font-size: 0.875rem; color: #555;">Registered as section 2.</p>
</section>
{/snippet}
</NavigationWrapper>
</div>
{/snippet}
</Story>

View File

@@ -0,0 +1,91 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import FontApplicator from './FontApplicator.svelte';
const { Story } = defineMeta({
title: 'Entities/FontApplicator',
component: FontApplicator,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Loads a font and applies it to children. Shows blur/scale loading state until font is ready, then reveals with a smooth transition.',
},
story: { inline: false },
},
layout: 'centered',
},
argTypes: {
weight: { control: 'number' },
},
});
</script>
<script lang="ts">
import { mockUnifiedFont } from '$entities/Font/lib/mocks';
import type { ComponentProps } from 'svelte';
const fontUnknown = mockUnifiedFont({ id: 'nonexistent-font-xk92z', name: 'Nonexistent Font Xk92z' });
const fontArial = mockUnifiedFont({ id: 'arial', name: 'Arial' });
const fontArialBold = mockUnifiedFont({ id: 'arial-bold', name: 'Arial' });
</script>
<Story
name="Loading State"
parameters={{
docs: {
description: {
story:
'Font that has never been loaded by appliedFontsManager. The component renders in its pending state: blurred, scaled down, and semi-transparent.',
},
},
}}
args={{ font: fontUnknown, weight: 400 }}
>
{#snippet template(args: ComponentProps<typeof FontApplicator>)}
<FontApplicator {...args}>
<p class="text-xl">The quick brown fox jumps over the lazy dog</p>
</FontApplicator>
{/snippet}
</Story>
<Story
name="Loaded State"
parameters={{
docs: {
description: {
story:
'Uses Arial, a system font available in all browsers. Because appliedFontsManager has not loaded it via FontFace, the manager status may remain pending — meaning the blur/scale state may still show. In a real app the manager would load the font and transition to the revealed state.',
},
},
}}
args={{ font: fontArial, weight: 400 }}
>
{#snippet template(args: ComponentProps<typeof FontApplicator>)}
<FontApplicator {...args}>
<p class="text-xl">The quick brown fox jumps over the lazy dog</p>
</FontApplicator>
{/snippet}
</Story>
<Story
name="Custom Weight"
parameters={{
docs: {
description: {
story:
'Demonstrates passing a custom weight (700). The weight is forwarded to appliedFontsManager for font resolution; visually identical to the loaded state story until the manager confirms the font.',
},
},
}}
args={{ font: fontArialBold, weight: 700 }}
>
{#snippet template(args: ComponentProps<typeof FontApplicator>)}
<FontApplicator {...args}>
<p class="text-xl">The quick brown fox jumps over the lazy dog</p>
</FontApplicator>
{/snippet}
</Story>

View File

@@ -0,0 +1,114 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import FontVirtualList from './FontVirtualList.svelte';
const { Story } = defineMeta({
title: 'Entities/FontVirtualList',
component: FontVirtualList,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'Virtualized font list backed by the `fontStore` singleton. Handles font loading registration (pin/touch) for visible items and triggers infinite scroll pagination via `fontStore.nextPage()`. Because the component reads directly from the `fontStore` singleton, stories render against a live (but empty/loading) store — no font data will appear unless the API is reachable from the Storybook host.',
},
story: { inline: false },
},
layout: 'padded',
},
argTypes: {
weight: { control: 'number', description: 'Font weight applied to visible fonts' },
itemHeight: { control: 'number', description: 'Height of each list item in pixels' },
},
});
</script>
<script lang="ts">
import type { ComponentProps } from 'svelte';
</script>
<Story
name="Loading Skeleton"
parameters={{
docs: {
description: {
story:
'Skeleton state shown while `fontStore.fonts` is empty and `fontStore.isLoading` is true. In a real session the skeleton fades out once the first page loads.',
},
},
}}
args={{ weight: 400, itemHeight: 72 }}
>
{#snippet template(args: ComponentProps<typeof FontVirtualList>)}
<div class="h-[400px] w-full">
<FontVirtualList {...args}>
{#snippet skeleton()}
<div class="flex flex-col gap-2 p-4">
{#each Array(6) as _}
<div class="h-16 animate-pulse rounded bg-neutral-200"></div>
{/each}
</div>
{/snippet}
{#snippet children({ item })}
<div class="border-b border-neutral-100 p-3">{item.name}</div>
{/snippet}
</FontVirtualList>
</div>
{/snippet}
</Story>
<Story
name="Empty State"
parameters={{
docs: {
description: {
story:
'No `skeleton` snippet provided. When `fontStore.fonts` is empty the underlying VirtualList renders its empty state directly.',
},
},
}}
args={{ weight: 400, itemHeight: 72 }}
>
{#snippet template(args: ComponentProps<typeof FontVirtualList>)}
<div class="h-[400px] w-full">
<FontVirtualList {...args}>
{#snippet children({ item })}
<div class="border-b border-neutral-100 p-3">{item.name}</div>
{/snippet}
</FontVirtualList>
</div>
{/snippet}
</Story>
<Story
name="With Item Renderer"
parameters={{
docs: {
description: {
story:
'Demonstrates how to configure a `children` snippet for item rendering. The list will be empty because `fontStore` is not populated in Storybook, but the template shows the expected slot shape: `{ item: UnifiedFont }`.',
},
},
}}
args={{ weight: 400, itemHeight: 80 }}
>
{#snippet template(args: ComponentProps<typeof FontVirtualList>)}
<div class="h-[400px] w-full">
<FontVirtualList {...args}>
{#snippet skeleton()}
<div class="flex flex-col gap-2 p-4">
{#each Array(6) as _}
<div class="h-16 animate-pulse rounded bg-neutral-200"></div>
{/each}
</div>
{/snippet}
{#snippet children({ item })}
<div class="flex items-center justify-between border-b border-neutral-100 px-4 py-3">
<span class="text-sm font-medium">{item.name}</span>
<span class="text-xs text-neutral-400">{item.category}</span>
</div>
{/snippet}
</FontVirtualList>
</div>
{/snippet}
</Story>