Civitai 组件预览实战:基于 Ladle + Mantine v7 + Tailwind 的隔离式 UI 开发与视觉回归工作流
2026/9/16 23:16:44 网站建设 项目流程

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.mjsStory 发现与 Vite 配置接入
.ladle/vite.config.tsVite 配置:~/路径别名 + 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.extendBadgeActionIconTooltip的组件级配置以及dark/blue色板都取自主主题,保证预览与生产视觉一致。主主题还定义了 Drawer、Popover、Rating、Switch、Menu 等更多组件配置与 gray/yellow/green/red/orange/lime 等完整色板,预览环境按需裁剪即可。
  • 明暗双主题驱动globalState.theme来自 Ladle 的全局状态,通过defaultColorSchemeforceColorScheme双重指定,让 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-mantinepostcss-simple-varspostcss-assign-layer等 Mantine v7 样式处理插件,保证 Tailwind 类与 Mantine 样式在预览环境中都能正确编译。

四步工作流

整个预览流程分为四步:创建 Story → 启动 Ladle → 双主题截图 → 展示并迭代。

第一步:创建/更新 Story

在要预览的组件旁边创建.stories.tsx文件:

src/components/MyComponent/MyComponent.stories.tsx src/pages/challenges/EligibleModels.stories.tsx

Story 的标准结构如下:

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;
  • 保留主题钩子:如果组件内部使用了useComputedColorSchemeuseMantineTheme,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.tsxModelCard.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.tsxglobalState.theme的取值);
  • 每个页面等待 800ms 渲染稳定;
  • 定位到.ladle-story-wrapper(正是 .ladle/components.tsx 中包裹 children 的容器)做裁剪截图,得到无多余留白、带统一 padding 的截图;
  • 截图命名采用crop-<theme>-<story-name>.png,暗色/亮色一目了然。

Story 路径格式:由文件名与导出名推导而来——kebab-case 文件名 +--+ kebab-case 导出名:

文件导出Story 路径
EligibleModels.stories.tsxDefaulteligible-models--default
ModelCard.stories.tsxWithBadgemodel-card--with-badge

第四步:展示与迭代

  1. 内联展示:用 Read 工具读取 PNG 截图,在对话中直接展示给用户;
  2. 打开本地查看:如需用户在系统图片查看器中打开,可执行:
    start "" "<path-to-screenshot>"
  3. 征求反馈:询问 "Does this look right? Want me to adjust anything?";
  4. 迭代闭环:如需修改,改组件 → 重新截图 → 再次展示,循环直至满意。

复杂组件的分级处理策略

并非所有组件都能直接丢进 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:

  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?"

即:完整 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询