design-component
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSkill: Design Component
Skill:设计组件
Produce a complete component specification matching the project format.
生成符合项目格式的完整组件规范。
Steps
步骤
- Read → "Component Quality Bar" (the 8-state table) and "Atomic Design"; the always-on 8-state table is in
.claude/rules/components.md→ Non-Negotiables.CLAUDE.md - Check if it already exists: ,
components/atoms.md,molecules.md,organisms.md,templates.md,navigation.md,feedback.md,forms-advanced.md. Match the existing spec format.overlays.md - Pull the ARIA pattern from and contrast/target rules from
accessibility/aria-patterns.md.accessibility/wcag-checklist.md - Map every value to tokens () — sizes via
tokens/*.json, states viasizing.json.states.json - Apply visual judgment from (states, focus, no slop).
taste/design-taste.md - Optional fast start: to emit a stub, then fill it in.
python3 scripts/scaffold_component.py "<Name>"
- 阅读 中的「组件质量标准」(8状态表格)和「Atomic Design」;通用8状态表格位于
.claude/rules/components.md的「不可妥协项」章节。CLAUDE.md - 检查组件是否已存在:查看 、
components/atoms.md、molecules.md、organisms.md、templates.md、navigation.md、feedback.md、forms-advanced.md。遵循已有规范的格式。overlays.md - 从 中提取ARIA模式,从
accessibility/aria-patterns.md中提取对比度/目标规则。accessibility/wcag-checklist.md - 将所有数值映射到token()——尺寸通过
tokens/*.json映射,状态通过sizing.json映射。states.json - 应用 中的视觉设计准则(含状态处理、焦点样式、无敷衍设计要求)。
taste/design-taste.md - 可选快速启动方式:运行 生成存根文件,再补充内容。
python3 scripts/scaffold_component.py "<组件名>"
Output
输出
Spec with: anatomy diagram, variants table, sizes table, all 8 applicable states, token mapping, accessibility (role/keyboard/SR), and a note to render via .
frameworks/adapter-protocol.md规范需包含:结构拆解图、变体表、尺寸表、所有适用的8种状态、token映射、可访问性(role/键盘操作/屏幕阅读器),以及通过 渲染的说明。
frameworks/adapter-protocol.mdAccuracy — verify every state, don't assume (mandatory when code is produced)
准确性——验证每一种状态,不要想当然(生成代码时强制要求)
A component is only "correct" when every variant × state renders right — not just the resting default. Build a states harness: render the component in each applicable state (default, hover, focus, active, disabled, loading , error , selected /) × each variant in one HTML file (see ). Then RUN the gates and report their real output (CLAUDE.md → Verification Protocol):
aria-busyaria-invalidaria-pressedaria-selectedexamples/component-states/button.html- — contrast of every element in default/hover/focus
node scripts/verify_states.mjs <harness> [--dark] - — ARIA/role/name/label correctness
node scripts/axe_audit.mjs <harness> [--dark] - — every text element AA
node scripts/measure_render.mjs <harness> [--dark] - overlays/modals also: Every state must pass in light AND dark before the component is "done". Never claim a state is correct without a gate proving it.
node scripts/verify_focustrap.mjs <harness> --open=<trigger>
只有当所有变体 × 所有状态的渲染都正确时,组件才算“正确”——而不仅仅是默认静止状态。请构建一个状态测试面板:在单个HTML文件中渲染每个变体的所有适用状态(默认、hover、focus、active、disabled、加载态 、错误态 、选中态 /)(参考 )。然后运行质量门禁并上报实际结果(见 → 验证协议):
aria-busyaria-invalidaria-pressedaria-selectedexamples/component-states/button.htmlCLAUDE.md- — 检查默认/hover/focus状态下所有元素的对比度
node scripts/verify_states.mjs <状态测试面板路径> [--dark] - — 检查ARIA/role/name/label的正确性
node scripts/axe_audit.mjs <状态测试面板路径> [--dark] - — 检查所有文本元素是否符合AA级对比度标准
node scripts/measure_render.mjs <状态测试面板路径> [--dark] - 浮层/模态框还需运行:
node scripts/verify_focustrap.mjs <状态测试面板路径> --open=<触发元素>
在组件“完成”之前,所有状态必须在亮色和暗色模式下都通过检查。没有质量门禁验证通过的情况下,绝不能声称某个状态是正确的。
Gates prove contrast/a11y — they do NOT prove pixels. RENDER AND LOOK.
质量门禁仅验证对比度/可访问性——不验证像素级表现。必须渲染并肉眼检查。
The contrast/axe gates pass while the UI is still visibly broken: a checkbox that doesn't toggle, a dash sitting at the bottom of its box, a checkmark and an indeterminate dash with mismatched stroke weight, a control that's too heavy. You must screenshot the harness and inspect it before claiming done — for every state, and after interaction. Playwright + system Chrome:
js
const b = await chromium.launch({channel:'chrome'});
const p = await b.newPage({deviceScaleFactor:4});
await p.goto('file://'+abs); await p.addStyleTag({content:'*{transition:none!important}'});
await p.mouse.move(2000,2000); // park pointer OFF the component
await p.locator('.stack').first().screenshot({path:'/tmp/x.png'});Read the PNG. Then look for, specifically:
- Functional: click each interactive element and assert the state actually changed (). A custom control whose overlay box covers the real
await loc.click(); expect(await loc.isChecked())will not toggle unless the box has<input>(or an enclosingpointer-events:noneforwards the click).<label> - Geometry: glyphs centered, not stacked/offset. If a box holds an
display:gridsibling plus aopacity:0, the pseudo lands in row 2 → use::afteron the hidden sibling, or one container child.display:none - Stroke consistency: a checkmark and its indeterminate dash must use the same rendering method (one , two
<svg>toggled by state — same<path>), never an svg check vs a CSSstroke-widthrect (they read as different weights).::after - Transition artifact: screenshot WITHOUT disabling transitions and a just-clicked control looks half-faded mid-animation — that is not a bug. Always disable transitions and park the pointer before judging a state.
Consistency across files is non-negotiable. The same component (e.g. checkbox) must use byte-identical CSS + markup in every harness/page. A checkbox that looks thin in and heavy (native ) in is a bug. Factor one pattern, reuse it verbatim.
form-controlsaccent-colordata-table对比度/axe质量门禁通过时,UI仍可能存在明显问题:比如复选框无法切换、短横线位于复选框底部、对勾和不确定状态短横线的描边粗细不一致、控件视觉过重等。在声称完成之前,必须对状态测试面板截图并检查——每一种状态都要检查,且要在交互后检查。使用Playwright + 系统Chrome:
js
const b = await chromium.launch({channel:'chrome'});
const p = await b.newPage({deviceScaleFactor:4});
await p.goto('file://'+abs); await p.addStyleTag({content:'*{transition:none!important}'});
await p.mouse.move(2000,2000); // park pointer OFF the component
await p.locator('.stack').first().screenshot({path:'/tmp/x.png'});查看截图,重点检查以下几点:
- 功能性:点击每个可交互元素,确认状态确实发生了变化()。如果自定义控件的覆盖层盖住了真实的
await loc.click(); expect(await loc.isChecked())元素,那么只有当覆盖层设置了<input>(或外层有pointer-events:none转发点击事件)时,控件才能正常切换。<label> - 几何位置:图标居中,无堆叠/偏移。如果一个 容器内有一个
display:grid的兄弟元素和一个opacity:0伪元素,伪元素会落在第二行 → 请给隐藏的兄弟元素设置::after,或者只保留一个容器子元素。display:none - 描边一致性:对勾和不确定状态的短横线必须使用相同的渲染方式(单个 内的两个
<svg>通过状态切换——<path>一致),绝不能用SVG对勾搭配CSSstroke-width矩形(两者视觉粗细会不一致)。::after - 过渡动画伪影:如果未禁用过渡动画就截图,刚点击的控件会处于动画中间的半透明状态——这不是bug。判断状态前,务必先禁用过渡动画并将指针移开。
跨文件一致性是硬性要求。 同一组件(例如复选框)在所有状态测试面板/页面中必须使用完全一致的CSS和标记。如果一个复选框在 中看起来很细,在 中却很粗(原生 效果),这就是bug。请提炼出统一的模式,直接复用。
form-controlsdata-tableaccent-colorVerified custom checkbox/radio pattern (thin, token-driven, gated + eyeballed)
已验证的自定义复选框/单选框模式(细线条、token驱动、通过质量门禁+肉眼检查)
Real underneath (keeps native a11y + keyboard); a drawn overlay with ; check + dash as two in one toggled by / ; 1.5px , radius, glyph, round caps. Reference: and (select-all uses ). Native renders too heavy — do not use it when the house look is "thin".
<input>.boxpointer-events:none<path><svg>:checked:indeterminateborder-strong.25rem.62remstroke-width:2examples/component-states/form-controls.htmldata-table.htmlindeterminateaccent-color底层使用真实 元素(保留原生可访问性和键盘操作能力);上层绘制 覆盖层,设置 ;对勾和短横线是单个 中的两个 ,通过 / 状态切换;1.5px 边框、 圆角、 图标尺寸、 圆头描边。参考文件: 和 (全选功能使用了 状态)。原生 渲染效果过粗——当内部设计风格为“细线条”时,请勿使用。
<input>.boxpointer-events:none<svg><path>:checked:indeterminateborder-strong.25rem.62remstroke-width:2examples/component-states/form-controls.htmldata-table.htmlindeterminateaccent-colorResponsive — every component, no sideways scroll (gated)
响应式——所有组件不得出现横向滚动(质量门禁检查)
Build mobile-first; a fixed-px width that can't shrink is a bug. Run — it loads each harness at 280/320/414px and fails on any horizontal overflow. The four recurring causes and their fixes:
node scripts/verify_responsive.mjs <file|dir>- fixed →
inline-size:Npx(cap, don't pin).inline-size:100%;max-inline-size:Npx - /
<ul>default 40px inline-start padding (a<ol>reset does NOT clear padding) →*{margin:0}on every list. This also silently mis-aligns a list's edge vs a sibling block (looks like unequal widths) — same fix.padding:0;margin:0 - non-wrapping flex rows (breadcrumb, stepper, tabs) → , or for tabs
flex-wrap:wrap+overflow-x:auto..tab{flex:none} - min larger than viewport →
grid minmax(Npx,1fr).minmax(min(Npx,100%),1fr)
采用移动优先的方式构建;固定像素宽度且无法收缩是bug。运行 —— 它会在280/320/414px宽度下加载每个状态测试面板,若出现横向溢出则判定失败。四个常见问题及修复方案:
node scripts/verify_responsive.mjs <文件|目录>- 固定 → 改为
inline-size:Npx(设置上限,而非固定宽度)。inline-size:100%;max-inline-size:Npx - /
<ul>默认40px inline-start内边距(<ol>重置不会清除内边距)→ 给所有列表设置*{margin:0}。这个问题还会导致列表边缘与兄弟块元素不对齐(看起来宽度不一致)——修复方法相同。padding:0;margin:0 - 不换行的flex行(面包屑、步骤条、标签页)→ 改为 ,标签页可使用
flex-wrap:wrap+overflow-x:auto。.tab{flex:none} - 最小值大于视口宽度 → 改为
grid minmax(Npx,1fr)。minmax(min(Npx,100%),1fr)
Motion — tokenized, real easing, animate the thing that moves
动效——token化、真实缓动、对实际移动的元素做动画
Timing/easing are tokens (, in the theme; = ). Never hardcode ms/curves. A component that toggles open/closed must animate its height, not just rotate a chevron — collapse via / alone reads as "rigid, no transition". Smooth-height pattern (no JS measuring): wrap content in an inner that clips overflow, animate the grid track:
--duration-fast|normal|slow--ease-out|in|in-out|emphasized--transition-microfast ease-outhiddendisplay:nonecss
.panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows var(--duration-normal) var(--ease-emphasized)}
.panel.open{grid-template-rows:1fr}
.panel > .inner{overflow:hidden;min-block-size:0}Keep a11y: expand = remove then add next frame; collapse = remove , set on . Reference: accordion in . Always honor .
hidden.open.openhiddentransitionendexamples/component-states/overlays.html@media(prefers-reduced-motion:reduce){…transition:none}时长/缓动都使用token(主题中的 、; = )。绝不要硬编码毫秒数或动画曲线。可开关的组件必须对高度做动画,而不仅仅是旋转箭头——仅通过 / 实现折叠会显得“生硬、无过渡”。平滑高度模式(无需JS测量):将内容包裹在一个裁剪溢出的内层容器中,对grid轨道做动画:
--duration-fast|normal|slow--ease-out|in|in-out|emphasized--transition-microfast ease-outhiddendisplay:nonecss
.panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows var(--duration-normal) var(--ease-emphasized)}
.panel.open{grid-template-rows:1fr}
.panel > .inner{overflow:hidden;min-block-size:0}保持可访问性:展开时先移除 ,再在下一帧添加 类;折叠时先移除 类,在 事件触发后设置 。参考: 中的手风琴组件。始终遵循 规则。
hidden.open.opentransitionendhiddenexamples/component-states/overlays.html@media(prefers-reduced-motion:reduce){…transition:none}Layout — fill the space, don't ship AI-empty filler
布局——填满空间,不要输出AI式的空白填充
- , never
auto-fitfor card grids.auto-fillkeeps empty phantom tracks so 3 cards cluster left with a void on the right;auto-fillcollapses empties so cards stretch to fill the row. Alwaysauto-fit.repeat(auto-fit,minmax(min(Npx,100%),1fr)) - Equal-height panels in a row: on the grid, AND make the shorter panel's body fill —
align-items:stretch+ the inner region.panel{display:flex;flex-direction:column}. A child sized withflex:1(e.g. chart bars) needs a definite-height ancestor (ablock-size:%box or explicit height), or the % resolves to 0 and the element collapses. Wrap the bar in aflex:1flex:1and give the bar.barboxof that.block-size:% - A main region that's 80% whitespace reads as machine-generated. Fill a dashboard with real, plausible content (stats row + activity list + a chart), not one lonely widget. Intentional density is the difference between "designed" and "AI slop".
- Trailing gap in a toolbar/header: a flex item with capped by
flex:1stops growing and leaves empty space after the last item. Push the right-hand cluster withmax-inline-sizeon its first element.margin-inline-start:auto - Mobile nav must not overlap. Putting the sidebar and main in the same grid area makes an opened sidebar paint over content. On mobile switch the shell to so opening the sidebar pushes main down. Reference:
display:block.examples/component-states/app-shell.html
- 卡片网格使用**,绝对不要用
auto-fit**。auto-fill会保留空的虚拟轨道,导致3张卡片挤在左边,右边有空隙;auto-fill会折叠空轨道,让卡片拉伸填满整行。始终使用auto-fit。repeat(auto-fit,minmax(min(Npx,100%),1fr)) - 同行面板等高:在grid容器上设置 ,同时让较矮面板的内容区填满高度 ——
align-items:stretch+ 内部区域设置.panel{display:flex;flex-direction:column}。使用flex:1定义尺寸的子元素(例如图表柱状条)需要一个高度确定的祖先元素(block-size:%容器或显式高度),否则百分比会解析为0,导致元素塌陷。将柱状条包裹在设置了flex:1的flex:1中,然后让柱状条的高度占满该容器。.barbox - 主区域80%都是空白会看起来像机器生成的。 仪表板要填充真实合理的内容(统计行 + 活动列表 + 图表),而不是只有一个孤零零的组件。有意图的密度是“精心设计”和“AI敷衍内容”的区别。
- 工具栏/头部尾部空隙:设置了 且有
flex:1上限的flex子元素会停止增长,在最后一个元素后面留下空白。请在右侧组的第一个元素上设置max-inline-size,将右侧组推到最右边。margin-inline-start:auto - 移动端导航不得重叠。 将侧边栏和主内容放在同一个grid区域会导致打开的侧边栏覆盖在内容上。移动端请将外壳切换为 ,这样打开侧边栏时会把主内容往下推。参考:
display:block。examples/component-states/app-shell.html
Icons — real lucide, referenced by name (never hand-draw paths)
图标——使用真实的lucide图标,按名称引用(绝不要手绘路径)
Hand-approximated SVG path data renders as broken glyphs (a help "?" became a dot; settings became a hamburger). Use verbatim lucide paths, referenced by name via an injected sprite — defines each icon once and uses it. No per-use path duplication, no network, offline + gate-safe. Add a new icon to once; never paste raw paths into markup. (Inline lucide is acceptable only if the path is copied verbatim from lucide.) — color via .
<symbol>examples/component-states/icons.js<svg class="ico" aria-hidden="true"><use href="#i-NAME"/></svg>icons.js.ico{stroke:currentColor;fill:none;stroke-width:2}currentColor手动估算的SVG路径数据会渲染出损坏的图标(帮助图标“?”变成一个点;设置图标变成汉堡菜单)。请使用完全原版的lucide路径,通过注入的 雪碧图按名称引用 —— 中定义了每个图标一次,使用方式为 。无需每次使用都复制路径,无需网络请求,离线可用且质量门禁兼容。新增图标只需在 中添加一次;绝不要将原始路径粘贴到标记中。(只有当路径是从lucide原样复制时,内联lucide图标才是可接受的。) —— 通过 设置颜色。
<symbol>examples/component-states/icons.js<svg class="ico" aria-hidden="true"><use href="#i-NAME"/></svg>icons.js.ico{stroke:currentColor;fill:none;stroke-width:2}currentColorGraphical / icon-only controls (3:1, theme-stable)
图形化/纯图标控件(3:1对比度,主题稳定)
A no-text control (carousel dot, kebab, icon button) is held to 3:1 (WCAG 1.4.11), not 4.5 — applies this automatically when an element has no direct text node. Two traps it catches:
verify_states- An empty keeps the UA
<button>(≈black) regardless of theme → set itscolor:buttontextto the actual indicator color and drive the visual viacolor(e.g. dot is acurrentColor), so the gate measures the real thing.::before{background:currentColor} - Theme-flipping tokens (,
--color-chart-N) invert between light/dark; white text or a teal indicator on them passes in one mode and fails the other. Use dark-aware values (override in--color-surface-brand) or stable tokens ([data-theme="dark"],--color-action-primarywhich adapts) for avatars, active dots, and selected states.--color-text-link
无文本控件(轮播指示点、更多菜单、图标按钮)需满足3:1对比度(WCAG 1.4.11),而非4.5:1 —— 当元素没有直接文本节点时, 会自动应用此标准。它能检测到两个常见陷阱:
verify_states- 空 会保留UA的
<button>(接近黑色),不受主题影响 → 请将其color:buttontext设置为实际的指示器颜色,并通过color控制视觉效果(例如指示点用currentColor实现),这样质量门禁才能测量到真实的对比度。::before{background:currentColor} - 会随主题翻转的token(、
--color-chart-N)在亮色/暗色模式下会反转;在这些token上使用白色文本或青绿色指示器,会在一种模式下通过,另一种模式下失败。请为头像、激活态指示点和选中状态使用适配暗色模式的值(在--color-surface-brand中覆盖)或稳定的token(例如[data-theme="dark"]、--color-action-primary这类自适应的token)。--color-text-link