component-preview

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Component Preview

组件预览

Preview React components in isolation using Ladle (lightweight Storybook alternative) with real Mantine v7 + Tailwind styling. No dev server needed.
使用Ladle(轻量级Storybook替代工具)搭配真实的Mantine v7 + Tailwind样式,独立预览React组件,无需启动开发服务器。

When to Use

适用场景

  • After modifying a UI component — proactively offer to preview it
  • When the user asks "show me what it looks like" or "generate a preview"
  • When debugging visual issues — create a story to reproduce and iterate
  • When reviewing component changes before committing
  • 修改UI组件后 — 主动提供预览
  • 当用户询问“展示一下它的样子”或“生成预览”时
  • 调试视觉问题时 — 创建故事来复现问题并迭代修复
  • 提交组件变更前 — 用于审核组件变更

Prerequisites

前置条件

Ladle is configured in the project root:
  • .ladle/components.tsx
    — Global provider with MantineProvider + theme
  • .ladle/config.mjs
    — Story discovery config
  • .ladle/vite.config.ts
    — Vite config with
    ~/
    path alias + PostCSS
If these don't exist in the current worktree, copy them from main or create them. See Setup Reference below.
项目根目录已配置Ladle:
  • .ladle/components.tsx
    — 包含MantineProvider和主题的全局提供者
  • .ladle/config.mjs
    — 故事发现配置
  • .ladle/vite.config.ts
    — 带有
    ~/
    路径别名和PostCSS的Vite配置
如果当前工作树中没有这些文件,请从主分支复制或创建它们。详见下方的配置参考

Workflow

操作流程

1. Create/Update the Story

1. 创建/更新故事

Create a
.stories.tsx
file near the component being previewed:
src/components/MyComponent/MyComponent.stories.tsx
src/pages/challenges/EligibleModels.stories.tsx
Story structure:
tsx
import { /* Mantine components */ } from '@mantine/core';
// Import the component or recreate the relevant JSX

// Mock data that represents realistic API responses
const mockData = [ ... ];

// Render the component with different states
function Preview({ data }) {
  return (
    <div style={{ width: 320 }}> {/* Constrain to realistic width */}
      <MyComponent data={data} />
    </div>
  );
}

/** Default state */
export const Default = () => <Preview data={mockData} />;

/** Empty state */
export const Empty = () => <Preview data={[]} />;

/** Loading or edge case states */
export const LongList = () => <Preview data={longMockData} />;
Important patterns:
  • Set a realistic
    width
    on the wrapper (e.g., 320px for sidebar, 600px for main content)
  • Copy the exact Mantine component props and Tailwind classes from the real component
  • Copy any inline
    styles
    props from the parent context (e.g., Accordion styles)
  • Use
    useComputedColorScheme
    and
    useMantineTheme
    if the component uses them
  • Create 2-4 variants showing different states (default, empty, single item, overflow)
在待预览组件的附近创建
.stories.tsx
文件:
src/components/MyComponent/MyComponent.stories.tsx
src/pages/challenges/EligibleModels.stories.tsx
故事结构:
tsx
import { /* Mantine components */ } from '@mantine/core';
// 导入组件或重新创建相关JSX

// 模拟真实API响应的假数据
const mockData = [ ... ];

// 渲染不同状态的组件
function Preview({ data }) {
  return (
    <div style={{ width: 320 }}> {/* 限制为真实场景的宽度 */}
      <MyComponent data={data} />
    </div>
  );
}

/** 默认状态 */
export const Default = () => <Preview data={mockData} />;

/** 空状态 */
export const Empty = () => <Preview data={[]} />;

/** 加载或边缘情况状态 */
export const LongList = () => <Preview data={longMockData} />;
重要模式:
  • 在包裹元素上设置符合真实场景的
    width
    (例如侧边栏设为320px,主内容设为600px)
  • 复制真实组件中完全相同的Mantine组件属性和Tailwind类
  • 复制父上下文的任何内联
    styles
    属性(例如Accordion样式)
  • 如果组件使用了
    useComputedColorScheme
    useMantineTheme
    ,请在故事中也使用
  • 创建2-4种变体来展示不同状态(默认、空、单项、溢出)

