双向同步:rakullapp_core ↔ Figma

代码 → Figma(把现有设计与 liquid_glass_ui 设计系统同步进 Figma)· Figma → 代码(把 Figma 里的 改动落回 Flutter,复用 liquid_glass_ui 组件与设计 token)。
整个流程由 Cursor 技能 .cursor/skills/figma-sync/ 编排,其中 SKILL.md 是操作手册, mapping.md 是 Figma↔Dart 的权威映射(见下文)。所有具体的 use_figma / get_design_context 调用都遵循已安装的 Figma 插件技能figma-usefigma-generate-libraryfigma-generate-designfigma-design-to-code)。

前置条件

本流程依赖 Figma MCP 服务(提供 use_figmaget_design_contextgenerate_figma_designsearch_design_system 等工具)。若未连接,请在 Cursor 设置 → Tools & MCP 中启用并登录 Figma 插件,然后重载窗口。

为什么用映射台账,而不是 Code Connect

Figma 官方的 Code Connect 只支持 React/TS、Compose/Kotlin、SwiftUI/Swift——不支持 Dart/Flutter。 因此我们无法用 .figma.ts / @FigmaConnect 把 Figma 组件绑定到 liquid_glass_ui 组件。 取而代之的是 .cursor/skills/figma-sync/mapping.md:记录 fileKey、页面结构、变量→token、 文字样式→字阶、组件→widget、页面→节点。两个方向开工前先读它,每次同步后更新它。

方向一:代码 → Figma

先建基础,再拼页面(增量执行,每步用 get_screenshot / get_metadata 校验,use_figma 串行不并行)。
  1. 变量Primitives(灰阶)、Color(Light/Dark 语义色,别名到 primitives)、SpacingRadius—— 每个变量都要设置明确的 scope,并写入 WEB code syntax。
  2. 文字样式displaySmall..labelLarge 字阶 + AppBar / NavBar 标签(字重/行高来自 main.dart,字号取 Material 3 默认值)。
  3. 效果样式:solid 面板阴影与调校后的玻璃阴影(Light/Dark)。
  4. 组件GlassButton(primary/neutral/outline/glass 变体)、GlassFieldGlassChipSolidSurface/GlassCardGlassAppBar 等,视觉属性一律绑定到上面的变量。
  5. 页面每个屏幕独占一个 Figma 页面(企业版页面无上限)。有断点的屏幕产出宽/窄两套画板(Dashboard 900、Reader 1024)。上传流程没有断点——在任意宽度下都是同一套单列布局,因此每个阶段都产出移动端(390)与全宽桌面(web,1440)两套画板。其余屏幕按其原生形态单套即可(移动端下钻流程、桌面控制台)。
因为 Web 是首要平台,可并行用 generate_figma_design 抓取运行中 Web 应用的像素级参考图与真实图片,再与组件化搭建对齐:
bash scripts/run_all_servers.sh   # 后端 8010-8015(模型在 AI APIs → Models 配置)
bash scripts/run_client_web.sh    # Flutter web-server 设备,http://localhost:8080
generate_figma_design 无法访问 localhost(截图可能来自云端渲染),可用隧道暴露公网 URL,或退回到 “本地截图 + 人工对齐”。组件化 use_figma 搭建才是交付物,不要卡在抓图上。

已同步页面

