# Prompt — Phase 6.8 · Button `canonicalTheme` axis 完成迁移

> **触发方式**：用户 → executor：`请按 docs/internal/_prompts/phase-6.8-button-canonical-theme.prompt.md 执行`
> **角色**：executor
> **plan owner**：Claude Code（已做 Sprint A.1 baseline + A.2 SoT cleanup；已拍板 Path A；本 prompt 是 Sprint A.3）
> **关联**：v0.5.0 Sprint A.3（pickup `next-session-pickup-2026-05-15-v0-5-0.md` §4）

---

## 1. 背景

### 1.1 决议出处

[`src/design-system/translation/divergences-decisions.json`](../../../src/design-system/translation/divergences-decisions.json) entry `id: button-canonical-api-migration`
[`src/design-system/translation/divergences.md`](../../../src/design-system/translation/divergences.md) "Button 双 API" 段

### 1.2 实证数字（ground truth — 不可用历史估算替代）

Figma Button 真源（2026-05-15 `jq` 实证）：
- 8 主 sets：`Button/dark L|M|S|XS` + `Button/light L|M|S|XS` ← **dark/light 是 theme axis**
- 1 url link set（4 variants）
- 20 孤立 `IconLoadingButton*` 节点（Phase 6.8 **不在**范围）
- 总计 29 entries / 3,224 variants

**ButtonBridge 现有 7 axis**（icon / style / color / status / radius / fixedWidth / size）已覆盖 figma 6 axis + size。
**唯一缺口 = `canonicalTheme` axis**（对应 figma `Button/dark` vs `Button/light` 组件集拆分）。

### 1.3 Path A 拍板（2026-05-15 用户 ack）

- ButtonBridge + base Button 各加 theme axis
- theme 类型：`'dark' | 'light'`，默认 inject('docsTheme', ref('dark')) 兜底
- 旧 API（`variant` / `size` / `disabled` / `loading`）加 `@deprecated` JSDoc
- decisions JSON `button-canonical-api-migration` status flip → `resolved`

---

## 2. Scope

**改动文件清单**（共 7 个）：

| # | 文件 | 改动 |
|---|---|---|
| 1 | `src/canonical/ButtonBridge.vue` | 加 `theme?: Theme` prop + inject fallback + resolvedTheme computed + 传 `:canonical-theme` |
| 2 | `src/components/Button/Button.vue` | 加 `CanonicalTheme` 类型 + `canonicalTheme?` prop + CanonicalContract.theme + resolvedCanonical.theme + figmaAttrs.data-figma-theme + legacy props @deprecated JSDoc |
| 3 | `tests/Button.test.ts` | 追加 3 个 canonicalTheme vitest case |
| 4 | `src/design-system/translation/divergences-decisions.json` | flip `button-canonical-api-migration` entry → resolved |
| 5 | `src/design-system/translation/divergences.md` | patch "Button 双 API" 段 status 行 |
| 6 | `docs/internal/runtime-additions.md` | 新增 `## Button.canonicalTheme` section |
| 7 | `playground/docs/pages/ButtonPage.vue` | 更新 bridgeCoverage + buttonApiRows + 加 Deprecated API 注释段 |

**禁止**：
- 不改任何 Figma 文件
- 不删 `variant` / `size` / `disabled` / `loading` 旧 props（仅加 @deprecated；v0.6.0 才真删）
- 不删 LEGACY_VARIANT_TO_CANONICAL、CANONICAL_TO_LEGACY_VARIANT、LEGACY_SIZE_TO_CANONICAL、CANONICAL_SIZE_TO_LEGACY 映射表
- 不改 Button 视觉 CSS（palette / geometry / fixedWidth 计算）
- 不改孤立的 20 个 `IconLoadingButton*` 节点（设计师 owner）
- 不改 `Button/url link` set（不在 Phase 6.8 范围）
- 不改任何 audit 脚本、RELEASING.md、CHANGELOG.md（release wrap-up 阶段的事）
- 不 commit / push / stash

---

## 3. 精确实施 spec

### 3.1 `src/components/Button/Button.vue`

#### 3.1.1 新增类型

在已有 `type CanonicalSize = 'XS' | 'S' | 'M' | 'L'` 行**之后**追加：

```ts
type CanonicalTheme = 'dark' | 'light'
```

#### 3.1.2 更新 `CanonicalContract` 类型

在 `CanonicalContract` 类型定义的最后一个字段 `size: CanonicalSize` 之后加：

