# Product Mockup Conventions（Path A — Figma）

> 任何 AI 工具在 TVU 产品 Figma 文件里画 UX mockup 时**必须遵循的硬约束**。
> 项目级真源；不要在对话或工具私有 memory 里复刻规则。
>
> **作用域**：在 TVU 产品 Figma 文件（如 `Micro-Apps-20250923` 等）画产品页面 mockup。**不**作用于设计系统库本身的迭代。

---

## 🤖 AI 读取指引（按需加载，避免全量吞）

本文件 ~1000 行，**不要起手就全量 Read**。按下表只读起手必读段，具体 M-rule jump to 时再读：

| 起手必读（任务前先吃这些） | 行段 |
|---|---|
| 本文件顶部 intro + §作用域 + §必读链路 + §真源 / 优先级 | §0–§2 (1–51) |
| §Convention Priority Hierarchy + §Task Entry Modes + §Migrated Rules Pointer Index | §3–§5 (52–82) |
| §M0 Phase 0 Element-to-Component Mapping（**所有任务前置**） | §M0 (85–126) |

| 触发后再读（按场景 jump，不预读） | 触发条件 |
|---|---|
| §M1 Top bar 公共基建 | 涉及产品顶栏时 |
| §M-COLOR (§C1/§C2/§C3) | 要填颜色 / icon fill / state color 时 |
| §M-INTEGRITY (§I1/§I2/§I3) | mockup 完成自检 / sibling 布局 / section 重叠 / children wrap 时 |
| §M23 + §M23.6 + §M33 | 要画 UX 交付注释 / 流程图 / annotation 字符串时 |
| §M24 | "已有 code 还原效果图"场景前置 |
| §M29 | 涉及表格 cell（copyable / empty placeholder）时 |
| §M31 | 决定 Auto Layout / Slot / Boolean / Absolute 时 |
| §M32 (含 M30 icon 扩展) | 要 instance 任何 product UI 元素前 |
| §M35 affordance 搜索 | 要找 chevron / sort / close 等"基本语义元素"前 |
| §legacy stub M27 / M28 / M30 / M34 | 仅为外部链接兼容；优先读 umbrella |

**红线**：跳过"起手必读段"直接进 M-rule = 协议违反（漏 M0 Phase 0 是历史最高频回归源）。

---

## 必读链路（起手按顺序读）

| 文件 | 内容 | 必读时机 |
|---|---|---|
| [`design-process.md`](./design-process.md) | 通用 process 规则（M22 / Pre-Phase 0 / Phase 0 / M11 / M14 / M15 / M16 / M21 / M6）| **所有任务** |
| [`domain-tvu.md`](./domain-tvu.md) | TVU 业务规则（M3 / M4 / M5 / M7 / M8 / M9）| **所有任务** |
| [`figma-technical-reference.md`](./figma-technical-reference.md) | Figma API quirks（Q1-Q4）| Path A 实现时 |
| [`tools/figma-quirks.md`](./tools/figma-quirks.md) | Figma quirks 快查表 | Path A 实现时 |
| 本文件 | Path A 专属：Figma 真源 / 组件优先级 / M0 / M1 / M10 / M23 | **所有 Path A 任务** |

---

## 真源 / 优先级（先读完这段再动手）

### Figma 库源文件

| 维度 | 值 |
|---|---|
| TVU 设计系统库源文件 fileKey | `YbsPRUVmNdsbN40NNwh1Gn` |
| Published library 名 | `TVU UX Design System` |
| Library key | `lk-057f6ba0f771bfa7f63a6a197502999462c2974f995488b381708ca5faadb7f9f6675e04aa442b3a5ffb31732150a7f5aa8ca3102073ab210edc684022fc6c21` |
| 源文件登记位置 | [`docs/site-review-manifest.json`](../site-review-manifest.json) `figmaFileKey` 字段 |
| 组件名 → code 映射 | [`figma-data/figma-to-code-mapping.json`](../../figma-data/figma-to-code-mapping.json) |

**任何 mockup 任务起步必须先 `cat docs/site-review-manifest.json | jq .figmaFileKey` 拿到真源 key，再做 Figma 库探索**——不要靠扫产品文件 instance 反推库归属。

### 组件取数优先级（默认顺序）

**0. 第 0 步必查：[Figma Component Catalog](./figma-component-catalog.md)**
任何 mockup 任务起手第一件事是 grep 这个 catalog。它已 bootstrap 了库的全部 49 个 component_set + 647 icon 的骨架，**已实证过的组件还附带 Primary use / Extension scenarios / Don't use**。70% 的"library 缺件/找不到"判断在这里就有答案。

1. **catalog 没命中 → published `TVU UX Design System` library**（用 library key 过滤搜索 + import sample 看 variants，结果回填 catalog）
2. **库里也没有 → file-local 组件集**（产品文件内的 component_set，少数特殊场景用，如 `APP Icons`）
3. **最后：自画 + 标 🟡 入库候选**（必须前两条都验证后无果，且把候选写回 catalog 让下次免坑）

**默认假设：library 已覆盖大多数通用组件**（Button / Input / Badge / Tooltip / Top bar / Notification / Switch / Tab / Drop down List / Form Item / Pagination / Slider 等——具体清单见 catalog）。除非实证证明无对应物，否则不允许跳到第 2 / 第 3 条。

### 库归属验证机制

- ❌ 错误做法：扫某个既有 page 的 instance 来选 key——产品文件里可能同时存在多代库（旧 `TVU UX Library` + 新 `TVU UX Design System`），盲选会撞旧库。
- ✅ 正确做法：`search_design_system` 时通过 `includeLibraryKeys: ["lk-057f6ba0..."]` 显式过滤；返回结果中检查 `libraryName: "TVU UX Design System"`。

---

## Convention Priority Hierarchy

