# INFRA-F104 — render-gate 豁免判据字段化（结构化字段级豁免表 + 双向红）

> **状态**：**已实施**（commit `ad3c51bf`…`44170dd1` 为 Task 1–6 逐任务实现 + review fix；本条文档收尾见同日收尾 commit）。owner 2026-08-07 逐节认可后同日开工、同日落地。
> **判据真源**：本文件。`tests/visual-verify/lib/drift-compare-core.ts` 与
> `scripts/audit-render-drift-gate.mjs` 的头注释回指这里。
> **数字/⛔清单真源**：**本文件**（原写 [backlog INFRA-F104]，但该 entry 已按仓库约定——完成即删除——从 `backlog.md` 删档；持久记录改落在本 spec + git 历史 + [STATUS.md](../../STATUS.md) 顶部 Last updated 摘要（session AK；下轮 wrap-up 时会随惯例 prepend 进 [STATUS-CHANGELOG](../../internal/STATUS-CHANGELOG.md)）。本文件写**判据设计**、**本轮新增的实证**与**落地当天的最终存量**，不再指望 backlog entry 复述。
> **owner 已拍（别重开）**：路线 A · 修法以 Figma 为真源 · 路线 B 已否 · 桶 4 走 code-first divergence · 行键用方案 A。

---

## 1. 一句话问题

能力 2（AI 拿 Figma URL → 1:1 还原）唯一的确定性验证闸，其降级判据是**对人写的散文警告做正则、且作用在整条 entry 上**——命中即该 entry 的**任何字段**的任何失配都不再 gate。今天 **188/936 entries = 20.1%** 落在这个免疫面里。

---

## 2. 本轮（2026-08-07）新增的实证

全部自跑，非引用。基线：936 entries · A=0 · B=304 · S1–S4 全绿 · node mismatches=0 / compared 295 / gaps 2739。

### 2a. 已有的「字段特定降级」今天已被 entry 级规则架空 —— 有实证

`classifyFailedCheck()` 第 402–409 行那条 border-collapse 降级，注释逐字写着做成 field-specific 是
`so a real left-padding drift on the SAME Table entry still classifies as A`。拿真分类器对同一条 Table entry
（`table--type-tbody--align-left--…--theme-dark`）做阴阳对照：

| 输入 | 结果 |
|---|---|
| A) `rootPadding.left` 漂移 + 该 entry 的**真实** warnings | `B_RESIDUAL_SCHEMA_GAP` ← **作者本意应为 A** |
| B) 同一 check，warnings 清空 | `A_TRUE_DRIFT_CANDIDATE` |
| C) 只保留**不含短语**的 border-collapse 句子 | `A_TRUE_DRIFT_CANDIDATE`（该类句子数 = **0**） |

根因：触发 407 行的那句警告本身含 `verifier resolution limitation`，于是 413 行照样兜住。

**⇒ 路线 A 不只是收紧，也是让一条已写下的设计意图第一次真的成立。**

### 2b. 免疫是潜伏的，不等于今天有失败

188 条免疫 entry 里，**Rating 10 条今天 0 个 fail check**。含义：按今天的失败面建表，Rating 一行都不会有——而它今天同样免疫。这直接支持 §6 的 S6（stale 行 → FAIL）：只有 shrink-only 判据能把潜伏免疫挤出来。

### 2c. 「39 条搭便车」按构造看不见的第二群：自认的真 code 债

backlog 的「39 条」判别轴是**警告有没有指名该字段**，指名了即算正当。但有两条警告**指名了字段、同时自陈那是待修的真 code 债**：

| 组件 | 被豁免的 check | 警告自陈（逐字） | 实值 |
|---|---|---|---|
| Slider ×10 | `rootWidth`×4 · `rootGap`×4 · `textFillHex`×2 | `real width fix needs explicit width: 240px on .slider__input` | ~~240 → **192**（差 20%，超 C_BOUNDARY 的 5%）~~ 🔴 **2026-08-28 推翻，见下方 §已知缺口表同名行** |
| Message ×10 | `textFillHex`×8 · `rootFillHex`×2 | `harness limitation **pending real fix in followup sprint**（add dark-theme override block or split token names）` | dark 主题下 `#0b2b13` → **`#f1fbf3`**（渲成 light 值） |