2. Start Ladle

2. 启动Ladle

bash
undefined
bash
undefined

Check if Ladle is already running

检查Ladle是否已在运行

curl -s -o /dev/null -w "%{http_code}" http://localhost:61111/
curl -s -o /dev/null -w "%{http_code}" http://localhost:61111/

If not running, start it (from project root or worktree root)

如果未运行,启动它(从项目根目录或工作树根目录)

cd <worktree-path> npx ladle serve --port 61111 &
cd <worktree-path> npx ladle serve --port 61111 &

Wait for it to be ready (~3-5 seconds)

等待启动完成(约3-5秒)


Ladle auto-discovers stories matching `src/**/*.stories.tsx`.

Ladle会自动发现匹配`src/**/*.stories.tsx`的故事。

3. Capture Screenshots

3. 捕获截图

Use the browser-automation skill to capture cropped, padded screenshots:
bash
undefined
使用浏览器自动化工具捕获带裁剪和内边距的截图:
bash
undefined

Create a browser session

创建浏览器会话

node ~/.claude/skills/browser-automation/cli.mjs session http://localhost:61111 --name ladle
node ~/.claude/skills/browser-automation/cli.mjs session http://localhost:61111 --name ladle

Capture all story variants in dark and light themes

捕获深色和浅色主题下的所有故事变体

node ~/.claude/skills/browser-automation/cli.mjs run " const stories = [ { name: 'default', path: 'my-component--default' }, { name: 'empty', path: 'my-component--empty' }, ]; const themes = ['dark', 'light']; const dir = '<session-screenshots-dir>';
for (const theme of themes) { for (const story of stories) { await page.goto('http://localhost:61111/?story=' + story.path + '&theme=' + theme + '&mode=preview'); await page.waitForTimeout(800); const wrapper = page.locator('.ladle-story-wrapper'); await wrapper.screenshot({ path: dir + '/crop-' + theme + '-' + story.name + '.png' }); } } " --label "Component preview screenshots" -s ladle

**Story path format:** The story path is derived from the file name and export name:
- File: `EligibleModels.stories.tsx`, Export: `Default` -> path: `eligible-models--default`
- File: `ModelCard.stories.tsx`, Export: `WithBadge` -> path: `model-card--with-badge`

Pattern: kebab-case filename + `--` + kebab-case export name.
node ~/.claude/skills/browser-automation/cli.mjs run " const stories = [ { name: 'default', path: 'my-component--default' }, { name: 'empty', path: 'my-component--empty' }, ]; const themes = ['dark', 'light']; const dir = '<session-screenshots-dir>';
for (const theme of themes) { for (const story of stories) { await page.goto('http://localhost:61111/?story=' + story.path + '&theme=' + theme + '&mode=preview'); await page.waitForTimeout(800); const wrapper = page.locator('.ladle-story-wrapper'); await wrapper.screenshot({ path: dir + '/crop-' + theme + '-' + story.name + '.png' }); } } " --label "Component preview screenshots" -s ladle

**故事路径格式:** 故事路径由文件名和导出名称派生而来:
- 文件:`EligibleModels.stories.tsx`,导出:`Default` -> 路径:`eligible-models--default`
- 文件:`ModelCard.stories.tsx`,导出:`WithBadge` -> 路径:`model-card--with-badge`

模式:短横线分隔的文件名 + `--` + 短横线分隔的导出名称。

4. Present to User

