home / skills / pluxity / pf-frontend / pf-storybook

pf-storybook skill

/.claude/skills/pf-storybook

This skill generates Storybook stories for UI components, scaffolding default and variant stories with args and controls.

npx playbooks add skill pluxity/pf-frontend --skill pf-storybook

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.9 KB
---
name: pf-storybook
description: Storybook 스토리 생성. "스토리북", "스토리 만들어", "Storybook" 요청 시 사용.
allowed-tools: Read, Write, Glob
---

# PF Storybook 스토리 생성기

$ARGUMENTS 컴포넌트에 대한 Storybook 스토리를 생성합니다.

---

## 스토리 파일 위치

```
packages/ui/src/atoms/Button/
├── Button.tsx
├── Button.stories.tsx  ← 생성
├── types.ts
└── variants.ts
```

---

## 기본 스토리 구조

```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

const meta: Meta<typeof Button> = {
  title: "Atoms/Button",
  component: Button,
  tags: ["autodocs"],
  parameters: {
    layout: "centered",
  },
  argTypes: {
    variant: {
      control: "select",
      options: ["default", "secondary", "outline", "ghost", "destructive"],
      description: "버튼 스타일 변형",
    },
    size: {
      control: "select",
      options: ["sm", "md", "lg"],
      description: "버튼 크기",
    },
    disabled: {
      control: "boolean",
      description: "비활성화 상태",
    },
    onClick: {
      action: "clicked",
    },
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

// 기본 스토리
export const Default: Story = {
  args: {
    children: "버튼",
    variant: "default",
    size: "md",
  },
};

// Variants
export const Secondary: Story = {
  args: {
    children: "Secondary",
    variant: "secondary",
  },
};

export const Outline: Story = {
  args: {
    children: "Outline",
    variant: "outline",
  },
};

export const Ghost: Story = {
  args: {
    children: "Ghost",
    variant: "ghost",
  },
};

export const Destructive: Story = {
  args: {
    children: "삭제",
    variant: "destructive",
  },
};

// Sizes
export const Small: Story = {
  args: {
    children: "Small",
    size: "sm",
  },
};

export const Large: Story = {
  args: {
    children: "Large",
    size: "lg",
  },
};

// States
export const Disabled: Story = {
  args: {
    children: "Disabled",
    disabled: true,
  },
};

export const Loading: Story = {
  args: {
    children: "Loading...",
    disabled: true,
  },
  render: (args) => (
    <Button {...args}>
      <Spinner className="mr-2 h-4 w-4 animate-spin" />
      Loading...
    </Button>
  ),
};

// With Icon
export const WithIcon: Story = {
  args: {
    children: "설정",
  },
  render: (args) => (
    <Button {...args}>
      <SettingsIcon className="mr-2 h-4 w-4" />
      설정
    </Button>
  ),
};
```

---

## 복합 컴포넌트 스토리 (Composition)

```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Sidebar } from "./Sidebar";
import { Home, Settings, Users, LogOut } from "lucide-react";

const meta: Meta<typeof Sidebar> = {
  title: "Organisms/Sidebar",
  component: Sidebar,
  tags: ["autodocs"],
  parameters: {
    layout: "fullscreen",
  },
  decorators: [
    (Story) => (
      <div className="h-screen">
        <Story />
      </div>
    ),
  ],
};

export default meta;
type Story = StoryObj<typeof Sidebar>;

export const Default: Story = {
  render: () => (
    <Sidebar defaultCollapsed={false}>
      <Sidebar.Header title="Dashboard">
        <Sidebar.CollapseButton iconOnly />
      </Sidebar.Header>

      <Sidebar.Content>
        <Sidebar.Section label="메인">
          <Sidebar.Item icon={<Home />} active>홈</Sidebar.Item>
          <Sidebar.Item icon={<Users />}>사용자</Sidebar.Item>
          <Sidebar.Item icon={<Settings />}>설정</Sidebar.Item>
        </Sidebar.Section>
      </Sidebar.Content>

      <Sidebar.Footer>
        <Sidebar.Item icon={<LogOut />}>로그아웃</Sidebar.Item>
      </Sidebar.Footer>
    </Sidebar>
  ),
};

export const Collapsed: Story = {
  render: () => (
    <Sidebar defaultCollapsed>
      {/* ... */}
    </Sidebar>
  ),
};

export const WithBadge: Story = {
  render: () => (
    <Sidebar>
      <Sidebar.Content>
        <Sidebar.Item icon={<Bell />} badge={5}>
          알림
        </Sidebar.Item>
      </Sidebar.Content>
    </Sidebar>
  ),
};
```

---

## 폼 컴포넌트 스토리

```tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Input } from "./Input";

const meta: Meta<typeof Input> = {
  title: "Atoms/Input",
  component: Input,
  tags: ["autodocs"],
  argTypes: {
    type: {
      control: "select",
      options: ["text", "email", "password", "number"],
    },
    disabled: { control: "boolean" },
    error: { control: "text" },
  },
};

export default meta;
type Story = StoryObj<typeof Input>;

export const Default: Story = {
  args: {
    placeholder: "입력하세요",
  },
};

export const WithLabel: Story = {
  render: () => (
    <div className="space-y-2">
      <label htmlFor="email" className="text-sm font-medium">
        이메일
      </label>
      <Input id="email" type="email" placeholder="[email protected]" />
    </div>
  ),
};

export const WithError: Story = {
  args: {
    placeholder: "이메일",
    error: "유효한 이메일을 입력하세요",
    defaultValue: "invalid-email",
  },
  render: (args) => (
    <div className="space-y-2">
      <Input {...args} aria-invalid="true" />
      <p className="text-sm text-red-500">{args.error}</p>
    </div>
  ),
};

export const Password: Story = {
  args: {
    type: "password",
    placeholder: "비밀번호",
  },
};
```

---

## 인터랙티브 스토리

```tsx
import { useState } from "react";

export const Controlled: Story = {
  render: function ControlledStory() {
    const [value, setValue] = useState("");

    return (
      <div className="space-y-4">
        <Input
          value={value}
          onChange={(e) => setValue(e.target.value)}
          placeholder="입력하세요"
        />
        <p className="text-sm text-gray-500">
          입력값: {value || "(없음)"}
        </p>
      </div>
    );
  },
};

export const WithValidation: Story = {
  render: function ValidationStory() {
    const [value, setValue] = useState("");
    const [error, setError] = useState("");

    const validate = (v: string) => {
      if (!v) setError("필수 입력입니다");
      else if (v.length < 3) setError("3자 이상 입력하세요");
      else setError("");
    };

    return (
      <div className="space-y-2">
        <Input
          value={value}
          onChange={(e) => {
            setValue(e.target.value);
            validate(e.target.value);
          }}
          aria-invalid={!!error}
        />
        {error && <p className="text-sm text-red-500">{error}</p>}
      </div>
    );
  },
};
```

---

## 실행

```bash
# Storybook 실행
pnpm storybook

# 빌드
pnpm build-storybook
```

---

## 스토리 체크리스트

- [ ] 모든 variants가 스토리로 존재
- [ ] 모든 sizes가 스토리로 존재
- [ ] disabled/loading 상태 스토리
- [ ] 에러 상태 스토리 (폼 컴포넌트)
- [ ] 인터랙티브 예제 (필요시)
- [ ] autodocs 태그 추가
- [ ] argTypes 정의 (controls)

Overview

This skill generates Storybook stories for components in the PF DEV monorepo. It creates well-structured .stories.tsx files that include basic stories, variants, sizes, states, composition examples, form examples, and interactive demos. The output follows the project's TypeScript conventions and Storybook autodocs patterns. It also adds argTypes and controls for convenient component exploration.

How this skill works

Point the generator at a component folder (e.g., packages/ui/src/atoms/Button) and it scaffolds a Storybook file with Meta, Story types, default story, variant stories, size stories, state stories (disabled/loading), and examples with custom render functions. For composite components it creates fullscreen layout examples with decorators to emulate real usage. For form and interactive components it includes controlled and validation examples using useState, plus argTypes for controls and action handlers for events.

When to use it

  • Adding Storybook coverage for a new component
  • Ensuring all variants and sizes have visual tests
  • Creating interactive or composed examples for complex components
  • Standardizing story structure across the monorepo
  • Preparing stories before running visual tests or demos

Best practices

  • Include variants and sizes for every visual component
  • Add disabled/loading/error states where applicable
  • Define argTypes with controls and descriptions for important props
  • Use render examples for composition and icon/slot usage
  • Add interactive controlled/validation examples for form inputs

Example use cases

  • Scaffold Button.stories.tsx with Default, Secondary, Outline, Ghost, Destructive, Small, Large, Disabled, Loading, and WithIcon stories
  • Create Sidebar stories that render full-screen layouts with Header, Content, and Footer composed children
  • Generate Input stories with Default, WithLabel, WithError, Password, Controlled, and WithValidation examples
  • Add argTypes for props like variant, size, disabled, type, and error to enable Storybook controls
  • Produce decorator-wrapped stories to preview components inside common layout containers

FAQ

Will the generator modify existing stories?

It can create new .stories.tsx files; if a story already exists, prefer reviewing diffs and merging manually to avoid overwriting custom examples.

Does it add actions and controls automatically?

Yes. The generator adds action handlers for events (e.g., onClick) and defines argTypes with control types and options for common props.