→ [`design-process.md §Convention Priority Hierarchy`](./design-process.md#convention-priority-hierarchy)（完整优先级表 P0-P3 + 冲突解决规则）

---

## Task Entry Modes

AI 接到任务后**先 classify** 再决定走哪条路径：

| ID | 用户表达 | 流程 |
|---|---|---|
| **US-1** Greenfield 产品 | "基于 TVU 设计一个 X 产品 mockup，功能大概 ABC..." | Pre-Phase 0 → M0 → 画 frame |
| **US-2** Greenfield 单 frame | "基于 TVU 给 X 产品画 dashboard frame" | Pre-Phase 0（单 frame 版）→ M0 → 画 |
| **US-3** Existing product 增/改/删 | "MicroApps Console mockup 加/改/删任意 frame 或元素" | Read handoff → Pre-Phase 0（仅变化部分）→ M0 → 画（仅 update 变化部分）|
| **US-4** Figma frame → code mirror | "把这个 frame 翻成代码" | **不归本 conventions**（走 [`code-conventions.md`](./code-conventions.md) US-4 路径）|
| **US-5** 小调整（1-2 element）| "改下按钮位置" | 跳过 Pre-Phase 0 + M0 → 直接改 |
| **US-6** Review / Audit | "帮我审 X frame 是否符合 TVU 规范" | 跳过 Pre-Phase 0 + M0 → 对照规则逐条审 → 产 audit report |

Classification 不确定时 → AI **主动问用户**哪种入口。

---

## Migrated Rules — Pointer Index

通用 process rules → [`design-process.md`](./design-process.md)：M22 / Pre-Phase 0 / Stage 0.5 / M21 / Library-First + Evidence Discipline（M2）/ M6 / M11 / M14 / M15 / M16 / Lazy Reference Loading

TVU 业务规则 → [`domain-tvu.md`](./domain-tvu.md)：M3 / M4 / M5 / M7 / M8 / M9

---

## Hard Rules（Path A 专属）

### M0 — Phase 0：Element-to-Component Mapping（前置硬规则）

多 frame 或 **≥ 3 个 element** 的 mockup 任务**必须**在画之前先产出 mapping table。trivial 单元素调整可省。

**Scope (2026-05-14 clarification)**：以下都算"多 frame / ≥3 element"，**不算 trivial，必须做 M0 mapping**：
- 主交付 frame（如 1920×1080 完整页面）
- **Variant state mini-frames**（Empty / Loading / Sort-active / dropdown / popover / state group 内嵌 mini）
- Annotation 内嵌的 UI（state group `_flow-state-N` 的 UI frame、condition label group 等）
- 任何包含 ≥3 个 iconographic / chip / badge / button 元素的次级 frame

**违例 history**: 2026-05-14 Plan B Row 4 6 个 variant minis（Empty B/C/D + Loading + Sort-active + App Picker）当成"次要快速摆"完全跳 M0 mapping → 4 个 M2 违例 (App icons placeholder rect / Sidebar Badge 手画 / Sort ↑ TEXT / chevron Unicode)。Mini-frames 不是 trivial。

#### Lookup 序列（按序）

1. **grep [`figma-component-catalog.md`](./figma-component-catalog.md)** — 第一优先；每个 element 至少 2-3 个同义词 / casing 变体试（M15 防"一次 negative 即定论"）
2. **catalog 命中 ✅ verified** → 直接采用 entry 的 Primary use / Extension
3. **catalog 命中 🟡 skeleton** → 标记需 import sample instance 验 variants
4. **catalog 全 miss** → `search_design_system` + import sample，仍**先把全部 element 的 mapping 拉完再开始画**

#### 输出格式（mandatory table）

| Element | Figma component_set (key) | Variants | Status | catalog 索引 |
|---|---|---|---|---|
| top bar | `Top bar` (`918b928e...`) | Tag=AfterLogin/BeforeLogin | ✅ verified | catalog §Verified |
| status badge | `Badge` (`4db5246d...`) | Type/Color/Tag | ✅ verified | catalog §Verified |
| confirmation modal | `prompt message` (待查) | pop-confirm variant | 🟡 skeleton → 需 import 验 | catalog §Skeleton |
| APP icon | `APP Icons` (file-local, `fefd6aac...`) | Tag × Color | ⚠️ file-local | §已知 file-local |

#### 处理流程

1. AI 产出整张 table
2. 发给用户校验（一次性决策 gap 处理 / 验证 🟡 是否进 import 流程）
3. 用户确认后才开始 import + 画 frame
4. 画的过程中若 🟡 skeleton 实证后升级为 ✅ → **回填 catalog**（不是 nice-to-have，是责任）
5. 新发现 element 没在 table 里 → **回到 Phase 0** 补一行，**禁止**现场判断"library 缺件"

#### Why

M2 / M15 反复踩坑根因是"边画边判断 library 缺件"。Phase 0 把所有判断前置到一张 table，gap 一次暴露，决策一次到位。

---

### M1 — TVU Top bar 是公共基建，不允许自画

任何 TVU 产品页面的 top bar **必须直接 instance** Figma library `Top bar` 组件（来自 `TVU UX Design System` library）。

- Library: `TVU UX Design System` (key `lk-057f6ba0f771bfa7f63a6a197502999462c2974f995488b381708ca5faadb7f9f6675e04aa442b3a5ffb31732150a7f5aa8ca3102073ab210edc684022fc6c21`)
- Component set: `Top bar` (key `918b928e4541f1573de5c98c32b408af663d7f2f`)
- Variants: `Tag=After Login`（默认, 1920×56）/ `Tag=Before Login`（1920×64）
- 关键 properties：`Show Menu` / `Show Search box`（BOOLEAN）/ `Menu` `Right_content`（SLOT）

⚠️ 文件里还有同名旧版 `Top bar`（来自 `TVU UX Library`，key `2f36c51ae9fcdfd450f3151c654c9931c9069d96`），**不要用**。验证库归属：`search_design_system` 调用时通过 `includeLibraryKeys: ["lk-057f6ba0..."]` 过滤；不要用 `findAll(INSTANCE)` 扫描既有页面（那些页面可能用旧库建的）。

- **不允许**自画 top bar
- **不允许**修改 top bar 结构（菜单数量 / 时区 / Tokens / Network Delay 等只能开关显示）
- **不允许**把 page-level 元素塞进 top bar（计数器 / 过滤项 / 批量操作等放 top bar 下方）

⚠️ **产品标题不属于 page-level 元素**——填进 top bar 自带的 product-name slot（默认 `Product Name`，主动覆写为实际产品名）。不要在 top bar 下方再加只写 title 的 page-header strip——冗余。

**Why:** TVU 全产品视觉一致性靠 top bar 统一。AI 自画会破坏跨产品体验。

---

### M-COLOR — Color Token Discipline（umbrella，覆盖原 M10 / M25 / M26）

凡是给 mockup 元素填色（fill / stroke / text color）时，必须按 token 规范取用。三个 sub-clauses 同源——都是"违反 token 系统语义"——所以合并为一组以便 audit 一次跑完。

**Legacy ID 映射**：M10 → §C1（icon fill），M25 → §C2（state color），M26 → §C3（bg vs text grey）。

**自动化 audit**：`scripts/audit-mockup-colors.mjs`（一次扫完 3 个 sub-clauses）。

---

#### C1 — Icon path fill 必须绑 Color Variable，不写 hex literal（原 M10）

任何 AI 工具在 figma 文件画 / 编辑 icon 时**必须**：

| 图标类型 | 正确做法 | 错误做法 |
|---|---|---|
| **单色非品牌图标**（默认 UI icon）| `<path fill={Color Variable: "Color Type/Icon/Default"} />` | `<path fill="#dbdbdb" />` literal |
| **单色品牌图标** | `<path fill={Color Variable}>` 绑 `--brand` / `--red` / `--blue` 等 | hex literal |
| **第三方 logo / app icon** | hex literal 可接受（封装在 catalog SVG 内部）| 加到 design system token 里 |
| **状态色多色图标** | 各 path 分别绑语义 Color Variable | 全 path 一律 hex |

**Why**：figma SVG export API 把 Color Variable 引用解析为 literal hex（pipeline 限制）。Variable 绑定语义在 figma 内部保留，未来 token 升级 / theme 切换自动同步；hex literal 不会同步。

**Code 端 fallback**：`figma-sync/export-icons.mjs` 的 `transformSvgCurrentColor` 把 `#dbdbdb` 转 `currentColor`；消费组件容器设 `color: var(--icon-default)` 实现 cascade。详见 [`translation/divergences.md`](../../src/design-system/translation/divergences.md) §"Figma SVG export pipeline limitation"。

**落实方法**：
1. 编辑既有 figma library icon 时，path fill 改用 Variable 绑定（不用 hex）
2. 画新 icon 时直接用 Variable 绑定
3. import / paste icon 后**自检**：path fill 是否 hex？是 → fix 绑 Variable
4. AI 生成 figma 效果图时，icon 实例继承 library 设置即可——**不要**在 instance 上 override 成 literal hex
5. **Icon 实例化后 fill 绑定自检**：任何 `createInstance(iconComp)` 调用后，立即 probe `inst.fills[0].boundVariables?.color`——若为空或 raw SOLID fill → Q2 二步法绑对应 `Color Type/Icon/*` token。批量创建场景每个实例均需检查，不因"library 默认已绑"而跳过。

---

#### C2 — State Color Discipline (hover ≠ brand)（原 M25）

TVU library 为状态色提供专门 token 套件，**不可省略**：

| State | Brand | Red | Blue | Orange |
|---|---|---|---|---|
| Default / Active | `UX/Brand/Brand` | `UX/Red/Default` | `UX/Blue/Default` | `UX/Orange/Default` |
| **Hover** | `UX/Brand/Hover` | `UX/Red/Hover 1` | `UX/Blue/Hover 1` | `UX/Orange/Hover 1` |
| Disabled | `UX/Brand/Disable` | — | — | — |
| Icon-specific active/hover | `Color Type/Icon/Active & Hover` | （同左） | （同左） | （同左） |
| Grey button hover | `Color Type/Background/Hover Grey Button` + `Color Type/Text/Hover Grey button` | — | — | — |

**规则**：
- **Active state** = primary token（pure brand）— 表达"现在选中的"身份
- **Hover state** = `*/Hover` variant — 比 active 略浅；提示"可点击"
- **Disabled** = `*/Disable` — 灰化但保留品牌指代
- 当 interactive element 是 **brand-colored** 时，hover bg **必须**用 brand-hover，不可用 Layer_3 生灰（后者是"中性 hover"，只对非品牌色 element 用）

**Why**：用 `Brand/Brand` 给 hover state 会让 hover 视觉等同 active，破坏 "click affordance vs selected state" 的区分。Layer_3 生灰则丢失品牌色暗示——用户看不到这是个 brand action。

---

#### C3 — BG vs Text Grey Discipline（原 M26）

TVU library 的灰色 token 分两套：

| 用途 | 命名空间 | 示例 |
|---|---|---|
| **Background fills**（FRAME_FILL / SHAPE_FILL）| `Color Type/Background/Layer_{1,2,3,4}` + `Top Bar` | Layer_1 canvas / Layer_2 card / Layer_3 chip / Layer_4 inactive bar |
| **Text / Border foreground**（TEXT_FILL / STROKE）| `UX/Grey/grey-{2..9}` + `Color Type/Text/*` + `Color Type/Line/*` | grey-7 tip text / grey-9 disabled text / Line/Deep Divider |

**禁用规则**：
- ❌ 把 `UX/Grey/grey-N` 用作 frame/shape fill（grey-7/8/9 是 text color，scope 没必要给 FRAME_FILL）
- ❌ 把 `Color Type/Text/*` 用作 background fill
- ❌ 把 `Color Type/Background/Layer_N` 用作文字色

**正例 vs 反例**：

| 元素 | ✅ 正确 | ❌ 反例 |
|---|---|---|
| Status bar inactive segment | `Color Type/Background/Layer_4` | `UX/Grey/grey-7` |
| Card bg | `Color Type/Background/Layer_2` (or hex if not published) | `UX/Grey/grey-9` |
| Tips text | `Color Type/Text/Tips` | `UX/Grey/grey-7` |
| Sidebar hover bg (brand element) | `UX/Brand/Hover @ low alpha` | `Color Type/Background/Layer_3` |

**Why**：Token scope 是设计意图的硬约束。`UX/Grey/grey-7` 的 scope 是 ALL_SCOPES（向后兼容），但它的 hex `#7B7B7B` 视觉上是中等灰，作为 BG 太亮、作为 text 比 tips 暗——错位使用会让浅色文字消失或深色 BG 过亮。

**Why（深层）**：Layer 系列灰是按视觉层级递增（layer_1 最暗 / layer_4 最亮 elevated），符合 BG 堆叠语义。Grey-N 系列是文本/边框灰度阶梯。混用是把"颜色"和"语义"切了。

---

#### M-COLOR 共用 Acceptance

- 一次 probe 跑全：`pnpm audit:mockup:colors -- --file <fileKey> --node <nodeId>`（脚本扫 §C1 / §C2 / §C3 三条）
- 任一 sub-clause 违例 → handoff doc 必含 fix log（"colors fixed: [§C1 N nodes, §C2 N nodes, §C3 N nodes]"）
- Legacy ID 引用兼容：M10 / M25 / M26 anchor 在文末"Legacy ID Map"段维护重定向

---

### M23 — UX 交付注释：多状态交互流程图格式

当一个 feature 存在多个明显不同的交互状态（如 Default / Loading / Applied），**必须**用标准化的多状态流程图格式来交付 UX 说明，而不是单一静态 frame。

#### 何时触发

- 功能有 ≥2 个用户可感知的交互状态
- 存在状态转换触发条件（用户操作 / 系统响应）
- 开发需要明确的状态边界和数据变化说明

#### 结构层级（5 层）

| 层 | 节点类型 | 命名规范 | 说明 |
|---|---|---|---|
| 1. Section header | FRAME | `"UX · [Feature] — Interaction States"` | 横跨所有 state frame，full-width |
| 2. State group | GROUP | `"_flow-state-N"` | 每个状态一个 GROUP，包含 label chip + UI frame |
| 3. Label chip | FRAME（GROUP 子节点）| 无固定命名 | 在 UI frame 正上方 8px，显示状态名 + 说明 |
| 4. 流程箭头 + 条件标签 | VECTOR + GROUP | 箭头: `"Vector"`；条件: `"_"` | 页面级节点，不放进 state group |
| 5. 规则卡 | FRAME | `"UX · [Feature] — Rules"` | 放在最后一个 state group 右侧 |

#### 主题无关配色系统（硬规则）

所有注释元素必须使用**显式填充容器**——禁止文字直接浮在 canvas 上。

| 元素 | Fill | Text color | 原则 |
|---|---|---|---|
| Header chip / Label chip / 规则卡背景 | `#2B2D42`（深海军蓝）| `#FFFFFF` | 深底略亮可读；浅底深色 chip 突出为 annotation overlay |
| 状态标签标题行 `[State Name]:` | — | `#33A4FD`（蓝色） | 与箭头颜色一致；深/浅背景对比度均达标 |
| 状态说明文字（次要信息）| — | `#A6ADB8`（浅灰） | 降噪，不抢夺标题视觉权重 |
| 流程箭头 stroke | — | `#33A4FD` | 2px stroke，手动绘制箭头（`strokeEndCap` 在 VECTOR 不支持）|
| 条件标签文字 | — | `#33A4FD` | 背景 `#2B2D42`，放在箭头正下方 |
| 规则卡左边框 | 3px `#33A4FD` left stroke | — | `strokeLeftWeight=3`，其余三边 = 0，`strokeAlign='INSIDE'` |

**Why**：依赖 canvas 背景色（白字 = 假设深底 / 黑字 = 假设浅底）会在主题切换时翻车。`#2B2D42` container 在深/浅 canvas 上均有明确边界，是主题无关的最小可读单元。

#### Figma canvas 层级硬规则

**条件标签必须用 GROUP，不能用 FRAME。**

FRAME 节点在 Figma canvas 上显示名称标签，会干扰流程图阅读。条件标签（arrow 下方小 chip）必须用 GROUP 实现：

```js
// ✅ 正确：RECTANGLE（背景）+ TEXT，打 GROUP
const rect = figma.createRectangle();  // navy fill, cornerRadius=4
const t = figma.createText();          // blue text
const grp = figma.group([rect, t], page);
grp.name = '_';  // 最短名，canvas 上不显眼

// ⚠️ 关键：RECTANGLE 必须在 index 0（下层），TEXT 在 index 1（上层）
// figma.group([rect, t], page) 中最后一项渲染在最上方
// 若顺序错误，rect 会遮住 text → 文字消失
grp.insertChild(0, rect);  // 确认 rect 在底部
```

**State group 同理**：每个「label chip + UI frame」对打一个 GROUP（`_flow-state-N`）。GROUP 无 canvas 名称标签，8px gap 可以紧贴，不会与 Figma UI 的 frame 名称标签冲突。

→ Figma FRAME vs GROUP canvas 标签行为详见 [`figma-technical-reference.md Q5`](./figma-technical-reference.md#q5--frame-vs-group：canvas-名称标签行为差异)

#### 箭头画法（VECTOR 手动箭头）

`strokeEndCap` 在 VECTOR 节点上**不支持**，箭头必须在 `vectorPaths.data` 里手动画：

```js
const len = x2 - x1, AH = 8;
vec.vectorPaths = [{ windingRule:'EVENODD',
  data: `M 0 ${AH} L ${len-AH} ${AH} M ${len-AH*2} 0 L ${len} ${AH} L ${len-AH*2} ${AH*2}` }];
// vec.x = x1（绝对坐标）；vectorPaths data 是相对于 vec.x/y 的局部坐标
```

→ VECTOR `strokeEndCap` 限制及完整说明详见 [`figma-technical-reference.md Q6`](./figma-technical-reference.md#q6--vector-节点不支持-strokeendcap，箭头必须手动画)

#### Reconnect map（快速重连）

每次画完流程图，**必须**把 arrow 位置信息存入 sharedPluginData，下次"重新连线"时直接读取，无需重探坐标：

```js
page.setSharedPluginData('ux_annotation', '[feature]_reconnect_map', JSON.stringify({
  arrows: [
    { id:'<nodeId>', label:'state-1 → state-2',
      x, y, w, h,
      from:{ frameId:'<id>', edge:'right', midY },
      to:  { frameId:'<id>', edge:'left',  midY } },
  ],
  stateGroups: [{ id, label:'state-N', chipId, frameId }],
  headerChipId, ruleCardId,
}));
// 读取：page.getSharedPluginData('ux_annotation', '[feature]_reconnect_map')
```

#### 反例（必须避免）

| ❌ 错误做法 | 原因 |
|---|---|
| 条件标签用 FRAME | canvas 上显示 "Frame" 名称标签，视觉干扰 |
| 文字直接浮在 canvas 上 | 颜色依赖主题背景 → 主题切换后不可读 |
| `strokeEndCap = 'TRIANGLE_ARROW'` 用在 VECTOR | VECTOR 不支持此属性，silent fail |
| GROUP 子节点顺序：TEXT 在前 RECTANGLE 在后 | RECTANGLE 渲染在最上层，遮住文字 |
| vectorPaths data 用绝对坐标 | data 是局部坐标（相对 vec.x/y），用绝对坐标会导致路径位置翻倍偏移 |

#### 双语支持（中英文必须同时提供）

所有 UX 注释文案**必须同时提供中文和英文**，排版按元素类型采用不同方式：

| 元素 | 排版方式 | 说明 |
|---|---|---|
| **State label chip** / **Header chip** 各行 | **内联式（Layout A-右）**：英文 + `  `（2 space）+ 中文，同一文本节点，中文 8px 浅灰 | 单行 label / 标题，EN 短，中文直接跟在右侧 |
| **条件标签 chip** | **内联式（Layout A-下）**：英文一行 + 中文换行另起一行，同缩进，中文 8px 浅灰 | 元素宽度有限，中文另起一行跟随 |
| **多行 bullet 列表 / 多行公式 / 含 `\n` 的结构化正文（UX delivery 注解卡 / spec 表）** | **内联式（Layout A-逐行）**：按 EN 行 / bullet 拆分，每行末尾 + `  `（2 space）+ 对应 ZH + `\n` 进下一行；ZH 仍是 Noto Sans SC，比 EN 小 2px，opacity 0.45 | EN 已按 `\n` 或 `•` 自然分行；逐行 ZH 比 Layout B 上下分节更易 1:1 对应 |
| **规则卡** | **分节式（Layout B）**：英文完整段落 → 分隔线（1px `#3A3D55`） → 中文完整段落（`规则说明：`为标题） | EN 是一大块 prose（无 `\n` / 无 `•`）且 ≥ 3 行；逐行内联放不下时退到此层 |

**布局选择决策树（A-右 vs A-下 vs A-逐行 vs B）**：

```
文本行数 ≥ 3 行 或 spec 级正文？
  └─ YES → EN 已按 \n / • 自然分行 且 每行 EN+ZH 能塞进容器宽度？
        └─ YES → Layout A-逐行（line-by-line interleaved）
        └─ NO  → Layout B（分节式上下分）
  └─ NO  → 该行英文字符 ≤ 50？ 且 chip 宽度充足？
        └─ YES → Layout A-右（中文直接内联到右侧，2-space 分隔）
        └─ NO  → Layout A-下（中文另起一行，同缩进）
```

**技术实现（use_figma）**：
- Layout A-右：`setRangeFills` / `setRangeFontName` / `setRangeFontSize` 在同一 text node 的不同字符区间分别设样式
- Layout A-下：text node 内插入 `\n` + ZH 行，同样用 range API 设中文样式
- Layout A-逐行：按 `\n` split EN 为行数组，每行配对 ZH，重建 characters，再对每个 ZH range 套样式：
  ```js
  const enLines = node.characters.split('\n');
  const zhLines = [/* 对应 zh per line, 空字符串跳过 */];
  const newChars = enLines.map((en, i) => zhLines[i] ? `${en}  ${zhLines[i]}` : en).join('\n');
  node.characters = newChars;
  // 然后游走 cursor 算出每个 ZH range [zhStart, zhEnd]，套 setRangeFontName/Size/Fills (opacity 0.45)
  // 关键：先用 setRangeFontName/Size/Fills 对 (0, newChars.length) reset 为 EN 样式（清掉前一版本残留），
  //       再对每个 ZH range 套 ZH 样式，避免 setCharacters 后 trailing 残留样式污染
  ```
- Layout B：追加独立 text node（`规则说明：` 标题 + 6 bullets）+ 1px 分隔 RECTANGLE
- **ZH 弱化**：对所有 `Noto Sans SC` 字符区间，setRangeFills 时加 `opacity: 0.45`；EN opacity 保持 1.0（默认）。EN 用什么底色 ZH 就用相同底色 + 半透明，无需记特殊颜色值

**视觉优先级原则（最重要）**：
> **英文优先，中文视觉弱化**。习惯读英文的工程师只看英文，中文不应干扰视线；习惯读中文的成员虽然中文弱化但仍可识别。双语不等于双权重——中文永远是附注，不是并列。

**逐行内联 > 上下分节（在能放下的前提下）**：
> Layout B 上下分节把工程师阅读路径切两段——先读完整段 EN，再回头读整段 ZH，对照费眼睛。Layout A-逐行 让每行 EN/ZH 同视框，1:1 对照零阅读跳跃。但前提是 EN 已自然分行（`\n` 或 `•`）且每行 EN+ZH 能塞进容器宽度；EN 是一大块 prose（无 `\n` 无 `•`）或单行太长塞不下时仍走 Layout B。

**字体规范**：
- 英文：`Inter`（现有约定不变）
- 中文：`Noto Sans SC`（Figma 可用简中字体；style 用 `Regular` / `Medium`；`PingFang SC` 在此环境不可用）
- 中文字号：比同层英文小 **2px**（如英文 10px → 中文 8px；宁小勿同）

**颜色规范**：
- 英文主文：`#FFFFFF`（白）或 `#33A4FD`（accent，视层级）
- 中文次级：`#A6ADB8`（浅灰）— 与英文形成明显对比度差，中文不抢视觉权重
- ⚠️ 禁止中文与英文用同一颜色 / 同一字号 → 视觉上文字量翻倍，注释难以快速扫读

**反例**：
- ❌ 只写英文（工程师无法判断中文产品文案）
- ❌ 只写中文（国际团队协作障碍）
- ❌ 多行 body 未按 `\n` / `•` 拆 line 就整块内联（EN 全段 + 2-space + ZH 全段）→ ZH 接在 EN 末尾继续 wrap，工程师看不到一一对应（用 Layout A-逐行 替代）
- ❌ state label 用分节式 → chip 高度膨胀，流程图显得松散
- ❌ 直接新增 child text node 到 chip（会导致 auto-layout 撑高）→ 用 range API 改写同一节点
- ❌ 中文与英文同颜色同字号（看起来文字量很多，失去弱化效果）
- ❌ 只靠字号差（1px）区分 EN/ZH —— 当 EN 本身已经是灰色时，1px 肉眼不可见；必须配合 opacity 0.45

#### 验收标准

- [ ] 深色 canvas 上：所有注释文字可读，颜色无依赖画布背景
- [ ] 浅色 canvas 上：navy chip 作为 overlay 突出可识别（无需截图验证，配色规则保证）
- [ ] Figma canvas 上无多余 "Frame" 标签（条件标签是 GROUP）
- [ ] Arrow ID 和坐标已存入 sharedPluginData（可通过 `getSharedPluginData` 验证）
- [ ] Header chip 和规则卡有语义化名称（非默认 "Frame"）
- [ ] 所有注释文案同时提供中英文，排版符合内联式（A-右 / A-下 / A-逐行）/ 分节式（B）分类规范
- [ ] 多行 body 走 A-逐行 时：每行 EN 后紧跟对应 ZH 同视框（无 ZH 整块甩末尾的反模式）
- [ ] 所有 Noto Sans SC 字符区间的 fill opacity ≈ 0.45（probe 脚本可验证，无需截图）

#### 验证策略（优先用 probe，截图最后手段）

| 验证目标 | 推荐方式 | 截图？ |
|---|---|---|
| ZH opacity 是否为 0.45 | `getRangeFills` 返回 JSON，检查 `opacity` 字段 | ❌ |
| EN 颜色是否正确（白/蓝/灰） | `getRangeFills` 检查 `color` | ❌ |
| 节点类型（GROUP vs FRAME） | `get_metadata` 或 `node.type` | ❌ |
| 位置 / 间距 / 宽高 | probe 脚本返回 x/y/w/h | ❌ |
| sharedPluginData 是否写入 | `getSharedPluginData` 返回字符串验证 | ❌ |
| 视觉布局（间距感、层叠、对齐感） | 截图 | ✅ 仅此类 |
| 用户最终拍板前确认 | 截图 | ✅ 仅此类 |

**probe 模板**（批量验证 ZH 弱化，一次 call 覆盖所有 text node）：

```js
// 检查所有 annotation text 的 ZH range opacity
function checkZHOpacity(node, expected = 0.45) {
  const len = node.characters.length;
  const fails = [];
  let i = 0;
  while (i < len) {
    const font = node.getRangeFontName(i, i + 1);
    if (font && font.family === 'Noto Sans SC') {
      let j = i + 1;
      while (j < len && node.getRangeFontName(j, j+1)?.family === 'Noto Sans SC') j++;
      const fill = node.getRangeFills(i, j)[0];
      const op = fill?.opacity ?? 1.0;
      if (Math.abs(op - expected) > 0.05) fails.push({ range: [i,j], opacity: op });
      i = j;
    } else { i++; }
  }
  return { pass: fails.length === 0, fails };
}
```

**Why**：截图消耗 Token 约为 probe 脚本的 10–20 倍；所有可量化属性（颜色、opacity、位置、类型）均可用 `use_figma` 返回 JSON 验证，截图只留给"眼睛才能判断"的视觉感受类问题。

**Why**：UX 交付注释本质是给工程师读的文档；如果注释本身因主题/缩放/层级问题无法清晰呈现，文档价值归零。双语确保跨语言团队无歧义对齐。

---

### M24 — Code→Token Mapping Table（仅 "已有 code 还原效果图" 场景前置硬规则）

**触发条件（严格 AND）**：
1. 任务表达明确含 "根据已有 code 还原效果图" / "match the running implementation" / "把 vue-app 翻成 figma" / "参考 demo HTML" 等措辞，**或**用户主动指向 source file 作 visual truth；**且**
2. 该 source 直接定义了视觉 token / CSS variables / 颜色常量（不是只引用别人发布的 token）。**source 的形式不限**：
   - 多文件项目：`.vue` / `.tsx` / `.jsx` / `.svelte` + 独立 `.css` / `.scss` / `tokens.css`
   - **单 HTML 文件 demo**：`<style>` 内联 CSS（含 `:root { --xxx: ... }` 块）/ `<script>` 内联 JS 常量
   - Tailwind config / theme.ts / tokens.json / Storybook theme
   - 任何形式只要能找到"定义视觉常量 → 后面被组件引用"的代码段

> ⚠️ **不只看文件后缀**——`.html` 单文件 demo 里 `<style>:root { --brand: #2fb54e; --brand-hover: #41c760; }` 与多文件项目的 `tokens.css` 等价，**同样触发 M24**。本次实战实证：`microapps-console-planb.html` 单文件 + `vue-app/src/assets/tokens.css` 多文件项目两种形式都是有效 source。

**不触发**（避免误用）：
- **Greenfield mockup**（US-1 / US-2）— 无 code，从 spec/brief 起 → 跳过此规则，按常规 Phase 0 走
- **纯 UX 探索 / brainstorm 阶段** — 还没确定方向 → 跳过
- **US-3 增量但无 code spec** — 只在既有 Figma 上加新元素 → 走 M21 sibling visual contract，不走 M24
- **改 mockup 视觉但 code 不存在** — 设计先行场景 → 不触发
- **spec.md 提到颜色但无 source code 引用** — 走常规 catalog grep + library search

**问题**：当 code 存在时，spec 文档 + code 里出现的 CSS 变量名（如 `--brand-hover` / `--bg-layer4` / `--status-inactive`）是**工程师的术语**，不是 Figma 库的 Variable 名（`UX/Brand/Hover` / `Color Type/Background/Layer_4`）。**AI 凭直觉把 "inactive grey" 映射到 `UX/Grey/grey-7` 是典型踩坑**——grey-7 是 TEXT 色不是 BG 色。

**触发后必做**：动手 Phase 0 之前先产 `Code→Token mapping table`：

| Source 引用 | 出现位置 | TVU library variable | Key | 用途 |
|---|---|---|---|---|
| `--brand` | sidebar active row | `UX/Brand/Brand` | `ea8c2383...` | active state primary |
| `--brand-hover` | sidebar hover | `UX/Brand/Hover` | `eab4ef3b...` | **hover state** — 严禁用 `UX/Brand/Brand` |
| `--bg-layer1` | canvas | `Color Type/Background/Layer_1` | `1c7389d2...` | page bg |
| `--bg-layer2` | card bg | （library 未发布，hex `#1f1f1f`） | — | 标记 🟡 入库候选 |
| `--bg-layer3` | row hover / chip bg | `Color Type/Background/Layer_3` | `93476370...` | elevated bg |
| `--bg-layer4` | inactive status segment / elevated header | `Color Type/Background/Layer_4` | `f576bb4f...` | inactive **fill**（不是 text） |
| `--text-tips` | secondary text | `Color Type/Text/Tips` | `b3b5983a...` | TEXT only — 禁止做 fill |
| `--brand-bg` (rgba 0.18) | translucent brand chip | — | — | library 未发布，用 child Rectangle + `node.opacity=0.18`（per Q3 嵌套 instance edge case） |

**Why**：spec 里写 `--status-inactive: var(--text-tips)` 时是说"pill TEXT 用 tips"，不是"任何叫 inactive 的元素都用 tips"。**Status BAR 的 inactive 段是 BG fill，必须用 Layer_N，不是 Text 系列 Grey**。

**反例实证**（MicroApps Console Plan B 2026-05-14）：
- Sidebar Hover 误用 `Layer_3`（生灰），缺品牌色 hover affordance → 修正为 `UX/Brand/Hover @ 0.10`
- 状态条 inactive 段误用 `UX/Grey/grey-7 #7B7B7B`（text color）→ 修正为 `Color Type/Background/Layer_4`
- 都因为没产前置 mapping table，凭"哪个 grey 看着对就用哪个"踩坑

**判断流程**：
1. 任务描述里有 "已有 code" / "running impl" / "翻成 figma" / "match the code" / "参考 demo" 类字样吗？→ 是 → 检查 (2)
2. 真有 source 可读吗（多文件项目 OR 单 HTML demo 含 `<style>:root` token 块 OR Tailwind/theme 配置 等任意形式）？→ 是 → **M24 触发**，产 mapping table
3. 否则 → **不触发**，按常规 Phase 0 + library catalog grep + sibling visual contract（M21）走

**自检反例**（如下情况误触发 M24 就是过度套规则）：
- "画一个 dashboard mockup" → greenfield，不触发
- "Plan A 已有 Figma，画 Plan B 对比方案" → 无 source code，走 M21
- "改下 button 颜色" → US-5 小调整，跳所有前置
- "设计师让我画 onboarding 流程" → 无 code，走 Pre-Phase 0 + Phase 0

---

### M-INTEGRITY — Mockup Layout Integrity（umbrella，覆盖原 M27 / M28 / M34）

凡是改 Section / Frame 布局或大小后，必须按 3 个 sub-clauses verify 完整性。同源——都是"layout 完整性的不同视角"——合并为一组以便 audit 一次跑完。

**Legacy ID 映射**：M27 → §I1（sibling layout consistency），M28 → §I2（sibling section overlap），M34 → §I3（children bbox wrap）。

**自动化 audit**：`scripts/audit-mockup-integrity.mjs`（一次扫完 3 个 sub-clauses）。

---

#### I1 — Sibling Plan/Version Layout Consistency（原 M27）

任何 product Figma 文件里同时存在多个 Plan / version section（如 `Plan A: ...` + `Plan B: ...` / `v1 ...` + `v2 ...`）时，**新 section 的 layout 风格必须 mirror 已存在的 sibling**。

**Mandatory probe (起手协议)**：开始画前必须执行：

```js
// 1. 找同 page 所有 SECTION
const sections = [];
for (let i = 0; i < page.children.length; i++) {
  try { const c = page.children[i]; if (c.type === 'SECTION') sections.push(c); } catch (e) {}
}
// 2. 找名字含 "Plan" / version 类的 sibling
const sibling = sections.find(s => /Plan [A-Z]|v\d|版本/.test(s.name) && s.id !== currentSection.id);
// 3. 走查 sibling 的 layout：frames 排列 / annotations 摆放 / connector 风格
```

**Layout style 要 mirror 的维度**：
- Frames 排列（横向 row / 纵向 stack / N×M grid）
- Pair 配对方式（按 view / 按 persona / 按 state）
- Annotations 摆放位置（4 边围绕 / 单侧列 / 散布）
- Connector 形态（短直线 / L 形 / 长对角）
- Section 长宽比（≈ 2:1 / ≈ 1:2）

**不 mirror 的情况**：用户**明确**说"这次跟 Plan A 风格不一样，因为..."。否则一律 mirror。

**违例 history**：2026-05-14 Plan B 没 probe Plan A 就画**纵向 stack** (3280×9806)；Plan A 实际是 **横向 3×2 pair** (11829×5130)。三轮 retrofit (v1 → v2 → v3) 才纠正。

---

#### I2 — Sibling Section Overlap Probe（section.resize 后 mandatory）（原 M28）

调用 `section.resize()` / `section.resizeWithoutConstraints()` 改 section bounds 后，**必须** probe 同 page 所有其他 SECTION 检查 bounds 不重叠。

```js
// mandatory after any section.resize
function checkOverlap(target, page) {
  const overlaps = [];
  for (let i = 0; i < page.children.length; i++) {
    try {
      const s = page.children[i];
      if (s.type !== 'SECTION' || s.id === target.id) continue;
      const overlap = !(s.x + s.width <= target.x || s.x >= target.x + target.width ||
                       s.y + s.height <= target.y || s.y >= target.y + target.height);
      if (overlap) overlaps.push({ id: s.id, name: s.name });
    } catch (e) {}
  }
  return overlaps;
}
// 若 overlaps.length > 0：STOP，调小 target section 或移 sibling
```

**Why**：Section 扩大后视觉上可能覆盖邻居 section 内容；Figma 不会拒绝重叠 section（不像 frame 的 absolute pos），但视觉效果错乱。Local Components / Reference / Draft section 都是常见邻居。

**违例 history**：2026-05-14 Plan B v2 纵向 stack resize 到 3280×9806（canvas y=8389-18195），压住 sibling `MicroApps PlanB — Local Components`（y=15463-17663）。重叠 ~2200 px 才被用户抓出，retrofit v3 才发现 + 修。

---

#### I3 — Section / Frame children-bbox wrap audit（原 M34）

任何 SECTION 或非-auto-layout FRAME 内塞了 **会自动撑大的子节点**（auto-layout 子 frame、text node with `textAutoResize='HEIGHT'` 等）后，**必须** probe 子节点最终 bbox 并按需把容器撑到包住所有子。

**Why**

- **Section 不是 layout 容器** —— 是组织容器，children 完全自由 positioned + 容器**不会** auto-hug
- **非-auto-layout frame 同理** —— 只有 `layoutMode !== 'NONE'` 的 frame 才会随子内容自动调整大小
- Auto-layout 子节点（`primaryAxisSizingMode='AUTO'`）的高度只在 append 后 / 子内容变化后才确定，初始 `resize(w, h)` 给的值会被覆盖

**必跑 probe**：

```js
const children = container.children.map(c => ({
  id: c.id, name: c.name,
  right: c.x + c.width, bottom: c.y + c.height,
}));
const maxRight = Math.max(...children.map(c => c.right));
const maxBottom = Math.max(...children.map(c => c.bottom));
const overflow = children.filter(c =>
  c.right > container.width || c.bottom > container.height ||
  c.x < 0 || c.y < 0
);
if (overflow.length > 0) {
  container.resizeWithoutConstraints(
    Math.max(container.width, maxRight + 40),
    Math.max(container.height, maxBottom + 40)
  );
}
return { overflowCount: overflow.length, overflow };
```

**触发时机**：

- `container.appendChild(autoLayoutChild)` 后
- `text.characters = '...'`（如果该 text 在 auto-layout 子里）后
- `child.itemSpacing` / `child.padding*` 变化后
- 任何 M23 注释 chip / 规则卡 内容改完后

**违例 history**：2026-05-18 Touch-Screen v8 — 4-source flow rules card 加双语规则文本后撑高到 651（auto-layout `primaryAxisSizingMode='AUTO'`），section 只 1500 高 → 底部 251 px rules card 内容跑到 section 外。fix: `section.resizeWithoutConstraints(1740, cardBottom + 40)`。**根因**：以为 section 是 frame-like 容器会自动 hug children。

---

#### M-INTEGRITY 共用 Acceptance

- 一次 probe 跑全：`pnpm audit:mockup:integrity -- --file <fileKey> --page <pageId>`（脚本扫 §I1 / §I2 / §I3 三条）
- handoff doc 必含 "Integrity audit: I1=pass / I2=overlap-gap N px / I3=overflow-count 0"
- Legacy ID 引用兼容：M27 / M28 / M34 anchor 在文末"Legacy ID Map"段维护重定向

---

### M27 — Sibling Plan/Version Layout Consistency

> **⚠️ Merged into [M-INTEGRITY §I1](#m-integrity--mockup-layout-integrityumbrella覆盖原-m27--m28--m34)**. This anchor is preserved for legacy reference.

---

### M28 — Sibling Section Overlap Probe

> **⚠️ Merged into [M-INTEGRITY §I2](#m-integrity--mockup-layout-integrityumbrella覆盖原-m27--m28--m34)**. This anchor is preserved for legacy reference.

---

### M29 — Table Cell Content Pattern（Copyable / Empty Placeholder）

带数据 table 的 cell（典型如 Event Name / Object ID / URL 列）有 2 种内容形态，**视觉规则不同**：

**Form 1 · Copyable（有值）**：

| 维度 | 规则 |
|---|---|
| Text | `textAutoResize = 'WIDTH_AND_HEIGHT'`（hug content），text fill 默认 token (`Text/Text_1` 或类似) |
| Action icon | TVU `icon/Edit/Copy` instance，紧跟 text：`icon.x = text.x + text.width + 4`（gap 4px） |
| Icon fill | bind to `Color Type/Icon/Default` (key `e8580f8f...`) via Q2 two-step |
| Icon size | 12×12 typical for table 行高 15-20，更大行可用 14×14 |

**Form 2 · Empty Placeholder（无值，显示 "—" / "-" / "–"）**：

| 维度 | 规则 |
|---|---|
| Text | "—" em dash (US-201C dash 优先)，text fill bind to `Color Type/Text/Placeholder & Button` (key `1fe2a119...`) |
| Action icon | **不要** copy/action icon — empty 值不可复制操作 |
| Detection | text.characters.trim() ∈ {"—", "-", "–"} → 走 Form 2 |

**生成时自动判断**：

```js
const EMPTY = new Set(['—', '-', '–']);
function applyCellPattern(cell, copyIconComp) {
  const text = cell.children.find(c => c.type === 'TEXT');
  if (EMPTY.has(text.characters.trim())) {
    // Form 2: bind placeholder color, no icon
    text.fills = bindToVar(text.fills, textPlaceholderVar);
    cell.children.filter(c => /Copy/.test(c.name)).forEach(c => c.remove());
  } else {
    // Form 1: hug + icon + bind Icon/Default
    text.textAutoResize = 'WIDTH_AND_HEIGHT';
    const icon = copyIconComp.createInstance(); /* ... */
    icon.x = text.x + text.width + 4;
  }
}
```

**Why**：Empty 占位若用同色 + Copy icon，dev 会写 copy logic 处理 "—"，QA 测出 bug；用户看见 "—" 但不知道是否可点击。Placeholder 色 + 无 icon = "this is intentionally empty, not copyable" 的视觉契约。

**违例 history**：2026-05-14 Plan B F2/F4 session table 一开始 44 cells 一律加 Copy icon，"—" cells 也加。用户指出后删了 6 处 empty cell 的 icon + 改 text 为 Placeholder 色。

**dev-side spec (text overflow)**：长 URL/ID 用 **center-ellipsis** 截断（如 `udp://237.0...0:1234`），保留协议前缀 + 尾段，确保 Copy icon 不被遮挡。Figma TEXT 无 native center-ellipsis，dev 走 CSS / JS 实现。

---

### M23.6 — Connector Tip Accuracy + Frame Content Avoidance（M23 extension）

M23 主体规则定义了 connector 形态（VECTOR + 双语 label + reconnect map）。本 sub-rule 补 **几何精度** + **路由避让** 两条 acceptance：

**(A) Connector Tip Accuracy**：每条 connector 的箭头 tip 必须落在 declared target element 的 `absoluteBoundingBox` 内（容差 ≤ AH 即 8px）。Reconnect map 里每条 arrow 必含 `tipCanvas: {x, y}` 字段供审计。

**Audit script**（post-design wrap-up 必跑）：

```js
for (const arrow of reconnectMap.arrows) {
  const target = await figma.getNodeByIdAsync(arrow.to.frameId);
  // ...probe target element by subElement path
  const targetBbox = targetElem.absoluteBoundingBox;
  const tip = arrow.tipCanvas;
  const inBbox = tip.x >= targetBbox.x - 8 && tip.x <= targetBbox.x + targetBbox.width + 8 &&
                 tip.y >= targetBbox.y - 8 && tip.y <= targetBbox.y + targetBbox.height + 8;
  if (!inBbox) console.error(`${arrow.label} tip (${tip.x},${tip.y}) outside target bbox`);
}
```

**(B) Frame Content Avoidance**：connector 的中间路径段（非起点非终点的 horizontal / vertical 段）**不应穿越** target frame 的内部 UI 内容区。允许穿越 frame margin / gap，禁止穿越 sub-header / table rows / cards 等可见内容。

**Routing 决策树**：
- target 在同行 same x band → 直 horizontal/vertical line
- target 跨行但 target_x 在 source 之间空白区 → 2-seg L
- target 跨行 + target_x 在 frame 内部（如 sub-header 按钮 x=1589 F local）→ **3-seg U-routing** 绕到 frame 外（above / below frame in row gap）再 dip 进 frame

**违例 history**：
- 2026-05-14 Plan B v3：#6 → AutoRefresh tip 偏 58px（误用 TopBar y 而非 Sub-header y）/ #8-F5 + #8-F6 tip 偏 68-89px（误用 element topMid 而非 leftMid/rightMid）→ v3.1 fix
- 2026-05-14 Plan B v3：#5 → F3/F4 button 用 L-v-first 横段穿 F3 sub-header / F4 filter bar 内容 → v3.1 改 U-3seg 绕到 F3 顶上方 + F3/F4 gap

**Acceptance**：handoff doc 必含 connector accuracy audit table（每条 arrow: tip canvas / target bbox / delta / pass-fail）。

---

### M30 — Icon Component Mandatory Library Lookup

> **⚠️ Merged into [M32](#m32--library-component-firstm30-扩展所有-product-ui-元素)** as the icon sub-clause (M32 §icons). M32 is the umbrella for all product UI elements (icons + buttons + chips + inputs + ...); M30 was the icon-specific subset and is now absorbed.
>
> The icon-specific synonym table + "no Unicode arrow" prohibitions + 2026-05-14 Plan B 实证 are preserved in M32 §icons + carried by M35 (Affordance-Category Search) which generalizes synonym expansion.
>
> This anchor is preserved for legacy reference.

---

### M31 — Layout function decision tree（按场景选 Auto Layout / Slot / Boolean / Absolute）

任何 product UI 或注释 container 创建 / 修改时，**必须按场景选最合适的 Figma 布局机制**，绝对定位仅在有充足理由时使用。"先 auto-layout，找不到合适机制再退到 absolute" 是默认顺序。

#### 决策树

| 场景 | 推荐机制 | 理由 |
|---|---|---|
| 内容长度可能变（文案 / item 数量 / padding 调整） | **Auto Layout** | 改内容自动重排，不需要重算坐标 |
| 同位置 swap 不同子组件（icon / text / icon+text 切换） | **Slot (Instance Swap property)** | 母组件保持布局，instance 只换内容 |
| 子元素 "显示 / 隐藏" 是状态属性（loading / empty / error / 可选 badge） | **Boolean property** | 单组件多状态，dev 端 prop 切换；避免组件爆炸 |
| 子元素位置随别的元素动（自适应 wrap / 对齐 baseline / 居中） | **Auto Layout + counter-axis align + itemSpacing** | 不是手算 x/y |
| 一次性 overlay 浮层（dialog / popover backdrop / canvas annotation arrow） | **Absolute 允许**（但 *必须* 备注理由） | 浮层没 layout 语义；用 `layoutPositioning='ABSOLUTE'` 嵌进 auto-layout parent，或直接 free x/y |
| Stand-alone annotation（流程图箭头、状态 chip overlay） | **Absolute 允许** | annotation 类元素跨 frame，没参与产品 UI layout |

#### 反例

| ❌ 错误做法 | 后果 |
|---|---|
| dialog box 内 title / body / button row 各自 x/y 绝对定位 | 改 title 字数 / 加按钮 / 调 padding → 全部要重算坐标，且看上去仍然"对齐"实际是巧合 |
| sidebar field row 用绝对定位 | 加一行字段就要平移下面所有行；改字段顺序要重排所有 y |
| "复制粘贴 3 个一模一样的 frame" 做按钮组 / list rows | 任一文案变长要手动改其他 frame；spacing 失控 |
| product UI 用了 absolute 但 handoff doc 不写理由 | 后续维护者无法判断是"故意的浮层"还是"图省事没用 auto-layout"|

#### Acceptance

- 任何 dialog box / sidebar / footer button row / 列表 row / chip → **必须 Auto Layout**（`layoutMode !== 'NONE'`）
- 任何 product UI 用绝对定位 → handoff doc 必须写明理由（"overlay on backdrop" / "anchor to canvas annotation" 等）
- probe 脚本：`product_frame.findAll(n => n.type === 'FRAME' && n.layoutMode === 'NONE').filter(non-overlay)` 命中数应 = 0

#### 实证

- **2026-05-18** Touch-Screen v8 — 4-source flow M1 dialog v1：dialog box + buttons row 全用 absolute x/y。改按钮文字 / 加 padding / 切按钮组件都要重算坐标。v2 重做：dialog (VERTICAL auto-layout, padding 24/20/24/24, gap 12) + buttons-row (HORIZONTAL auto-layout, `primaryAxisAlignItems='MAX'`, gap 12)，内容长度变化全部自动重排。

---

### M32 — Library Component-First（M30 扩展：所有 product UI 元素）

M30 把"按库取用"约束限定在 icon。**所有 product UI 元素**（button / chip / badge / input / dropdown / list row / dialog action 按钮）同样适用——先 lookup component 再考虑自画。

#### 优先级（与 M30 对齐）

| 优先级 | 来源 | 触发动作 |
|---|---|---|
| **1** | 当前 Figma 文件内 `COMPONENT` / `COMPONENT_SET`（含 variants） | `findAll(n => (n.type==='COMPONENT_SET'\|\|n.type==='COMPONENT') && /<role>/i.test(n.name))` |
| **2** | 已发布 Team Library 组件 | `importComponentByKeyAsync(key)`（key 由用户提供或从 instance.mainComponent.key 反查）|
| **3（最后手段）** | 自画 + 🟡 标注库候补 | 仅 **1 / 2 均 verified miss** 时；handoff doc 必含 "TBD: replace with `<component>` from library" |

#### 同义词扫描（按 role 类别）

| 想画 | 必须穷尽的关键词 |
|---|---|
| 按钮（dialog action / footer action / sidebar primary） | `button`、`btn`、`LCD Button`、`home button`、`primary`、`secondary`、`back` |
| 输入框 | `input`、`text field`、`textbox`、`form` |
| List item / row | `list item`、`row`、`menu item`、`option`、`cell` |
| Toggle / Switch | `switch`、`toggle` |
| Card / Panel | `card`、`panel`、`tile`、`container` |

#### 反例

| ❌ 错误做法 | 后果 |
|---|---|
| 画 gradient rectangle + 文字当 "Stop all" button | dev 拿不到 variant 的 hover / disabled / pressed state；改组件颜色时这一处不会跟着变 |
| dialog 两个按钮用自画 frame，不用 `LCD Button` variants | 按钮尺寸 / 圆角 / 字号 / spacing 偏离规范；和单路画面里的按钮视觉不一致 |
| 一次 `findAll` 没结果就降级自画 | 同义词没穷尽 / 团队库没尝试 import / 没问用户要 key |
| 用 instance + 改内部 fill 颜色 "扮成" 别的 variant | variant 是 component 的合法 state，不要用 paint 改 "伪装"——直接 `setProperties({type: '<variant>'})` |

#### Acceptance

- handoff doc 必含一行 "Buttons used: `<component name>` variants \[\<list\>\]"
- 任何 product UI 里出现"渐变填充矩形 + 文字" 且不是 `INSTANCE` → 触发 flag
- 自画必备 escalation 痕迹：handoff doc 写 "`<component>` lookup miss across local + team library; user provided no key → flagged as TBD"

#### 实证

- **2026-05-18** Touch-Screen v8 M1 dialog v1：Skip / Configure 两个按钮用自画 gradient rectangle，没用 `LCD Button` 的 `Type=Back/Status=Normal` + `Type=Primary/Status=Normal` variant。v2 重做用 instance + override text，自动对齐设备风格。**根因同 M30**：lookup 步骤跳过 / 同义词没穷尽。

---

### M33 — Annotation vs Product UI 语言隔离（M23 扩展）

M23 双语硬规则**仅作用于 UX 注释层**（navy chips / 规则卡 / condition labels / header chip 等 annotation 元素）。**产品 UI 层**（设备界面内真实出现的画面 / dialog body / button label / tooltip / menu item）走**设备语言**单语。

#### 边界判定

| 元素归属 | 语言策略 |
|---|---|
| 注释 chip / 规则卡 / annotation overlay（在 frame 外，浮在 canvas 上 / 作为 annotation 节点） | **双语**（M23 verbatim：EN 主 + ZH Noto Sans SC opacity 0.45）|
| 产品 frame **内**的 title / body / button label / tooltip / menu item / inline help | **单语**（设备语言；TVU 终端通常 = EN）|
| 产品 frame **上方**的 state label chip（M23 第 3 层结构） | **双语**（属注释层，挂在 frame 外）|
| 在产品 frame **里**叠加的 ZH 翻译 sticky note / overlay text | ❌ **不合规**——属错位 annotation，违反"注释不进 frame"原则 |

#### 反例

| ❌ 错误做法 | 后果 |
|---|---|
| dialog title 写 `2 channels have no Receiver  2 路通道未选择 Receiver` | dev 实现时不知道哪句是设备 UI；产品 UI 视觉拥挤；i18n key 命名歧义 |
| button label `Skip 跳过` 混排 | 按钮宽度被 ZH 撑大；和设备其他按钮视觉不齐 |
| 用注释字体（Noto Sans SC opacity 0.45）写产品 UI | 视觉风格 ≠ 设备实际渲染（设备根本没有 Noto Sans SC 字体可用） |

#### Acceptance

- 任何 product frame（识别：name 命中设备 mockup 模式如 `S<N>` / `M<N>` / 设备屏幕 frame） → probe 所有 TEXT 节点 `fontName.family`，**不应**出现 `Noto Sans SC`
- 注释 chip 内 ZH range opacity 仍走 0.45（M23 verbatim 不变）
- handoff doc 必含一行 "Product UI language: \<device locale, e.g. en-US\>"

#### 实证

- **2026-05-18** Touch-Screen v8 — 4-source flow M1 dialog v1：title / body / 按钮全部双语 → 用户拍板"产品 UI 不需要双语"。v2 strip 掉所有 ZH，仅注释层（state label chip + rules card）保留双语。**根因**：把 M23（针对注释）的双语规则错误外推到产品 UI 层，没区分"注释 annotation"和"设备真实画面"两个层级。

---

### M34 — Section / Frame children-bbox wrap audit

> **⚠️ Merged into [M-INTEGRITY §I3](#m-integrity--mockup-layout-integrityumbrella覆盖原-m27--m28--m34)**. This anchor is preserved for legacy reference.

---

### M35 — Affordance-Category Search Discipline（思考产物外化为强制 trace）

任何要新增 / 借用一个 **"基础语义元素"**（方向指示、排序、关闭、添加、checkmark、loading、warning 等）**之前**，必须按 affordance category 显式分类 + 视觉词汇扫描 + 跨上下文复用判断，**思考过程外化为 3 行 mandatory trace**。

#### Why（核心问题）

M30 / M32 / §"组件取数优先级" 规定了 **WHERE 去搜**，但没规定 **HOW 思考再搜**：

- AI 默认搜索是 "exact name regex"（搜 `link` / `dropdown` 等功能词）→ miss 一切只按形状 / 类别命名的资产（如本地 `down` / `up` instance, library `Arrow/Sorting`, `icon/Arrow/Previous` 等）
- 一旦 name 没命中 → 默认 fall back 自画 / Unicode，没有"逛"图层 / "扫"视觉词汇的本能
- 候选若在文件里别处已使用（如 pagination chevron）→ AI 倾向认为"用途不同不能借"，错失设计系统视觉一致性

**人类设计师**遇到"我要个 dropdown 小三角"时，本能扫"这文件里**所有上下方向的三角**"——pagination 的 chevron、sort 的 indicator、even 任何方向箭头都是候选；只要几何形状对，**复用优先**。

规则若只说"搜得彻底点"无法约束 AI 行为（vibe）；规则若强制 **AI 把分类思考输出成 deliverable 产物**，AI 不写就完不成 task，思考被外化为可 audit / 可拦截的 trace。

#### Step 1 · 显式分类（必须 1 句话）

> "我要画的是【方向 / 操作 / 状态】类指示，affordance category = **`<category>`**，意图是 '\<purpose\>'"

| 反例 | 正例 |
|---|---|
| "我要个 dropdown arrow" | "我要个**向下方向指示**，affordance = `directional-vertical`，意图是 '点击展开列表'" |
| "需要个 link 图标" | "我要个**链接 / 共享指示**，affordance = `link-share`，意图是 '标记此字段值 mirror 到他处'" |
| "画个 close 按钮" | "我要个**取消 / 关闭**指示，affordance = `dismissive`，意图是 '点击移除当前 layer'" |

#### Step 2 · 视觉词汇扫描（按形状词 / 类别词，不只名字词）

按 affordance category 查**固定同义词矩阵**——AI 必须扫这些 keyword 的 OR 命中集，**不预筛 "用途是否吻合"**：

| Affordance | 必扫关键词（OR 命中即候选） |
|---|---|
| `directional-vertical` | `up`, `down`, `chevron`, `arrow`, `triangle`, `caret`, `expand`, `collapse`, `sort`, `pagination`, `more` |
| `directional-horizontal` | `left`, `right`, `back`, `forward`, `previous`, `next`, `chevron`, `breadcrumb` |
| `sort` | `sort`, `sorting`, `order`, `ascending`, `descending`, `up`, `down`, `arrow` |
| `dismissive` | `close`, `cancel`, `delete`, `remove`, `x`, `clear` |
| `additive` | `add`, `plus`, `create`, `new` |
| `status-positive` | `success`, `done`, `check`, `selected`, `confirm`, `tick` |
| `status-warning` | `warning`, `alert`, `caution` |
| `status-negative` | `error`, `fail`, `stop`, `block` |
| `link-share` | `link`, `chain`, `connect`, `share`, `external`, `open` |
| `loading` | `loading`, `spinner`, `progress`, `wait` |

扫描范围（按 §"组件取数优先级"已有 4 层）：
- 0: Figma Component Catalog grep
- 1: Published library via MCP `search_design_system(includeLibraryKeys: ["lk-057f6ba0..."])`
- 2: 当前文件 COMPONENT / COMPONENT_SET / INSTANCE 的 `mainComponent.name`
- 2.5: 当前文件**未组件化 LAYER**（VECTOR / BOOLEAN_OPERATION / GROUP 命名带关键词的）

候选集 = 所有命中节点；**不预筛**。

#### Step 3 · 跨上下文复用判定

候选集得到后**默认 reuse**，除非以下"硬阻断"理由之一：

| ✅ 构成"不 reuse"理由 | ❌ 不构成 |
|---|---|
| 几何形状不匹配（要单向三角，候选是双箭头）| "它原本是 pagination / sort 用的" |
| 描边粗细 / 内边距 / 比例在新场景渲染破图 | "它当前在另一个 frame 被用" |
| 候选只有大尺寸（24px+），新场景要 8-12px 且无可缩放 svg path | "名字看上去和我场景不一样" |
| 颜色 token 绑死且不可改（极少见，多数库组件颜色可 override） | "意图描述不一样" |

→ 设计系统一致性 = **视觉词汇复用**，不是"语义恰好对齐"。pagination chevron 用作 dropdown indicator 完全合规。

#### Step 4 · Unicode / 自画黑名单（自查触发词，命中即 STOP）

| 黑名单 | STOP 后回到 |
|---|---|
| 想用 `▲ ▼ ◀ ▶ ↑ ↓ ← → ‹ › ✓ ✗` 任一 Unicode | Step 1 |
| 想 `figma.createVector()` 画三角 / 箭头 / 复合形 | Step 1 |
| 想用 `[D]` `<` `>` `*` 字符当占位 | Step 1 |
| 想用 emoji `🔗 📎 ✅` 等当 product UI 图标 | Step 1（emoji 用 annotation 内允许，product UI 不允许） |

只有 Step 1+2+3 verified miss **且** 用户授权 → 才允许进入自画路径（同 M30 priority 3 既有条款 + backlog 候补）。

#### Acceptance

每次 affordance 类元素创建前，handoff doc / annotation / commit message **必含 3 行 trace**：

```
1. Affordance: <category> (intent: <purpose>)
2. Vocabulary scan: [<candidate1 id+name>, <candidate2>, ...]
3. Chosen: <which + why>  // OR: None match → custom + backlog BRIDGE-MOCKUP-NNN
```

probe 命令：

```sh
grep -E "Affordance:|Vocabulary scan:|Chosen:" handoff-*.md
# 任何 mockup deliverable 加 affordance 元素后应有 3 行匹配
```

#### 与 M30 / M32 关系

- M30 / M32 = **search infrastructure rules**（WHERE）
- M35 = **search cognition rule**（HOW 思考再 WHERE）
- 三条联用：M35 先强制分类 → M30/M32 决定按 priority 在哪一层 invoke 搜索
- M35 不取代 M30 priority 1 "必须 MCP search_design_system 不能只 findAll"——只是把 query 的关键词从功能词扩展到 affordance 同义词矩阵

#### 实证

- **2026-05-18** Touch-Screen v8 — S6 dropdown indicator 我用 Unicode `▼` / 自画 vector triangle；文件里 `down` / `up` instance（pagination 用）几何形完全合用，名字直白但我没换搜词。**根因**：搜 `dropdown` `chevron` `arrow` 错过 `down` / `up` 这种纯方向命名；即使搜到也下意识认为"是 pagination 用的不能借"。M35 强制 Step 1 分类（`directional-vertical`）+ Step 2 同义词扫描（含 `up` / `down` / `pagination`）+ Step 3 默认复用 → 这次 miss 不会再发生。
- **2026-05-18** Touch-Screen v8 — link 图标搜索：我用 `findAll` regex `/link/i.test(name)` → 命中只有 `starlink` 干扰项 → fall back 自画 placeholder。**根因 1**：跳过 priority 1 published library MCP search（违 M30）；**根因 2**：query 用功能词 `link` 而非 affordance 同义词集 `[link, chain, connect, share, external, open]`（M35 缺）。M35 + M30 priority 1 双修才完整。

---

## 已知 file-local 组件（产品文件内）

对应 Figma 文件：`Micro-Apps-20250923`（fileKey `DtZcMkhNy6qh6jbQQnhreQ`）。

| 组件名 | nodeId / key | 变体维度 | 备注 |
|---|---|---|---|
| `APP Icons` | id `3:14402`, key `fefd6aacdbb0a6e1acaf4106446e831fe3c5b8ff` | `Tag` × `Color` | 18 个 variant；**已知缺**：Color Correction / Graphics Insertion / Test Pattern Generator 变体——需要时**补到既有 set**，不要新建组件。 |

> 这类 file-local 组件遵循 M2，**优先使用**而不是自画。

---

## 历史背景

各 M-rule 对应历史复盘见 [`retrospection/`](./retrospection/)。

- **2026-04-30** Session Dashboard v1 → M1–M6
- **2026-05-06** Session Dashboard v2 review → M2 acceptance criteria 补强 + M7/M8/M9
- **2026-05-09** MicroApps mockup → M11–M20（M17-M20 已迁 `figma-technical-reference.md`；M12/M13 合并进 M11）
- **2026-05-11** SaaS Dashboard chart → M22 + M14 范围扩展 + M21 mandatory probe
- **2026-05-12** Architecture 重构（Steps 1-5）→ 通用规则迁 `design-process.md` / TVU 业务规则迁 `domain-tvu.md` / Figma quirks 迁 `figma-technical-reference.md` + `tools/figma-quirks.md`
- **2026-05-12** SaaS Dashboard Date Range Picker UX 交付注释实战 → M23（多状态交互流程图格式 + 主题无关配色 + GROUP vs FRAME + reconnect map）
- **2026-05-14** MicroApps Console Plan B Figma 实战 → M24（Code→Token mapping table 起手前置）+ M25（State Color Discipline，hover ≠ brand）+ M26（BG vs Text Grey Discipline）；Q7 嵌套 instance paint.opacity 失效（迁 `figma-technical-reference.md`）。**3 个 M-rule 同源：AI 把 "inactive grey" 凭直觉映射到 `UX/Grey/grey-7`（错的），把 sidebar hover 用 `Layer_3`（也错的），都因起手没产 Code→Token mapping table。共同根因是"凭看上去像哪个 token 就用哪个"，解药是把映射步骤显式化、前置化、产物化**。
- **2026-05-18** MicroApps Console Plan B 规则回流 → **M30**（图标强制从库取用：优先级表 + 禁止 Unicode 替代 + 同义词策略）；**M10 item 5**（product mockup icon 实例化后 fill 绑定，M2.1）；Q8 已回流 `figma-technical-reference.md`（HORIZONTAL auto-layout insertChild）。根因：V3 Sort TEXT "↑" + V4-V5 Unicode chevron 两个违例同源——跳过 icon library lookup 直接用文字占位，换词搜索即可命中。
- **2026-05-18** Touch-Screen v8 — 4-source Receiver flow 规则回流 → **M31**（Layout function decision tree：Auto Layout 默认，absolute 必备理由）+ **M32**（M30 的扩展：所有 product UI 元素 library-first，不止 icon）+ **M33**（M23 边界澄清：双语只对注释层；产品 UI 层单语）+ **M34**（M28 兄弟：Section/Frame children-bbox wrap audit，section 不会 auto-hug）+ **M35**（思考产物外化：affordance category 分类 + 视觉词汇扫描 + 跨上下文复用 3 行强制 trace）。**5 个 M-rule 同源根因**：M1 dialog v1 实现时把 (a) 产品 UI 当注释做了双语、(b) 没找 LCD Button 自画 gradient rectangle、(c) dialog 内布局全用 absolute x/y、(d) rules card auto-layout 撑大后没补 section.resize、(e) link 图标 search 用功能词没用 affordance 同义词矩阵 + 跳过 published library MCP search 5 个问题一起出现。共同主线是"在 product UI 层套用了 annotation 层的范式 / 跳过库 lookup / 跳过 layout 决策 / 思考过程没外化为产物"。**M30-M34 描述 WHERE 搜，M35 强制 HOW 思考再搜——三层联用才覆盖完。** code 侧同步镜像：**R13**（M35 mirror）+ **R14**（M31 mirror）+ **R15**（M32 mirror）+ **R16**（M33 mirror）一并 commit；用户主动要求 "不是所有的都得等实证再约束，早发现早解决"——双向规则同源消除 mockup ↔ code 之间的 anti-pattern 漂移。

---

## AI 工具集成

本规则被 `~/.claude/skills/tvu-design-mockup/SKILL.md`（Claude Code 全局 skill）引用作为真源。

**规则改动权**：Path A 专属改本文件；通用 process rules 改 [`design-process.md`](./design-process.md)；TVU 业务规则改 [`domain-tvu.md`](./domain-tvu.md)。**不**修改 skill，skill 只做 trigger + 指路。仅当本文件新增规则改变起手协议时才回头同步 skill。
