design-component

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Skill: Design Component

Skill:设计组件

Produce a complete component specification matching the project format.
生成符合项目格式的完整组件规范。

Steps

步骤

  1. Read
    .claude/rules/components.md
    → "Component Quality Bar" (the 8-state table) and "Atomic Design"; the always-on 8-state table is in
    CLAUDE.md
    → Non-Negotiables.
  2. Check if it already exists:
    components/atoms.md
    ,
    molecules.md
    ,
    organisms.md
    ,
    templates.md
    ,
    navigation.md
    ,
    feedback.md
    ,
    forms-advanced.md
    ,
    overlays.md
    . Match the existing spec format.
  3. Pull the ARIA pattern from
    accessibility/aria-patterns.md
    and contrast/target rules from
    accessibility/wcag-checklist.md
    .
  4. Map every value to tokens (
    tokens/*.json
    ) — sizes via
    sizing.json
    , states via
    states.json
    .
  5. Apply visual judgment from
    taste/design-taste.md
    (states, focus, no slop).
  6. Optional fast start:
    python3 scripts/scaffold_component.py "<Name>"
    to emit a stub, then fill it in.
  1. 阅读
    .claude/rules/components.md
    中的「组件质量标准」(8状态表格)和「Atomic Design」;通用8状态表格位于
    CLAUDE.md
    的「不可妥协项」章节。
  2. 检查组件是否已存在:查看
    components/atoms.md
    、
    molecules.md
    、
    organisms.md
    、
    templates.md
    、
    navigation.md
    、
    feedback.md
    、
    forms-advanced.md
    、
    overlays.md
    。遵循已有规范的格式。
  3. 从
    accessibility/aria-patterns.md
    中提取ARIA模式,从
    accessibility/wcag-checklist.md
    中提取对比度/目标规则。
  4. 将所有数值映射到token(
    tokens/*.json
    )——尺寸通过
    sizing.json
    映射,状态通过
    states.json
    映射。
  5. 应用
    taste/design-taste.md
    中的视觉设计准则(含状态处理、焦点样式、无敷衍设计要求)。
  6. 可选快速启动方式:运行
    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.md
渲染的说明。

Accuracy — 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
aria-busy
, error
aria-invalid
, selected
aria-pressed
/
aria-selected
) × each variant in one HTML file (see
examples/component-states/button.html
). Then RUN the gates and report their real output (CLAUDE.md → Verification Protocol):
  • node scripts/verify_states.mjs <harness> [--dark]
    — contrast of every element in default/hover/focus
  • node scripts/axe_audit.mjs <harness> [--dark]
    — ARIA/role/name/label correctness
  • node scripts/measure_render.mjs <harness> [--dark]
    — every text element AA
  • overlays/modals also:
    node scripts/verify_focustrap.mjs <harness> --open=<trigger>
    Every state must pass in light AND dark before the component is "done". Never claim a state is correct without a gate proving it.
只有当所有变体 × 所有状态的渲染都正确时,组件才算“正确”——而不仅仅是默认静止状态。请构建一个状态测试面板:在单个HTML文件中渲染每个变体的所有适用状态(默认、hover、focus、active、disabled、加载态
aria-busy
、错误态
aria-invalid
、选中态
aria-pressed
/
aria-selected
)(参考
examples/component-states/button.html
)。然后运行质量门禁并上报实际结果(见
CLAUDE.md
→ 验证协议):
  • node scripts/verify_states.mjs <状态测试面板路径> [--dark]
    — 检查默认/hover/focus状态下所有元素的对比度
  • node scripts/axe_audit.mjs <状态测试面板路径> [--dark]
    — 检查ARIA/role/name/label的正确性
  • node scripts/measure_render.mjs <状态测试面板路径> [--dark]
    — 检查所有文本元素是否符合AA级对比度标准
  • 浮层/模态框还需运行:
    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 (
    await loc.click(); expect(await loc.isChecked())
    ). A custom control whose overlay box covers the real
    <input>
    will not toggle unless the box has
    pointer-events:none
    (or an enclosing
    <label>
    forwards the click).
  • Geometry: glyphs centered, not stacked/offset. If a
    display:grid
    box holds an
    opacity:0
    sibling plus a
    ::after
    , the pseudo lands in row 2 → use
    display:none
    on the hidden sibling, or one container child.
  • Stroke consistency: a checkmark and its indeterminate dash must use the same rendering method (one
    <svg>
    , two
    <path>
    toggled by state — same
    stroke-width
    ), never an svg check vs a CSS
    ::after
    rect (they read as different weights).
  • 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
form-controls
and heavy (native
accent-color
) in
data-table
is a bug. Factor one pattern, reuse it verbatim.
对比度/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>
    通过状态切换——
    stroke-width
    一致),绝不能用SVG对勾搭配CSS
    ::after
    矩形(两者视觉粗细会不一致)。
  • 过渡动画伪影:如果未禁用过渡动画就截图,刚点击的控件会处于动画中间的半透明状态——这不是bug。判断状态前,务必先禁用过渡动画并将指针移开。
跨文件一致性是硬性要求。 同一组件(例如复选框)在所有状态测试面板/页面中必须使用完全一致的CSS和标记。如果一个复选框在
form-controls
中看起来很细,在
data-table
中却很粗(原生
accent-color
效果),这就是bug。请提炼出统一的模式,直接复用。

Verified custom checkbox/radio pattern (thin, token-driven, gated + eyeballed)

已验证的自定义复选框/单选框模式(细线条、token驱动、通过质量门禁+肉眼检查)

Real
<input>
underneath (keeps native a11y + keyboard); a drawn
.box
overlay with
pointer-events:none
; check + dash as two
<path>
in one
<svg>
toggled by
:checked
/
:indeterminate
; 1.5px
border-strong
,
.25rem
radius,
.62rem
glyph,
stroke-width:2
round caps. Reference:
examples/component-states/form-controls.html
and
data-table.html
(select-all uses
indeterminate
). Native
accent-color
renders too heavy — do not use it when the house look is "thin".
底层使用真实
<input>
元素(保留原生可访问性和键盘操作能力);上层绘制
.box
覆盖层,设置
pointer-events:none
;对勾和短横线是单个
<svg>
中的两个
<path>
,通过
:checked
/
:indeterminate
状态切换;1.5px
border-strong
边框、
.25rem
圆角、
.62rem
图标尺寸、
stroke-width:2
圆头描边。参考文件:
examples/component-states/form-controls.html
和
data-table.html
(全选功能使用了
indeterminate
状态)。原生
accent-color
渲染效果过粗——当内部设计风格为“细线条”时,请勿使用。

Responsive — every component, no sideways scroll (gated)

响应式——所有组件不得出现横向滚动(质量门禁检查)

Build mobile-first; a fixed-px width that can't shrink is a bug. Run
node scripts/verify_responsive.mjs <file|dir>
— it loads each harness at 280/320/414px and fails on any horizontal overflow. The four recurring causes and their fixes:
  • fixed
    inline-size:Npx
    →
    inline-size:100%;max-inline-size:Npx
    (cap, don't pin).
  • <ul>
    /
    <ol>
    default 40px inline-start padding
    (a
    *{margin:0}
    reset does NOT clear padding) →
    padding:0;margin:0
    on every list. This also silently mis-aligns a list's edge vs a sibling block (looks like unequal widths) — same fix.
  • non-wrapping flex rows (breadcrumb, stepper, tabs) →
    flex-wrap:wrap
    , or for tabs
    overflow-x:auto
    +
    .tab{flex:none}
    .
  • grid minmax(Npx,1fr)
    min larger than viewport
    →
    minmax(min(Npx,100%),1fr)
    .
采用移动优先的方式构建;固定像素宽度且无法收缩是bug。运行
node scripts/verify_responsive.mjs <文件|目录>
—— 它会在280/320/414px宽度下加载每个状态测试面板,若出现横向溢出则判定失败。四个常见问题及修复方案:
  • 固定
    inline-size:Npx
    → 改为
    inline-size:100%;max-inline-size:Npx
    (设置上限,而非固定宽度)。
  • <ul>
    /
    <ol>
    默认40px inline-start内边距
    (
    *{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 (
--duration-fast|normal|slow
,
--ease-out|in|in-out|emphasized
in the theme;
--transition-micro
=
fast ease-out
). Never hardcode ms/curves. A component that toggles open/closed must animate its height, not just rotate a chevron — collapse via
hidden
/
display:none
alone reads as "rigid, no transition". Smooth-height pattern (no JS measuring): wrap content in an inner that clips overflow, animate the grid track:
css
.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
hidden
then add
.open
next frame; collapse = remove
.open
, set
hidden
on
transitionend
. Reference: accordion in
examples/component-states/overlays.html
. Always honor
@media(prefers-reduced-motion:reduce){…transition:none}
.
时长/缓动都使用token(主题中的
--duration-fast|normal|slow
、
--ease-out|in|in-out|emphasized
;
--transition-micro
=
fast ease-out
)。绝不要硬编码毫秒数或动画曲线。可开关的组件必须对高度做动画,而不仅仅是旋转箭头——仅通过
hidden
/
display:none
实现折叠会显得“生硬、无过渡”。平滑高度模式(无需JS测量):将内容包裹在一个裁剪溢出的内层容器中,对grid轨道做动画:
css
.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
类;折叠时先移除
.open
类,在
transitionend
事件触发后设置
hidden
。参考:
examples/component-states/overlays.html
中的手风琴组件。始终遵循
@media(prefers-reduced-motion:reduce){…transition:none}
规则。

Layout — fill the space, don't ship AI-empty filler

布局——填满空间,不要输出AI式的空白填充

  • auto-fit
    , never
    auto-fill
    for card grids.
    auto-fill
    keeps empty phantom tracks so 3 cards cluster left with a void on the right;
    auto-fit
    collapses empties so cards stretch to fill the row. Always
    repeat(auto-fit,minmax(min(Npx,100%),1fr))
    .
  • Equal-height panels in a row:
    align-items:stretch
    on the grid, AND make the shorter panel's body fill —
    .panel{display:flex;flex-direction:column}
    + the inner region
    flex:1
    . A child sized with
    block-size:%
    (e.g. chart bars) needs a definite-height ancestor (a
    flex:1
    box or explicit height), or the % resolves to 0 and the element collapses. Wrap the bar in a
    flex:1
    .barbox
    and give the bar
    block-size:%
    of that.
  • 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
    flex:1
    capped by
    max-inline-size
    stops growing and leaves empty space after the last item. Push the right-hand cluster with
    margin-inline-start:auto
    on its first element.
  • 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
    display:block
    so opening the sidebar pushes main down. Reference:
    examples/component-states/app-shell.html
    .
  • 卡片网格使用**
    auto-fit
    ,绝对不要用
    auto-fill
    **。
    auto-fill
    会保留空的虚拟轨道,导致3张卡片挤在左边,右边有空隙;
    auto-fit
    会折叠空轨道,让卡片拉伸填满整行。始终使用
    repeat(auto-fit,minmax(min(Npx,100%),1fr))
    。
  • 同行面板等高:在grid容器上设置
    align-items:stretch
    ,同时让较矮面板的内容区填满高度 ——
    .panel{display:flex;flex-direction:column}
    + 内部区域设置
    flex:1
    。使用
    block-size:%
    定义尺寸的子元素(例如图表柱状条)需要一个高度确定的祖先元素(
    flex:1
    容器或显式高度),否则百分比会解析为0,导致元素塌陷。将柱状条包裹在设置了
    flex:1
    的
    .barbox
    中,然后让柱状条的高度占满该容器。
  • 主区域80%都是空白会看起来像机器生成的。 仪表板要填充真实合理的内容(统计行 + 活动列表 + 图表),而不是只有一个孤零零的组件。有意图的密度是“精心设计”和“AI敷衍内容”的区别。
  • 工具栏/头部尾部空隙:设置了
    flex:1
    且有
    max-inline-size
    上限的flex子元素会停止增长,在最后一个元素后面留下空白。请在右侧组的第一个元素上设置
    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
<symbol>
sprite —
examples/component-states/icons.js
defines each icon once and
<svg class="ico" aria-hidden="true"><use href="#i-NAME"/></svg>
uses it. No per-use path duplication, no network, offline + gate-safe. Add a new icon to
icons.js
once; never paste raw paths into markup. (Inline lucide is acceptable only if the path is copied verbatim from lucide.)
.ico{stroke:currentColor;fill:none;stroke-width:2}
— color via
currentColor
.
手动估算的SVG路径数据会渲染出损坏的图标(帮助图标“?”变成一个点;设置图标变成汉堡菜单)。请使用完全原版的lucide路径,通过注入的
<symbol>
雪碧图按名称引用 ——
examples/component-states/icons.js
中定义了每个图标一次,使用方式为
<svg class="ico" aria-hidden="true"><use href="#i-NAME"/></svg>
。无需每次使用都复制路径,无需网络请求,离线可用且质量门禁兼容。新增图标只需在
icons.js
中添加一次;绝不要将原始路径粘贴到标记中。(只有当路径是从lucide原样复制时,内联lucide图标才是可接受的。)
.ico{stroke:currentColor;fill:none;stroke-width:2}
—— 通过
currentColor
设置颜色。

Graphical / 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 —
verify_states
applies this automatically when an element has no direct text node. Two traps it catches:
  • An empty
    <button>
    keeps the UA
    color:buttontext
    (≈black) regardless of theme → set its
    color
    to the actual indicator color and drive the visual via
    currentColor
    (e.g. dot is a
    ::before{background:currentColor}
    ), so the gate measures the real thing.
  • Theme-flipping tokens (
    --color-chart-N
    ,
    --color-surface-brand
    ) 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
    [data-theme="dark"]
    ) or stable tokens (
    --color-action-primary
    ,
    --color-text-link
    which adapts) for avatars, active dots, and selected states.
无文本控件(轮播指示点、更多菜单、图标按钮)需满足3:1对比度(WCAG 1.4.11),而非4.5:1 —— 当元素没有直接文本节点时,
verify_states
会自动应用此标准。它能检测到两个常见陷阱:
  • 空
    <button>
    会保留UA的
    color:buttontext
    (接近黑色),不受主题影响 → 请将其
    color
    设置为实际的指示器颜色,并通过
    currentColor
    控制视觉效果(例如指示点用
    ::before{background:currentColor}
    实现),这样质量门禁才能测量到真实的对比度。
  • 会随主题翻转的token(
    --color-chart-N
    、
    --color-surface-brand
    )在亮色/暗色模式下会反转;在这些token上使用白色文本或青绿色指示器,会在一种模式下通过,另一种模式下失败。请为头像、激活态指示点和选中状态使用适配暗色模式的值(在
    [data-theme="dark"]
    中覆盖)或稳定的token(例如
    --color-action-primary
    、
    --color-text-link
    这类自适应的token)。