figma-lens: 构建与 Figma 匹配的 UI
figma-lens 是一个 Figma 插件加本地桥接器。用户在 Figma 桌面版中选择画框并点击 Export to Claude。桥接器会为每个画框写入一个文件夹:
design/
INDEX.md ← 所有导出画框的列表(从这里开始)
<frame>/ ← 画框名称对应的文件夹
spec.md ← 首先阅读:紧凑的图层树,包含精确的值
spec.json ← 完整数据(+ Figma 中每个图层的 CSS),当 spec.md 不够用时使用
tokens.json ← 变量(所有模式)、样式、原始值的使用次数
[email protected] ← 完整渲染图(默认 2 倍)
sections/NN-name.png← 顶层块的全分辨率裁切
assets/*.svg ← 图标/矢量图,可直接使用
images/* ← 用作图像填充的原始位图
compare/ ← 由 `figma-lens check` 写入(report.md、side-by-side.png、diff.png)
figma-lens.map.json ← (项目根目录)Figma 组件/令牌到此项目的代码映射
figma-lens 命令是 figma-lens 仓库中的 CLI(在那里运行 npm link 可使其全局可用;否则运行 node <figma-lens>/bin/figma-lens.js …)。运行 figma-lens help 查看所有命令。
如果尚未导出
- 为用户项目启动桥接器并保持运行(后台):
figma-lens bridge --out <project>/design。 - 要求用户在 Figma 桌面版中选择画框 → 插件 → 开发 → Figma Lens → Export to Claude。
- 读取
design/INDEX.md确认导出已到达。
如果可以通过导出来完成,永远不要仅凭描述猜测设计。请要求用户导出。
工作流程
- 完整阅读
spec.md(它很简短)。注意画框尺寸、布局树、变量、样式和资源。 - 查看图片。 逐个阅读
sections/*.png以了解细节。[email protected]仅用于全局概览,因为大图在查看时会被缩小。值始终来自 spec,切勿通过测量像素获取。 - 使用组件映射(见下方“组件映射”)。spec.md 中每个
⇒ USE …都是强制性的:使用该 widget/组件及其属性映射,并使用映射后的令牌((color/primary ⇒ AppColors.primary))代替原始值。对于标记为(not in map)的项,在代码库中搜索现有的主题和 widget(Flutter 中为ThemeData、AppColors、TextTheme、间距常量、lib/widgets;Web 中为 CSS 变量、Tailwind 配置、src/components)。仅在没有令牌存在时使用原始值,并告知用户哪些是你添加的。 - 复用组件。 标记为
component Button {Type=Primary}的图层应使用项目中现有的 Button widget 或组件及其匹配的属性。仅当不存在时才新建一个可复用的组件,并将其添加到映射中。 - 自上而下构建,遵循树结构。 自动布局映射为 Row/Column/flex(见下方表格)。仅
@x,y图层(无自动布局,或absolute)才需要 Stack/Positioned 或绝对定位。 - 使用导出的文件。 将
assets/*.svg和images/*放入项目的资源文件夹并引用它们。切勿重新绘制图标或替换为不同的图标集。 - 精确复制文本,包括 spec.md 中
"…"内的大小写、标点和阿拉伯语/RTL 字符串。包含多个run行的文本需要富文本(TextSpan / 嵌套 spans)。 - 使用对比修复循环(见下文)进行验证。不要仅靠阅读代码就宣布画面完成;必须通过截图差异来证明。
对比修复循环
构建完画面后,将实际结果与 Figma 对比并修复差异:
- 运行构建以显示该画面:
- Web:启动开发服务器并打开该路由。
- Flutter Web:
flutter run -d web-server --web-port 8080,然后使用该 URL。 - Flutter Android:在模拟器/设备上运行应用并导航到该画面。
- 进行检查:
- Web / Flutter Web:
figma-lens check --design design/<frame> --url http://localhost:<port>/<route>(视口会自动设置为画框的尺寸和缩放比例)。 - Android:
figma-lens check --design design/<frame> --adb --crop-top <status bar px> [--crop-bottom <nav bar px>](状态栏 ≈ 24–48 dp × 设备密度;裁剪掉它以便仅比较应用内容)。
- Web / Flutter Web:
- 阅读
design/<frame>/compare/report.md(不匹配百分比、差异最大的区域以及每个区域中的 Figma 图层 ID),并查看compare/side-by-side.png(Figma | 构建 | 差异,红色 = 不同)。 - 自上而下修复:首先修复高度或间距差异(它们会使其下方的所有内容偏移),然后是尺寸、颜色、排版、圆角、阴影。在 spec.md 中查找每个列出的图层 ID,获取精确值。
- 重新运行检查。 报告会显示之前的分数,因此你可以看到进度。
- 当不匹配度 ≤ 3%(文本抗锯齿永远无法达到 0)或连续两次运行不再改进时停止。然后报告最终分数以及你无法匹配的任何内容及其原因(缺少字体、平台渲染、动态数据)。
规则:修复真实代码,永远不要欺骗差异(不要将设计截图或图片放入应用)。检查时使用与设计相同的数据/文本(必要时使用模拟数据),否则文本差异会主导分数。
组件映射
figma-lens.map.json(项目根目录)将 Figma 组件和令牌映射到此项目的代码:
{
"components": {
"Button": { "flutter": "lib/widgets/app_button.dart#AppButton", "web": "src/components/Button.tsx#Button", "props": { "Label": "label", "Type": "variant" } },
"Icon/*": "SvgPicture.asset('assets/icons/<name>.svg')"
},
"tokens": { "color/primary": { "flutter": "AppColors.primary", "web": "var(--color-primary)" } }
}
- 尚无映射 → 运行
figma-lens map --design design。它会将导出中的每个组件和令牌添加为空值 ("")。 - 自己填充它:在代码库中搜索每个组件(按名称、外观、属性)和每个令牌值;写入
path#Symbol(或一个表达式)。仅在尚无对应项时保留""。"Prefix/*"匹配整个系列(例如图标)。 - 编辑映射后,运行
figma-lens render design/<frame>,以便 spec.md 显示⇒ USE箭头。桥接器也会在每次新导出时应用映射。 - 当你为未映射的组件创建新的可复用 widget 时,将其添加到映射中。
阅读 spec.md
- 单位是设计像素,等同于 CSS px 和 Flutter 逻辑像素。切勿乘以截图缩放比例。
row|column gap G pad t,r,b,l main:X cross:Y表示自动布局。pad 16表示所有四边都是 16。gap auto表示 space-between。w:FILL h:HUG给出每个轴上的尺寸:FIXED(显示精确尺寸)、HUG(包裹内容)或 FILL(占据可用空间)。@x,y是相对于父容器的位置。absolute表示自动布局内的绝对定位子元素。pin H/V给出约束(MIN = 左/上,MAX = 右/下,STRETCH = 两者,CENTER,SCALE)。bg A + B从底到顶列出填充。(name)是 Figma 变量,[style name]是 Figma 样式。- 颜色格式为
#RRGGBB或#RRGGBBAA(Alpha 在最后)。 - 文本格式为
"chars" · Family Style (weight) size/lineHeight color。lineHeight 单位为 px、% 或 auto。ls是字母间距,单位为 px 或字体大小的百分比。 radius 12设置所有角。radius tl,tr,br,bl分别设置每个角。smoothing是 iOS 风格的圆角平滑;可近似处理,除非项目使用了 squircle 包,否则 Flutter 中可忽略。drop-shadow x,y blur B spread S color、inner-shadow、layer-blur R、background-blur R。→ assets/x.svg表示该图层是一个图标。使用指定尺寸的 SVG。vars{itemSpacing=space/md}表示图层属性绑定到该变量。使用该令牌。
Flutter 映射表
| Figma | Flutter |
|---|---|
| column / row | Column / Row(Flutter ≥ 3.27 使用 spacing: G,否则使用 SizedBox 间隙) |
| wrap, rowGap R | Wrap(spacing: G, runSpacing: R) |
| pad t,r,b,l | Padding(padding: EdgeInsets.fromLTRB(l, t, r, b))。注意顺序。 |
| main MIN / CENTER / MAX / SPACE_BETWEEN | MainAxisAlignment.start / center / end / spaceBetween |
| cross MIN / CENTER / MAX / BASELINE | CrossAxisAlignment.start / center / end / baseline(baseline 需要 textBaseline) |
| HUG on main axis | mainAxisSize: MainAxisSize.min |
| FILL on parent's main axis | Expanded(或 Flexible);grow N 设置 flex: N |
| FILL on parent's cross axis | 父级设置 CrossAxisAlignment.stretch,或使用 SizedBox(width: double.infinity) |
| FIXED w×h | SizedBox(width: w, height: h) |
@x,y / absolute | Stack + Positioned(left: x, top: y);使用 pin 实现 right/bottom |
| bg solid | BoxDecoration(color: Color(0xAARRGGBB))。将 Alpha 移到前面:#11223380 → 0x80112233。 |
| gradient-linear from/to | LinearGradient(begin: Alignment(fx*2-1, fy*2-1), end: Alignment(tx*2-1, ty*2-1), colors, stops),使用 spec.json 中的 from/to |
| radius | BorderRadius.circular(r) 或 BorderRadius.only(...) |
| border W color inside | Border.all(width: W, color: …)(inside 是 Flutter 的默认值;center/outside 需要 strokeAlign) |
| drop-shadow x,y blur B spread S | BoxShadow(offset: Offset(x, y), blurRadius: B, spreadRadius: S, color: …) |
| background-blur R | ClipRRect + BackdropFilter(filter: ImageFilter.blur(sigmaX: R/2, sigmaY: R/2)) |
| opacity | Opacity(或 paints 的颜色透明度) |
| text size/lineHeight | TextStyle(fontSize: size, height: lineHeight / size)。对于百分比:height: pct/100。设置 leadingDistribution: TextLeadingDistribution.even 以匹配 Figma 的垂直居中。 |
| weight 600 | FontWeight.w600 |
| ls px / ls % | letterSpacing: px / letterSpacing: size * pct / 100 |
| ellipsis maxLines N | maxLines: N, overflow: TextOverflow.ellipsis |
| align CENTER | textAlign: TextAlign.center。对于 RTL 画面,使用 TextAlign.start 和 EdgeInsetsDirectional。 |
| assets/x.svg | SvgPicture.asset('assets/…/x.svg', width:, height:)(flutter_svg) |
| image fill FILL / FIT | Image.asset(fit: BoxFit.cover / BoxFit.contain) |
Web(CSS / Tailwind / React)映射表
| Figma | CSS |
|---|---|
| column / row | display:flex; flex-direction: column / row; gap: Gpx |
| wrap, rowGap R | flex-wrap: wrap; column-gap: G; row-gap: R |
| pad t,r,b,l | padding: t r b l(与 CSS 顺序相同) |
| main MIN / CENTER / MAX / SPACE_BETWEEN | justify-content: flex-start / center / flex-end / space-between |
| cross MIN / CENTER / MAX / BASELINE | align-items: flex-start / center / flex-end / baseline |
| HUG | width/height: fit-content(或直接 auto) |
| FILL(主轴) | flex: 1 1 0; min-width: 0 |
| FILL(交叉轴) | align-self: stretch |
| FIXED | width: Wpx; height: Hpx; flex-shrink: 0 |
@x,y / absolute | 父级 position: relative;子级 position:absolute; left:x; top:y(使用 pin 实现 right/bottom) |
colors #RRGGBBAA | 直接作为有效的 CSS 使用 |
gradient-linear Ndeg | linear-gradient(Ndeg, c1 p1%, c2 p2%) |
| drop-shadow | box-shadow: x y B S color(inner-shadow 添加 inset) |
| layer-blur / background-blur R | filter: blur(R/2 px) / backdrop-filter: blur(R/2 px) |
| text | font: weight size/lineHeight family; letter-spacing(ls % → pct/100 em) |
| ellipsis maxLines N | display:-webkit-box; -webkit-line-clamp:N; -webkit-box-orient:vertical; overflow:hidden |
| assets/x.svg | <img src> 或内联 SVG;保持导出的尺寸 |
spec.json 还包含 Figma 自身生成的 CSS(css),适用于大多数图层。用它来交叉检查 CSS 值;令牌和布局结构仍来自上述树结构。
规则
- Spec 中的值优先于你在图片中看到的。当它们冲突时,相信 spec 并提及冲突。
- 除非用户要求规范化到令牌,否则不要四舍五入或“改进”数值(14 保持 14,而不是 16)。
- 隐藏图层不会被导出。不要臆想导出中不存在的状态(悬停、错误、空状态);请要求用户导出那些画框。
- 标记缺失的字体。如果项目中未捆绑该字体家族,请说明并告知用户需要添加什么(pubspec fonts / @font-face)。
- 对于多个断点或状态,要求用户导出每个画框。每个画框都会成为自己的文件夹。