4. 向用户展示

  1. Show screenshots inline using the Read tool on the PNG files
  2. Open for the user if they want to see them in their image viewer:
    bash
    start "" "<path-to-screenshot>"
  3. Ask for feedback — "Does this look right? Want me to adjust anything?"
  4. Iterate — if they want changes, modify the component, re-capture, re-present
  1. 内联展示截图:使用读取工具打开PNG文件进行展示
  2. 为用户打开截图:如果用户想在图片查看器中查看:
    bash
    start "" "<path-to-screenshot>"
  3. 请求反馈 — “这个看起来没问题吧?需要我调整什么吗?”
  4. 迭代优化 — 如果用户需要修改,调整组件后重新捕获并展示

Handling Complex Components

处理复杂组件

Some components depend heavily on app context. When this happens:
有些组件严重依赖应用上下文,遇到这种情况时:

Easy (just do it)

简单场景(直接处理)

  • Presentational components (badges, cards, lists, accordions)
  • Components that only use Mantine + Tailwind
  • Components with simple props
  • 展示型组件(徽章、卡片、列表、折叠面板)
  • 仅使用Mantine + Tailwind的组件
  • 带有简单属性的组件

Medium (mock the data)

中等场景(模拟数据)

  • Components that use tRPC data — extract the type and create mock objects
  • Components with images — use placeholder divs or null image fallbacks
  • Components with links — use
    <div>
    or
    <a href="#">
    instead of Next.js
    <Link>
  • 使用tRPC数据的组件 — 提取类型并创建模拟对象
  • 包含图片的组件 — 使用占位div或空图片回退
  • 包含链接的组件 — 使用
    <div>
    <a href="#">
    替代Next.js的
    <Link>

Hard (raise to user)

复杂场景(告知用户)

  • Components deeply coupled to multiple providers (auth, router, tRPC context)
  • Components using complex hooks that call APIs
  • Components with heavy CSS module dependencies
When encountering hard cases, tell the user:
"This component depends on [auth/router/tRPC context]. I can either:
  1. Mock out the dependencies (more setup, more accurate)
  2. Extract just the visual parts into the story (faster, close enough)
  3. Skip the preview and we can check it on the dev server instead
What would you prefer?"
  • 深度耦合多个提供者的组件(认证、路由、tRPC上下文)
  • 使用复杂钩子调用API的组件
  • 依赖复杂CSS模块的组件
遇到复杂场景时,告知用户:
“此组件依赖[认证/路由/tRPC上下文]。我可以选择:
  1. 模拟依赖项(设置更多,更准确)
  2. 仅提取视觉部分到故事中(更快,效果接近)
  3. 跳过预览,直接在开发服务器上查看
您倾向于哪种方式?”

Setup Reference

配置参考

If Ladle isn't configured in the worktree, create these files:
如果工作树中未配置Ladle,请创建以下文件:

.ladle/components.tsx

.ladle/components.tsx

tsx
import { MantineProvider, createTheme, Modal } from '@mantine/core';
import type { GlobalProvider } from '@ladle/react';

import '@mantine/core/styles.layer.css';
import '../src/styles/globals.css';

// Theme subset from src/providers/ThemeProvider.tsx
const theme = createTheme({
  components: {
    Badge: {
      styles: { leftSection: { lineHeight: 1 } },
      defaultProps: { radius: 'sm', variant: 'light' },
    },
    ActionIcon: {
      defaultProps: { color: 'gray', variant: 'subtle' },
    },
    Tooltip: {
      defaultProps: { withArrow: true },
    },
  },
  colors: {
    dark: ['#C1C2C5','#A6A7AB','#8c8fa3','#5C5F66','#373A40','#2C2E33','#25262B','#1A1B1E','#141517','#101113'],
    blue: ['#E7F5FF','#D0EBFF','#A5D8FF','#74C0FC','#4DABF7','#339AF0','#228BE6','#1C7ED6','#1971C2','#1864AB'],
  },
  white: '#fefefe',
  black: '#222',
});

