# Prompt: DS 合并 Phase 3 — Sync A 组件卡生成器（迁移 6 + 生成 30，canonical props）（交 Codex 执行 · Claude 复核）

- **日期**：2026-07-23
- **分工（PROJECT_MAP §6）**：Claude 出本 prompt + 复核；Codex 执行代码；Owner 审本 prompt 后才交 Codex。
- **STOP 约定**：Codex 执行完 **STOP**，列出改动 + 未验证项 + 风险，等 Owner/Claude 复核。**不要 commit、不要 push。**
- **上位 plan**：`docs/superpowers/plans/2026-07-23-ds-merge-two-systems.md` §Phase 3。**前置 = Phase 1 spike 裁决**（已定，见下）与 Phase 2（无耦合，可独立跑）。

## ⚙️ 执行环境
- **工作树（唯一）**：`/Users/nancy/Documents/AICoding/VS_Code/tvu-design-system`，分支 `master`。执行前 `pwd` 自证。
- **包管理**：`pnpm`。测试 `pnpm vitest run <file>`。生成器 `.mjs` 若读 `.ts`/`.vue` 数据，按现有 gen 脚本模式跑（先 `grep -n '"gen' package.json` 照抄，别自造 loader）。

## 🔑 Phase 1 运行时裁决（已定，勿再 spike）
**组件卡模板走 `window.TvuDS.<Name>` React 封装 + `React.createElement`**（沿用现有 Sync A 卡风格），**不是**实例化裸 `<tvu-*>` WC。
- 依据：`.design-sync/NOTES.md` §「Merge-plan runtime spike verdict」（2026-07-23 亲验）——`window.TvuDS.<Name>` 是 react-pilot React 函数组件，`React.createElement(TvuDS.Button,{fill:'filling',color:'green'},'Start')` 渲染成功、`<tvu-button>` CE upgrade、props 透传为 DOM property。
- 运行时载入（卡 head）：merged `_vendor/react.js`（同时给 `window.React` + `window.ReactDOM`）→ `_ds_bundle.js`（自注册 WC + `window.TvuDS`）→ `_ds_bundle.css`（`:root` token）。**`_vendor/react-dom.js` 是 26 字节 stub，别单独引。**

## 目的
Sync A（`bfca04be`）当前 6 张组件卡是远端手作、prop 名有 drift（见 Phase 0 审计）。本 Phase：用**本地 canonical 真值**重新生成全 **35** 组件卡（6 迁移 + 30 新增，**不含 Icon**——Icon 归 Phase 2 基础层），建在 repo 标准运行时上。**只产出生成器 + 卡到本地 outDir，不上传**（Phase 5 主 session 上传）。

## 硬约束
1. **只新建下列文件**，不改 `src/`、`figma-data/`、现有 `scripts/*`（除 `package.json` 加一行）、gate。
2. **prop 名/值以 `src/canonical/*.vue` 真源为准，绝不照抄远端 Sync A 卡**（已确认 drift）。见下「canonical 真值 + Prop gotchas」。
3. **demo 借鉴本地 `.design-sync/previews/<Name>.tsx`**（35 个 committed、canonical-correct、已 grade good），**不要**去够远端 Sync A 卡（DesignSync 仅主 session，你拿不到，且它们有 drift）。previews 里的 JSX props 就是正确 prop 名/值范例。
4. **两级路径卡**：`<outDir>/components/<Name>/<Name>.html`，卡头首行 `<!-- @dsCard group="Components" name="…" subtitle="…" -->`，head 引 **root 运行时**（`../../_ds_bundle.js`、`../../_vendor/react.js`、`../../_ds_bundle.css` 等——注意两级层深相对路径 `../../`）。
5. **运行时就绪再渲染**：`whenReady`/轮询 `window.TvuDS` 就绪后 `React.createElement(TvuDS[Name], demoProps, children)`（context-required 子组件在父内组合，见 gotchas）。
6. **TDD**：先写失败测试 → FAIL → 实现 → PASS。断言「卡只用 canonical 合法 prop 名、不含 drift 名」。
7. 完成后 **STOP**，不 commit / 不 push。

