diff --git a/.gitignore b/.gitignore index c0c59a2..1834d9d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ next-env.d.ts *.md !README.md + +*storybook.log +storybook-static diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..1b0718f --- /dev/null +++ b/.storybook/main.ts @@ -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), + '$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 diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..133286b --- /dev/null +++ b/.storybook/preview.ts @@ -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 diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..162f8fc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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; diff --git a/package.json b/package.json index 170cd38..3724585 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/entities/experience/ui/ExperienceCard.stories.tsx b/src/entities/experience/ui/ExperienceCard.stories.tsx new file mode 100644 index 0000000..3a79152 --- /dev/null +++ b/src/entities/experience/ui/ExperienceCard.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import { ExperienceCard } from './ExperienceCard' + +const meta: Meta = { + title: 'Entities/ExperienceCard', + component: ExperienceCard, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +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: () => ( +
+ +
+ ), +} diff --git a/src/entities/project/ui/DetailedProjectCard.stories.tsx b/src/entities/project/ui/DetailedProjectCard.stories.tsx new file mode 100644 index 0000000..4299636 --- /dev/null +++ b/src/entities/project/ui/DetailedProjectCard.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import { DetailedProjectCard } from './DetailedProjectCard' + +const meta: Meta = { + title: 'Entities/DetailedProjectCard', + component: DetailedProjectCard, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +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', + }, +} diff --git a/src/entities/project/ui/ProjectCard.stories.tsx b/src/entities/project/ui/ProjectCard.stories.tsx new file mode 100644 index 0000000..f8c3f76 --- /dev/null +++ b/src/entities/project/ui/ProjectCard.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import { ProjectCard } from './ProjectCard' + +const meta: Meta = { + title: 'Entities/ProjectCard', + component: ProjectCard, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +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', + }, +} diff --git a/src/entities/project/ui/ProjectMetadata.stories.tsx b/src/entities/project/ui/ProjectMetadata.stories.tsx new file mode 100644 index 0000000..2773220 --- /dev/null +++ b/src/entities/project/ui/ProjectMetadata.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import { ProjectMetadata } from './ProjectMetadata' + +const meta: Meta = { + title: 'Entities/ProjectMetadata', + component: ProjectMetadata, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + year: '2024', + role: 'Lead Frontend Engineer', + stack: ['React', 'TypeScript', 'Next.js', 'Tailwind'], + }, +} diff --git a/src/shared/ui/Badge/ui/Badge.stories.tsx b/src/shared/ui/Badge/ui/Badge.stories.tsx new file mode 100644 index 0000000..e3150bc --- /dev/null +++ b/src/shared/ui/Badge/ui/Badge.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import { Badge } from './Badge' + +const meta: Meta = { + title: 'Shared/Badge', + component: Badge, +} + +export default meta + +type Story = StoryObj + +export const AllVariants: Story = { + render: () => ( +
+ Default + Primary + Secondary + Outline +
+ ), +} diff --git a/src/shared/ui/Button/ui/Button.stories.tsx b/src/shared/ui/Button/ui/Button.stories.tsx new file mode 100644 index 0000000..702c81c --- /dev/null +++ b/src/shared/ui/Button/ui/Button.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import { Button } from './Button' + +const meta: Meta = { + title: 'Shared/Button', + component: Button, +} + +export default meta + +type Story = StoryObj + +export const AllVariants: Story = { + render: () => ( +
+ + + + +
+ ), +} + +export const Sizes: Story = { + render: () => ( +
+ + + +
+ ), +} + +export const Disabled: Story = { + args: { + variant: 'primary', + disabled: true, + children: 'Disabled', + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} diff --git a/src/shared/ui/Card/ui/Card.stories.tsx b/src/shared/ui/Card/ui/Card.stories.tsx new file mode 100644 index 0000000..345ebe3 --- /dev/null +++ b/src/shared/ui/Card/ui/Card.stories.tsx @@ -0,0 +1,70 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './Card' + +const meta: Meta = { + title: 'Shared/Card', + component: Card, +} + +export default meta + +type Story = StoryObj + +export const AllBackgrounds: Story = { + render: () => ( +
+ + + Ochre Card + Background ochre-clay variant + + Footer content + + + + Slate Card + Background slate-indigo variant + + Footer content + + + + White Card + Background white variant + + Footer content + +
+ ), +} + +export const NoPadding: Story = { + render: () => ( +
+ +
+ Image placeholder +
+
+
+ ), +} + +export const FullComposition: Story = { + render: () => ( +
+ + + Full Composition + A card using all available slot components + + +

This is the main body content of the card, placed inside CardContent.

+
+ + Card footer + +
+
+ ), +} diff --git a/src/shared/ui/Input/ui/Input.stories.tsx b/src/shared/ui/Input/ui/Input.stories.tsx new file mode 100644 index 0000000..e89be1b --- /dev/null +++ b/src/shared/ui/Input/ui/Input.stories.tsx @@ -0,0 +1,59 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' +import { Input, Textarea } from './Input' + +const meta: Meta = { + title: 'Shared/Input', + component: Input, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +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: () => ( +
+