当前 Figma 文件(企业版)里,每个屏幕都是独立页面,另有 FoundationsComponents 两个设计系统页面:
页面源文件画板
Begininglib/app/begining_screen.dart单套
Loginlib/user_manager/screens/login_screen.dart单套
Dashboardlib/app/dashboard_screen.dart宽 + 窄(断点 900)
Readerlib/immersive_study/screens/reader_screen.dart宽 + 窄(断点 1024)
Collectionslib/collection/screens/collections_screen.dart列表 → 语法分组 → 例句(移动端下钻)
Article Uploadlib/immersive_study/screens/article_upload_screen.dart表单 → 处理中 → 完成——每个阶段含移动端(390)+ 桌面(web,1440)
Video Uploadlib/immersive_study/screens/video_upload_screen.dart表单 → 处理中(含 4 步指示器)——每个阶段含移动端(390)+ 桌面(web,1440)
Adminlib/user_manager/screens/admin_screen.dart桌面控制台,5 个标签页(已建 Users / Articles)
Scenario Cataloglib/scenario_training/screens/scenario_catalog_screen.dart桌面 + 移动端(tab 内嵌形态),另含无结果空态
Scenario Detaillib/scenario_training/screens/scenario_detail_screen.dart桌面(宽屏双栏)+ 移动端(堆叠)
Scenario Sessionlib/scenario_training/screens/scenario_session_screen.dart桌面 + 移动端(断点 1000)
Scenario Reportlib/scenario_training/screens/scenario_report_screen.dart桌面 + 移动端(统计卡 4 列 / 2 列,断点 760)
Scenario Lessonlib/scenario_training/screens/scenario_reference_lesson_screen.dart桌面 + 移动端
Vocabularylib/adaptive_learning/screens/vocabulary_screen.dart桌面 + 移动端(tab 内嵌形态)
Vocabulary Entrylib/adaptive_learning/screens/vocabulary_entry_screen.dart桌面(居中 720 栏)+ 移动端
Vocabulary Reviewlib/adaptive_learning/screens/vocabulary_review_screen.dart提问 → 揭示答案 → 本轮完成,各含桌面 + 移动端
Learner Modellib/adaptive_learning/screens/learner_model_screen.dart桌面 + 移动端
Admin Learner Modellib/adaptive_learning/screens/admin_learner_model_screen.dart桌面(root 1040)+ 移动端
Article Editlib/immersive_study/widgets/article_edit_panel.dart桌面 + 移动端,另含”正文为空”错误态
Weekly Adminlib/immersive_study/screens/weekly_recommendations_admin_screen.dart桌面(root 1040)+ 移动端,另含空态
Overviewlib/immersive_study/widgets/dashboard_overview.dart桌面 + 移动端(tab 内嵌形态),另含访客态
Privacy Policylib/user_manager/screens/login_screen.dartPrivacyPolicyScreen移动端 + 桌面
覆盖已补齐:用户能走到的每个整屏都有独立页面。上表中 Overview、Scenario Catalog、Vocabulary 三页是 Dashboard 的 tab 页面体而非 push 路由,因此按 tab 内嵌形态绘制(手机是 large title + 底部五格导航,桌面是 wordmark + SegmentedTabBar),不画返回箭头只有当布局确实不同时才单独出画板(Vocabulary Review 的三态、Overview 的访客态、两个空态); 加载转圈、错误 + 重试、模态对话框不单独出图——它们是一个居中的控件,不是一套布局。
搭建过程暴露了两个真问题,应当从源头修,而不是每个页面各自绕开:
  1. color/field-fill 在 Light 模式下是坏的——它与 color/surface 都别名到 VariableID:3:3,也就是 color/white,所以卡片上的输入框不是”对比度低”,而是与卡片同色。 新页面目前统一用 color/fill-secondary 替代。
  2. setBoundVariableForPaint 不会同步 paint 的字面色值——用黑色占位创建的 paint 即使绑定 正确也仍然渲染成黑色(曾整屏变黑)。绑定前必须顺着别名链解析出真实 RGBA(含 alpha, 它同样会被丢弃)写进 paint。

跳转关系图

页面 · Flows — Desktop & Mobile(节点 192:2)用低保真缩略图画出全部 27 个整屏 + 15 个关键弹层 之间的跳转。两个 Section 共用同一套节点栅格,因此同一份 edge list 渲染两遍:Desktop(≥900, 卡片 260×164)与 Mobile(<900,卡片 124×268),各 42 个节点、64 条边。 这张图要立的结论是:跳转拓扑与平台无关。没有任何一个目的地是单平台独占的,两条泳道的 64 条边 完全一致;桌面与移动的差别只有三处,而且都是呈现上的:
  1. Dashboard(900):5 个 tab 从顶栏 SegmentedTabBar 换成底部 NavigationBar,窄屏另外用 large title 标出当前 tab。
  2. Reader(1024):三栏可拖拽布局塌成单列句卡;假名开关与编辑按钮从面板头移进 AppBar; 取词结果宽屏是锚定在点击位置的 OverlayEntry,窄屏是 showModalBottomSheet —— 这是唯一一个 形态(而不只是尺寸)由断点决定的弹层。
  3. Scenario Session(1000):对话与教师面板从并排变上下堆叠;回复候选 <640 走底部抽屉,否则是 560×560 对话框。
其余全是重排。把这一点显式记下来是有价值的:默认”移动端要另开一套路由”很省事,而这张图是它不需要的证据。 连线区分 push / tab 切换(IndexedStack 交换,不是路由)/ modal / pushReplacement / 清栈 pushAndRemoveUntil / pop 六种。三条回溯太远的路径(Scenario Report 的 popUntil(isFirst)、 两个上传页的 pop(null))画成卡片上的返回徽标而不是连线。
Design 文件里没有 Connector 节点(figma.createConnector() 只在 FigJam 可用),所以连线是 VectorNode 折线 + 独立的三角形箭头,且一律从目标卡片顶部进入。完整的栅格参数、节点键位与 路由规则记在 mapping.md 的 Flows 段落里。

