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:
@@ -42,3 +42,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
*.md
|
*.md
|
||||||
!README.md
|
!README.md
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
storybook-static
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 { defineConfig, globalIgnores } from "eslint/config";
|
||||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
import nextTs from "eslint-config-next/typescript";
|
import nextTs from "eslint-config-next/typescript";
|
||||||
@@ -13,6 +16,7 @@ const eslintConfig = defineConfig([
|
|||||||
"build/**",
|
"build/**",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
]),
|
]),
|
||||||
|
...storybook.configs["flat/recommended"]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
|||||||
+13
-1
@@ -8,7 +8,9 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"build-storybook": "storybook build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -18,6 +20,11 @@
|
|||||||
"tailwind-merge": "^3.5.0"
|
"tailwind-merge": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
@@ -27,9 +34,14 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"@vitest/browser-playwright": "4.1.4",
|
||||||
|
"@vitest/coverage-v8": "4.1.4",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.4",
|
"eslint-config-next": "16.2.4",
|
||||||
|
"eslint-plugin-storybook": "^10.3.5",
|
||||||
"jsdom": "^29.0.2",
|
"jsdom": "^29.0.2",
|
||||||
|
"playwright": "^1.59.1",
|
||||||
|
"storybook": "^10.3.5",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"vite": "^8.0.8",
|
"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'],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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 = {}
|
||||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
|||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: ['./src/test/setup.ts'],
|
setupFiles: ['./src/test/setup.ts'],
|
||||||
|
exclude: ['**/*.stories.tsx', 'node_modules/**'],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in New Issue
Block a user