export const Provider: GlobalProvider = ({ children, globalState }) => (
  <MantineProvider
    theme={theme}
    defaultColorScheme={globalState.theme === 'dark' ? 'dark' : 'light'}
    forceColorScheme={globalState.theme === 'dark' ? 'dark' : 'light'}
  >
    <div className="ladle-story-wrapper" style={{ padding: 24, width: 'fit-content' }}>
      {children}
    </div>
  </MantineProvider>
);
tsx
import { MantineProvider, createTheme, Modal } from '@mantine/core';
import type { GlobalProvider } from '@ladle/react';

import '@mantine/core/styles.layer.css';
import '../src/styles/globals.css';

// 从src/providers/ThemeProvider.tsx中提取的主题子集
const theme = createTheme({
  components: {
    Badge: {
      styles: { leftSection: { lineHeight: 1 } },
      defaultProps: { radius: 'sm', variant: 'light' },
    },
    ActionIcon: {
      defaultProps: { color: 'gray', variant: 'subtle' },
    },
    Tooltip: {
      defaultProps: { withArrow: true },
    },
  },
  colors: {
    dark: ['#C1C2C5','#A6A7AB','#8c8fa3','#5C5F66','#373A40','#2C2E33','#25262B','#1A1B1E','#141517','#101113'],
    blue: ['#E7F5FF','#D0EBFF','#A5D8FF','#74C0FC','#4DABF7','#339AF0','#228BE6','#1C7ED6','#1971C2','#1864AB'],
  },
  white: '#fefefe',
  black: '#222',
});

export const Provider: GlobalProvider = ({ children, globalState }) => (
  <MantineProvider
    theme={theme}
    defaultColorScheme={globalState.theme === 'dark' ? 'dark' : 'light'}
    forceColorScheme={globalState.theme === 'dark' ? 'dark' : 'light'}
  >
    <div className="ladle-story-wrapper" style={{ padding: 24, width: 'fit-content' }}>
      {children}
    </div>
  </MantineProvider>
);

.ladle/config.mjs

.ladle/config.mjs

js
/** @type {import('@ladle/react').UserConfig} */
export default {
  stories: 'src/**/*.stories.tsx',
  defaultStory: '',
  viteConfig: '.ladle/vite.config.ts',
};
js
/** @type {import('@ladle/react').UserConfig} */
export default {
  stories: 'src/**/*.stories.tsx',
  defaultStory: '',
  viteConfig: '.ladle/vite.config.ts',
};

.ladle/vite.config.ts

.ladle/vite.config.ts

ts
import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: { '~': path.resolve(__dirname, '../src') },
  },
  css: {
    postcss: path.resolve(__dirname, '..'),
  },
});
ts
import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
  resolve: {
    alias: { '~': path.resolve(__dirname, '../src') },
  },
  css: {
    postcss: path.resolve(__dirname, '..'),
  },
});

Ensure Ladle is installed

确保Ladle已安装

bash
pnpm add -D @ladle/react
bash
pnpm add -D @ladle/react

Tips

小贴士

  • Dark theme first — Civitai defaults to dark mode, so capture dark first
  • Constrain width — always set a width matching the real context (sidebar = ~320px, main content = ~600px, full page = ~1200px)
  • Copy parent styles — if the component lives inside an Accordion, Card, or other container, replicate those parent styles in the story
  • Keep stories temporary — stories for one-off reviews can be deleted after; stories for reusable components can stay
  • Ladle port — always use 61111 to avoid conflicts with dev server (3000) and other services
  • 优先深色主题 — Civitai默认使用深色模式,所以先捕获深色模式截图
  • 限制宽度 — 始终设置与真实场景匹配的宽度(侧边栏≈320px,主内容≈600px,全屏≈1200px)
  • 复制父组件样式 — 如果组件位于折叠面板、卡片或其他容器内,在故事中复现这些父组件样式
  • 故事可临时创建 — 用于一次性审核的故事可在之后删除;可复用组件的故事可保留
  • Ladle端口 — 始终使用61111端口,避免与开发服务器(3000)及其他服务冲突