方向二:Figma → 代码

对每个改动的画板/组件:
  1. 对具体节点调用 get_design_context(URL 必须带 node-id)。
  2. 返回的 React/Tailwind 仅作参考。用 Flutter 重新实现,按 mapping.md 的组件→widget 表复用 liquid_glass_ui,把 Figma 变量映射回 GlassSpacing/GlassRadii/AppColors/文字样式——不要贴裸 hex/px。
  3. Figma 里新增的 token(新的灰阶、间距、圆角、字号角色)先加进 token 源(app_colors.dart / radii.dart / spacing.dart / main.dart),再同步到 mapping.md。调色板保持单色(R=G=B)。
  4. 校验(在 rakullapp_core 内,Flutter SDK 见 flutter-dev 技能):
export PATH="$HOME/code/pkgs/flutter/flutter/bin:$PATH"
flutter analyze && flutter test
  1. AGENTS.md 走分支 → PR 流程;绝不改下游镜像仓库。

设备边界:灵动岛、圆角与底部横条

手机画板画的是整块屏幕,不是应用能自由使用的矩形。Figma 里有一个 Device 变量集合, 四个模式覆盖在售 iPhone 的取值范围:
模式机型逻辑尺寸顶部安全区底部安全区屏幕圆角
SmalliPhone 16e · 17e · 14 · 13(刘海)390 × 844473447.33
RegulariPhone 15 · 15 Pro · 16 · 14 Pro393 × 852593455
Large(参考机型)iPhone 16 Pro · 17 · 17 Pro402 × 874623462
MaxiPhone 16 Pro Max · 17 Pro Max440 × 956623462
画板的宽高、四个圆角、Device / StatusBarDevice / HomeIndicator 两个 chrome 组件全部 绑定到变量;切换模式即可整屏重排,验收要求四个模式都成立。状态栏高 54 而安全区是 62—— 多出来的是灵动岛下方的呼吸空间,排版一律对齐 62。圆角是连续曲线且半径接近页面内边距的 四倍,所以”左右各留 16”并不能避开圆弧:只有会被系统裁切的通栏填充可以伸进去。
这些数字不允许进入 Dart。 四个模式存在于 Figma 是为了让设计能看到取值范围;代码侧同样的 范围由 MediaQuery 在运行时读出。客户端里写死一个 6234,就是这套机制要消灭的 bug 本身。
代码侧的三处落点:
  • GlassAppBar 根据它实际绘制的表面色推导 systemOverlayStyle。它的 backgroundColor 是透明的 (真正的表面画在 flexibleSpace 里),而 AppBar 默认用 estimateBrightnessForColor 猜, computeLuminance 又忽略 alpha——透明会被当成纯黑,于是浅色模式下时钟/信号/电量是白底白字。
  • EdgeInsets.addBottomInset(context)lib/shared/utils/safe_area.dart)把底部安全区补回内容 内边距。没有显式 padding: 的滚动视图本来就会自己吃掉 MediaQuery.padding;一旦传了显式 padding: 就退出了这个机制,最后一行便停在横条下面。补在内容内边距而不是视口上,内容仍能滚到 横条下方——这才是平台行为,所以整屏套一个 SafeArea 是错的。
  • main() 开启 SystemUiMode.edgeToEdge。Android 15+ 本来就强制如此,而且只有开了它,上面两处 修复才看得见。
test/shared/widget/device_safe_area_test.dart 按四个模式参数化跑安全区断言,是客户端里唯一 允许出现这些数字的文件。

约束

  • 单色:无任何品牌色/亮色,只有灰阶 + 黑白(要求 7)。
  • token 优先:两个方向都优先用 token,不写魔法值。
  • 一屏一页:每个屏幕独占一个 Figma 页面;有断点的屏幕(Dashboard/Reader)做宽/窄两套,上传流程做移动端 + 全宽桌面(web)两套,其余按原生形态单套。
  • 更新台账:每次 Figma 搭建与每次落回代码后更新 mapping.md(它是 Code Connect 的替代)。
  • 保持文档同步:本页与其 i18n/en/ 英文镜像一起更新。

Figma↔Dart 映射台账

台账位于 .cursor/skills/figma-sync/mapping.md,覆盖:
段落内容
FilefileKey、文件 URL、Figma 字体族(对应打包的 NotoSansJP / NotoSansSC / NotoSansTC
VariablesPrimitives / Color / Spacing / Radius 的取值与 var id
Text styles字阶(字号 = M3 默认,字重/行高来自 main.dart
Effect stylessolid / glass 阴影
Components → widgetsFigma 组件 ↔ liquid_glass_ui widget
Screens → nodes页面源文件 ↔ 宽/窄画板节点