```diff
 type CanonicalContract = {
   color: CanonicalColor
   style: CanonicalStyle
   radius: CanonicalRadius
   fixedWidth: CanonicalFixedWidth
   status: CanonicalStatus
   icon: CanonicalIcon
   size: CanonicalSize
+  theme: CanonicalTheme
 }
```

#### 3.1.3 更新 `defineProps` — 加 `canonicalTheme` + `@deprecated` JSDoc

在 `defineProps<{...}>()` 的泛型类型里：
1. 给旧 legacy props 加 `@deprecated` JSDoc
2. 在 `canonicalSize?: CanonicalSize` 行**之后**加 `canonicalTheme?: CanonicalTheme`

```diff
 const props = withDefaults(
   defineProps<{
+    /** @deprecated since 0.5.0, use canonicalStyle + canonicalColor instead. Removed in v0.6.0. */
     variant?: LegacyVariant
+    /** @deprecated since 0.5.0, use canonicalSize instead. Removed in v0.6.0. */
     size?: LegacySize
+    /** @deprecated since 0.5.0, use canonicalStatus='disable' instead. Removed in v0.6.0. */
     disabled?: boolean
+    /** @deprecated since 0.5.0, use canonicalStatus='loading' + canonicalIcon='loading' instead. Removed in v0.6.0. */
     loading?: boolean
     canonicalColor?: CanonicalColor
     canonicalStyle?: CanonicalStyle
     canonicalRadius?: CanonicalRadius
     canonicalFixedWidth?: CanonicalFixedWidth
     canonicalStatus?: CanonicalStatus
     canonicalIcon?: CanonicalIcon
     canonicalSize?: CanonicalSize
+    canonicalTheme?: CanonicalTheme
   }>(),
   {
     variant: 'fill-green',
     size: 'm',
     disabled: false,
     loading: false,
   }
 )
```

#### 3.1.4 更新 `resolvedCanonical` computed

在 `resolvedCanonical` 的 return object 最后（`size: ...` 行之后）加：

```diff
   return {
     color: props.canonicalColor ?? legacyContract.color,
     style: props.canonicalStyle ?? legacyContract.style,
     radius: props.canonicalRadius ?? 'square',
     fixedWidth: props.canonicalFixedWidth ?? 'no',
     status: ...,
     icon: ...,
     size: props.canonicalSize ?? LEGACY_SIZE_TO_CANONICAL[props.size],
+    theme: props.canonicalTheme ?? 'dark',
   }
```

#### 3.1.5 更新 `figmaAttrs` computed

在 `figmaAttrs` return object 最后（`'data-figma-size'` 行之后）加：

```diff
   return {
     'data-figma-color': resolvedCanonical.value.color,
     'data-figma-style': resolvedCanonical.value.style,
     'data-figma-radius': resolvedCanonical.value.radius,
     'data-figma-fixed-width': resolvedCanonical.value.fixedWidth,
     'data-figma-status': resolvedCanonical.value.status,
     'data-figma-icon': resolvedCanonical.value.icon,
     'data-figma-size': resolvedCanonical.value.size,
+    'data-figma-theme': resolvedCanonical.value.theme,
     'data-figma-opacity': surfaceOpacity.value,
   }
```

**注意**：`data-figma-opacity` 行不动，theme 加在 size 之后、opacity 之前。

---

### 3.2 `src/canonical/ButtonBridge.vue`

#### 3.2.1 import 加 inject + ref

现有：`import { computed } from 'vue'`

改为：

```ts
import { computed, inject, ref, type Ref } from 'vue'
```

#### 3.2.2 新增类型

在已有的 `type Style = 'filling' | 'ghost' | 'rimless'` 行之后加：

```ts
type Theme = 'dark' | 'light'
```

#### 3.2.3 更新 `defineProps` — 加 `theme?`

在 `style?: Style` 行之后加 `theme?: Theme`：

```diff
   defineProps<{
     color?: Color
     fixedWidth?: FixedWidth
     icon?: Icon
     radius?: Radius
     size?: Size
     status?: Status
     style?: Style
+    theme?: Theme
   }>(),
   {
     color: 'gray 1',
     fixedWidth: 'no',
     icon: 'no',
     radius: 'square',
     size: 'M',
     status: 'default',
     style: 'filling',
   }
```

**不要**为 `theme` 加 withDefaults 默认值——inject fallback 负责兜底（见下一步）。

