Civitai 组件预览实战:基于 Ladle + Mantine v7 + Tailwind 的隔离式 UI 开发与视觉回归工作流
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
本文介绍 Civitai 仓库中一套面向 UI 开发与视觉排障的组件预览工作流:基于 Ladle(轻量级 Storybook 替代品)在真实 Mantine v7 + Tailwind 样式环境下隔离渲染 React 组件,无需启动完整开发服务器即可快速查看、截图并迭代组件外观。读完本文,你将掌握从创建 Story、启动 Ladle、暗色/亮色双主题截图到复杂组件分级处理的完整闭环方法,并了解本仓库.ladle/目录下的真实工程配置。
为什么需要隔离式组件预览
Civitai 是一个包含数千个组件(src/components 下 2500+ 文件)的大型 Next.js 应用。在完整开发服务器上预览单个组件的成本很高:路由、鉴权、tRPC 上下文、全局 Provider 都会拖慢启动与迭代节奏。
Ladle 方案的核心优势在于:无需 dev server。它把单个组件放进一个极简的 HTML 页面中,配合与主应用一致的 Mantine 主题与 Tailwind 样式,让你可以在几秒内完成「改代码 → 看效果 → 再改」的循环。这正好覆盖以下四类典型场景(对应技能文档中的 "When to Use"):
- 修改 UI 组件之后:主动预览改动效果,确认视觉没有回归;
- 用户要求查看效果时:例如 "show me what it looks like" 或 "generate a preview";
- 排查视觉 Bug 时:为出问题的组件创建 Story 来复现问题并迭代修复;
- 提交前审查组件改动:在 commit 之前对变更做一次快速的视觉走查。
前置条件:项目根目录的 Ladle 工程配置
Ladle 的配置集中在仓库根目录的 .ladle 文件夹下,包含三个文件:
| 文件 | 作用 |
|---|---|
| .ladle/components.tsx | 全局 Provider:MantineProvider + 主题子集 |
| .ladle/config.mjs | Story 发现与 Vite 配置接入 |
| .ladle/vite.config.ts | Vite 配置:~/路径别名 + PostCSS |
此外,@ladle/react已作为开发依赖安装在 package.json("@ladle/react": "^5.1.1")。如果当前 worktree 中缺少上述文件,可以从 main 分支拷贝,或参考本文末尾的「Setup Reference」一节手动重建。
.ladle/components.tsx:让预览贴近真实运行环境
预览质量的关键在于「渲染环境与主应用一致」。该文件通过 Ladle 的GlobalProvider把每个 Story 包进一个与主应用同源的 MantineProvider 中:
import { MantineProvider, createTheme, Modal } from '@mantine/core'; import type { GlobalProvider } from '@ladle/react'; import '@mantine/core/styles.layer.css'; import '../src/styles/globals.css'; // Subset of the app theme (from src/providers/ThemeProvider.tsx) const theme = createTheme({ components: { Modal: Modal.extend({ styles: { content: { maxWidth: '100%', overflowX: 'hidden' }, inner: { paddingLeft: 0, paddingRight: 0 }, }, }), 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> );这段代码与仓库实际文件一致,几个关键点值得说明:
- 主题子集:注释明确指出这份主题是主应用主题的裁剪版,来源是 src/providers/ThemeProvider.tsx。对照源码可以看到,
Modal.extend、Badge、ActionIcon、Tooltip的组件级配置以及dark/blue色板都取自主主题,保证预览与生产视觉一致。主主题还定义了 Drawer、Popover、Rating、Switch、Menu 等更多组件配置与 gray/yellow/green/red/orange/lime 等完整色板,预览环境按需裁剪即可。 - 明暗双主题驱动:
globalState.theme来自 Ladle 的全局状态,通过defaultColorScheme与forceColorScheme双重指定,让 Story 可以随 URL 参数&theme=dark|light切换。 - 样式导入:
@mantine/core/styles.layer.css是 Mantine v7 的层叠样式入口,../src/styles/globals.css(即 src/styles/globals.css)是主应用的全局样式。Tailwind 依赖 PostCSS 管线在构建期注入,见下文 Vite 配置。 ladle-story-wrapper类名:这是截图阶段用于定位渲染区域的钩子(后面会用到),固定 24px padding 与fit-content宽度,保证不同 Story 的截图尺寸一致。
.ladle/config.mjs:Story 发现规则
/** @type {import('@ladle/react').UserConfig} */ export default { stories: 'src/**/*.stories.tsx', defaultStory: '', viteConfig: '.ladle/vite.config.ts', };stories字段声明了 Story 的自动发现范围:src/**/*.stories.tsx。也就是说,任何放在src下、以.stories.tsx结尾的文件都会被 Ladle 自动收录为 Story,无需手工注册。
.ladle/vite.config.ts:路径别名与 PostCSS
import { defineConfig } from 'vite'; import path from 'path'; export default defineConfig({ resolve: { alias: { '~': path.resolve(__dirname, '../src'), }, }, css: { postcss: path.resolve(__dirname, '..'), }, });~别名:把~解析到src目录,与主应用 tsconfig 的路径映射保持一致(见 tsconfig.json 中"~/*": ["./src/*"])。这样 Story 里可以直接import { x } from '~/shared/...',与主应用代码写法一致。- PostCSS:指向仓库根目录的 postcss.config.js,从而启用 Tailwind 以及 package.json 中声明的
postcss-preset-mantine、postcss-simple-vars、postcss-assign-layer等 Mantine v7 样式处理插件,保证 Tailwind 类与 Mantine 样式在预览环境中都能正确编译。
四步工作流
整个预览流程分为四步:创建 Story → 启动 Ladle → 双主题截图 → 展示并迭代。
第一步:创建/更新 Story
在要预览的组件旁边创建.stories.tsx文件:
src/components/MyComponent/MyComponent.stories.tsx src/pages/challenges/EligibleModels.stories.tsxStory 的标准结构如下:
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} />;需要严格遵守的编写规范:
- 约束真实宽度:在外层 wrapper 上设置贴近真实场景的
width(侧边栏 320px、主内容区 600px),避免组件在无限宽度下变形; - 原样拷贝 props:从真实组件中复制 Mantine 组件 props 和 Tailwind 类名,保证预览即生产;
- 继承父容器样式:如果组件原本位于 Accordion、Card 等容器内,要把父容器的内联
styles(如 Accordion styles)一并复制进 Story; - 保留主题钩子:如果组件内部使用了
useComputedColorScheme、useMantineTheme,Story 中也要保留,以便暗色/亮色主题正确生效; - 2~4 个变体:至少覆盖默认态、空态、单项态、溢出态等关键状态(default / empty / single item / overflow),每个变体对应一个具名导出。
第二步:启动 Ladle
启动前先探测端口是否已有实例在运行(Ladle 约定固定使用 61111 端口,避免与 3000 的开发服务器及其它服务冲突):
# Check if Ladle is already running 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 & # Wait for it to be ready (~3-5 seconds)启动后 Ladle 会自动发现所有匹配src/**/*.stories.tsx的 Story(即第一步创建的EligibleModels.stories.tsx、ModelCard.stories.tsx等文件)。
第三步:双主题截图
截图环节借助仓库中的浏览器自动化技能(.claude/skills/browser-automation/SKILL.md)完成:
# Create a browser session 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这段脚本的关键点:
- URL 参数
?story=<story-path>&theme=<dark|light>&mode=preview直接定位到某个 Story 的某个变体,并指定明暗主题(对应.ladle/components.tsx中globalState.theme的取值); - 每个页面等待 800ms 渲染稳定;
- 定位到
.ladle-story-wrapper(正是 .ladle/components.tsx 中包裹 children 的容器)做裁剪截图,得到无多余留白、带统一 padding 的截图; - 截图命名采用
crop-<theme>-<story-name>.png,暗色/亮色一目了然。
Story 路径格式:由文件名与导出名推导而来——kebab-case 文件名 +--+ kebab-case 导出名:
| 文件 | 导出 | Story 路径 |
|---|---|---|
EligibleModels.stories.tsx | Default | eligible-models--default |
ModelCard.stories.tsx | WithBadge | model-card--with-badge |
第四步:展示与迭代
- 内联展示:用 Read 工具读取 PNG 截图,在对话中直接展示给用户;
- 打开本地查看:如需用户在系统图片查看器中打开,可执行:
start "" "<path-to-screenshot>" - 征求反馈:询问 "Does this look right? Want me to adjust anything?";
- 迭代闭环:如需修改,改组件 → 重新截图 → 再次展示,循环直至满意。
复杂组件的分级处理策略
并非所有组件都能直接丢进 Story。技能文档把组件按耦合程度分为三档:
简单(直接做)
- 纯展示组件:badge、card、list、accordion 等;
- 只依赖 Mantine + Tailwind 的组件;
- props 简单的组件。
这类组件只需按第一步的规范写 Story 即可。
中等(Mock 数据)
- 依赖 tRPC 数据的组件:从类型定义中提取出类型,构造逼真的 mock 对象;
- 包含图片的组件:用占位 div 或空图片 fallback;
- 包含链接的组件:用
<div>或<a href="#">替代 Next.js 的<Link>(Ladle 环境没有 Next.js 路由上下文)。
困难(上报给用户)
- 深度耦合多个 Provider(auth、router、tRPC context)的组件;
- 使用复杂 hooks、会发起 API 调用的组件;
- 重度依赖 CSS Module 的组件。
遇到困难案例时,向用户说明并提供三个选项:
"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?"
即:完整 mock 依赖(更准确但更费时)、只抽取视觉部分(更快但近似)、跳过预览改用 dev server。由用户权衡取舍。
Setup Reference:手动重建 Ladle 配置
如果当前 worktree 缺少.ladle/三件套,可以按以下内容从零创建。上文已给出 .ladle/components.tsx、.ladle/config.mjs、.ladle/vite.config.ts 的完整内容,此处补充安装命令:
pnpm add -D @ladle/react本仓库使用 pnpm 管理依赖(见 pnpm-workspace.yaml 与根目录 package.json),安装后即可在任意 worktree 中执行npx ladle serve --port 61111。
实战 Tips 汇总
- 先暗后亮:Civitai 默认是暗色模式,所以截图时先拍 dark 主题,再拍 light;
- 约束宽度:始终设置与真实上下文一致的宽度——侧边栏约 320px、主内容区约 600px、整页约 1200px;
- 复制父级样式:组件若位于 Accordion、Card 等容器内部,要在 Story 中复刻父容器的样式,否则间距、圆角、阴影会失真;
- Story 生命周期:一次性审查用的 Story 可在用后删除;可复用组件的 Story 可以保留,成为长期的视觉回归资产;
- 固定端口:始终使用 61111,避免与 dev server(3000)及其它服务端口冲突。
小结
这套工作流把「查看组件效果」从分钟级的 dev server 启动中解放出来:Story 即文档、截图即证据、双主题即覆盖。结合 .ladle/components.tsx 对 src/providers/ThemeProvider.tsx 主题子集的复用,预览环境与生产环境的视觉保真度得以保证;而「简单直做 / 中等 mock / 困难上报」的分级策略,则让它在面对 Civitai 这种高度依赖 auth、router、tRPC 上下文的大型应用时依然可控可落地。
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考