# Prompt — Phase 6.6a · FormItem.label 双形态

> **触发方式**：用户 → executor：`请按 docs/internal/_prompts/phase-6.6a-formitem-label-dual-form.prompt.md 执行`
> **角色**：executor
> **plan owner**：Claude Code（已写完 spec 澄清 + 本 prompt）
> **关联**：v0.4.0 Sprint A（pickup `next-session-pickup-2026-05-15-v0-4-0.md` §5）

---

## 1. 背景 + 重要 spec 澄清

### 1.1 决议出处

[`src/design-system/translation/divergences.md`](../../../src/design-system/translation/divergences.md) §`## API 双形态映射` → `### FormItem.label ↔ Figma Label SLOT`
[`src/design-system/translation/divergences-decisions.json`](../../../src/design-system/translation/divergences-decisions.json) entry `id: formitem-label-dual-form`

### 1.2 spec 澄清（plan owner 已审）

**原 JSON `codeSide`** 措辞：`"string prop or default slot; slot wins if both exist."`

**问题**：FormItem 的 **default slot 已用于 content/control 区**（[`src/components/FormItem/FormItem.vue:111-145`](../../../src/components/FormItem/FormItem.vue#L111-L145) — 渲染 input / textarea / radio / checkbox / switch）。**default slot 不能既做 content 又做 label。**

**澄清后的实施 spec**：
- `label` prop（string）保留——简单文本场景
- **新增 `#label` 命名 slot**——复杂内容（图标、富文本）
- **default slot 保持原语义**——content/control 区域，不动
- **优先级**：当 `#label` slot 存在时，slot 内容渲染替代 `label` prop
- **required 星号**：保持 label 元素外层渲染，不进 slot（视觉契约不变）

**precedent**：Element Plus `<el-form-item>` 用 `#label` 命名 slot，default slot 给 control（同一范式）；本仓库 [`src/canonical/PopupBox.vue:35`](../../../src/canonical/PopupBox.vue#L35) + [`src/components/Tooltip/Tooltip.vue:23`](../../../src/components/Tooltip/Tooltip.vue#L23) 已用 `useSlots` idiom，模式成熟。

执行本 prompt 时**按澄清后的命名 slot 路线**实施；同时把 `divergences-decisions.json` + `divergences.md` 的措辞同步更新（见 §3）。

---

## 2. Scope

实施 FormItem.label 双形态（**FormItem only**——Tooltip 走 Sprint B）。

### 2.1 改动文件清单

| # | 文件 | 改动 |
|---|---|---|
| 1 | `src/components/FormItem/FormItem.vue` | 加 `useSlots` + `#label` 命名 slot 渲染 |
| 2 | `src/canonical/FormItem.vue` | 加 `#label` slot 透传 |
| 3 | `src/design-system/translation/divergences-decisions.json` | patch `formitem-label-dual-form` entry: codeSide + notes |
| 4 | `src/design-system/translation/divergences.md` | patch `### FormItem.label` 段措辞 |
| 5 | `src/design-system/translation/prop-aliases.json` | 新增 entry `vue-ecosystem-addition-007`（`slot:label` for FormItem）|
| 6 | `tests/FormItem.test.ts` | **新建** vitest — 3 个 case |
| 7 | `playground/docs/pages/FormItemPage.vue` | 加 1 个 "Custom Label Slot" demo block |

**禁止**：
- 不改 Tooltip（Sprint B 处理）
- 不改 Badge（Sprint C / Phase 6.7 处理）
- 不删既有 `label` prop（向后兼容）
- 不改 default slot 语义
- 不改 figmaAttrs / 视觉 css

---

## 3. 精确实施 spec

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

**Import 加 useSlots**：

```diff
-import { computed } from 'vue'
+import { computed, useSlots } from 'vue'
```

**`<script setup>` 段加**（建议放在 `const props = ...` 之后、`const isDark = ...` 之前）：

```ts
const slots = useSlots()
const hasLabelSlot = computed(() => Boolean(slots.label))
```

**`<template>` 段——label 渲染**改造（line 105-108 附近）：

```diff
       <label class="form-item__label" :class="{ 'form-item__label--dynamic': labelWidth === 'Dynamic' }">
-        <span>{{ labelText }}</span>
+        <span v-if="hasLabelSlot"><slot name="label" /></span>
+        <span v-else>{{ labelText }}</span>
         <span v-if="required" class="form-item__required">*</span>
       </label>
```

**注意**：
- `labelText` computed 的 switch+right-aligned 冒号后缀逻辑只在 prop 路径有意义；slot 路径让 caller 自己决定是否带冒号——这是有意为之，不要让 slot 也走 `labelText`
- required 星号保持在外层 label 元素，slot/prop 都共用

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

`<template>` 段：

```diff
 <template>
   <BaseFormItem v-bind="{ ...props, ...figmaAttrs }">
+    <template v-if="$slots.label" #label>
+      <slot name="label" />
+    </template>
     <slot />
   </BaseFormItem>
 </template>
```

**不需要**改 script 段（slot 透传不需要 useSlots 显式声明，`$slots` 模板内置即可）。

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

定位 `id: "formitem-label-dual-form"` entry，patch 三个字段：

```diff
 {
   "id": "formitem-label-dual-form",
   "category": "dual-form-mapping",
   "component": "FormItem",
   "components": null,
   "subject": "FormItem.label ↔ Figma Label SLOT",
   "figmaSide": "Label is a Figma SLOT.",
-  "codeSide": "Code should support string prop or default slot; slot wins if both exist.",
+  "codeSide": "Code supports string prop or named #label slot; named slot wins when both exist. Default slot reserved for content/control area.",
   "status": "approved-dual-form-mapping",
   "reason": "Simple text should use prop; complex content should use slot.",
   "resolvedAt": null,
   "resolutionRef": null,
   "phase": "Phase 6.6",
   "verifyHint": null,
-  "notes": "Implementation pending."
+  "notes": "Phase 6.6a (2026-05-14): FormItem named #label slot implemented. Default slot kept as content/control area (Element Plus convention). Tooltip part still pending — Sprint B."
 }
```

**不要**改 `category` / `status` / `resolvedAt` / `resolutionRef`——整个 dual-form decision 要等 Tooltip 也做完才整体 flip resolved（在 v0.4.0 release wrap-up 做）。

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

定位 `### FormItem.label ↔ Figma Label SLOT` 段（约 line 226-236），整段替换为：

```markdown
### FormItem.label ↔ Figma Label SLOT

- Figma form: `SLOT`
- Code form: 双形态（`string` prop 或 named `#label` slot）
- 实现规则：
  - 简单文本 → 用 prop（如 `label="Form Label"`）
  - 复杂内容（含图标、富文本）→ 用 `#label` 命名 slot
  - 同时存在时命名 slot 优先
  - **default slot 保持原语义**（content/control 区域），不冲突
- Status: ✅ implemented in Phase 6.6a (2026-05-14); Tooltip part Phase 6.6b pending
- **决议详情**：[`divergences-decisions.json`](./divergences-decisions.json) `id: formitem-label-dual-form`
```

### 3.5 `src/design-system/translation/prop-aliases.json`

定位 `entries` array，**追加**一个新 entry（建议放在 `vue-ecosystem-addition-006` 后紧邻位置）：

```json
{
  "id": "vue-ecosystem-addition-007",
  "scope": "vue-ecosystem-addition",
  "component": "FormItem",
  "components": null,
  "codeName": "slot:label",
  "figmaName": "Label SLOT",
  "canonicalAxis": null,
  "derivedValue": null,
  "figmaComponentSet": null,
  "diffType": null,
  "canonicalPropState": null,
  "canonical": null,
  "props": null,
  "aliasScope": null,
  "status": "vue-ecosystem",
  "notes": "Phase 6.6a: named #label slot 双形态搭配 label prop；slot 优先。"
}
```

**注意**：
- 这是 array 追加，必须保持 JSON 整体合法
- 不要动 `vue-ecosystem-addition-006` 现有内容（`slot:default` 跨 5 组件那个 entry）
- 同时更新 array 中所有可能存在的 `lastId` / counter（grep 一下 prop-aliases.json 顶部是否有 metadata 字段）；如无则忽略

### 3.6 `tests/FormItem.test.ts`（**新建**）

参考 [`tests/Radio.test.ts`](../../../tests/Radio.test.ts) 既有 vitest 范式（@vue/test-utils `mount` + assertion）。最小 3 个 case：

```ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import FormItem from '../src/canonical/FormItem.vue'

describe('FormItem.label dual-form', () => {
  it('renders label as text via prop', () => {
    const wrapper = mount(FormItem, { props: { label: 'Email' } })
    expect(wrapper.find('.form-item__label').text()).toContain('Email')
  })

  it('renders #label slot when provided', () => {
    const wrapper = mount(FormItem, {
      props: { label: 'Fallback' },
      slots: { label: '<strong>Rich Label</strong>' },
    })
    const label = wrapper.find('.form-item__label')
    expect(label.html()).toContain('<strong>Rich Label</strong>')
    expect(label.text()).not.toContain('Fallback')
  })

  it('renders required asterisk regardless of slot/prop form', () => {
    const wrapperPropForm = mount(FormItem, { props: { label: 'A', required: true } })
    expect(wrapperPropForm.find('.form-item__required').exists()).toBe(true)

    const wrapperSlotForm = mount(FormItem, {
      props: { required: true },
      slots: { label: '<span>B</span>' },
    })
    expect(wrapperSlotForm.find('.form-item__required').exists()).toBe(true)
  })
})
```

**禁止扩 case**：不要再加什么 layout / status / theme 的 case，那不是本 sprint 范围。

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

加一个新 demo block "Custom Label Slot"。位置：找一个语义合适的位置（如 "Basic Usage" 之后 / "Layout" 之前），最小 demo：

```vue
<DocSection title="Custom Label Slot" desc="复杂 label 内容用 #label 命名 slot；default slot 仍为 content 区。">
  <DocPreviewBox>
    <FormItem>
      <template #label>
        <Icon name="info" :size="14" /> Email <span style="color: var(--orange)">(必填)</span>
      </template>
    </FormItem>
  </DocPreviewBox>
  <DocCodeBlock lang="vue">
&lt;FormItem&gt;
  &lt;template #label&gt;
    &lt;Icon name="info" :size="14" /&gt; Email
  &lt;/template&gt;
&lt;/FormItem&gt;
  </DocCodeBlock>
</DocSection>
```

**自适应**：
- 如果 FormItemPage.vue 的现有 demo block 模式跟上面不一样（用 `<DemoBlock>` / `<PreviewBox>` / 别的命名），照已有范式来；上面只是 placeholder
- 如果 Icon 已 import 复用即可；没 import 则按 page 顶部既有 import 风格加
- demo 内容不要追求好看，只要展示 slot 用法即可

如果该 page 用了某种自动生成 demo 框架，且加 demo 会导致冲突，**STOP 跟 plan owner 报告**，不强行塞。

---

## 4. Verify

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

```bash
# 1. 单元测试（新增 FormItem.test.ts 必跑过；其它 test 不能 regress）
pnpm test
# Expected: 109 passed | 1 skipped (108 + 1 new + 1 skipped — 但具体计数依赖 tests/FormItem.test.ts 是几个 it)
# Strictly: vitest exit 0, FormItem describe block 全绿, 其它原有 17 个 test file 不能新报 fail

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

# 3. translation-completeness audit（spec 文档动了必跑）
pnpm run audit:translation-completeness
# Expected: 9 findings / 0 active / 9 allowlisted（同 baseline）。
# 注意：FormItem.label 改 implemented 不应触发新 finding（仍 approved-dual-form-mapping）

# 4. 完整 release pipeline
pnpm run prepublishOnly
# Expected: 10 strict gates 全通过
```

任一失败 → STOP，列出失败 stdout，不要 commit。

---

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

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

给 plan owner 报：

1. 改动文件清单 + 各自 +/- 行数（`git diff --stat`）
2. `pnpm test` 完整 stdout 最后 20 行（含 test counts）
3. `pnpm run audit:translation-completeness` stdout
4. `pnpm run prepublishOnly` 最后 20 行（应见 10 strict gates pass + Components 38 matched + Icons diff=0）
5. 任何"我犹豫了但没改"的边界 case（如 docs page demo 范式不一致、prop-aliases 没找到合适插入点、等）
6. **不要写 commit message、不要 commit SHA**——commit 是 plan owner / 用户事

plan owner 复审通过 + 用户 ack 后，由用户（或 plan owner 经用户授权）执行 commit；之后才推荐进 Sprint B（Tooltip.content）。

---

## 7. 边界 case 提醒

- **若 `slots.label` 检查失败**：可能 vitest setup 环境 `useSlots` 行为差异。先确认 `useSlots()` 返回的 `slots.label` 在 prop-only mount 时是 `undefined`，slot mount 时是 function。如有偏差，用 `Boolean($slots.label)` 模板内 `$slots` 替代，统一从 template 检测（不依赖 setup script）
- **若 `FormItemPage.vue` 用代码生成机制（如 vue-component-meta）**：加 demo 不能写死代码块，按 page 既有 pattern 走
- **若 prop-aliases.json 顶部有 `lastId` / `counter` / metadata**：同步更新（grep 一下文件前 30 行确认）
- **不要顺手 reformat 任何文件其它部分**——只做本 prompt 列举的精确改动

完。