## 交付物 / 接口
- **新建** `scripts/lib/component-card-template.mjs` — `buildComponentCard(name, demo)`：产两级卡 HTML（`@dsCard group="Components"` 头 + head 引 root 运行时 + `whenReady` 渲染脚本）。
- **新建** `scripts/data/component-card-demos.mjs` — 导出 `demos`：每组件的策展 demo 配方（`{ props, children, examples[] }`），**prop 名/值取自 canonical + previews**。
- **新建** `scripts/gen-sync-a-component-cards.mjs` — `genSyncAComponentCards({outDir})`：写全 35 卡 + 复制 root 运行时（`_ds_bundle.js`/`_ds_bundle.css`/`styles.css`/`_vendor/*`/`_card.css`）到 `<outDir>`；`--out=` CLI；`package.json` 加 `"gen:sync-a-cards": "node scripts/gen-sync-a-component-cards.mjs"`。
- **新建** `test/gen-sync-a-component-cards.test.mjs` — TDD 断言（见下）。

## 35 组件清单（`.design-sync/config.json` `componentSrcMap`，Icon 不在内）
Badge, Breadcrumb, BreadcrumbItem, Button, Chart, CheckBox, DropDownListSelect, FormItem, Input, InputBoxFilled, InputNumber, Logo, MenuList, Message, Notification, Pagination, PillCounter, PillStatus, PopupBox, Progress, Radio, Rating, SelectBoxFilled, SelectBoxLine, Slider, StepItem, Steps, Switch, Tab, TabItem, TabList, Table, Tooltip, TopBar, UserMenu。
（其中 6 个与 Sync A 现有卡重叠 = Badge/Button/MenuList/TopBar/UserMenu + 从远端迁的其余；其余为新增。全部用 canonical 真值生成，不区分新旧。）

## canonical 真值 + Prop gotchas（Claude 已对活源核实，直接用）
**真源** = `src/canonical/*.vue`（对外 API）+ `.design-sync/NOTES.md` §「Prop gotchas」+ `.design-sync/previews/*.tsx`。Codex 每个组件**必须 Read 对应 `src/canonical/<Name>.vue` 的 `defineProps` + 对应 `.design-sync/previews/<Name>.tsx`** 确认 prop 名/值，别凭印象。

Phase 0 审计确认的 6 个 drift（**生成时必须用右侧 canonical 真值**）：

| 组件 | ❌ drift（远端卡）| ✅ canonical 真值 |
|---|---|---|
| Badge | `tag` | **`fill`**（`Filled`/`Line`）；且**不得**把会话运行态（Live/Preview/OnAir/Inactive）揽进 Badge——运行态归 PillStatus |
| Button | `variant` | **`fill`**（`filling`/`ghost`/`rimless`）|
| Icon | `spritePath`、四值 `size` | （Icon 归 Phase 2 基础层，本 Phase 不产 Icon 卡）|
| MenuList | item.`type: section\|divider` | item **无** `type` 字段 |
| UserMenu | `items[]`、`defaultOpen` | **`actions[]`** + **`languages[]`**（无 `items`/`defaultOpen`）|
| TopBar | （符合，无 drift）| — |

其余高频 gotchas（`.design-sync/NOTES.md` §Prop gotchas 全文，摘要）：
- Input/InputBoxFilled/SelectBox* 禁用用 `enable="off"`（非 bool）；**InputNumber 例外**用真 `disabled` bool + `type` stepper 枚举（`Default`/`Only Add`/`Only Reduce`/`Readonly`）。
- Tab 家族（Tab/TabList/TabItem）：`fill`（非 `type`）、`color`（非 `property2`）、`state`（非 `property1`，`Normal`/`Active`）。
- Steps/StepItem：`type`（`number`/`icon`，非 `stepStyle`）；`index` **0-based**（previews 已 0-based）。
- `FormItem.type` 的 `'Label & checkbox'` 是小写 `c`——别"纠正"。
- `Rating.value` 是**字符串** `'1'..'5'`；`Switch.status` 三态 `off/on/live`（live 红）；`CheckBox.status` `some`=indeterminate。
- `DropDownListSelect` items 需 5 键 `{label,value,checked,active,disabled}`；`Slider` 需显式宽度包裹、`size` 仅 `M/S`；SelectBox* `size` 仅 `M/L`。
- context-required 子组件**在父内组合**：BreadcrumbItem→Breadcrumb、TabList/TabItem→Tab、StepItem→Steps。
- 交互态（hover/focus/open-dropdown/multi-select/editable）静态不渲染，**不作为卡的默认展示**（同 previews 的取舍）。