⇒ 真 code 债不止桶 4 的 10 条，**至少还有 20 条**，只是措辞把它们藏在「正当」那一侧。这是否掉路线 B 的第二个独立理由：**判据不能建在措辞上，因为措辞可以同时既指名字段又承认自己是 bug。**

**顺带核销一条**：Table 那句 `Tracked as CANONICAL-029` 指的 entry **已不在 Active backlog**，且 Table 的 `rootPadding.*` 今天 **12 条全 pass** —— 那条债已还，只是警告句留在原地。Table 现存 6 条 fail 全是 `rootBorderHex`（border-collapse），是真限制。

### 2d. 桶 1 的 clamp 前提 —— 补上 backlog 标注缺失的浏览器实测

backlog 标了 evidenceLevel：「两者 clamp 后都是 10」只有规范结论 + 2026-06-12 的注释，无本轮实测。补测（chromium，40×20 盒子，含阴阳对照）：

```
radius=20px  computed=20px box=40x20
radius=100px computed=100px box=40x20
radius=10px  computed=10px box=40x20
radius=4px   computed=4px box=40x20

像素等价 20px vs 100px : true
像素等价 20px vs  10px : true    (阳性对照：应 true)
像素等价 20px vs   4px : false   (阴性对照：必须 false，否则探针没在测东西)
```

**前提成立。⚠️ 一个实现要点**：`getComputedStyle().borderRadius` 返回**声明值**（20px / 100px），**不是** clamp 后的值 —— 判据必须自己按量到的盒子算有效半径，不能指望读计算样式。

### 2e. backlog / pickup 两处需就地订正的小数字

| 处 | 原文 | 实测 |
|---|---|---|
| backlog 免疫面 | 「14 个组件」 | **13**（`Tab` 在 report 里的 `figmaName` 记作 `Tab List`，两者合并计数） |
| pickup §3 第 2 问的字段闭集 | 列了 8 个 | 实际还有 `rootOpacity` · `rootBorderWidth` · `textInset.left/right` · `renderTarget` · `navigation`。闭集真源 = `buildChecks` 本身，**校验器必须从代码派生，不许照抄那 8 个** |
| pickup 桶 3 归因 | `rootFillHex`×6 全属 `Disabled=Yes, Hover=Yes` | 6 条里 **4 条**是 disabled+hover，**2 条**是 `is-parent-yes / checked-yes / active-yes / disabled-no / hover-no` —— **两个成因，必须拆成两行** |

---

## 3. 判据的新形状

`classifyFailedCheck()` 现有 5 条分支，只动 2 条：

| 行 | 现状 | 改动 | 理由 |
|---|---|---|---|
| 398 | `renderTarget`/`navigation` → B | **不动** | 基础设施信号，非视觉判据；F97 头注释已解释其中一支不可达 |
| 399 | `textFillHex` + `state==='placeholder'` + 散文正则 → B | **删** | 今日命中数 = **0**，是死分支；散文锚定正是本条要清的形态。真出现就按表加一行 |
| 407 | `rootBorderHex` + `border-collapse` 正则 → B | **迁进表** | 见 §2a：其设计意图今天已失效；留着 = 两套机制并存，表不再是唯一豁免面 |
| 413 | 六短语 entry 级正则 → B | **删，换查表** | 本条主体 |
| 416 | `rootWidth/rootHeight` ≤5% → C | **不动** | 数值容差，非散文 |

**外加桶 1**：`rootRadius` 改比**有效半径**

```
effective(r, w, h) = min(r, min(w, h) / 2)
```

两侧各按自己的盒子算：actual 用量到的 `rootWidth/rootHeight`，expected 用 Figma 的期望值。
**盒子拿不到时（`suppressRootSize` / mode-skip / 非数字）退回字面比较**（fail closed —— 不因为量不到就放行）。
落地后 Switch 那 4 条变**真 PASS**，不进表。

---

## 4. 表的形态

落点 `figma-data/audit-allowlist/render-excused-fields.json`（与 `variables-staleness-ack.json` / `canonical-exempt.json` 同目录，形态照抄前者已验证的「带到期日、到期即红」）。