#### 3.2.4 加 inject + resolvedTheme computed

在 `const props = ...` 声明之后、`const figmaAttrs = ...` 之前加：

```ts
const injectedTheme = inject<Readonly<Ref<'dark' | 'light'>>>('docsTheme', ref('dark'))
const resolvedTheme = computed<'dark' | 'light'>(() => props.theme ?? injectedTheme.value)
```

#### 3.2.5 更新 `figmaAttrs`

在 figmaAttrs return object 加 `'data-figma-theme'`（加在 `'data-figma-style': props.style` 之后）：

```diff
 const figmaAttrs = computed(() => ({
   'data-figma-component': 'Button',
   'data-figma-source-layer': 'components-tokenized',
   'data-figma-color': props.color,
   'data-figma-fixed-width': props.fixedWidth,
   'data-figma-icon': props.icon,
   'data-figma-radius': props.radius,
   'data-figma-size': props.size,
   'data-figma-status': props.status,
   'data-figma-style': props.style,
+  'data-figma-theme': resolvedTheme.value,
 }))
```

#### 3.2.6 更新 `<template>` — 传 `canonical-theme`

在 `:canonical-style="style"` 行**之后**（`<slot name="prefix">` 之前）加：

```diff
   <RuntimeButton
     v-bind="figmaAttrs"
     :canonical-color="color"
     :canonical-fixed-width="fixedWidth"
     :canonical-icon="icon"
     :canonical-radius="radius"
     :canonical-size="size"
     :canonical-status="status"
     :canonical-style="style"
+    :canonical-theme="resolvedTheme"
   >
```

---

### 3.3 `tests/Button.test.ts`（追加到文件末尾 `})` 之前）

在最后一个 `it(...)` 的右括号 `})` 之后、外层 `describe('Button', () => {` 的闭合 `})` 之前，**追加 3 个 case**：

```ts
  it('defaults data-figma-theme to dark when canonicalTheme is not provided', () => {
    const wrapper = mount(Button, {
      props: {
        canonicalColor: 'green',
        canonicalStyle: 'filling',
      },
      slots: { default: 'Save' },
    })
    expect(wrapper.attributes('data-figma-theme')).toBe('dark')
  })
  it('sets data-figma-theme to light when canonicalTheme="light"', () => {
    const wrapper = mount(Button, {
      props: {
        canonicalColor: 'green',
        canonicalStyle: 'filling',
        canonicalTheme: 'light',
      },
      slots: { default: 'Save' },
    })
    expect(wrapper.attributes('data-figma-theme')).toBe('light')
  })
  it('canonical bridge forwards theme axis to runtime data-figma-theme', () => {
    const wrapper = mount(CanonicalButton, {
      props: {
        color: 'green',
        style: 'filling',
        theme: 'light',
      },
      slots: { default: 'Save' },
    })
    expect(wrapper.attributes('data-figma-theme')).toBe('light')
  })
```

**注意**：`CanonicalButton` 已在文件顶部 `import CanonicalButton from '../src/canonical/ButtonBridge.vue'` — 不需要重复 import。

---

### 3.4 `src/design-system/translation/divergences-decisions.json`

定位 `id: "button-canonical-api-migration"` entry，patch 4 个字段：

```diff
 {
   "id": "button-canonical-api-migration",
   ...
-  "status": "approved-migration-plan",
+  "status": "resolved",
   ...
-  "resolvedAt": null,
+  "resolvedAt": "2026-05-18",
-  "resolutionRef": null,
+  "resolutionRef": "TBD — fill with commit SHA after plan owner commit",
   "phase": "Phase 6.8",
   "verifyHint": null,
-  "notes": "Add canonicalTheme, document canonical API, deprecate old API for one version, then remove."
+  "notes": "Phase 6.8 (2026-05-18): canonicalTheme axis added to ButtonBridge.vue + Button.vue; legacy props (variant/size/disabled/loading) @deprecated since 0.5.0, removed in v0.6.0."
 }
```

**不要**改 `category` / `component` / `subject` / `figmaSide` / `codeSide` / `reason` / `phase` 等其它字段。

---

### 3.5 `src/design-system/translation/divergences.md`

找到 "Button 双 API" 段（grep `Button 双 API` 或 `button-canonical-api-migration`）。**只改 status 行**：

将该段的 status 行从：

```markdown
- Status: approved-migration-plan — Phase 6.8 pending
```

