# Fumadocs (Framework Mode, React Router): 元件情境
URL: /zh-TW/docs/react-router/integrations/story
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/(framework)/[framework]/integrations/story/index.zh-TW.mdx

透過控制項展示元件。

<story.WithControl />

## 簡介 [#introduction]

Fumadocs Story 可用來展示元件並為其撰寫文件。
它是 [Storybook](https://storybook.js.org) 的輕量替代方案，以文件為核心，主要為元件庫而設計。

如果您初次接觸元件情境 (Story) 的概念，以下是 **Storybook** 文件中的簡短說明：

<blockquote>
  元件情境記錄了 UI 元件已渲染的狀態。它是一個帶有註解的物件，
  用來描述元件在給定一組引數時的行為與外觀。
</blockquote>

<Callout title="何時該選用它，而不是 Storybook？">
Fumadocs Story 並非用來取代 Storybook。若要進行完整的 UI 測試，我們仍建議使用 Storybook。
</Callout>

## 安裝 [#installation]

```npm
npm i @fumadocs/story
```

加入 Tailwind CSS 預設集：

```css title="Tailwind CSS"
/* [!code ++] */
@import '@fumadocs/story/css/preset.css';
```

新增 Vite 外掛：

```ts title="vite.config.ts"
import { defineConfig } from 'vite';
import story from '@fumadocs/story/vite';

export default defineConfig({
  plugins: [story()],
});
```

### 建立元件情境 [#create-a-story]

建立一個元件情境工廠，用來存放您的全域設定：

```ts title="lib/story.ts"
import { defineStoryFactory } from '@fumadocs/story/vite/client';

export const { defineStory } = defineStoryFactory();
```

接著建立您的第一個元件情境：

```tsx tab="my-component.story.tsx"
import { defineStory } from '@/lib/story';
import { MyComponent } from './my-component';

export const story = defineStory({
  Component: MyComponent,
  args: {
    // 預設 props（建議設定）
    initial: {},
  },
});
```

```tsx tab="my-component.tsx"
export interface MyComponentProps {
  title?: string;
}

export function MyComponent({ title }: MyComponentProps) {
  return <p>{title}</p>;
}
```

<Callout type="warn">
此外掛只會轉換符合 `*.story.{js,jsx,ts,tsx}` 的檔案。您可以透過 Vite 外掛的 `filter` 選項變更此規則。
</Callout>

### 算繪元件情境 [#render-a-story]

在 MDX 檔案中算繪元件情境：

```mdx title="index.mdx"
import { story } from '@/components/my-component.story';

<story.WithControl />
```

## 設定 [#configurations]

### 變體 [#variants]

您可以透過 `args` 為元件提供多個變體：

```ts
import { defineStory } from '@/lib/story';
import { Callout } from '@/components/callout';

export const story = defineStory({
  Component: Callout,
  args: [
    {
      variant: 'Default',
      initial: {
        title: 'This is a Callout',
      },
    },
    {
      variant: 'Warning',
      initial: {
        title: 'This is a Callout',
      },
      // props 的固定值
      fixed: {
        type: 'warning',
      },
    },
  ],
});
```

### `controls` [#controls]

控制項會根據元件的 props 自動產生。您可以指定控制項節點來取代它們：

```tsx
import { defineStory } from '@/lib/story';
import { GraphView } from '@/components/graph-view';

export const story = defineStory({
  Component: GraphView,
  args: {
    controls: {
      node: {
        type: 'object',
        properties: [],
      },
    },
  },
});
```

### `frame` [#frame]

填滿整個 viewport 的元件 (例如版面配置) 可以在 iframe 中算繪。
元件情境面板會將 frame 縮小至合適的大小，並提供按鈕讓您以全螢幕開啟。

```tsx title="components/my-layout.story.tsx"
import { defineStory } from '@/lib/story';
import { MyLayout } from './my-layout.lazy';
import { Window } from './window';

export const story = defineStory({
  Component: MyLayout,
  frame: {
    src: '/story/my-layout',
    // 檢視區大小（選填）
    width: 1280,
    height: 800,
    // 包裹 frame 的容器，例如視窗（選填）
    Container: Window,
  },
});
```

在 `src` 建立一個頁面來算繪元件。此頁面會接收來自元件情境面板的引數：

```tsx tab="app/routes/story.my-layout.tsx"
import { story } from '@/components/my-layout.story';

export default function Page() {
  return <story.Frame />;
}
```

```ts tab="app/routes.ts"
import { route, type RouteConfig } from '@react-router/dev/routes';

export default [
  // [!code ++]
  route('story/my-layout', 'routes/story.my-layout.tsx'),
  // ...
] satisfies RouteConfig;
```

只要頁面算繪了元件情境，就會載入其元件。建議改為傳入延遲載入的元件，讓元件只在 frame 中載入：

```tsx title="components/my-layout.lazy.tsx"
import { lazy } from 'react';

export const MyLayout = lazy(() =>
  import('./my-layout').then((mod) => ({ default: mod.MyLayout })),
);
```

<Callout title="小提醒">
* 此頁面必須與您的文件位於相同來源 (origin) 。
* 引數是透過 `postMessage()` 傳送，因此必須可序列化 (不能包含函式或 JSX) 。
</Callout>

### 國際化 [#internationalization]

假設您已在 UI 層級設定好[國際化](/docs/react-router/internationalization)：

```tsx title="layout.shared.tsx"
import { defineI18n } from 'fumadocs-core/i18n';
import { uiTranslations } from 'fumadocs-ui/i18n';
import { storyTranslations } from '@fumadocs/story/i18n';

const i18n = defineI18n({
  languages: ['en', 'cn'],
  defaultLanguage: 'en',
});

export const translations = i18n
  .translations()
  .extend(uiTranslations())
  // [!code ++]
  .extend(storyTranslations())
  .add({
    cn: {
      displayName: 'Chinese',
      // [!code ++]
      'Add Item(story arguments form)': '新增項目',
    },
  });
```

詳情請參閱[翻譯](/docs/ui/translations)。