Astro กับ Storybook: Component Documentation
#astro13 เม.ย. 2569
Astro กับ Storybook: Component Documentation
Storybook เป็น tool สำหรับพัฒนาและ document UI components แบบ isolated ทำให้ทีมพัฒนาสามารถสร้าง, ทดสอบ, และ document components ได้อย่างมีประสิทธิภาพ บทความนี้จะแนะนำการตั้งค่า Storybook สำหรับ Astro project
ทำไมต้องใช้ Storybook?
- Component Isolation: พัฒนา components แยกจาก application
- Visual Testing: ดู components ในทุก states
- Documentation: สร้าง living documentation อัตโนมัติ
- Collaboration: ทีม design และ dev ทำงานร่วมกันได้ดีขึ้น
- Accessibility Testing: ตรวจสอบ accessibility ได้ง่าย
การติดตั้ง
npx storybook@latest init
Storybook จะตรวจจับ framework อัตโนมัติและติดตั้ง dependencies ที่จำเป็น
หรือติดตั้งแบบ manual สำหรับ React components:
npm install -D @storybook/react @storybook/react-vite storybook
กำหนดค่า Storybook
สร้าง .storybook/main.ts:
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-themes',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;
สร้าง .storybook/preview.ts:
import type { Preview } from '@storybook/react';
import '../src/styles/global.css';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1a1a1a' },
{ name: 'gray', value: '#f5f5f5' },
],
},
},
globalTypes: {
locale: {
description: 'Internationalization locale',
defaultValue: 'th',
toolbar: {
icon: 'globe',
items: [
{ value: 'th', title: 'ภาษาไทย' },
{ value: 'en', title: 'English' },
],
},
},
},
};
export default preview;
สร้าง Component Stories
สร้าง Button component:
// src/components/Button/Button.tsx
import React from 'react';
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
fullWidth?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export function Button({
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
fullWidth = false,
onClick,
children,
}: ButtonProps) {
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors';
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-300',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 disabled:bg-gray-100',
danger: 'bg-red-600 text-white hover:bg-red-700 disabled:bg-red-300',
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 border border-gray-300',
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button
className={[
baseClasses,
variantClasses[variant],
sizeClasses[size],
fullWidth ? 'w-full' : '',
disabled || loading ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
].join(' ')}
disabled={disabled || loading}
onClick={onClick}
>
{loading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
)}
{children}
</button>
);
}
สร้าง stories:
// src/components/Button/Button.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Button component ที่ใช้งานทั่วไปใน application',
},
},
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger', 'ghost'],
description: 'รูปแบบของ button',
},
size: {
control: 'radio',
options: ['sm', 'md', 'lg'],
description: 'ขนาดของ button',
},
onClick: { action: 'clicked' },
},
args: { onClick: fn() },
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'ปุ่มหลัก',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'ปุ่มรอง',
},
};
export const Danger: Story = {
args: {
variant: 'danger',
children: 'ลบข้อมูล',
},
};
export const Loading: Story = {
args: {
loading: true,
children: 'กำลังโหลด...',
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: 'ปิดใช้งาน',
},
};
export const AllVariants: Story = {
render: () => (
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};
MDX Documentation
{/* src/components/Button/Button.mdx */}
import { Canvas, Meta, Controls, Story } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';
<Meta of={ButtonStories} />
# Button Component
Button component ที่ใช้งานทั่วไปใน application รองรับหลาย variants และ sizes
## การใช้งาน
```tsx
import { Button } from '@/components/Button';
<Button variant="primary" onClick={() => console.log('clicked')}>
คลิกที่นี่
</Button>
ตัวอย่าง
Props
Variants
```Interaction Testing
// src/components/LoginForm/LoginForm.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect } from '@storybook/test';
import { LoginForm } from './LoginForm';
const meta: Meta<typeof LoginForm> = {
title: 'Forms/LoginForm',
component: LoginForm,
};
export default meta;
type Story = StoryObj<typeof meta>;
export const FilledForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(
canvas.getByLabelText('อีเมล'),
'user@example.com'
);
await userEvent.type(
canvas.getByLabelText('รหัสผ่าน'),
'password123'
);
await userEvent.click(canvas.getByRole('button', { name: 'เข้าสู่ระบบ' }));
await expect(canvas.getByText('กำลังเข้าสู่ระบบ...')).toBeInTheDocument();
},
};
สรุป
Storybook กับ Astro ช่วยให้ทีมพัฒนาสามารถสร้าง component library ที่มี documentation ครบครัน ด้วย interactive stories, MDX documentation, และ interaction testing ทำให้ components มีคุณภาพสูงและ reusable ได้ง่าย