## TDD 断言（照 plan §Phase 3 Step 1，可扩展）
```js
// test/gen-sync-a-component-cards.test.mjs
import { test, expect } from 'vitest'
import { buildComponentCard } from '../scripts/lib/component-card-template.mjs'
import { demos } from '../scripts/data/component-card-demos.mjs'

test('Button card uses fill not variant', () => {
  const html = buildComponentCard('Button', demos.Button)
  expect(html).toContain('fill:'); expect(html).not.toContain('variant:')
})
test('Badge card uses fill and claims no session run-state', () => {
  const html = buildComponentCard('Badge', demos.Badge)
  expect(html).toContain('fill:')
  expect(html.toLowerCase()).not.toMatch(/on-?air|=live|preview 运行/i)
})
test('UserMenu card uses actions[] + languages[], not items/defaultOpen', () => {
  const html = buildComponentCard('UserMenu', demos.UserMenu)
  expect(html).toContain('actions:'); expect(html).toContain('languages:')
  expect(html).not.toContain('defaultOpen'); expect(html).not.toContain('items:')
})
// 分批扩展：每组件加断言「该卡不含任何 canonical 之外的 prop 名」
```

## 执行步骤（照 plan §Phase 3 Step 1–8，去掉 commit）
1. 写上面失败测试 → `pnpm vitest run test/gen-sync-a-component-cards.test.mjs` FAIL。
2. 定义 6 个重叠组件 demo 配方（canonical 真值）in `component-card-demos.mjs`。
3. 实现 `buildComponentCard`（据 Phase 1 裁决产两级卡 + `whenReady` React.createElement）→ 相关测试 PASS。
4. 为其余组件写 demo 配方：每个先 Read `src/canonical/<Name>.vue` + `.design-sync/previews/<Name>.tsx`，给 2–4 个代表性示例。**分批 TDD**，每批加「无 canonical 外 prop 名」断言。
5. 组装 `gen-sync-a-component-cards.mjs` → 写全 35 卡 + 复制 root 运行时到 `<outDir>`；加 `package.json` script。
6. 本地渲染回归：`pnpm gen:sync-a-cards --out=scratch/sync-a-out`，起静态服务（或 Playwright headless）**逐卡确认真的渲染出组件**（尤其 6 迁移卡视觉不回退）。**关键纪律：用了组件≠渲染对了**——必须核 `<tvu-*>` 有 shadowRoot/内容，别看暗底 contact sheet 就判 good（NOTES §CRITICAL 教训）。**`scratch/` 不提交。**

## STOP — 完成后停，报告以下，等复核
- 新建文件清单 + `git diff --stat`（4 新建 + `package.json` 一行）。
- `pnpm vitest run test/gen-sync-a-component-cards.test.mjs` 实际输出（全 PASS）。
- 35 组件 demo 配方里**实际用到的 prop 名清单**（证明全 ∈ canonical，无 drift 名残留）。
- 本地渲染回归证据：至少 6 迁移卡 + 抽样 5 新增卡的渲染截图/Playwright 断言（`<tvu-*>` shadowRoot 有内容）。
- 任何组件 canonical 与 previews 不一致、或 prop 无法确定处（别猜，报告交 Claude）。

## Claude 复核清单（Codex STOP 后）
- [ ] 无越界：只碰 4 新建 + `package.json` 一行；`src/`/canonical/previews 未改。
- [ ] 全 35 卡 `@dsCard group="Components"`，两级路径 + `../../` 运行时相对路径正确。
- [ ] **零 drift prop 名**：亲 grep 生成产物无 `variant:`/`tag:`（Badge/Button）/`items:`/`defaultOpen`（UserMenu）/MenuList item `type:`/`spritePath`。
- [ ] 6 drift 组件确用 canonical 真值；context 子组件在父内组合。
- [ ] 亲跑 vitest 复核全 PASS；断言是真覆盖非空跑。
- [ ] 渲染回归证据真实存在（截图/断言 `<tvu-*>` shadowRoot 有内容），非暗底 contact sheet 蒙混、非文字声明。
- [ ] Icon **未**被误产成组件卡（归 Phase 2 基础层）。
