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:
@@ -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'],
|
||||
},
|
||||
}
|
||||
@@ -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>
|
||||
),
|
||||
],
|
||||
}
|
||||
@@ -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>
|
||||
),
|
||||
}
|
||||
@@ -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 = {}
|
||||
Reference in New Issue
Block a user