改为（措辞类似，按实际找到的内容匹配）：

```markdown
- Status: ✅ resolved in Phase 6.8 (2026-05-18) — canonicalTheme axis added; legacy API @deprecated since v0.5.0, removed in v0.6.0
```

如果该段没有独立的 status 行，就在段末追加上面这行。**不要**改段落其余内容。

---

### 3.6 `docs/internal/runtime-additions.md`

在文件末尾（`## Input.XL（待决）` 段之后）追加新 section：

```markdown

## Button.canonicalTheme

- 来源：Figma Button 组件集拆分为 `Button/dark` 和 `Button/light` 两族（8 主 sets = dark/light × L/M/S/XS）
- 决策：作为 `canonicalTheme` prop 暴露；ButtonBridge.vue inject 'docsTheme' 兜底
- API：`canonicalTheme?: 'dark' | 'light'`，默认 `inject('docsTheme', ref('dark'))`
- 行为：控制 `data-figma-theme` 属性（用于 figma 溯源）；实际色彩渲染由 CSS token 响应 `data-theme` 切换
- ✅ 已实现于 Phase 6.8 (2026-05-18)
```

---

### 3.7 `playground/docs/pages/ButtonPage.vue`

#### 3.7.1 更新 `bridgeCoverage` 数组的 "Axes covered" item

找到以下对象（在 `bridgeCoverage` 数组里）：

```ts
{
  label: ['Axes covered on this page', '当前页面已覆盖的轴'],
  value: 'icon / style / color / status / radius / fixed width',
  note: [
    'All non-size axes from Button/dark M are intentionally visible here.',
    'Button/dark M 除 size 以外的全部轴，都在这一页里被显性展示。',
  ],
},
```

把 `value` 字段改为（加 `/ theme`）：

```ts
  value: 'icon / style / color / status / radius / fixed width / theme',
```

同时把 note 改为：

```ts
  note: [
    'All non-size axes from Button/dark M are intentionally visible here. Theme axis added in Phase 6.8.',
    'Button/dark M 除 size 以外的全部轴，都在这一页里被显性展示。theme 轴在 Phase 6.8 加入。',
  ],
```

#### 3.7.2 在 `buttonApiRows` 数组末尾加 `theme` prop entry

找到 `buttonApiRows` 数组，在最后一个 entry（`size`）的 `}` 之后、`]` 之前追加：

```ts
  ,{
    name: 'theme',
    description: ['Theme axis for Figma component set selection. Defaults to the global docsTheme injection.', '对应 Figma Button dark/light 组件集切换。默认从 docsTheme 全局注入；传入显式值可覆盖。'],
    type: `'dark' | 'light'`,
    defaultValue: `inject('docsTheme', 'dark')`,
  }
```

#### 3.7.3 在 `usageCode` 示例中加 theme 一行

找到 `const usageCode = \`...\`` 模板字符串，在 `size="M"` 行之后、`>` 之前加：

```diff
 const usageCode = `import { Button } from '@nancyzeng0210/tvu-design-system'

 <Button
   color="green"
   style="filling"
   radius="square"
   status="default"
   icon="left"
   fixed-width="yes"
   size="M"
+  theme="dark"
 >
   Primary
 </Button>`
```

#### 3.7.4 在 script 段顶部的 Type 定义区加 `CanonicalTheme`

找到 `type CanonicalSize = 'XS' | 'S' | 'M' | 'L'` 行，**之后**加：

```ts
type CanonicalTheme = 'dark' | 'light'
```

同时在 `ButtonContract` 类型里加 `theme: CanonicalTheme`（放在 `style` 之后）：

```diff
 type ButtonContract = {
   color: CanonicalColor
   fixedWidth: CanonicalFixedWidth
   icon: CanonicalIcon
   radius: CanonicalRadius
   size: CanonicalSize
   status: CanonicalStatus
   style: CanonicalStyle
+  theme: CanonicalTheme
 }
```

---

## 4. Verify

依次跑下面命令，**每条都要 exit 0 + 无新 fail**：

```bash
# 1. 单元测试（3 个新 case 必绿；原有 17 file 不能 regress）
pnpm test
# Expected: 117 passed | 1 skipped（原 114 + 3 新 case；如 skipped 数变化说明有问题）

# 2. 类型检查
pnpm exec vue-tsc --noEmit
# Expected: 0 errors

# 3. translation-completeness audit（decisions JSON 动了必跑）
pnpm run audit:translation-completeness
# Expected: 9 findings / 0 active / 9 allowlisted（同 baseline；resolvedAt 改不触发新 finding）

# 4. 完整 release pipeline
pnpm run prepublishOnly
# Expected: 10 strict gates 全通过（含 audit:translation-completeness gate）
```

