agent-skills 评测夹具解析:菜单组件设计规范与 React 下拉菜单的无障碍实现约定
本文以 agent-skills 仓库中 evals/fixtures/frontend-ui-engineering/design-system.md 为骨架,完整解读菜单组件的设计系统约定:React + TypeScript 框架约束、menu-* 工具类与组件 API 契约、键盘与读屏器支持、焦点归还与按键行为,以及新下拉组件的接口规格。结合仓库中的参考实现 Button.tsx 与评测用例 frontend-ui-engineering.json,读者可以掌握一套“可被 AI 编码 Agent 直接执行并自动验收”的前端组件规范写法。
一、这份文档在仓库中的角色:行为评测的约束输入
design-system.md 并非普通的项目文档,而是 frontend-ui-engineering 技能行为评测(behavioral eval)的夹具(fixture)文件。根据 evals/README.md 的说明,Tier 3 行为评测会把 evals/fixtures/ 下的真实项目输入物化到一次性 Git 工作区并作为基线提交,让无头 Agent 在真实代码上动手实现,再由评分器按 expectations[] 逐条验收。
对应的评测用例 frontend-ui-engineering.json 中声明了执行材料:
"files": ["frontend-ui-engineering"]
即把 evals/fixtures/frontend-ui-engineering/ 整个目录(包含 design-system.md 与 Button.tsx)拷入评测工作区。执行入口 scripts/run-evals.js 中的 materializeWorkspace() 会为该 eval 创建临时目录、递归复制夹具、git init 并打一个 fixture baseline 提交,保证 Agent 可以 diff、修改和提交,评分器看到的是完整的执行轨迹而非口头描述。
该用例的提示词与验收标准,恰好就是本夹具要解决的任务:
{
"id": 1,
"prompt": "Build a dropdown menu component for the design system.",
"expected_output": "An accessible, keyboard-navigable component following project conventions",
"expectations": [
"Keyboard interaction and focus management are implemented, not just mouse clicks",
"ARIA roles or semantic elements are used correctly",
"Component state is managed deliberately rather than ad hoc"
]
}
换言之,design-system.md 就是 Agent 拿到这个任务时必须遵守的“项目惯例”来源——文档中的每一条约定,都对应评分器的一条验收维度。
二、菜单组件的六条核心约定
design-system.md 全文以“Menu component conventions”为题,给出了六条必须遵守的约定,下面逐条展开。
2.1 框架约定:React with TypeScript
Framework: React with TypeScript.
组件必须用 React 编写且使用 TypeScript。这一条的实战含义在夹具的参考实现 Button.tsx 中可以看到——它使用了 forwardRef 和 ButtonHTMLAttributes<HTMLButtonElement> 这类类型约束,新组件应当保持相同的类型化程度,而不是引入 any 或裸 JS。
2.2 样式约定:复用 menu-* 工具类,禁止引入样式依赖
Styling: existing
menu-*utility classes; do not add a styling dependency.
样式只能使用项目中现存的 menu-* 工具类,不允许新增任何样式依赖(如 CSS-in-JS 库或新的 CSS 框架)。这条约定对应技能文档 skills/frontend-ui-engineering/SKILL.md 中“Don't invent values / 遵循既有设计系统”的原则:不创造新的设计令牌,不引入“AI 审美”式的自定义样式。它同时是一条评测红线——如果 Agent 的实现 package.json 里多了一个样式依赖,说明它没有读约定。
2.3 组件 API 契约:接受 className 并转发 DOM ref
Public components accept
classNameand forward a DOM ref.
每个对外发布的组件都必须满足两件事:
- 接受
className参数,允许使用方覆盖/追加样式; - 通过
forwardRef把 DOM ref 转发到底层原生元素,允许使用方拿到真实 DOM 节点(例如做焦点管理)。
夹具中的 Button.tsx 正是这条契约的范本实现,全文如下:
import { forwardRef, type ButtonHTMLAttributes } from 'react';
export const Button = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
function Button({ className = '', ...props }, ref) {
return <button ref={ref} className={`button ${className}`.trim()} {...props} />;
},
);
实现要点:className 默认值取空串,与基础类 button 拼接后 trim() 避免多余空格;ref 直接绑定到 <button> 上;其余 ...props 透传。新的 Menu 组件应当以同样的模式暴露 API——约定里说的“public components”意味着这一契约适用于菜单体系中的每一个对外组件(触发器、菜单项等),而不只是顶层容器。
2.4 无障碍基线:必须支持纯键盘与读屏器用户
Components must support keyboard-only and screen-reader users.
组件的可用性底线是键盘独占用户(不依赖鼠标)和读屏器用户都能完整操作。这条约定在 references/accessibility-checklist.md 中有对应的检查项:所有可交互元素可通过 Tab 聚焦、焦点顺序符合视觉逻辑、自定义控件必须有键盘支持(Enter 激活、Escape 关闭)、不得存在键盘陷阱。
2.5 焦点管理:菜单关闭时焦点回到触发器
Focus returns to the trigger when a menu closes.
下拉菜单打开期间焦点会移入菜单项,那么无论以何种方式关闭(点击菜单项、点击外部、按 Escape),焦点都必须归还给触发器按钮,而不是遗留在已卸载的菜单节点上。这与无障碍清单中“Modals trap focus while open, return focus on close”是同一类模式:任何临时聚焦的浮层,关闭时都要把焦点送回去,否则键盘用户会“掉出”上下文。frontend-ui-engineering 技能的 Focus Management 一节给出的模式与此一致——通过 ref 显式调用 .focus() 完成焦点转移。
2.6 按键行为:Escape 关闭,方向键在启用项之间移动
Escape closes the menu; arrow keys move between enabled items.
Escape:关闭菜单(并触发 2.5 的焦点归还);- 上/下方向键:在**启用(enabled)**的菜单项之间移动焦点——注意“enabled”三个字是规格的一部分,箭头键不应停靠在禁用项上。
无障碍清单的“Common Anti-Patterns”表也把“Custom dropdown with no ARIA”列为高危反模式,并给出修复方向:使用原生 <select> 或“proper ARIA listbox”。由于本任务明确要求下拉菜单(需要自定义外观与菜单项结构),实现时应当完整实现 ARIA listbox/menu 语义(aria-haspopup、aria-expanded、role="menu"/role="menuitem" 或 listbox 语义、aria-disabled 等),这正是评测期望第二条“ARIA roles or semantic elements are used correctly”的考点。
三、新下拉组件的接口规格
约定末尾给出了本次要新增组件的具体规格:
The new dropdown should expose a trigger label and an array of actions. Disabled actions remain visible but cannot receive focus or execute.
拆解为可验证的接口要求:
- 触发器暴露 label:触发器按钮需要有可见文本(label),供用户和读屏器理解菜单用途——这也对应技能文档中“按钮和链接必须有描述性文本”的要求;
- 动作以数组传入:菜单项的数据模型是一个 actions 数组,而非写死的子节点,组件负责把它渲染为菜单项列表;
- 禁用项的三重约束:
disabled的动作项- 仍然可见(remain visible)——保留信息完整性,让用户知道该操作存在;
- 不可获得焦点(cannot receive focus)——键盘 Tab/方向键遍历必须跳过;
- 不可执行(cannot execute)——点击或回车均无副作用。
一个满足规格的组件签名大致形如(结合 2.3 的 API 契约):
// 满足 design-system.md 约定的形状示意
type MenuAction = {
label: string;
disabled?: boolean;
onSelect?: () => void;
};
function Menu({
triggerLabel,
actions,
className,
// ...通过 forwardRef 转发底层 DOM ref
}: MenuProps) { /* 见约定 2.1–2.6 */ }
配合 2.6 的按键行为,禁用项在方向键导航中被跳过,恰好同时满足“不可获得焦点”和“不可执行”。
四、仓库佐证:约定如何被参考实现与评测闭环验证
4.1 参考实现:Button.tsx 与约定的对应关系
| 约定(design-system.md) | Button.tsx 中的体现 |
|---|---|
| React with TypeScript | forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>> 全量类型化 |
| 复用工具类、不加样式依赖 | 直接拼接既有类名 `button ${className}`,无内联样式、无新增依赖 |
接受 className |
className = '' 默认值 + 拼接透传 |
| 转发 DOM ref | ref 绑定到底层 <button> |
从源码结构看,Button.tsx 之所以被放进同一个夹具目录,就是为了让 Agent 在实现 Menu 时有一个“按惯例写出来”的样板可以参照——评测工作区里它与 design-system.md 同时存在(见 scripts/run-evals.js 中按 files[] 递归 cpSync 夹具目录的逻辑)。
4.2 验收闭环:expectations 与约定的映射
评测用例的三条期望,分别把文档约定翻译成了评分维度:
- “Keyboard interaction and focus management are implemented, not just mouse clicks” —— 验收约定 2.5、2.6(焦点归还 + Escape/方向键);
- “ARIA roles or semantic elements are used correctly” —— 验收约定 2.4(读屏器可用);
- “Component state is managed deliberately rather than ad hoc” —— 验收约定整体下组件状态的显式管理(打开/关闭、当前高亮项等状态应有明确归属,而非散落的事件副作用)。
也就是说,这份十几行的约定文件定义了“什么是对的实现”,而 frontend-ui-engineering.json 的 expectations[] 定义了“如何证明它对”,两者构成 agent-skills 评测体系里“约束 → 执行 → 验收”的完整闭环。技能本体 skills/frontend-ui-engineering/SKILL.md 则提供更大的工程框架:组件文件同置、组合优于配置、状态管理选型(本地 state 优先)、WCAG 2.1 AA 键盘/ARIA/焦点管理细则,以及“Red Flags”中列出的“无键盘导航测试”“颜色作为状态唯一指示”等反模式——这些是约定文件未逐条列出、但同一评测中同样应遵守的上位规范。
五、如何运行相关评测(只读视角)
在本仓库内可以按 evals/README.md 说明的命令观察这套约束如何被验证:
# Tier 2:确定性门,校验夹具路径、expectations 结构、触发路由
node scripts/run-evals.js
node scripts/run-evals.js --min-rank1 80
# Tier 3:行为评测,物化 frontend-ui-engineering 夹具并评分(消耗 tokens)
node scripts/run-evals.js --behavioral frontend-ui-engineering # 实际执行
node scripts/run-evals.js --behavioral frontend-ui-engineering --dry-run # 只打印执行计划
其中 --dry-run 模式只输出计划、不真正执行,适合先确认该技能的行为评测将以 frontend-ui-engineering 夹具目录为基线运行。注意执行类评测要求非空的 files[],缺失夹具会直接报错(见 scripts/run-evals.js 的确定性校验分支)。
六、把这份约定落地到新菜单组件时的自检清单
把 design-system.md 的全部条款压缩为可实现前的核对单:
- [ ] 组件使用 React + TypeScript 编写,无
any滥用; - [ ] 样式仅使用既有
menu-*工具类,未新增任何样式依赖; - [ ] 每个公共组件接受
className并用forwardRef转发 DOM ref; - [ ] 触发器有可见 label,动作通过 actions 数组传入;
- [ ] 禁用项保持可见,但焦点遍历跳过、交互无副作用;
- [ ]
Escape关闭菜单;方向键仅在启用项之间移动焦点; - [ ] 菜单关闭(含选择、外部点击、Escape 各路径)后焦点回到触发器;
- [ ] ARIA 角色/语义正确使用(listbox/menu 语义、展开状态、禁用标记),可通过读屏器完整操作。
这份夹具的价值在于示范了一个可复用的工程实践:把设计系统约定写成 Agent 可读的约束文件,配一个符合约定的参考实现样板,再用带 expectations[] 的评测用例做机器可验收的闭环。design-system.md 的每一行都能被翻译为一条可验证的实现要求,这正是它能作为行为评测输入的前提。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00