双向同步:rakullapp_core ↔ Figma
代码 → Figma(把现有设计与
liquid_glass_ui 设计系统同步进 Figma)· Figma → 代码(把 Figma 里的
改动落回 Flutter,复用 liquid_glass_ui 组件与设计 token)。.cursor/skills/figma-sync/ 编排,其中 SKILL.md 是操作手册,
mapping.md 是 Figma↔Dart 的权威映射(见下文)。所有具体的 use_figma /
get_design_context 调用都遵循已安装的 Figma 插件技能(figma-use、figma-generate-library、
figma-generate-design、figma-design-to-code)。
前置条件
本流程依赖 Figma MCP 服务(提供
use_figma、get_design_context、generate_figma_design、
search_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 串行不并行)。
- 变量:
Primitives(灰阶)、Color(Light/Dark 语义色,别名到 primitives)、Spacing、Radius—— 每个变量都要设置明确的 scope,并写入 WEB code syntax。 - 文字样式:
displaySmall..labelLarge字阶 + AppBar / NavBar 标签(字重/行高来自main.dart,字号取 Material 3 默认值)。 - 效果样式:solid 面板阴影与调校后的玻璃阴影(Light/Dark)。
- 组件:
GlassButton(primary/neutral/outline/glass 变体)、GlassField、GlassChip、SolidSurface/GlassCard、GlassAppBar等,视觉属性一律绑定到上面的变量。 - 页面:每个屏幕独占一个 Figma 页面(企业版页面无上限)。有断点的屏幕产出宽/窄两套画板(Dashboard 900、Reader 1024)。上传流程没有断点——在任意宽度下都是同一套单列布局,因此每个阶段都产出移动端(390)与全宽桌面(web,1440)两套画板。其余屏幕按其原生形态单套即可(移动端下钻流程、桌面控制台)。
generate_figma_design 抓取运行中 Web 应用的像素级参考图与真实图片,再与组件化搭建对齐:
若
generate_figma_design 无法访问 localhost(截图可能来自云端渲染),可用隧道暴露公网 URL,或退回到
“本地截图 + 人工对齐”。组件化 use_figma 搭建才是交付物,不要卡在抓图上。已同步页面
当前 Figma 文件(企业版)里,每个屏幕都是独立页面,另有Foundations、Components 两个设计系统页面:
| 页面 | 源文件 | 画板 |
|---|---|---|
| Begining | lib/app/begining_screen.dart | 单套 |
| Login | lib/user_manager/screens/login_screen.dart | 单套 |
| Dashboard | lib/app/dashboard_screen.dart | 宽 + 窄(断点 900) |
| Reader | lib/immersive_study/screens/reader_screen.dart | 宽 + 窄(断点 1024) |
| Collections | lib/collection/screens/collections_screen.dart | 列表 → 语法分组 → 例句(移动端下钻) |
| Article Upload | lib/immersive_study/screens/article_upload_screen.dart | 表单 → 处理中 → 完成——每个阶段含移动端(390)+ 桌面(web,1440) |
| Video Upload | lib/immersive_study/screens/video_upload_screen.dart | 表单 → 处理中(含 4 步指示器)——每个阶段含移动端(390)+ 桌面(web,1440) |
| Admin | lib/user_manager/screens/admin_screen.dart | 桌面控制台,5 个标签页(已建 Users / Articles) |
| Scenario Catalog | lib/scenario_training/screens/scenario_catalog_screen.dart | 桌面 + 移动端(tab 内嵌形态),另含无结果空态 |
| Scenario Detail | lib/scenario_training/screens/scenario_detail_screen.dart | 桌面(宽屏双栏)+ 移动端(堆叠) |
| Scenario Session | lib/scenario_training/screens/scenario_session_screen.dart | 桌面 + 移动端(断点 1000) |
| Scenario Report | lib/scenario_training/screens/scenario_report_screen.dart | 桌面 + 移动端(统计卡 4 列 / 2 列,断点 760) |
| Scenario Lesson | lib/scenario_training/screens/scenario_reference_lesson_screen.dart | 桌面 + 移动端 |
| Vocabulary | lib/adaptive_learning/screens/vocabulary_screen.dart | 桌面 + 移动端(tab 内嵌形态) |
| Vocabulary Entry | lib/adaptive_learning/screens/vocabulary_entry_screen.dart | 桌面(居中 720 栏)+ 移动端 |
| Vocabulary Review | lib/adaptive_learning/screens/vocabulary_review_screen.dart | 提问 → 揭示答案 → 本轮完成,各含桌面 + 移动端 |
| Learner Model | lib/adaptive_learning/screens/learner_model_screen.dart | 桌面 + 移动端 |
| Admin Learner Model | lib/adaptive_learning/screens/admin_learner_model_screen.dart | 桌面(root 1040)+ 移动端 |
| Article Edit | lib/immersive_study/widgets/article_edit_panel.dart | 桌面 + 移动端,另含”正文为空”错误态 |
| Weekly Admin | lib/immersive_study/screens/weekly_recommendations_admin_screen.dart | 桌面(root 1040)+ 移动端,另含空态 |
| Overview | lib/immersive_study/widgets/dashboard_overview.dart | 桌面 + 移动端(tab 内嵌形态),另含访客态 |
| Privacy Policy | lib/user_manager/screens/login_screen.dart(PrivacyPolicyScreen) | 移动端 + 桌面 |
覆盖已补齐:用户能走到的每个整屏都有独立页面。上表中 Overview、Scenario Catalog、Vocabulary
三页是 Dashboard 的 tab 页面体而非 push 路由,因此按 tab 内嵌形态绘制(手机是 large title +
底部五格导航,桌面是 wordmark +
SegmentedTabBar),不画返回箭头。只有当布局确实不同时才单独出画板(Vocabulary Review 的三态、Overview 的访客态、两个空态);
加载转圈、错误 + 重试、模态对话框不单独出图——它们是一个居中的控件,不是一套布局。跳转关系图
页面· Flows — Desktop & Mobile(节点 192:2)用低保真缩略图画出全部 27 个整屏 + 15 个关键弹层
之间的跳转。两个 Section 共用同一套节点栅格,因此同一份 edge list 渲染两遍:Desktop(≥900,
卡片 260×164)与 Mobile(<900,卡片 124×268),各 42 个节点、64 条边。
这张图要立的结论是:跳转拓扑与平台无关。没有任何一个目的地是单平台独占的,两条泳道的 64 条边
完全一致;桌面与移动的差别只有三处,而且都是呈现上的:
- Dashboard(900):5 个 tab 从顶栏
SegmentedTabBar换成底部NavigationBar,窄屏另外用 large title 标出当前 tab。 - Reader(1024):三栏可拖拽布局塌成单列句卡;假名开关与编辑按钮从面板头移进 AppBar;
取词结果宽屏是锚定在点击位置的
OverlayEntry,窄屏是showModalBottomSheet—— 这是唯一一个 形态(而不只是尺寸)由断点决定的弹层。 - Scenario Session(1000):对话与教师面板从并排变上下堆叠;回复候选 <640 走底部抽屉,否则是 560×560 对话框。
IndexedStack 交换,不是路由)/ modal / pushReplacement / 清栈
pushAndRemoveUntil / pop 六种。三条回溯太远的路径(Scenario Report 的 popUntil(isFirst)、
两个上传页的 pop(null))画成卡片上的返回徽标而不是连线。
Design 文件里没有
Connector 节点(figma.createConnector() 只在 FigJam 可用),所以连线是
VectorNode 折线 + 独立的三角形箭头,且一律从目标卡片顶部进入。完整的栅格参数、节点键位与
路由规则记在 mapping.md 的 Flows 段落里。方向二:Figma → 代码
对每个改动的画板/组件:- 对具体节点调用
get_design_context(URL 必须带node-id)。 - 返回的 React/Tailwind 仅作参考。用 Flutter 重新实现,按
mapping.md的组件→widget 表复用liquid_glass_ui,把 Figma 变量映射回GlassSpacing/GlassRadii/AppColors/文字样式——不要贴裸 hex/px。 - Figma 里新增的 token(新的灰阶、间距、圆角、字号角色)先加进 token 源(
app_colors.dart/radii.dart/spacing.dart/main.dart),再同步到mapping.md。调色板保持单色(R=G=B)。 - 校验(在
rakullapp_core内,Flutter SDK 见flutter-dev技能):
- 按
AGENTS.md走分支 → PR 流程;绝不改下游镜像仓库。
设备边界:灵动岛、圆角与底部横条
手机画板画的是整块屏幕,不是应用能自由使用的矩形。Figma 里有一个Device 变量集合,
四个模式覆盖在售 iPhone 的取值范围:
| 模式 | 机型 | 逻辑尺寸 | 顶部安全区 | 底部安全区 | 屏幕圆角 |
|---|---|---|---|---|---|
Small | iPhone 16e · 17e · 14 · 13(刘海) | 390 × 844 | 47 | 34 | 47.33 |
Regular | iPhone 15 · 15 Pro · 16 · 14 Pro | 393 × 852 | 59 | 34 | 55 |
Large(参考机型) | iPhone 16 Pro · 17 · 17 Pro | 402 × 874 | 62 | 34 | 62 |
Max | iPhone 16 Pro Max · 17 Pro Max | 440 × 956 | 62 | 34 | 62 |
Device / StatusBar 与 Device / HomeIndicator 两个 chrome 组件全部
绑定到变量;切换模式即可整屏重排,验收要求四个模式都成立。状态栏高 54 而安全区是 62——
多出来的是灵动岛下方的呼吸空间,排版一律对齐 62。圆角是连续曲线且半径接近页面内边距的
四倍,所以”左右各留 16”并不能避开圆弧:只有会被系统裁切的通栏填充可以伸进去。
代码侧的三处落点:
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,覆盖:
| 段落 | 内容 |
|---|---|
| File | fileKey、文件 URL、Figma 字体族(对应打包的 NotoSansJP / NotoSansSC / NotoSansTC) |
| Variables | Primitives / Color / Spacing / Radius 的取值与 var id |
| Text styles | 字阶(字号 = M3 默认,字重/行高来自 main.dart) |
| Effect styles | solid / glass 阴影 |
| Components → widgets | Figma 组件 ↔ liquid_glass_ui widget |
| Screens → nodes | 页面源文件 ↔ 宽/窄画板节点 |