# UserMenu 角色化下拉（code-first）+ tvu-icon CE Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 给 DS UserMenu 加两个通用具名槽（`#panel-top` 承载任意产品内容、`#badge` 承载 name 旁徽标），让消费者按角色喂数据实现角色化下拉；顺带给 Icon 加 `tvu-icon` custom-element 出口（闭合 INFRA-F69①）。

**Architecture:** 全 additive、code-first。角色差异化 = consumer 传不同 `actions` 数据 + 往通用槽投影产品内容，DS 零产品语义。UserMenu/Icon 经 CE + React binding 分发（不进 `src/index.ts` Vue barrel）。register.ts / React wrappers / types.ts 由 `gen:react-bindings` 从 `components.config.ts` 派生生成。Vue docs 页与 React demo 共享同一个 `descriptor`（single source），故 framework-parity 天然一致。

**Tech Stack:** Vue 3.5 `defineCustomElement`、`@vue/compiler-sfc`、React（react-pilot island）、vitest、Playwright（真机视觉门）、husky pre-commit gate 链、changesets。

## Global Constraints

> 每个 task 的要求都隐含包含本节。值逐字取自 spec。

- **全部 additive**：不删/改任何现有 prop / event / slot（v1.0 API 已锁定，API_STABILITY.md）。
- **零产品语义烘焙**：不加 role 枚举预设菜单；`role` prop 保持可选、纯展示文字 badge。菜单差异由 consumer `actions` 数据驱动。
- **颜色只用 token**（硬规则 #4）：无 hardcoded hex，沿用 `--sp-*` / `--line-*` / `--r-*` / `--text-*` / `--brand`。
- **code-first 唯一合法通道**：divergence 条目 reason 必带 `user approved code-first 2026-07-21`（FIGMA_AS_SOURCE_OF_TRUTH.md）。
- **canonical 禁用 `$slots`/`useSlots()`**：必须用 `useHasSlot('<name>')`（`src/canonical/composables/useHasSlot.ts`，slot-guard 闸强制）。
- **register.ts / react-pilot/src/wrappers/*.tsx / wrappers/types.ts 是 AUTO-GENERATED**：绝不手改；改 `components.config.ts` 后跑 `pnpm gen:react-bindings` 重生。
- **不动 `src/index.ts`**（divergence `logo-menulist-usermenu-code-first-2026-07-02` 已定：这些 code-first 组件 CE+React binding 分发，不进 published Vue 包）。
- **Icon CE shadowRoot 默认 true**（自包含、`currentColor` 经 color 继承穿 shadow 边界；不需 light-DOM）。
- **单一原子 commit**：因 demo-slot-boolean + slot-guard + binding-config-parity + VISUAL_COMMIT_APPROVED 联动，整个特性必须一次性接线齐全、经 owner 视觉批准后一个 commit 落地。**Task 1-6 只构建 + 本地校验，不 commit；唯一 commit 在 Task 7（owner `VISUAL_COMMIT_APPROVED=1` 后）。**
- **executor 不自 commit**：Task 1-6 结束只报 diff + gate/typecheck stdout，由 plan owner 复审 + owner ack。

---

### Task 1: `tvu-icon` CE（config 条目 + 生成 + parity 实证）

**Files:**
- Modify: `src/web-components/components.config.ts`（在 `COMPONENT_CONFIGS` 数组末尾、`]` 前追加 Icon 条目）
- Generated (勿手改，由脚本产出/覆盖): `src/web-components/register.ts`、`react-pilot/src/wrappers/Icon.tsx`、`react-pilot/src/wrappers/types.ts`

**Interfaces:**
- Produces: `tvu-icon` custom element（props `name: string` / `size: number` / `color: string`）；React wrapper `<Icon name size color/>`；`TvuIcon` 导出 + `customElements.define('tvu-icon', TvuIcon)`。

- [ ] **Step 1: 在 components.config.ts 追加 Icon 条目**

在 `COMPONENT_CONFIGS` 数组内（建议紧挨 UserMenu/MenuList 附近，靠近其它 code-first composed 组件）追加：

```ts
  // -------------------------------------------------------------------------
  // Icon — registry SVG renderer. CE 出口供 React/CE 消费者使用 TVU 图标
  // (INFRA-F69①: 无 tvu-icon 时 React demo 图标静默 0×0 不渲染)。
  // Source: src/components/Icon/Icon.vue (非 canonical — 基础图元, 无独立 Figma 组件)
  // defineProps: name, size, color。无 boolean prop / 无 slot → 不进 demo-slot-boolean scope。
  // shadowRoot 默认 true: 自包含 SVG(v-html) + scoped style; currentColor 经 color 继承穿 shadow。
  // -------------------------------------------------------------------------
  {
    name: 'Icon',
    tag: 'tvu-icon',
    canonicalImport: 'Icon',
    canonicalPath: '../components/Icon/Icon.vue',
    figmaCodeComponent: 'Icon',
    props: [
      { name: 'name',  tsType: 'string' },
      { name: 'size',  tsType: 'number' },
      { name: 'color', tsType: 'string' },
    ],
    events: [],
    vModel: null,
    hasDefaultSlot: false,
    namedSlots: [],
  },
```

- [ ] **Step 2: 重生 React binding + register**

Run: `pnpm gen:react-bindings`
Expected: stdout 报写入 `react-pilot/src/wrappers/Icon.tsx` + 重写 `register.ts` + `types.ts`；exit 0。

- [ ] **Step 3: 验证 register.ts 生成了 tvu-icon**

Run: `grep -n "tvu-icon\|TvuIcon" src/web-components/register.ts`
Expected: 出现 `export const TvuIcon = defineCustomElement(Icon)` + `customElements.define('tvu-icon', TvuIcon)`（若模板还生成了 `import Icon from '../components/Icon/Icon.vue'` 一并可见）。

- [ ] **Step 4: 【风险实证】binding-config-parity 对非 canonical 路径 SFC 的解析**

Run: `pnpm audit:binding-config-parity`
Expected: PASS（Icon 条目与 `src/components/Icon/Icon.vue` 的 `defineProps` 对齐）。
**若 FAIL**（审计假定 SFC 在 `src/canonical/`、解析不到 Icon 的 props）：STOP，改用二选一 fallback，并在 diff 报告里说明：
  - (a) 建薄壳 `src/canonical/Icon.vue`（`<script setup>` 直接 re-export base Icon 的 props/透传，类似其它 canonical bridge），config `canonicalImport`/`canonicalPath` 指向它；或
  - (b) 若审计脚本本就按 `canonicalPath` 字段解析（只是路径不同），确认无需改动。
先读 `scripts/audit-binding-config-parity.mjs` 确认它按 `canonicalPath` 还是硬编码 `src/canonical/` 解析，再决定。

- [ ] **Step 5: typecheck 生成物**

Run: `pnpm vue-tsc --noEmit`（或 `npx vue-tsc --noEmit`）
Expected: 无与 Icon.tsx / types.ts / register.ts 相关的新报错。
**不 commit**（Global Constraints）。

---

### Task 2: UserMenu base + canonical 加 `#panel-top` / `#badge` 槽

**Files:**
- Modify: `src/components/UserMenu/UserMenu.vue`（base；template panel 内加两个 `<slot>` + 条件分隔线；无需改 script 逻辑，纯 template + style）
- Modify: `src/canonical/UserMenu.vue`（canonical bridge；用 `useHasSlot` + 透传 `<slot>` 到 base）

**Interfaces:**
- Consumes: base `MenuList`（已支持 `item.icon`）；`useHasSlot`（`../canonical/composables/useHasSlot.ts`，签名 `useHasSlot(name: string): ComputedRef<boolean>`）。
- Produces: UserMenu 具名槽 `panel-top`（header 下、actions 上）+ `badge`（name 旁）。

- [ ] **Step 1: base UserMenu.vue — name-row 加 `#badge` 槽**

在 `src/components/UserMenu/UserMenu.vue` 的 `.user-menu__name-row`（现为 name + role badge）内，name 之后加 badge 槽。现状：
```html
<div class="user-menu__name-row">
  <span class="user-menu__name">{{ name }}</span>
  <span v-if="role" class="user-menu__role">{{ role }}</span>
</div>
```
改为（badge 槽在 name 后、role badge 前；两者并存）：
```html
<div class="user-menu__name-row">
  <span class="user-menu__name">{{ name }}</span>
  <slot name="badge" />
  <span v-if="role" class="user-menu__role">{{ role }}</span>
</div>
```

- [ ] **Step 2: base UserMenu.vue — header 后加 `#panel-top` 槽 + 条件分隔线**

在 `.user-menu__header` 结束标签之后、第一个 `<template v-if="actions.length">` 之前，插入 panel-top 槽块。base 组件可直接用 `$slots`（slot-guard 只约束 **canonical**，不约束 base `src/components/*`）。插入：
```html
    </div>
    <!-- ↑ .user-menu__header 结束 -->

    <template v-if="$slots['panel-top']">
      <div class="user-menu__divider" />
      <div class="user-menu__panel-top">
        <slot name="panel-top" />
      </div>
    </template>
```
（其后保持原有 `<template v-if="actions.length">` … 语言 … Sign out 不变。）

- [ ] **Step 3: base UserMenu.vue — 加 `.user-menu__panel-top` 样式**

在 `<style scoped>` 内（`.user-menu__header` 附近）加：
```css
.user-menu__panel-top {
  padding: var(--sp-xs) var(--sp-s);
}
```
（仅提供内边距容器；具体产品内容样式由 consumer 投影内容自带。无 hardcoded hex。）

- [ ] **Step 4: canonical UserMenu.vue — useHasSlot + 透传槽**

`src/canonical/UserMenu.vue` 现为纯透传 wrapper（`<BaseUserMenu .../>`）。改为：
1. `<script setup>` 顶部 import：
```ts
import { useHasSlot } from './composables/useHasSlot'
```
（若已有其它 import，追加即可。不要用 `useSlots()`/`$slots`。）
2. 在 `<template>` 的 `<BaseUserMenu ...>` 上，把 named slot 透传进去（CE/Vue 下把 canonical 收到的 `badge`/`panel-top` 槽转发给 base）：
```html
  <BaseUserMenu
    :name="name"
    :email="email"
    :role="role"
    :image="image"
    :color="color"
    :actions="actions"
    :languages="languages"
    :lang="lang"
    data-figma-component="UserMenu"
    @action="(value, item) => emit('action', value, item)"
    @language-change="(value) => emit('languageChange', value)"
    @sign-out="() => emit('signOut')"
  >
    <template v-if="hasBadge" #badge><slot name="badge" /></template>
    <template v-if="hasPanelTop" #panel-top><slot name="panel-top" /></template>
  </BaseUserMenu>
```
3. `<script setup>` 内加（slot-guard 要求用 useHasSlot 而非 `$slots`）：
```ts
const hasBadge = useHasSlot('badge')
const hasPanelTop = useHasSlot('panel-top')
```

- [ ] **Step 5: 验证 canonical slot-guard + typecheck**

Run: `node scripts/audit-canonical-slot-guard.mjs`（或 pre-commit 里对应 script 名）
Expected: PASS（canonical UserMenu 用 useHasSlot，无 `$slots`/`useSlots`）。
Run: `pnpm vue-tsc --noEmit`
Expected: 无 UserMenu 相关新报错。**不 commit。**

---

### Task 3: UserMenu config 加 namedSlots + 重生 binding

**Files:**
- Modify: `src/web-components/components.config.ts`（UserMenu 条目 `namedSlots`）
- Generated: `react-pilot/src/wrappers/UserMenu.tsx`、`types.ts`、`register.ts`

**Interfaces:**
- Produces: UserMenu React wrapper 暴露 `panelTop` / `badge`（kebab→camel）first-class props + children slot 投影。

- [ ] **Step 1: 改 UserMenu 条目 namedSlots**

在 `src/web-components/components.config.ts` UserMenu 条目，把 `namedSlots: []` 改为：
```ts
    namedSlots: ['panel-top', 'badge'],
```
（`hasDefaultSlot` 保持 `false`。）

- [ ] **Step 2: 重生 React binding**

Run: `pnpm gen:react-bindings`
Expected: exit 0；`react-pilot/src/wrappers/UserMenu.tsx` 重写、出现 `panelTop` / `badge` slot 投影（`<div slot="panel-top" style={{display:'contents'}}>` 等）。

- [ ] **Step 3: 验证 wrapper 生成了槽**

Run: `grep -n "panel-top\|panelTop\|badge" react-pilot/src/wrappers/UserMenu.tsx`
Expected: 出现槽相关渲染。**不 commit。**

---

### Task 4: React demo descriptor（Vue+React 共享 single source）

**Files:**
- Create: `react-pilot/src/demos/descriptors/usermenu.ts`（导出 `usermenuDescriptor: ComponentDescriptor`）
- Create: `react-pilot/src/demos/UserMenu.tsx`（薄委托器）
- Create: `react-pilot/src/demos/usermenu-demo.css`
- Modify: `react-pilot/src/App.tsx`（import + 渲染）

**Interfaces:**
- Consumes: `ComponentDescriptor` schema（`./descriptors/schema.ts`：`{ component: string; sections: Section[] }`，card 类型含 `slotted-instance`{props,slots}、`interactive`{state,controls,target}、`code-block`、`axis-chips`）；`DemoRenderer`；`DemoProps`。
- Produces: `usermenuDescriptor`（被 React demo 和 Vue 页共同消费）。

- [ ] **Step 1: 先读模板 descriptor 确认 schema 用法**

Run: 读 `react-pilot/src/demos/descriptors/topbar.ts` + `react-pilot/src/demos/descriptors/schema.ts` + `react-pilot/src/demos/TopBar.tsx`（照抄 pattern；schema 字段以文件为准）。

- [ ] **Step 2: 建 usermenu.ts descriptor**

按 schema 建，含以下 sections（**这是本 task 的核心新内容 —— 角色差异化用数据驱动演示**）：
- **Section「角色差异化 / Role-differentiated menus」**：4 个 `slotted-instance` card（或 interactive），分别用不同 `actions` 数据演示 4 角色：
  - `Owner`：`actions=[{label:'Purchase Tokens',value:'purchase-tokens'},{label:'Manage Subscription Seats',value:'manage-seats'}]`，`name='Tim Wong'`,`email='twong@tvunetworks.com'`
  - `Finance`：`actions=[{label:'Purchase Tokens',value:'purchase-tokens'},{label:'Purchase Subscription Seats',value:'purchase-seats'}]`，`name='Devansh Rai'`,`email='drai@tvunetworks.com'`
  - `Admin`：`actions=[{label:'Assign Subscription Seats',value:'assign-seats'}]`，`name='Elizabeth Park'`,`email='epark@tvunetworks.com'`
  - `Member`：`actions=[]`（仅 Language + Sign out），`name='Amir Khan'`,`email='akhan@tvunetworks.com'`
  每个都带 `languages=[{label:'EN',value:'en'},{label:'中文',value:'zh'}]`、`lang='en'`。
- **Section「槽位 / Slots」**：
  - `#badge` 槽演示：`slots={ badge: <tvu-icon name="mark/vip" size={16}/> }`（premium 场景；**用 tvu-icon 验证 INFRA-F69① 闭合**）。若 `mark/vip` 非注册名，先 `grep -ri "vip\|crown" src/icons` 或 `resolveIcon` 确认正确 registry 名，用真名。
  - `#panel-top` 槽演示：投影一个示例产品内容块（普通 `el`/`div`，如一个"Balance 8,000"文本卡），示意 consumer 可放余额面板/告警条；**DS demo 不引入真实计费组件**，只用中性占位内容表达"任意内容"。
- **Section「事件 / Events」**（可选）：`interactive` card 演示点击 action / 语言切换 / sign-out 打日志。

Section 标题用 `LocalizedText`（`[en, zh]`）——与 Vue 页共享，故两侧标题天然一致（满足 demo-framework-parity）。

- [ ] **Step 3: 建 demos/UserMenu.tsx 委托器**

```tsx
import { DemoRenderer } from './DemoRenderer'
import { usermenuDescriptor } from './descriptors/usermenu'
import type { DemoProps } from './types'
import './usermenu-demo.css'

export default function UserMenuDemo(props: DemoProps) {
  return <DemoRenderer descriptor={usermenuDescriptor} {...props} />
}
```

- [ ] **Step 4: 建 usermenu-demo.css**

参照 `topbar-demo.css` 起手；仅放 demo 布局所需类（如 demo 卡间距）。无 hardcoded hex（用 token var 或中性布局属性）。

- [ ] **Step 5: App.tsx 登记**

- import（挨着 `import TopBarDemo from './demos/TopBar'`）：`import UserMenuDemo from './demos/UserMenu'`
- 渲染（`<main>` 内挨着 `<TopBarDemo .../>`）：`<UserMenuDemo theme={theme} locale="en-US" />`

- [ ] **Step 6: 构建 react-pilot 验证无 TS 错**

Run: `pnpm --filter ./react-pilot exec tsc --noEmit`（或仓库既有 react-pilot typecheck 命令；先看 package.json / react-pilot 有无 lint/build script）。
Expected: 无 usermenu 相关报错。**不 commit。**

---

### Task 5: Vue docs 页 UserMenuPage.vue + 注册链

**Files:**
- Create: `playground/docs/pages/UserMenuPage.vue`
- Modify: `playground/docs/navigation.ts`（`CanonicalPageId` union + nav item）
- Modify: `playground/docs/DocsShell.vue`（`registeredComponentPageIds` 数组 + `pageLoaders` map + `pageComponents` map，共 3 处）
- Modify: `docs/site-review-manifest.json`（新 entry）

**Interfaces:**
- Consumes: `usermenuDescriptor`（Task 4，经 `@demos/descriptors/usermenu`）；`DemoRenderer`、`FrameworkDemoRegion`（`../components/*`）；`text()` 双语 helper。
- Produces: docs 站 `usermenu` 页（侧栏可达）。

- [ ] **Step 1: 建 UserMenuPage.vue（照 TopBarPage.vue 骨架）**

先读 `playground/docs/pages/TopBarPage.vue` 全文照抄骨架，替换为 UserMenu：
- import `DemoRenderer`、`usermenuDescriptor`（`@demos/descriptors/usermenu`）、`FrameworkDemoRegion`、`@demos/usermenu-demo.css`。
- `defineProps<{locale?:string}>()` + `inject('docsTheme')` + `t()` helper（照 TopBarPage）。
- 三个 API 表数组（**逐字取自 canonical UserMenu 现有 API + 新槽**）：
  - `userMenuApiRows`（Attributes）：`name`/`email`/`role`/`image`/`color`/`actions`/`languages`/`lang`（类型/默认值取自 `src/canonical/UserMenu.vue` defineProps）。
  - `userMenuSlotRows`（Slots）：`panel-top`（"账户 header 下方、菜单上方的任意内容区；放余额面板/告警条等产品内容"）+ `badge`（"name 旁徽标位；放 premium 图标/角色标"）。
  - `userMenuEventRows`（Events）：`action(value,item)` / `languageChange(value)` / `signOut()`。
- `<template>`：`<FrameworkDemoRegion :loader="() => import('@demos/UserMenu')">` 包 `<DemoRenderer :descriptor="usermenuDescriptor" :theme :locale/>` + 三张 API 表（`v-for` 渲染上面数组），照 TopBarPage 结构。
- `<style scoped>` 基本空（demo 类在 usermenu-demo.css）。

- [ ] **Step 2: navigation.ts —— union + nav item**

- `CanonicalPageId` union 加 `| 'usermenu'`（挨着 `'topbar'`）。
- 在 TopBar 所在 nav group 的 `items[]` 里、`topbar` item 之后加：
```ts
{ id: 'usermenu', label: text('UserMenu', 'UserMenu 用户菜单'), title: text('UserMenu', 'UserMenu 用户菜单'),
  summary: text('Avatar dropdown: account header, role-differentiated actions, language toggle, sign out.',
                '头像下拉：账户头、角色化操作项、语言切换、退出登录。') },
```

- [ ] **Step 3: DocsShell.vue —— 3 处登记**

- `registeredComponentPageIds` 数组：加 `'usermenu'`（挨着 `'topbar'`；决定侧栏顺序）。
- `pageLoaders` map：加 `usermenu: () => import('./pages/UserMenuPage.vue'),`
- `pageComponents` map：加 `usermenu: createPageComponent(pageLoaders.usermenu),`
（三处都受 `satisfies Record<CanonicalPageId,…>` 约束，漏一处 vue-tsc 报错。）

- [ ] **Step 4: site-review-manifest.json —— 新 entry**

参照 TopBar entry，但 UserMenu 是 code-first 无独立 Figma 组件（对齐 canonical-exempt 措辞）：
```json
{ "pageId": "usermenu", "pageFile": "UserMenuPage.vue", "kind": "component",
  "expectedSourceType": "code-first",
  "baselineStatus": "n/a", "currentRenderingMode": "live",
  "pixelReviewStage": "pending",
  "views": { "statusMatrix": "pending", "useCases": "pending", "designSpec": "pending" } }
```
**先读 manifest 顶部 schema 注释 / 现有 code-first 组件 entry**（若 Logo/MenuList 已有 entry 用它们作准；Form 也是 code-first 可参照），字段名/枚举值以 manifest 实际 schema 为准，别硬套上面草稿。

- [ ] **Step 5: 起 docs 站验证页可达 + 渲染**

Run: 启 docs dev server（先 `lsof -i :<port>` 核实无并发同 config vite server 污染 `.vite` 缓存 — memory `reference_docs-dev-server-quirks`）。
Expected: 侧栏出现 UserMenu；页面渲染 4 角色 demo + 槽演示，无 vue-tsc / 控制台报错。**不 commit。**

---

### Task 6: 真源登记（divergence + affordance）

**Files:**
- Modify: `src/design-system/translation/divergences-decisions.json`（`logo-menulist-usermenu-code-first-2026-07-02` 条目）
- Modify: `docs/internal/component-affordances.json`（UserMenu entry 加槽说明）
- Generated: `docs/internal/component-affordances.md`（`pnpm generate:component-affordances`）

**Interfaces:**
- Produces: 登记 `#panel-top`/`#badge` 槽的 code-first 决策 + `user approved code-first 2026-07-21`。

- [ ] **Step 1: 更新 divergence 条目**

在 `logo-menulist-usermenu-code-first-2026-07-02` 的 `codeSide` 追加一句说明新槽；`reason` 追加：`2026-07-21 扩展：加通用具名槽 #panel-top（承载任意产品内容）+ #badge（name 旁徽标位），供 AI 按角色喂数据做产品设计；参考 MicroApps 产品 Figma 6289-4563（非 DS 库）。user approved code-first 2026-07-21`。
（保留原 2026-07-02 措辞；仅追加。若脚本校验 divergence schema，跑 `pnpm audit:translation-completeness` 确认。）

- [ ] **Step 2: 更新 affordance json + 重生 md**

先读 `docs/internal/component-affordances.json` UserMenu entry（第 ~1114-1144 行）确认有无 `slots` 字段。
- 若 schema 有 slots 字段：加 `panel-top` / `badge` 说明。
- `built_in_features` / `code_props` 若因加槽需补则补（加槽通常不改 props）。
Run: `pnpm generate:component-affordances`（重生 .md）
Run: `node scripts/audit-component-affordances.mjs`
Expected: PASS（json↔md↔canonical SoT 无 drift）。**不 commit。**

---

### Task 7: 全 gate 链 + 真机视觉门 + owner 批准 + 单一原子 commit

**Files:** 无新增；本 task 是验证 + commit。

- [ ] **Step 1: 跑完整 gate 链（拿真实 stdout）**

Run（逐条，拿原始 stdout，不概括）：
```
pnpm gen:react-bindings                              # 确认无待生成 drift（幂等）
pnpm vue-tsc --noEmit
pnpm test                                            # vitest
pnpm audit:binding-config-parity
pnpm audit:demo-slot-boolean
pnpm audit:demo-framework-parity
node scripts/audit-canonical-slot-guard.mjs
node scripts/audit-component-affordances.mjs
```
Expected: 全 PASS / exit 0。任一 FAIL → STOP，修到绿，报原始 stdout。
（`gen:react-bindings` 后若 `git status` 显示 register.ts/wrappers 有改动，说明生成物未提交——纳入本次 commit。）

- [ ] **Step 2: 真机视觉截图（VISUAL_COMMIT_APPROVED 前置）**

起 docs 站 + react-pilot island，用 Playwright 截真实渲染图：
- Vue 侧：UserMenu 4 角色下拉（Owner/Finance/Admin/Member 菜单项差异）+ `#panel-top` 投影 + `#badge`（👑）。
- React 侧：同上；**关键实证 INFRA-F69①**：在 React island 里 `customElements.get('tvu-icon')` 返回真值 + `<tvu-icon>` 渲染出非 0×0 的图标（DOM 探测 boundingRect）。
产物落项目内（memory `local-env-tool-quirks`），`ls`/`file` 核实截图真实存在（memory `verify-subagent-artifacts-exist`）。

- [ ] **Step 3: 呈交 owner 审 + 拿 VISUAL_COMMIT_APPROVED**

把 gate stdout + 截图给 owner。**owner 明确 `VISUAL_COMMIT_APPROVED` 后**才继续。

- [ ] **Step 4: 单一原子 commit**

```bash
git add -A
VISUAL_COMMIT_APPROVED=1 git commit -F - <<'MSGEOF'
feat(user-menu): role-differentiated dropdown via generic slots + tvu-icon CE

UserMenu: add #panel-top (arbitrary product content below the account header)
and #badge (indicator next to name) named slots — role differentiation stays
consumer-data-driven (actions), no product semantics baked into the DS. Adds
the tvu-icon custom element so React/CE consumers can render TVU icons
(closes INFRA-F69①). Docs page + React demo (shared descriptor) show
Owner/Finance/Admin/Member menus. Code-first (owner approved 2026-07-21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MSGEOF
```
Expected: pre-commit 全绿 + commit 成功。记录真实 commit hash（逐字，不编造 — 硬规则 #9）。

- [ ] **Step 5: 还原构建产物污染（若跑过 pnpm build）**

若期间跑过 `pnpm build`（重建 tracked 的 playground-dist/react-pilot/dist）且不属本 commit：`git checkout HEAD -- playground-dist react-pilot/dist && git clean -fdq`（pickup 纪律）。本任务未要求 build，通常无需。

---

### Task 8: backlog + STATUS + changeset + 收尾

**Files:**
- Modify: `docs/internal/backlog.md`（INFRA-F69①）
- Modify: `docs/STATUS.md`
- Create: `.changeset/<name>.md`

- [ ] **Step 1: backlog INFRA-F69①**

INFRA-F69 子项① 标 shipped/移除（改法：若整条只剩子项②，把①的段落删或标 `[✅ shipped 2026-07-21]`；子项② React change-trigger 保留）。同步 STATUS Active count（Backlog active 计数镜像，`audit:status-consistency` 核）。
Run: `pnpm audit:status-consistency`
Expected: PASS。

- [ ] **Step 2: STATUS.md**

`Last updated` 改到 2026-07-21 + 当日摘要（旧摘要 prepend 进 `docs/internal/STATUS-CHANGELOG.md`，遵 status-keep-only-unfinished）。

- [ ] **Step 3: changeset（minor）**

建 `.changeset/*.md`：`minor`，描述 = UserMenu 加 `#panel-top`/`#badge` 槽 + tvu-icon CE（additive，无破坏）。

- [ ] **Step 4: commit 收尾文档**

```bash
git add docs/ .changeset/
git commit --no-verify -m "docs(status): UserMenu role dropdown + tvu-icon CE shipped; INFRA-F69① closed"
```
（docs/changeset-only，无 .vue/.tsx，可不走视觉门；`--no-verify` 仅因 fresh-worktree 环境 hook 需 deps —— 若 node_modules 已装则去掉 `--no-verify` 让 hook 正常跑。）

- [ ] **Step 5: Claude Design 文件包更新（landing 后）**

Run: `pnpm build && pnpm export:claude-design-bundle`
只报变更散文件（触发词/流程见 memory `project_claude-design-bundle`）。

- [ ] **Step 6: 合并/推送决策**

worktree 分支 `worktree-usermenu-role-dropdown` → 按 `superpowers:finishing-a-development-branch` 决定合并 master（owner fast-lane 直 commit master OK）。push 落地以 `git ls-remote` 双 remote（GitHub+Gitea）核实（memory `local-env-tool-quirks`）。

---

## Self-Review

**1. Spec coverage：**
- §3.1 新增 API `#panel-top`/`#badge` → Task 2（.vue 出口）+ Task 3（config namedSlots）✓
- §3.2 DOM/渲染顺序 → Task 2 Step 2（header 后、actions 前）✓
- §3.3 shadowRoot:true 不变 → Global Constraints + Task 2（无 shadowRoot 改动）✓
- §4 tvu-icon CE → Task 1 ✓（含 §8 parity 风险实证 = Task 1 Step 4）
- §4.3 互锁（React demo 用 tvu-icon）→ Task 4 Step 2 + Task 7 Step 2（INFRA-F69① 实证）✓
- §5.1 config + gen → Task 1/3 ✓；§5.2 docs 页 + React demo + slot-boolean → Task 4/5 ✓；§5.3 divergence + affordance + canonical-exempt(Icon 无需) → Task 6 + Task 1 Step 4 说明 ✓
- §5.4 不做（src/index.ts / shadowRoot / role 枚举）→ Global Constraints ✓
- §6 验证（gate stdout + 真机 + INFRA-F69① 实证）→ Task 7 ✓
- §7 收尾（STATUS/backlog/changeset/bundle）→ Task 8 ✓

**2. Placeholder scan：** 无 TBD/TODO。"先读模板文件照抄"类步骤给了明确文件路径 + 要替换的具体内容（4 角色数据、config 条目、divergence 文本逐字），boilerplate 引用 repo 内模板（descriptor/page 单一真源模式下，重复 143 行模板反而易错）——非 placeholder，是"读真源模板 + 填具体内容"的诚实指令。

**3. Type consistency：** 槽名 `panel-top`/`badge` 全程一致；React prop 映射 `panelTop`/`badge`（kebab→camel，Task 3）一致；`useHasSlot('panel-top')`/`useHasSlot('badge')` 与槽名一致；config Icon `name`/`size`/`color` 与 Icon.vue defineProps 一致。

**待实现时钉死的两点**（已在计划内标为实证步，非 placeholder）：Task 1 Step 4 binding-config-parity 对非 canonical 路径的解析行为；Task 5 Step 4 site-review-manifest 的 code-first entry 精确 schema。
