# UserMenu 角色化下拉（code-first）+ tvu-icon CE — Design Spec

> **Date**: 2026-07-21
> **Status**: design approved (owner 2026-07-21), spec pending review
> **Scope**: 一个原子交付 = UserMenu 通用槽位扩展（主）+ `tvu-icon` CE（顺带解 INFRA-F69①）
> **Reference**（MicroApps **产品** Figma，非 DS 库）: `DtZcMkhNy6qh6jbQQnhreQ` node `6289-4563`（ROLES · permission by role）
> **Backlog**: closes INFRA-F69①；INFRA-F69② + APID-01 Table 另排

---

## 1. 背景与问题

`TopBar` 登录后右侧用 `UserMenu`（`src/canonical/UserMenu.vue` + base `src/components/UserMenu/UserMenu.vue`）。现状 UserMenu **已实现**点击头像展开下拉：avatar 点击 toggle → panel（账户 header: name + `role` 文字 badge + email）+ 自定义 `actions`（`MenuList` 垂直）+ 内联语言切换 + Sign out + 外部点击关闭（`composedPath` 跨 shadow 边界）。

MicroApps 产品设计（6289-4563）展示了**按角色差异化的下拉菜单**（owner / finance / admin / member，+ premium 修饰态），且每个菜单里承载了 MicroApps 计费产品内容：余额面板（Balance / tokens / rates）、余额状态告警条（Healthy / Soft / Hard / Credit-Limit）、premium 👑 图标。

**问题**：DS UserMenu 缺两样通用能力才能承载"AI 按角色做产品设计直接用"：
1. 账户 header 与菜单之间没有**放任意产品内容**的区域（余额面板/告警条无处安放）。
2. name 旁没有**徽标/图标位**（premium 👑 / 角色图标）。

**并且**（INFRA-F69①，APID-02 实施暴露）：`tvu-icon` 无 web-component 出口 → React demo 里 `<tvu-icon>` 不 upgrade、静默 0×0 空渲染。UserMenu 的 React demo 要用图标（👑 / 菜单项图标），两件事互锁。

---

## 2. 设计原则（约束）

- **不烘焙产品语义进 DS**（PROJECT_GOAL §不要做）：`owner/finance/admin/member` 的菜单项、余额面板、token/billing 全是 MicroApps 产品概念，**不能**进共享库。角色差异化由 consumer 传不同 `actions` 数据 + 往通用槽位投影产品内容实现。
- **Figma 真源 + 合法 code-first**（FIGMA_AS_SOURCE_OF_TRUTH.md）：Figma 无 standalone UserMenu 组件（只在 TopBar 内部图层 + 本次参考的是**产品**文件非 DS 库）。走唯一合法 code-first 通道 = divergence + `user approved code-first 2026-07-21`。
- **向后兼容 v1.0 锁定 API**（API_STABILITY.md）：本次全部为 **additive**（新增具名槽 + Icon 新 CE），不删/改任何现有 prop / event / slot。
- **原子级完整接线**（AGENTS 硬规则 #8 + pre-commit gate 链）：公开组件改动 = 一次到位（config + react binding + docs 页 + React demo + slot-boolean 双框架 + divergence + affordance），不分阶段 commit 半成品。

---

## 3. 组件 A：UserMenu 通用槽位

### 3.1 新增 API（additive）

| 类型 | 名称 | 作用 |
|---|---|---|
| 具名槽 | `#panel-top` | 账户 header 下方、菜单（actions/language/signout）上方的**任意内容区**。consumer 往里投影余额面板、告警条等产品内容。有内容才渲染（含分隔线）。 |
| 具名槽 | `#badge` | header 中 name 右侧的**徽标/图标位**。放 premium 👑（`<tvu-icon name="mark/vip">`）/ 角色图标等。与现有 `role` 文字 badge **并存**（产品用槽、不设 `role`）。 |