```jsonc
{
  "component": "Drop down List/Item",              // === report entry 的 figmaName，精确匹配
  "field": "rootFillHex",                          // 必须 ∈ buildChecks 派生的字段闭集
  "entryScope": "*--disabled-yes-hover-yes--*",     // manifestId glob（* 通配）；见下方收窄规则
  "reason": "…",                                    // 散文，给人读 —— 不再是判据
  "fixDirection": "extract-pipeline | harness-verifier | code-fix-deferred | code-first-divergence | figma-schema-gap",
  "divergenceRef": "translation/divergences.md#dropdown-disabled-hover-preview-state",  // 可选
  "addedAt": "2026-08-07",
  "reviewBy": "2027-02-07"
}
```

### 4a. 判据与注释的分界（本条的全部要点）

**判据 = `component` + `field` + `entryScope` 三个结构化字段。`reason` 怎么写都不影响红绿。**

这是路线 A 与被否的路线 B 的唯一实质差别：B 仍然让人写的句子决定红绿，只是把作用域从 entry 缩到 field；A 让句子彻底退出判据。

### 4b. `entryScope` 收窄规则

- 默认必须**语义收窄到今天真实命中的面**，写 `*` 需在 `reason` 里单独说明为什么整组件该字段都豁免。
- **放大必须写出来**：某行的 glob 命中数 > 今天失败的 entry 数时，差额即放大面，`reason` 必须解释。
- 一个 (component, field) 对若有**两个不同成因**（如 §2e 的 DropDownList `rootFillHex`），**拆成两行**，各带各的 `reason` / `fixDirection`。

### 4c. `reviewBy` 策略

| `fixDirection` | 默认 `reviewBy` | 理由 |
|---|---|---|
| `code-fix-deferred` | **+90 天** | 自认的债，不是限制 —— 短闹钟 |
| 其余四类 | +180 天 | 限制类，复核周期可长 |

**无例外**：每行都必须有 `reviewBy`，`code-first-divergence` 也不豁免（到期时重新确认那条 divergence 仍是 owner 想要的）。到期即红。

---

## 5. 四个调用面怎么共用同一张表

新模块 `tests/visual-verify/lib/render-excused-fields.ts` —— 加载 + 校验 + 匹配，module 级缓存：

```ts
export function loadExcusedFields(): ExcuseRow[]   // 校验失败即 throw（fail closed）
export function getExcusedFields(): ExcuseRow[]    // 缓存
export function isExcused(rows, entry, field): boolean
```

`classifyFailedCheck(entry, check, excuses = getExcusedFields())` —— 默认参数走缓存，单测注入 fixture。

四个面因此自动同表：

| # | 面 | 怎么拿到表 |
|---|---|---|
| ① | 库 API `drift-compare-core.ts` | 就是它自己 |
| ② | React 链 `tests/render-verification-react/react-drift-full.spec.ts` | 调的是**同一个** `buildChecks` —— 已核实：`:204` 那份副本是 `status` 表达式，**不是**分类逻辑 |
| ③ | 单测 `tests/render-report-summary.test.ts` | 注入 fixture |
| ④ | CLI 闸 `scripts/audit-render-drift-gate.mjs` | **不碰分类**，只读数字，见 §6 |

⛔ **只做「同表」，不做「同闸」** —— React 链的闸是 [[INFRA-F100]]，且那条 entry 明写「别照抄 Vue 那份闸，先判两条链判据是否本就该不同」。

---

## 6. 闸加什么（S5–S7）

`audit-render-drift-gate.mjs` 头注释有一条 ⛔：**测量量在 spec 侧算、闸只读数字，别让闸自己走 `report.entries` 派生**（否则判据在两文件各写一遍，将来加第二种基础设施失败时闸会把它当真测量 → 静默失效）。据此分两类：

| 判据 | 需要 report？ | 落点 | 内容 |
|---|---|---|---|
| **S5** 表 schema 合法 | 否 | 闸直接做 | 字段名 ∈ 闭集 · `addedAt`/`reviewBy` 是 ISO 且 `addedAt` 不在未来 · 必填齐全 · `entryScope:"*"` 必须有 `reason` |
| **S6** 无 stale 行 | 是 | `summarize()` 算，闸读数字 | `summary.excuse.unmatched.length === 0`，否则 FAIL 并点名要求删行 |
| **S7** 无过期行 | 否 | 闸直接做 | 任一行 `reviewBy < 今天` → FAIL |

`summarize()` 新增输出：

```ts
summary.excuse = { rows: number, matched: number, unmatched: string[], excusedChecks: number }
```

