Composer Tool Profile 菜单设计

范围:ControlsCluster 中 Tools 行右侧齿轮打开的二级菜单。本文同时作为单任务设计、实施 brief 和证据 ledger。

二级菜单使用已安装的 Base UI Menu.SubmenuRootSubmenuTriggerPortalPositioner。打开状态受 React 控制;齿轮只响应点击,鼠标移出不关闭;点击外部、Escape、选择 profile 或关闭一级菜单时关闭。

当前实现与已验证缺口

一级菜单通过 Menu.Portal 挂载到 document body,positioner 使用 z-index: 200。失败实现把二级菜单作为 composer 内的 position: fixed 节点,并增加透明 backdrop。composer 的 .inputArea 自身是 z-index: 5 的层叠上下文,因此二级节点即使设置 z-index: 9999,仍位于 body portal 的一级菜单之后。截图中的一级菜单覆盖二级菜单由此产生。

参考实现比较

方案能力结论
Base UI submenu受控 open、click trigger、outside press、Escape、focus、portal、floating position采用;项目已安装且原代码已使用
手写 fixed + backdrop点击状态可实现;缺少 portal 层级、自动 flip、完整键盘与 focus 行为拒绝;重复已有基础设施并产生当前缺陷
原生 Popover APItop layer 与 light dismiss;不提供现有 Menu 的 roving focus、submenu 关系和定位合同本次不采用;会同时维护两套菜单语义

目标状态与边界

状态

  • profileMenuOpen 是唯一二级 open 状态。
  • 一级关闭时显式清除二级状态。
  • profile 选择沿用 switchProfile

DOM 与层级

  • 两级菜单均由 Base UI portal 挂到 body。
  • 一级 positioner 为 200,二级为 201。
  • Base UI 负责 anchor、flip 和碰撞处理。

明确排除

  • 不修改 Tools 行开关语义。
  • 不增加依赖或全局 overlay 管理器。
  • 不改 profile 数据、后端或其他弹层。

信任边界、隐私、并发和取消均不涉及:本改动只控制本地展示状态,不读写新的持久数据,不发起网络请求。

Tools 视觉行使用一个 role="none" 容器,内部放置两个同级且非嵌套的 menuitem:Tools Menu.Item 和齿轮 SubmenuTrigger。鼠标和键盘都能分别激活两个动作;激活 Tools 会切换开关并关闭 profile,激活齿轮只切换 profile。视觉必须保持原布局:Tools 标签在左,右侧先显示齿轮,启用时再在最右侧显示勾;齿轮的 22px 尺寸、14px 图标及其与勾的间距不变。Base UI 会用 sibling-open 原因报告父菜单 item 的 pointer move,本组件只忽略这一种关闭原因;outside press、Escape、item press 和父菜单关闭等其他原因仍由 Base UI 处理。

实施 brief 与验收

生产文件:web/components/chat/composer/controls/controls-cluster.tsxcomposer.module.css。回归入口:npm run check:composer-tool-profile。兼容要求:Tools 行仍切换 tools;profile 项仍调用现有回调;一级菜单其他项不变。

当前实现与验证证据

项目证据状态
菜单行为npm run check:composer-tool-profile 覆盖受控二级菜单、父菜单关闭、层级、键盘和 store 归属通过
类型检查npx tsc --noEmit通过
实现约束sibling-open 是唯一被忽略的父菜单关闭原因;outside press、Escape、item press 与父菜单关闭保持有效符合
安装版入口可见验收使用 /Applications/OpenProgram.app、默认 profile 和端口 18100;本页不以第二实例作为验收依据固定
完整前端检查cd web && npm run check,包含新增菜单检查通过
生产构建cd web && npm run build,29 个静态页面生成通过
文档python -m tools.docs_site.checklinks:0 broken links通过
视觉约束gear 位于 check 左侧,按钮为 22×22px、图标为 14px,并保留既有间距与整行 hover符合