**不变**：
- `role` prop —— 可选、纯展示文字 badge，语义不驱动菜单内容。
- 菜单项差异 —— 继续由 `actions: MenuListItem[]` 数据驱动（`MenuList` 已支持前导 `item.icon`）。
- `name` / `email` / `image` / `color` / `languages` / `lang` props；`action` / `languageChange` / `signOut` events —— 全部不变。
- **不加默认槽**（YAGNI；`#panel-top` 已覆盖产品内容需求）。
- **不加尾随箭头**：Figma menu-item 的 `icon/Arrow/Next` 是 hidden 图层、产品不显示，非必需。

### 3.2 DOM / 渲染顺序（base UserMenu.vue panel 内）

```
avatar (trigger)
panel[role=menu]
├─ header: [avatar] [name] [#badge slot] [role badge?] · [email]
├─ divider (若 #panel-top 有内容)
├─ #panel-top slot          ← 新增：余额面板 / 告警条等产品内容
├─ divider + MenuList(actions)   (actions.length 时)
├─ divider + language row        (languages.length 时)
└─ divider + Sign out
```

- `#panel-top` 槽紧接 header 之后、actions 之前渲染；仅当槽有内容时连同其分隔线一起出现（Vue `$slots['panel-top']` / CE slot assignedNodes 判空）。
- `#badge` 槽渲染在 `.user-menu__name-row` 内，name 之后、role badge 之前（或之后，实现时定，视觉对齐 Figma 👑 紧贴 name）。
- 全部走 token（无 hardcoded hex）；沿用现有 `--sp-*` / `--line-*` / `--r-*`。

### 3.3 CE / shadowRoot

- UserMenu 现为默认 **shadowRoot:true**（panel 是组件自身 DOM 内 `position:absolute`，**未 teleport**，scoped 样式在 shadow root 内正常工作）。**保持 shadowRoot:true 不变** —— 下拉不 teleport 到 body，故不需要 pickup 提到的 light-DOM 改造。
- 槽位内容（`#panel-top` / `#badge`）由 consumer 提供，CE 下经原生 `<slot>` 投影。样式责任在 consumer 侧内容自身。

---

## 4. 组件 B：tvu-icon CE（INFRA-F69①）

### 4.1 现状

`Icon`（`src/components/Icon/Icon.vue`）是 Vue-only 组件：经 `src/index.ts` 作 Vue 组件导出 + `app.component('Icon', ...)`，且作为 `NestedStyle_*` 被 light-DOM CE 注入样式用。**无 CE 出口、无 components.config 条目**。已有 docs 页 `playground/docs/pages/IconPage.vue`。

### 4.2 设计

- **加 `tvu-icon` CE**：`components.config.ts` 新增 Icon 条目（props 从 Icon.vue `defineProps` 逐字镜像，实现时读取确认 —— 预期 `name` / `size` / `color` 等；无 v-model）；`register.ts` `defineCustomElement(Icon)` 定义 `tvu-icon`。
- **shadowRoot**：默认 true（Icon 渲染 registry SVG + scoped 样式，standalone CE 用 shadow 封装 OK）。实现时验证 SVG 在 shadow 下正常渲染。
- **React binding**：`gen:react-bindings` 重生，产出 `tvu-icon` React wrapper。
- **slot-boolean in-scope 判定**：Icon 若无 slot、无 boolean prop → 不进 demo-slot-boolean in-scope，不需要额外 slot/boolean demo（实现时按 Icon.vue 实际 props 确认；若有 boolean prop 则补 live 切换 demo）。
- Icon 已有 docs 页；本次在其上补"CE / React 用法"说明即可，不新建页。

### 4.3 与组件 A 的互锁

UserMenu 的 React demo（§5.2）用 `<tvu-icon name="mark/vip">` 渲染 premium 👑 / 菜单项图标 —— 直接验证 tvu-icon CE 在 React island 真机 upgrade + 渲染（闭合 INFRA-F69① 的实证）。

---

## 5. 原子接线清单（一次 commit 到位）

> 顺序不代表分阶段 commit；最小 commit 单元 = 下列全绿。