### 6a. fail-closed 矩阵

| 输入形态 | 结果 |
|---|---|
| 表文件缺失 | **FAIL**（不是「没有豁免所以放行」，也不是「读不到所以跳过」） |
| JSON 解析失败 / 顶层不是数组 | **FAIL** |
| 任一行不合 schema（含字段名拼错成另一个真字段名） | **FAIL** |
| entry 匹配不到任何行 | 空列表（最严），**绝不视为通配** |
| 表为 `[]` | 合法。此时所有 B 转 A → 闸红。**表空着是终态不是待办** |

### 6b. 不动的

- **S1–S4（INFRA-F97 的测量量下界）原样保留，一个字不动。**
- **`BASELINE_A` 保持 0，一格不抬。**
- enforcement 层级仍是 **L5**：`.gitea/workflows/pr-checks.yml`（1 处）+ `.github/workflows/ci.yml`（2 处）。
- ⛔ **不改**「render-gate 进不了 `prepublishOnly`」的格局（Gitea runner 非特权装不了 chromium，既有事实），收尾时也别宣称改了。

---

## 7. 落地当天的存量（实测，非估算）

304 条 fail = **300 条进表** + **4 条转真 PASS**。

| 桶 | check 数 | 基线行数 | `fixDirection` |
|---|---|---|---|
| 1 Switch `rootRadius` | 4 | **0** | 改 clamp 判据 → 真 PASS |
| 2 Tooltips extract gap | 90 | 9 | `extract-pipeline` |
| 3 DropDownListSelect | 7 | 2（按成因拆成 **3**，见 §2e） | `figma-schema-gap` + `divergenceRef` |
| 4 Button URL-link | 14 | 4 | `code-first-divergence` |
| **新** Slider（3）/ Message（2） | 20 | 5 | `code-fix-deferred`（90 天） |
| 其余：switch padding×4 + fill×1（拆成 **2**）· Step/Item 1 · Chart 4 · Breadcrumb/Item 1 · Tab List 2 · Tab/Item 1 | 163 | 14 | 逐行按实际性质 |
| R3 迁入（Table `rootBorderHex`） | 6 | 1 | `harness-verifier` |
| **合计** | **300** | **37（实测）** | — |

**为什么曾给两个行数（施工前的估算，现已由实测取代）**：按 (component, field) 建行本估算 **35** 行；§4b 要求按成因拆行，已知两处要拆 —— switch `rootFillHex`（`enable-no` ×12 + `status-live/enable-yes` ×2）与 DropDownList `rootFillHex`（disabled+hover ×4 + is-parent ×2）→ **37 行**。**落地当天（2026-08-07）实测行数 = 37**，与拆后估算逐字相同，未再出现第三处成因分叉。最终行数仍由闸 S5 每次运行自印，本文档数字不作为运行时判据，只作历史记录。

**35 行里 16 行需要写 `entryScope` glob**，其余 19 行今天零放大可写 `*`。两处**已知且接受的放大**必须在 `reason` 里写明：

| 行 | 放大 | 为什么接受 |
|---|---|---|
| `Chart @ textFillHex` | +1（donut/light 今天 pass） | ECharts 内联 SVG 不可内省是全 chart type 通用限制，那一条 pass 是偶然 |
| `Step/Item @ textFillHex` | +4（vertical+icon 组合） | 同一个 `textTargetSelector` 缺陷，命中与否取决于该变体有没有渲染出 icon |

### 7a. 桶 4 的 divergence 登记（owner 2026-08-07 拍板路线 2）

在 `src/design-system/translation/divergences-decisions.json` 新增一条**第 6 类（用户显式 code-first）**：
「URL link 不由 canonical Button 承载」，注释必须含 `user approved code-first 2026-08-07`。
表里 4 行 `divergenceRef` 回指它。

被否的路线 1（canonical Button 补 `style=link` 变体）不做，理由：那句既有警告已承认 typography/underline 不实现，padding 与 hover 底色跟着不实现是**同一决定的三个面**；只补一半会让 4 条 `textFillHex` 照样留豁免行，变成两种理由并存。将来要真做变体：删 divergence + 删表行，闸自己宣布。

---

## 8. 被否掉的候选（**别重新提议**）

