feat: add Storybook with component stories

Installs @storybook/nextjs-vite. Stories co-located with components,
grouped by layer (Shared/Entities/Widgets). Multi-variant cases use
render functions instead of one story per variant/size.
This commit is contained in:
Ilia Mashkov
2026-04-19 09:19:17 +03:00
parent a47341ffcb
commit de03d21429
21 changed files with 2052 additions and 16 deletions
+22
View File
@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { Badge } from './Badge'
const meta: Meta<typeof Badge> = {
title: 'Shared/Badge',
component: Badge,
}
export default meta
type Story = StoryObj<typeof Badge>
export const AllVariants: Story = {
render: () => (
<div className="flex gap-3 p-8 bg-ochre-clay">
<Badge variant="default">Default</Badge>
<Badge variant="primary">Primary</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
</div>
),
}
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { Button } from './Button'
const meta: Meta<typeof Button> = {
title: 'Shared/Button',
component: Button,
}
export default meta
type Story = StoryObj<typeof Button>
export const AllVariants: Story = {
render: () => (
<div className="flex gap-4 flex-wrap p-8 bg-ochre-clay">
<Button variant="primary" size="md">Primary</Button>
<Button variant="secondary" size="md">Secondary</Button>
<Button variant="outline" size="md">Outline</Button>
<Button variant="ghost" size="md">Ghost</Button>
</div>
),
}
export const Sizes: Story = {
render: () => (
<div className="flex gap-4 items-center flex-wrap p-8 bg-ochre-clay">
<Button variant="primary" size="sm">Small</Button>
<Button variant="primary" size="md">Medium</Button>
<Button variant="primary" size="lg">Large</Button>
</div>
),
}
export const Disabled: Story = {
args: {
variant: 'primary',
disabled: true,
children: 'Disabled',
},
decorators: [
(Story) => (
<div className="p-8 bg-ochre-clay">
<Story />
</div>
),
],
}
+70
View File
@@ -0,0 +1,70 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card'
const meta: Meta<typeof Card> = {
title: 'Shared/Card',
component: Card,
}
export default meta
type Story = StoryObj<typeof Card>
export const AllBackgrounds: Story = {
render: () => (
<div className="flex gap-6 flex-wrap p-8 bg-white">
<Card background="ochre" className="w-64">
<CardHeader>
<CardTitle>Ochre Card</CardTitle>
<CardDescription>Background ochre-clay variant</CardDescription>
</CardHeader>
<CardFooter>Footer content</CardFooter>
</Card>
<Card background="slate" className="w-64">
<CardHeader>
<CardTitle>Slate Card</CardTitle>
<CardDescription>Background slate-indigo variant</CardDescription>
</CardHeader>
<CardFooter>Footer content</CardFooter>
</Card>
<Card background="white" className="w-64">
<CardHeader>
<CardTitle>White Card</CardTitle>
<CardDescription>Background white variant</CardDescription>
</CardHeader>
<CardFooter>Footer content</CardFooter>
</Card>
</div>
),
}
export const NoPadding: Story = {
render: () => (
<div className="p-8 bg-ochre-clay">
<Card noPadding className="w-64 overflow-hidden">
<div className="h-40 bg-slate-indigo flex items-center justify-center text-ochre-clay">
Image placeholder
</div>
</Card>
</div>
),
}
export const FullComposition: Story = {
render: () => (
<div className="p-8 bg-white max-w-md">
<Card background="ochre">
<CardHeader>
<CardTitle>Full Composition</CardTitle>
<CardDescription>A card using all available slot components</CardDescription>
</CardHeader>
<CardContent>
<p>This is the main body content of the card, placed inside CardContent.</p>
</CardContent>
<CardFooter>
<span className="text-sm opacity-70">Card footer</span>
</CardFooter>
</Card>
</div>
),
}
+59
View File
@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { Input, Textarea } from './Input'
const meta: Meta<typeof Input> = {
title: 'Shared/Input',
component: Input,
decorators: [
(Story) => (
<div className="p-8 bg-ochre-clay max-w-sm">
<Story />
</div>
),
],
}
export default meta
type Story = StoryObj<typeof Input>
export const Default: Story = {
args: {},
}
export const WithLabel: Story = {
args: {
label: 'Email address',
},
}
export const WithError: Story = {
args: {
label: 'Email',
error: 'This field is required',
},
}
export const WithPlaceholder: Story = {
args: {
placeholder: 'Enter your email',
type: 'email',
},
}
export const TextareaStory: Story = {
name: 'Textarea',
render: () => (
<div className="p-8 bg-ochre-clay max-w-sm">
<Textarea label="Message" rows={4} />
</div>
),
}
export const TextareaWithError: Story = {
render: () => (
<div className="p-8 bg-ochre-clay max-w-sm">
<Textarea label="Message" error="Too short" rows={4} />
</div>
),
}
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { Section, Container } from './Section'
const meta: Meta<typeof Section> = {
title: 'Shared/Section',
component: Section,
}
export default meta
type Story = StoryObj<typeof Section>
export const AllBackgrounds: Story = {
render: () => (
<div>
<Section background="ochre" className="py-12">
<Container>
<h2>Ochre Section</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</Container>
</Section>
<Section background="slate" className="py-12">
<Container>
<h2>Slate Section</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</Container>
</Section>
<Section background="white" className="py-12">
<Container>
<h2>White Section</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</Container>
</Section>
</div>
),
}
export const Bordered: Story = {
render: () => (
<Section background="ochre" bordered className="py-12">
<Container>
<h2>Bordered Section</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
</Container>
</Section>
),
}
@@ -0,0 +1,44 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { SectionAccordion } from './SectionAccordion'
const meta: Meta<typeof SectionAccordion> = {
title: 'Shared/SectionAccordion',
component: SectionAccordion,
decorators: [
(Story) => (
<div className="p-8 bg-ochre-clay">
<Story />
</div>
),
],
}
export default meta
type Story = StoryObj<typeof SectionAccordion>
export const Active: Story = {
args: {
number: '01',
title: 'Biography',
id: 'bio',
isActive: true,
onClick: () => {},
children: (
<p>This is the expanded section content. It is visible because isActive is true.</p>
),
},
}
export const Collapsed: Story = {
args: {
number: '02',
title: 'Work',
id: 'work',
isActive: false,
onClick: () => console.log('section clicked'),
children: (
<p>This content is hidden in collapsed state.</p>
),
},
}
@@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { TechStackGrid, TechStackBrick } from './TechStack'
const meta: Meta<typeof TechStackGrid> = {
title: 'Shared/TechStack',
component: TechStackGrid,
decorators: [
(Story) => (
<div className="p-8 bg-ochre-clay">
<Story />
</div>
),
],
}
export default meta
type Story = StoryObj<typeof TechStackGrid>
export const Grid: Story = {
args: {
skills: [
'React',
'TypeScript',
'Next.js',
'Go',
'PostgreSQL',
'Redis',
'Docker',
'Kubernetes',
'Tailwind',
'Figma',
'GraphQL',
'Rust',
],
},
}
export const SingleBrick: Story = {
render: () => (
<div className="p-8 bg-ochre-clay inline-block">
<TechStackBrick name="TypeScript" />
</div>
),
}