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 ได้ง่าย