| 候选 | 否掉的理由 |
|---|---|
| **路线 B**：把散文正则收成 field 级 | 它把 `padding/fill/border/radius…` 这组词表**制度化成判据**，而判据依赖人写的措辞 —— 一句 `harness limitation across all checks` 就能无声重开免疫（今天已有 63 条走这条路）。§2c 给出第二个独立理由：措辞可以同时既指名字段又承认自己是 bug |
| **路线 C**：只加降级量上限 S8 | owner 已定「不作终态」。本条一次落到位后它是冗余判据，且会在 manifest 正常扩容（930→936 已发生过）时误报 |
| **行键 = (component, field)，不带 scope** | 最省（34 行），但 `Drop down List/Item @ textFillHex` 一行就把 81 条 entry 的文字颜色永久放走 —— 这是今天 entry 级免疫的小一号翻版，只是从「整条 entry」缩到「整个组件的某字段」 |
| **行键 = 精确 manifestId 列表** | 零放大最严，但 300 条 check 摊成上百行；manifest 一扩容新 entry 就变红，而红的是**已知的老问题** → 闸天天喊狼，第一次扩容就会有人把它关掉 |
| **保留 399 行的 placeholder 分支** | 今日命中 0 = 死分支，且它正是本条要清的散文锚定形态 |
| **把 407 行的 border-collapse 留在代码里** | §2a 实证其意图已失效；留着 = 豁免面有两个真源 |
| **顺手修 Slider / Message 的 code** | 本条交付物是判据。修它们各自是独立的组件保真工作（Message 那条要动 token 命名或补 dark override，不是一行）。表里带 90 天闹钟，让它们从「藏在措辞里」变成「可见的债」 |
| **顺手给 React 链建闸** | [[INFRA-F100]]，且那条明写别照抄 Vue 那份 |

---

## 9. 怎么证明它不是空过

三组，缺一不可。

### 9a. 阴阳对照（用今天真实的数据）

⚠️ **2026-08-07 实施期订正 —— 原判据写错了，照它做会追一个不可能达成的目标。**
原文写的是「`rootFillHex #0b2b13 → #ff00ff`（荒谬值）落地后必须判 A」。**不成立**：字段级豁免的语义是「**这个字段**在这些 entry 上豁免」，豁免的是字段不是值，所以表内字段喂任何值都仍是 B —— 那是设计本身，不是漏网。把值级语义误安到字段级设计上是原文的错。

真正能证明 entry 级免疫已死的，是**同一条 entry 上的表外字段**（实测取 `message--status-success--size-l--closable-true--…--theme-dark`，该 entry 的 warnings 含旧短语）：

| 输入 | 落地前 | 落地后（2026-08-07 实测） |
|---|---|---|
| **表内**字段 `rootFillHex` + 荒谬值 `#ff00ff` | `B` | `B` ← 字段级豁免的定义 |
| **表内**字段 `rootFillHex` + 真实值 `#f1fbf3` | `B` | `B` |
| **表外**字段 `rootGap 8 → 999` | `B` | **`A`** ✅ |
| **表外**字段 `rootPadding.left 16 → 0` | `B` | **`A`** ✅ |
| **表外**字段 `rootBorderHex null → #ff0000` | `B` | **`A`** ✅ |

⇒ 「同一 entry 的其它字段还能不能变红」这个问题，落地前对这批 entry 的答案是**不能（无论改成什么值）**，落地后是**能**。这才是本条要买的东西。

⚠️ **探针的运行方式**：不能用 `node --experimental-strip-types` 直接跑 —— `drift-compare-core.ts` 用的是无扩展名 import（`./render-excused-fields`），靠打包器解析，Node 原生 ESM 解析不了。**用 vitest 跑临时测试文件**（放 `tests/` 下、跑完删）。

### 9b. 不带修复的致败探针（逐条必须转红）

| 注入 | 期望 |
|---|---|
| 删掉表里任一行 | 对应 check 转 A → 闸 exit 1 |
| 某行 `reviewBy` 改成昨天 | **S7** 红 |
| 某行 `field` 写成不存在的名字 | **S5** 红 |
| 某行 `field` 写成**另一个真字段名**（拼错的危险形态） | **S5** 通过、**S6** 红（该行不再命中） |
| 表文件改成 `[]` | **S6** 点名全部行 stale + 300 条转红 |
| 表文件删除 / 写成 `{}` / 写坏 JSON | **S5** 红（fail closed） |

