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
+3
View File
@@ -42,3 +42,6 @@ next-env.d.ts
*.md
!README.md
*storybook.log
storybook-static
+35
View File
@@ -0,0 +1,35 @@
import path from 'path'
import { fileURLToPath } from 'url'
import type { StorybookConfig } from '@storybook/nextjs-vite'
const dirname = path.dirname(fileURLToPath(import.meta.url))
const config: StorybookConfig = {
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
'@chromatic-com/storybook',
'@storybook/addon-vitest',
'@storybook/addon-a11y',
'@storybook/addon-docs',
],
framework: '@storybook/nextjs-vite',
staticDirs: ['../public'],
viteFinal: async (config) => {
config.resolve ??= {}
config.resolve.alias = {
...(config.resolve.alias as Record<string, string>),
'$shared': path.resolve(dirname, '../src/shared'),
'$entities': path.resolve(dirname, '../src/entities'),
'$features': path.resolve(dirname, '../src/features'),
'$widgets': path.resolve(dirname, '../src/widgets'),
'$app': path.resolve(dirname, '../src/app'),
'$routes': path.resolve(dirname, '../src/routes'),
}
return config
},
}
export default config
+21
View File
@@ -0,0 +1,21 @@
import type { Preview } from '@storybook/nextjs-vite'
import '../app/globals.css'
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo',
},
},
}
export default preview
+4
View File
@@ -1,3 +1,6 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from "eslint-plugin-storybook";
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
@@ -13,6 +16,7 @@ const eslintConfig = defineConfig([
"build/**",
"next-env.d.ts",
]),
...storybook.configs["flat/recommended"]
]);
export default eslintConfig;
+13 -1
View File
@@ -8,7 +8,9 @@
"start": "next start",
"lint": "eslint",
"test": "vitest run",
"test:watch": "vitest"
"test:watch": "vitest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"clsx": "^2.1.1",
@@ -18,6 +20,11 @@
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.1.2",
"@storybook/addon-a11y": "^10.3.5",
"@storybook/addon-docs": "^10.3.5",
"@storybook/addon-vitest": "^10.3.5",
"@storybook/nextjs-vite": "^10.3.5",
"@tailwindcss/postcss": "^4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
@@ -27,9 +34,14 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/browser-playwright": "4.1.4",
"@vitest/coverage-v8": "4.1.4",
"eslint": "^9",
"eslint-config-next": "16.2.4",
"eslint-plugin-storybook": "^10.3.5",
"jsdom": "^29.0.2",
"playwright": "^1.59.1",
"storybook": "^10.3.5",
"tailwindcss": "^4",
"typescript": "^5",
"vite": "^8.0.8",
@@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { ExperienceCard } from './ExperienceCard'
const meta: Meta<typeof ExperienceCard> = {
title: 'Entities/ExperienceCard',
component: ExperienceCard,
decorators: [
(Story) => (
<div className="p-8 bg-white max-w-2xl">
<Story />
</div>
),
],
}
export default meta
type Story = StoryObj<typeof ExperienceCard>
const baseArgs = {
title: 'Senior Frontend Engineer',
company: 'Acme Corp',
period: '2021 2024',
description: 'Led frontend development for the core product, established design system practices, and mentored junior engineers across two distributed teams.',
}
export const Default: Story = {
args: baseArgs,
}
export const SlateBackground: Story = {
render: () => (
<div className="bg-slate-indigo p-8 max-w-2xl">
<ExperienceCard
{...baseArgs}
className="border-ochre-clay"
/>
</div>
),
}
@@ -0,0 +1,42 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { DetailedProjectCard } from './DetailedProjectCard'
const meta: Meta<typeof DetailedProjectCard> = {
title: 'Entities/DetailedProjectCard',
component: DetailedProjectCard,
decorators: [
(Story) => (
<div className="p-8 bg-ochre-clay">
<Story />
</div>
),
],
}
export default meta
type Story = StoryObj<typeof DetailedProjectCard>
const baseArgs = {
title: 'Design System',
year: '2024',
role: 'Lead Frontend Engineer',
stack: ['React', 'TypeScript', 'Tailwind CSS', 'Storybook'],
description: 'A comprehensive design system built for a large-scale SaaS product, covering components, tokens, and documentation.',
details: [
'Established token system covering color, spacing, and typography.',
'Built 40+ accessible components with full test coverage.',
'Integrated Storybook for visual regression testing and documentation.',
],
}
export const Default: Story = {
args: baseArgs,
}
export const WithImage: Story = {
args: {
...baseArgs,
imageUrl: 'https://placehold.co/800x450/3B4A59/D9B48F?text=Project',
},
}
@@ -0,0 +1,37 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { ProjectCard } from './ProjectCard'
const meta: Meta<typeof ProjectCard> = {
title: 'Entities/ProjectCard',
component: ProjectCard,
decorators: [
(Story) => (
<div className="p-8 bg-white max-w-md">
<Story />
</div>
),
],
}
export default meta
type Story = StoryObj<typeof ProjectCard>
export const Default: Story = {
args: {
title: 'Portfolio Website',
year: '2024',
description: 'A brutalist portfolio site built with Next.js and Tailwind CSS.',
tags: ['React', 'TypeScript', 'Next.js'],
},
}
export const WithImage: Story = {
args: {
title: 'Portfolio Website',
year: '2024',
description: 'A brutalist portfolio site built with Next.js and Tailwind CSS.',
tags: ['React', 'TypeScript', 'Next.js'],
imageUrl: 'https://placehold.co/800x450/3B4A59/D9B48F?text=Project',
},
}
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { ProjectMetadata } from './ProjectMetadata'
const meta: Meta<typeof ProjectMetadata> = {
title: 'Entities/ProjectMetadata',
component: ProjectMetadata,
decorators: [
(Story) => (
<div className="p-8 bg-ochre-clay max-w-xs">
<Story />
</div>
),
],
}
export default meta
type Story = StoryObj<typeof ProjectMetadata>
export const Default: Story = {
args: {
year: '2024',
role: 'Lead Frontend Engineer',
stack: ['React', 'TypeScript', 'Next.js', 'Tailwind'],
},
}
+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>
),
}
@@ -0,0 +1,28 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { MobileNav } from './MobileNav'
// MobileNav is lg:hidden — it renders only on mobile viewports.
// Use the viewport toolbar in Storybook to switch to a mobile size to see it.
const meta: Meta<typeof MobileNav> = {
title: 'Widgets/MobileNav',
component: MobileNav,
parameters: {
viewport: {
defaultViewport: 'mobile1',
},
},
}
export default meta
type Story = StoryObj<typeof MobileNav>
export const Default: Story = {
args: {
items: [
{ id: 'bio', label: 'Bio', number: '01' },
{ id: 'work', label: 'Work', number: '02' },
{ id: 'contact', label: 'Contact', number: '03' },
],
},
}
@@ -0,0 +1,29 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { SidebarNav } from './SidebarNav'
// SidebarNav is hidden lg:block — it renders only on desktop viewports.
// Use the viewport toolbar in Storybook to switch to a desktop size to see it.
const meta: Meta<typeof SidebarNav> = {
title: 'Widgets/SidebarNav',
component: SidebarNav,
parameters: {
layout: 'fullscreen',
viewport: {
defaultViewport: 'desktop',
},
},
}
export default meta
type Story = StoryObj<typeof SidebarNav>
export const Default: Story = {
args: {
items: [
{ id: 'bio', label: 'Bio', number: '01' },
{ id: 'work', label: 'Work', number: '02' },
{ id: 'contact', label: 'Contact', number: '03' },
],
},
}
@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { UtilityBar } from './UtilityBar'
const meta: Meta<typeof UtilityBar> = {
title: 'Widgets/UtilityBar',
component: UtilityBar,
decorators: [
(Story) => (
<div className="relative h-24 bg-ochre-clay">
<Story />
</div>
),
],
}
export default meta
type Story = StoryObj<typeof UtilityBar>
export const Default: Story = {}
+1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
exclude: ['**/*.stories.tsx', 'node_modules/**'],
},
resolve: {
alias: {
+1419 -15
View File
File diff suppressed because it is too large Load Diff