component-preview
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseComponent 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:
- — Global provider with MantineProvider + theme
.ladle/components.tsx - — Story discovery config
.ladle/config.mjs - — Vite config with
.ladle/vite.config.tspath alias + PostCSS~/
If these don't exist in the current worktree, copy them from main or create them. See Setup Reference below.
项目根目录已配置Ladle:
- — 包含MantineProvider和主题的全局提供者
.ladle/components.tsx - — 故事发现配置
.ladle/config.mjs - — 带有
.ladle/vite.config.ts路径别名和PostCSS的Vite配置~/
如果当前工作树中没有这些文件,请从主分支复制或创建它们。详见下方的配置参考。
Workflow
操作流程
1. Create/Update the Story
1. 创建/更新故事
Create a file near the component being previewed:
.stories.tsxsrc/components/MyComponent/MyComponent.stories.tsx
src/pages/challenges/EligibleModels.stories.tsxStory 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 on the wrapper (e.g., 320px for sidebar, 600px for main content)
width - Copy the exact Mantine component props and Tailwind classes from the real component
- Copy any inline props from the parent context (e.g., Accordion styles)
styles - Use and
useComputedColorSchemeif the component uses themuseMantineTheme - Create 2-4 variants showing different states (default, empty, single item, overflow)
在待预览组件的附近创建文件:
.stories.tsxsrc/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} />;重要模式:
- 在包裹元素上设置符合真实场景的(例如侧边栏设为320px,主内容设为600px)
width - 复制真实组件中完全相同的Mantine组件属性和Tailwind类
- 复制父上下文的任何内联属性(例如Accordion样式)
styles - 如果组件使用了和
useComputedColorScheme,请在故事中也使用useMantineTheme - 创建2-4种变体来展示不同状态(默认、空、单项、溢出)
2. Start Ladle
2. 启动Ladle
bash
undefinedbash
undefinedCheck 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
undefinedCreate 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. 向用户展示
- Show screenshots inline using the Read tool on the PNG files
- Open for the user if they want to see them in their image viewer:
bash
start "" "<path-to-screenshot>" - Ask for feedback — "Does this look right? Want me to adjust anything?"
- Iterate — if they want changes, modify the component, re-capture, re-present
- 内联展示截图:使用读取工具打开PNG文件进行展示
- 为用户打开截图:如果用户想在图片查看器中查看:
bash
start "" "<path-to-screenshot>" - 请求反馈 — “这个看起来没问题吧?需要我调整什么吗?”
- 迭代优化 — 如果用户需要修改,调整组件后重新捕获并展示
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 or
<div>instead of Next.js<a href="#"><Link>
- 使用tRPC数据的组件 — 提取类型并创建模拟对象
- 包含图片的组件 — 使用占位div或空图片回退
- 包含链接的组件 — 使用或
<div>替代Next.js的<a href="#"><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:
- Mock out the dependencies (more setup, more accurate)
- Extract just the visual parts into the story (faster, close enough)
- Skip the preview and we can check it on the dev server instead
What would you prefer?"
- 深度耦合多个提供者的组件(认证、路由、tRPC上下文)
- 使用复杂钩子调用API的组件
- 依赖复杂CSS模块的组件
遇到复杂场景时,告知用户:
“此组件依赖[认证/路由/tRPC上下文]。我可以选择:
- 模拟依赖项(设置更多,更准确)
- 仅提取视觉部分到故事中(更快,效果接近)
- 跳过预览,直接在开发服务器上查看
您倾向于哪种方式?”
Setup Reference
配置参考
If Ladle isn't configured in the worktree, create these files:
如果工作树中未配置Ladle,请创建以下文件:
.ladle/components.tsx
.ladle/components.tsx.ladle/components.tsx
.ladle/components.tsxtsx
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.ladle/config.mjs
.ladle/config.mjsjs
/** @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.ladle/vite.config.ts
.ladle/vite.config.tsts
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/reactbash
pnpm add -D @ladle/reactTips
小贴士
- 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)及其他服务冲突