⚠️ **2026-08-07 实施期订正 —— 「表文件改成 `[]`」那一行的期望数字原写不精确，照它核对会得出「探针没完全命中」的误判。**
原写「300 条转 A」。**实测（Task 6 Step 6）是 `A_TRUE_DRIFT_CANDIDATE=294` + `C_BOUNDARY_CASE=6`**，二者相加仍是拆解前的 300——原 `B_RESIDUAL_SCHEMA_GAP=300` 全部被拆开，但其中 6 条（Tooltips `rootWidth` 偏差 2.3%）落进分类器**既有的、与豁免表无关**的 `≤5%` 边界容差分支，而不是全部落进 A。这不是判据空过：闸依然强烈转红（`exit 1`，`A_TRUE_DRIFT_CANDIDATE=294 > baseline 0`），只是精确数字的口径是 `294+6=300`，不是单一的 `300→A`。原文把"字段级豁免消失后这批 check 会变成什么"简化成了单一分类，而实际分类器在无豁免时仍然会做它自己的容差判断——这一步不该被压掉。

复原后必须 `git diff` 无残留、report sha 与基线一致。

### 9c. 探针名纪律

造故障用的组件名 / 字段名 / 文件名先 `grep` 确认**全仓零命中**，避免表与探针互相自引用（pickup §4 第 3 件；同类实证见 memory `fault-probe-name-must-be-absent-from-repo`）。

---

## 10. 边界（不做什么）

- 不给 React 链建闸（同表 ≠ 同闸）。
- 不修 Slider / Message / Button 的 code。
- 不动 `prepublishOnly` 格局。
- 不加路线 C 的 S8。
- 不动 S1–S4，不抬 `BASELINE_A`。
- 不改 `figma-data/render-verification-manifest.json` 的生成逻辑。

---

## 11. 本条派生、但**不在本条做**的独立项

| 项 | 形状 | 去处 |
|---|---|---|
| Slider `rootWidth` 240 | ~~`.slider__input` 缺显式宽度，警告自陈~~ 🔴 **2026-08-28 整条推翻** | 本行原诊断与修法方向**都是错的**：`.slider__input` **有** `width:100%`，不缺显式宽度；240 是 `.slider` wrapper 的宽，而 code 那里**逐字就是 240**、零 drift ⇒ 「差 20%」只在拿轨道比 root frame 的**对照物错配**下才成立。owner 2026-08-28 裁定按 harness-verifier 修判据侧：generator 的 `rootTargetSelector` 改 `.slider` + 补 `gapTargetSelector`，实测 `rootWidth`/`rootGap` 转绿、两行豁免已删。⛔ **别照本行原文去给 `.slider__input` 加 `width:240px`** —— 那会让闸转绿而渲染破相（子元素溢出自己的 grid area 48px）。终态与新暴露的 `rootHeight` 一问见 [[INFRA-F106]] |
| Message dark 主题 token | `--prompt-*-bg` / `--prompt-*-text` 未 theme-varied | ✅ **2026-08-07 已立项** [[INFRA-F107]]，表里 2 行 `code-fix-deferred` + `reviewBy 2026-11-05` |
| Table 警告句里 `Tracked as CANONICAL-029` 已失效 | 那条债已还、entry 已删档 | 迁行时顺手把 `reason` 写准（本条内，属改注释不改判据） |
| React 链无闸 | 已立项 | [[INFRA-F100]] |
| `audit:render-coverage-gaps` 零挂载 | full-tree 的 9.7% 覆盖无人守（`nodeChecks total=3034 pass=295 gaps=2739`，见 §2） | ⚠️ **2026-08-07 订正**：原写「backlog F104 已如实登记，独立处置」，但 F104 落地后已按仓库约定从 `backlog.md` 删档，该指针会悬空。此事实**不是新发现、也不属本条待办**——`audit:render-coverage-gaps` 脚本自身头注释与 [`render-coverage-gaps.md`](../../internal/render-coverage-gaps.md) 已自陈"report-only，退出码恒 0"，本条只是复述这一既有边界，未新增待做动作。若要真正收口（把 full-tree 覆盖率纳入门禁），需要独立设计问题（确定性 Figma↔DOM 子元素对应），未立项。 |