任一失败 → **STOP**，列出失败 stdout + 你的判断，**不要 commit**。

---

## 5. STOP + 报告（**不要 commit**）

verify 4 项全绿后 **STOP，不 commit、不 push、不 stash**——按 AGENTS.md §标准闭环 step 5-7，diff 留工作树由 plan owner 复审 + 用户 ack 后才 commit。

给 plan owner 的报告格式：

```markdown
## Phase 6.8 完成报告

### Diff 状态
`git diff --stat` 输出（**未 commit**）：
[stat 输出]

### 改动文件验收清单
- [ ] F1 src/canonical/ButtonBridge.vue（inject + resolvedTheme + canonical-theme 透传）
- [ ] F2 src/components/Button/Button.vue（canonicalTheme prop + @deprecated + resolvedCanonical.theme + data-figma-theme）
- [ ] F3 tests/Button.test.ts（+3 canonicalTheme case，原有 case 全绿）
- [ ] F4 divergences-decisions.json（button-canonical-api-migration status=resolved）
- [ ] F5 divergences.md（Button 双 API 段 status 行更新）
- [ ] F6 runtime-additions.md（+## Button.canonicalTheme section）
- [ ] F7 playground/docs/pages/ButtonPage.vue（bridgeCoverage + buttonApiRows + usageCode）

### 测试结果
- pnpm test: [passed count] passed | [skipped count] skipped
- pnpm exec vue-tsc --noEmit: [0 errors / 报错行]
- pnpm run audit:translation-completeness: [X findings / Y active / Z allowlisted]
- pnpm run prepublishOnly: [10 strict gates 全通过 / fail]

### 边界 case / plan owner 决策项
[列任何犹豫：
- ButtonPage.vue 某段我改了但不确定的地方
- divergences.md 未找到精确目标行的处理方式
- vitest inject 行为是否需要 provide mock
- ...]

### 未做（按 §6 边界）
- ❌ commit / push / stash
- ❌ 删 legacy props（只加 @deprecated）
- ❌ 改 audit 脚本 / RELEASING.md / CHANGELOG.md
```

---

## 6. 严格不做的事（executor 边界）

- ❌ **commit / push / stash / amend** 任何 git 操作
- ❌ 删 `variant` / `size` / `disabled` / `loading` legacy props（@deprecated 而非删除；v0.6.0 才真删）
- ❌ 删 `LEGACY_VARIANT_TO_CANONICAL` 等 mapping 表
- ❌ 改 Button 视觉 CSS（palette / geometry / fixedWidth）
- ❌ 改 `Button/url link` 相关逻辑（不在范围）
- ❌ 改 audit 脚本（`figma-sync/audit-*.mjs`）
- ❌ 改 RELEASING.md / CHANGELOG.md（release wrap-up 阶段的事）
- ❌ 改 STATUS.md / tracker.md（release wrap-up 阶段的事）
- ❌ 自行 provide mock for inject（vitest mount 时 inject 会 fallback 到 `ref('dark')`，无需模拟）
- ❌ 编造 "项目 contract / convention / rule" 当 adapt 理由（参考 memory `feedback_review-result-vs-rationale`）
- ❌ 超范围顺手优化其它文件（不在上面 7 个文件清单内的不动）

如发现 spec 跟现状不一致（如 ButtonPage.vue 的 bridgeCoverage / buttonApiRows 结构跟本 prompt 描述对不上）→ **STOP** 报告实际结构，不自决修正。

---

## 7. 完成后 STOP

把报告交给 plan owner，由 plan owner 复审 + 用户决定：

- 是否 commit Phase 6.8（commit message 将由 plan owner / 用户生成）
- commit 后 plan owner 回填 `resolutionRef` SHA 到 divergences-decisions.json（可在 wrap-up commit 一并做）
- 是否同步启动 Sprint B（Tier 1-B mockup audit 并行——0 文件冲突）
- 是否进 v0.5.0 release wrap-up（需 Sprint B + Tier 2-E/F 全完成）

**绝对不在 executor 阶段做的事**：commit / push / tag / changeset / publish / STATUS sync / tracker sync。
