Merge pull request 'feature/storybook-coverage' (#18) from feature/storybook-coverage into main
All checks were successful
Workflow / build (push) Successful in 42s
All checks were successful
Workflow / build (push) Successful in 42s
Reviewed-on: #18
This commit was merged in pull request #18.
This commit is contained in:
16
.storybook/StoryStage.svelte
Normal file
16
.storybook/StoryStage.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
children: import('svelte').Snippet;
|
||||||
|
width?: string; // Optional width override
|
||||||
|
}
|
||||||
|
|
||||||
|
let { children, width = 'max-w-3xl' }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex min-h-screen w-full items-center justify-center bg-slate-50 p-8">
|
||||||
|
<div class="w-full bg-white shadow-xl ring-1 ring-slate-200 rounded-xl p-12 {width}">
|
||||||
|
<div class="relative flex justify-center items-center">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,4 +1,16 @@
|
|||||||
import type { StorybookConfig } from '@storybook/svelte-vite';
|
import type { StorybookConfig } from '@storybook/svelte-vite';
|
||||||
|
import {
|
||||||
|
dirname,
|
||||||
|
resolve,
|
||||||
|
} from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import {
|
||||||
|
loadConfigFromFile,
|
||||||
|
mergeConfig,
|
||||||
|
} from 'vite';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
'stories': [
|
'stories': [
|
||||||
@@ -18,5 +30,17 @@ const config: StorybookConfig = {
|
|||||||
'@storybook/addon-docs',
|
'@storybook/addon-docs',
|
||||||
],
|
],
|
||||||
'framework': '@storybook/svelte-vite',
|
'framework': '@storybook/svelte-vite',
|
||||||
|
async viteFinal(config) {
|
||||||
|
// This attempts to find your actual vite.config.ts
|
||||||
|
const { config: userConfig } = await loadConfigFromFile(
|
||||||
|
{ command: 'serve', mode: 'development' },
|
||||||
|
resolve(__dirname, '../vite.config.ts'),
|
||||||
|
) || {};
|
||||||
|
|
||||||
|
return mergeConfig(config, {
|
||||||
|
// Merge only the resolve/alias parts if you want to be safe
|
||||||
|
resolve: userConfig?.resolve || {},
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { Preview } from '@storybook/svelte-vite';
|
import type { Preview } from '@storybook/svelte-vite';
|
||||||
|
import StoryStage from './StoryStage.svelte';
|
||||||
import '../src/app/styles/app.css';
|
import '../src/app/styles/app.css';
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
parameters: {
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
controls: {
|
controls: {
|
||||||
matchers: {
|
matchers: {
|
||||||
color: /(background|color)$/i,
|
color: /(background|color)$/i,
|
||||||
@@ -17,7 +18,31 @@ const preview: Preview = {
|
|||||||
// 'off' - skip a11y checks entirely
|
// 'off' - skip a11y checks entirely
|
||||||
test: 'todo',
|
test: 'todo',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
docs: {
|
||||||
|
story: {
|
||||||
|
// This sets the default height for the iframe in Autodocs
|
||||||
|
iframeHeight: '400px',
|
||||||
|
// Ensure the story isn't forced into a tiny inline box
|
||||||
|
// inline: true,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(storyFn, { parameters }) => {
|
||||||
|
const { Component, props } = storyFn();
|
||||||
|
return {
|
||||||
|
Component: StoryStage,
|
||||||
|
// We pass the actual story component into the Stage via a snippet/slot
|
||||||
|
// Svelte 5 Storybook handles this mapping internally when you return this structure
|
||||||
|
props: {
|
||||||
|
children: Component,
|
||||||
|
width: parameters.stageWidth || 'max-w-3xl',
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default preview;
|
export default preview;
|
||||||
|
|||||||
73
src/shared/ui/CheckboxFilter/CheckboxFilter.stories.svelte
Normal file
73
src/shared/ui/CheckboxFilter/CheckboxFilter.stories.svelte
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<script module>
|
||||||
|
import { createFilter } from '$shared/lib';
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import CheckboxFilter from './CheckboxFilter.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Shared/CheckboxFilter',
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'A collapsible property filter with checkboxes. Displays selected count as a badge and supports reduced motion for accessibility. Open by default for immediate visibility and interaction.',
|
||||||
|
},
|
||||||
|
story: { inline: false }, // Render stories in iframe for state isolation
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
displayedLabel: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Label for this filter group (e.g., "Properties", "Tags")',
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
control: 'object',
|
||||||
|
description: 'Filter entity managing properties and selection state',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
const defaultFilter = createFilter({
|
||||||
|
properties: [{
|
||||||
|
id: 'cats',
|
||||||
|
name: 'Cats',
|
||||||
|
value: 'cats',
|
||||||
|
}, {
|
||||||
|
id: 'dogs',
|
||||||
|
name: 'Dogs',
|
||||||
|
value: 'dogs',
|
||||||
|
}, {
|
||||||
|
id: 'birds',
|
||||||
|
name: 'Birds',
|
||||||
|
value: 'birds',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const selectedFilter = createFilter({
|
||||||
|
properties: [{
|
||||||
|
id: 'rice',
|
||||||
|
name: 'Rice',
|
||||||
|
value: 'rice',
|
||||||
|
selected: true,
|
||||||
|
}, {
|
||||||
|
id: 'beans',
|
||||||
|
name: 'Beans',
|
||||||
|
value: 'beans',
|
||||||
|
selected: true,
|
||||||
|
}, {
|
||||||
|
id: 'potatoes',
|
||||||
|
name: 'Potatoes',
|
||||||
|
value: 'potatoes',
|
||||||
|
selected: true,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story name="Default">
|
||||||
|
<CheckboxFilter filter={defaultFilter} displayedLabel="Zoo" />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story name="Selected">
|
||||||
|
<CheckboxFilter filter={selectedFilter} displayedLabel="Shopping list" />
|
||||||
|
</Story>
|
||||||
99
src/shared/ui/ComboControl/ComboControl.stories.svelte
Normal file
99
src/shared/ui/ComboControl/ComboControl.stories.svelte
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<script module>
|
||||||
|
import { createTypographyControl } from '$shared/lib';
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import ComboControl from './ComboControl.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Shared/ComboControl',
|
||||||
|
component: ComboControl,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'Provides multiple ways to change a numeric value via decrease/increase buttons, slider, and direct input. All three methods are synchronized, giving users flexibility based on precision needs.',
|
||||||
|
},
|
||||||
|
story: { inline: false }, // Render stories in iframe for state isolation
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
control: {
|
||||||
|
control: 'object',
|
||||||
|
description: 'TypographyControl instance managing the value and bounds',
|
||||||
|
},
|
||||||
|
decreaseLabel: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Accessibility label for the decrease button',
|
||||||
|
},
|
||||||
|
increaseLabel: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Accessibility label for the increase button',
|
||||||
|
},
|
||||||
|
controlLabel: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Accessibility label for the control button (opens popover)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
const defaultControl = createTypographyControl({ value: 77, min: 0, max: 100, step: 1 });
|
||||||
|
const atMinimumControl = createTypographyControl({ value: 0, min: 0, max: 100, step: 1 });
|
||||||
|
const atMaximumControl = createTypographyControl({ value: 100, min: 0, max: 100, step: 1 });
|
||||||
|
const withFloatControl = createTypographyControl({ value: 77.5, min: 0, max: 100, step: 0.1 });
|
||||||
|
const customLabelsControl = createTypographyControl({ value: 50, min: 0, max: 100, step: 1 });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Default"
|
||||||
|
args={{
|
||||||
|
control: defaultControl,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ComboControl control={defaultControl} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="At Minimum"
|
||||||
|
args={{
|
||||||
|
control: atMinimumControl,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ComboControl control={atMinimumControl} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="At Maximum"
|
||||||
|
args={{
|
||||||
|
control: atMaximumControl,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ComboControl control={atMaximumControl} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="With Float"
|
||||||
|
args={{
|
||||||
|
control: withFloatControl,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ComboControl control={withFloatControl} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Custom Labels"
|
||||||
|
args={{
|
||||||
|
control: customLabelsControl,
|
||||||
|
decreaseLabel: 'Decrease font size',
|
||||||
|
increaseLabel: 'Increase font size',
|
||||||
|
controlLabel: 'Open font size controls',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ComboControl
|
||||||
|
control={customLabelsControl}
|
||||||
|
decreaseLabel="Decrease font size"
|
||||||
|
increaseLabel="Increase font size"
|
||||||
|
controlLabel="Open font size controls"
|
||||||
|
/>
|
||||||
|
</Story>
|
||||||
107
src/shared/ui/ContentEditable/ContentEditable.stories.svelte
Normal file
107
src/shared/ui/ContentEditable/ContentEditable.stories.svelte
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import ContentEditable from './ContentEditable.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Shared/ContentEditable',
|
||||||
|
component: ContentEditable,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'A contenteditable div with custom font and text properties. Allows inline text editing with support for font size, line height, and letter spacing. The text is two-way bindable for form use cases.',
|
||||||
|
},
|
||||||
|
story: { inline: false }, // Render stories in iframe for state isolation
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
text: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Visible text content (two-way bindable)',
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
control: { type: 'number', min: 8, max: 200 },
|
||||||
|
description: 'Font size in pixels',
|
||||||
|
},
|
||||||
|
lineHeight: {
|
||||||
|
control: { type: 'number', min: 0.8, max: 3, step: 0.1 },
|
||||||
|
description: 'Line height multiplier',
|
||||||
|
},
|
||||||
|
letterSpacing: {
|
||||||
|
control: { type: 'number', min: -0.5, max: 1, step: 0.05 },
|
||||||
|
description: 'Letter spacing in em units',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let value = $state('Here we can type and edit the content. Try it!');
|
||||||
|
let smallValue = $state('Small font size for compact text.');
|
||||||
|
let largeValue = $state('Large font size for emphasis.');
|
||||||
|
let spacedValue = $state('Wide letter spacing.');
|
||||||
|
let longValue = $state(
|
||||||
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Default"
|
||||||
|
args={{
|
||||||
|
text: value,
|
||||||
|
fontSize: 48,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
letterSpacing: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentEditable bind:text={value} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Small Font"
|
||||||
|
args={{
|
||||||
|
text: smallValue,
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
letterSpacing: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentEditable bind:text={smallValue} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Large Font"
|
||||||
|
args={{
|
||||||
|
text: largeValue,
|
||||||
|
fontSize: 72,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
letterSpacing: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentEditable bind:text={largeValue} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Wide Letter Spacing"
|
||||||
|
args={{
|
||||||
|
text: spacedValue,
|
||||||
|
fontSize: 32,
|
||||||
|
lineHeight: 1.3,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentEditable bind:text={spacedValue} />
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Long Text"
|
||||||
|
args={{
|
||||||
|
text: longValue,
|
||||||
|
fontSize: 24,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
letterSpacing: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentEditable bind:text={longValue} />
|
||||||
|
</Story>
|
||||||
82
src/shared/ui/SearchBar/SearchBar.stories.svelte
Normal file
82
src/shared/ui/SearchBar/SearchBar.stories.svelte
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import SearchBar from './SearchBar.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Shared/SearchBar',
|
||||||
|
component: SearchBar,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'Search input with popover dropdown for results/suggestions. Features keyboard navigation (ArrowDown/Up/Enter) and auto-focus prevention on popover open. The input field serves as the popover trigger.',
|
||||||
|
},
|
||||||
|
story: { inline: false }, // Render stories in iframe for state isolation
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
value: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Current search value (two-way bindable)',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Placeholder text for the input',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
control: 'text',
|
||||||
|
description: 'Optional label displayed above the input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let defaultSearchValue = $state('');
|
||||||
|
let withLabelValue = $state('');
|
||||||
|
let noChildrenValue = $state('');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Default"
|
||||||
|
args={{
|
||||||
|
value: defaultSearchValue,
|
||||||
|
placeholder: 'Type here...',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SearchBar bind:value={defaultSearchValue} placeholder="Type here...">
|
||||||
|
Here will be the search result
|
||||||
|
<br />
|
||||||
|
Popover closes only when the user clicks outside the search bar or presses the Escape key.
|
||||||
|
</SearchBar>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="With Label"
|
||||||
|
args={{
|
||||||
|
value: withLabelValue,
|
||||||
|
placeholder: 'Search products...',
|
||||||
|
label: 'Search',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SearchBar bind:value={withLabelValue} placeholder="Search products..." label="Search">
|
||||||
|
<div class="p-4">
|
||||||
|
<p class="text-sm text-muted-foreground">No results found</p>
|
||||||
|
</div>
|
||||||
|
</SearchBar>
|
||||||
|
</Story>
|
||||||
|
|
||||||
|
<Story
|
||||||
|
name="Minimal Content"
|
||||||
|
args={{
|
||||||
|
value: noChildrenValue,
|
||||||
|
placeholder: 'Quick search...',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SearchBar bind:value={noChildrenValue} placeholder="Quick search...">
|
||||||
|
<div class="p-4 text-center text-sm text-muted-foreground">
|
||||||
|
Start typing to see results
|
||||||
|
</div>
|
||||||
|
</SearchBar>
|
||||||
|
</Story>
|
||||||
@@ -18,7 +18,7 @@ import type { Snippet } from 'svelte';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Unique identifier for the input element */
|
/** Unique identifier for the input element */
|
||||||
id: string;
|
id?: string;
|
||||||
/** Current search value (bindable) */
|
/** Current search value (bindable) */
|
||||||
value: string;
|
value: string;
|
||||||
/** Additional CSS classes for the container */
|
/** Additional CSS classes for the container */
|
||||||
|
|||||||
70
src/shared/ui/VirtualList/VirtualList.stories.svelte
Normal file
70
src/shared/ui/VirtualList/VirtualList.stories.svelte
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<script module>
|
||||||
|
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||||
|
import VirtualList from './VirtualList.svelte';
|
||||||
|
|
||||||
|
const { Story } = defineMeta({
|
||||||
|
title: 'Shared/VirtualList',
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'High-performance virtualized list for large datasets. Only renders visible items plus an overscan buffer for optimal performance. Supports keyboard navigation (ArrowUp/Down, Home, End) and fixed or dynamic item heights.',
|
||||||
|
},
|
||||||
|
story: { inline: false }, // Render stories in iframe for state isolation
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
items: {
|
||||||
|
description: 'Array of items to render',
|
||||||
|
},
|
||||||
|
itemHeight: {
|
||||||
|
control: 'number',
|
||||||
|
description: 'Height of each item in pixels',
|
||||||
|
},
|
||||||
|
overscan: {
|
||||||
|
control: 'number',
|
||||||
|
description: 'Number of extra items to render above and below the visible area',
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
description: 'CSS class to apply to the root element',
|
||||||
|
},
|
||||||
|
onVisibleItemsChange: {
|
||||||
|
description: 'Callback invoked when the visible items change',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
const smallDataSet = Array.from({ length: 20 }, (_, i) => `${i + 1}) I will not waste chalk.`);
|
||||||
|
const largeDataSet = Array.from(
|
||||||
|
{ length: 10000 },
|
||||||
|
(_, 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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<Story name="Empty Dataset">
|
||||||
|
<VirtualList items={emptyDataSet} itemHeight={40}>
|
||||||
|
{#snippet children({ item })}
|
||||||
|
<div class="p-2 m-0.5 rounded-sm hover:bg-accent">{item}</div>
|
||||||
|
{/snippet}
|
||||||
|
</VirtualList>
|
||||||
|
</Story>
|
||||||
Reference in New Issue
Block a user