### 5.1 生成 / 配置
- [ ] `components.config.ts`：UserMenu 条目加 `namedSlots: ['panel-top','badge']`（`hasDefaultSlot` 保持 false）；新增 Icon 条目（`tvu-icon`）。
- [ ] `pnpm gen:react-bindings`：重生 UserMenu / Icon React wrapper。
- [ ] `register.ts`：新增 `TvuIcon = defineCustomElement(Icon)` + `customElements.define('tvu-icon', ...)`（若 Icon 需 light-DOM nested style，按现有 pattern 处理；预期 shadowRoot:true 无需）。

### 5.2 Docs / demo（双框架，硬规则 #8）
- [ ] **新建 `playground/docs/pages/UserMenuPage.vue`** —— 演示：① 4 角色（Owner/Finance/Admin/Member）用不同 `actions` 数据；② `#panel-top` 槽投影（示例产品内容块）；③ `#badge` 槽（👑）。含 API 表（props/slots/events）。
- [ ] `navigation.ts` + DocsShell **双 map** + `site-review-manifest`：登记 UserMenu 页（+ views 字段）。
- [ ] **新建 `react-pilot/src/demos/UserMenu.tsx` + `App.tsx` 接线** —— 镜像 Vue demo：slot 投影（`#panel-top` / `#badge` 用 `<tvu-icon>`）+ 角色场景。
- [ ] `demo-slot-boolean` gate：UserMenu 因新 named slots 进 in-scope → Vue + React 两侧 slot 投影 demo 满足（无 boolean prop，故无 boolean 切换要求）。

### 5.3 真源登记
- [ ] `divergences-decisions.json`：更新 `logo-menulist-usermenu-code-first-2026-07-02` 条目（`codeSide` 补 `#panel-top`/`#badge` 槽 + `reason` 补 `user approved code-first 2026-07-21`），或视需要加子条目。**必带** `user approved code-first 2026-07-21`。
- [ ] `canonical-exempt.json`：UserMenu / Icon —— UserMenu 已 exempt（槽位不改 render 覆盖，无需动）；Icon 若加 CE 但无 standalone Figma 组件，评估是否需 exempt 条目（实现时判：Icon 有 registry 来源，render-verif 覆盖情况按脚本实际报告定）。
- [ ] `component-affordances.json` + `.md`：UserMenu 若引入新 affordance（如 `#badge` 图标位）按需更新；无新增可不动。

### 5.4 不做
- **不动 `src/index.ts`**（divergence 已定：Logo/MenuList/UserMenu 经 CE + React binding 分发，不进 Vue barrel published 包）。
- 不改 shadowRoot（保持 UserMenu shadowRoot:true）。
- 不加 role 枚举预设菜单（owner 已否 B 选项）。

---

## 6. 验证

- **gate stdout**：`pnpm gen:react-bindings` 后跑 pre-commit gate 链（含 `audit:demo-slot-boolean`），拿真实 stdout。
- **真机渲染**（`.vue`/`.tsx` commit 前的视觉门）：起 docs 站 + react-pilot island，Playwright 截真实渲染图 —— UserMenu 4 角色 + 槽位投影 + tvu-icon 在 React 下真机 upgrade（非 0×0）—— 给 owner 审，`VISUAL_COMMIT_APPROVED` 后才 commit。
- **INFRA-F69① 闭合实证**：React demo 里 `customElements.get('tvu-icon')` 真值 + 图标非空渲染（DOM 探测）。

---

## 7. 收尾（实现后）

- STATUS.md：Last updated 今天 + 摘要；backlog INFRA-F69① 标 shipped/移除 + Active count 同步（`audit:status-consistency` 核）。
- changeset（minor，随下个 release）。
- retrospection（按 WRAP-UP 触发判断）。
- Claude Design 文件包更新（`pnpm build && pnpm export:claude-design-bundle`，只报变更散文件）—— UserMenu 落地后。

---

## 8. 未纳入（另排）
- **APID-01 Table 数据网格**（也 code-first，大工程，单独 brainstorm + 分阶段）。
- **INFRA-F69②** React change-trigger 真机复核（`pnpm test:framework-parity`，低优先）。
