设计系统

判断层标准、风格轴配方与组件清单——判断从哪来、为什么这么定。

风格轴

同一批 token 变量名 × 不同取值 = 一条风格轴。切配方即换轴,组件代码不动。

玻璃

glass-apple

默认轴。半透明 + 细边框 + 模糊,靠背后有东西才成立。

极简

minimal

没有浮起、没有模糊,阴影退化成一条发丝线。

拟态

neumorphic

同色浮起,没有边框也没有模糊。

判断层标准

每篇的结构一样:标准 / 实测 / 动作 / 检查项。数字都带基线 commit,过期就跑检查项重测。

动效验收标准

motion.md

本站动效的真实现状:零动效 token,时长散落在三个不同的声明面上,共 33 个声明点。 --text-scale 已经把字号收成单一真值了,动效这块还没有对应的东西。

三个面必须一起数,只 grep ms 会漏掉一半:

声明面 写法 处数
CSS / 内联字面量 200ms 带后缀 19 处(分布在 13 行)
JS 数值常量 const ENTER_MS = 400 无后缀 2 处(400 入场 / 320 退场)
Tailwind 工具类 duration-200 ease-out 无后缀 12 处(200×7、300×3、150×2;ease-out×4)

去重后,转场类实际只用了 8 个不同时长:120 / 150 / 200 / 250 / 260 / 300 / 320 / 400。 曲线则是 3 条自定义 cubic-bezier(11 处)+ 1 条内置 ease-out(4 处)。

写这份标准前必须先解决一件事:那 13 个数值里有 4 个压根不是动效时长。 不加分类就去"收敛到 4 档",会把积分步长和防抖延迟也收敛掉。

0. 先分类,再谈数值

类别 定义 本站实例 受 300ms 约束?
转场 transition 用户动作引起的状态变化,可打断 8 个值:120 / 150 / 200 / 250 / 260 / 300 / 320 / 400ms 是
循环 loop 无人值守的持续环境动 islandEq 900ms ease-in-out infinite(global.css:549 区段) 否,另立 --loop-*
停留 dwell 计时器,不是动画 toast 自动消失 3500ms(notes.md:22) 否,另立 --dwell-*
序列 sequence 一次性展示型编排 dockBounce 780ms、text-decode 默认 900ms 否,属"罕见/首次"档,允许更长
非动效数值 被写成 ms 但不是时长 spring 积分步长钳 dt ≤ 32ms;解码速率 15~20ms/字符;折射滤镜防抖 120ms 必须排除在时长体系外

这一分类是本标准的第一条硬约束:任何新的 ms 值落地前,先声明它属于哪一类。 类别名进变量命名(--dur-press / --loop-eq / --dwell-toast),不允许再出现裸值。


1. 该不该动

标准(animate 频率表 + HIG Motion 的节制原则):每天 100+ 次的键盘动作永远不动; 几十次的高频面(hover、列表导航)只做几乎察觉不到的微动;偶发(模态、抽屉、通知)用标准动效; 罕见/首次才允许花哨。合法目的只有五种:反馈、空间一致性、状态指示、防止突兀跳变、解释。

实测:⚠️ 两处与频率表冲突。

  • 工作台切组件的内容切换用 .anim-fade-up(fadeUp 200ms)——这是每天几十次的高频操作,属"微动或不动"档,勉强合规
  • 侧栏行 hover(.glass-hover:hover 改 4 个属性 200ms)同样在高频面上

动作:保留但降档——hover 的 transform 分量去掉,只留颜色与阴影变化。 检查项:新增动效时必须能一句话说出它属于五种目的里的哪一种;说不出就不加。

2. 时长档位

标准(转场类,Emil 四档 + M3 阶梯交叉验证):

语义 token 值 覆盖 依据
--dur-press 120ms 按压反馈 Emil 100–160ms;我们已是 120ms ✓
--dur-fast 200ms hover、颜色、小浮层、内容切换 Emil 125–200ms;M3 short4 = 200ms
--dur-base 250ms 下拉、分段切换、开关平移 Emil 150–250ms;M3 medium1 = 250ms
--dur-exit 180ms 退场:浮层卸载计时、路由旧页让位 退场取入场的 ~0.6×(300→180):离开不该让人等;M3 的退场同样短于入场
--dur-slow 300ms 模态、抽屉入场 Emil 上限 300ms;M3 medium2 = 300ms
--dur-enter 400ms 仅页面级入场与退场编排 M3 medium4 = 400ms;⚠️ 超出 Emil 的 300ms 上限,须逐例说明

M3 还有 450–1000ms 的 Long/ExtraLong 六档,本站不采纳:那六档服务的是一级导航与全屏形变,我们没有这类面。

实测:8 个转场值里有 6 个能干净落进五档,两处需要判断,一个暴露了档位设计的真问题:

值 处数 归属 判定
200ms CSS 11 + Tailwind 7 = 18 --dur-fast 主力档,无异议
300ms CSS 2 + Tailwind 3 = 5 --dur-slow 无异议
250ms 1(Segmented.tsx:82 内联) --dur-base 无异议
120ms 1(.press-feedback) --dur-press 落在 Emil 100–160 带内 ✓
260ms 1(segmentedStretch) 并进 --dur-base 距 250 仅 10ms,属随手取值
320ms 1(EXIT_MS) 并进 --dur-slow 距 300 仅 20ms,同上
150ms 2(Dock.tsx:143、ComponentList.tsx:136) 卡在 press(120) 与 fast(200) 之间 需要决定:加第 6 档,或并进 --dur-fast 接受 +50ms。建议后者——tooltip / chip 淡入 200ms 仍在 Emil 合法带内,而多一档会让"该用哪档"重新变成判断题
400ms CSS 1(stagger-in)+ ENTER_MS --dur-enter 超出 300ms 上限,须逐例说明(页面级入场 / 通知堆叠时序)

动作:立五个 --dur-* 替换 31 个转场声明点;两处 JS 常量改成读取 CSS token 或直接命名为 --dur-slow 的引用; --dur-enter 的每次使用必须在 notes 里写明为什么不能是 300ms。

检查项(必须三个面一起扫,只 grep ms 会漏掉 12 处):

# 三个声明面的残留裸值
grep -rn "[0-9]\+ms" --include="*.css" --include="*.tsx" src/ | grep -v "var(--dur\|--loop\|--dwell\|--seq"
grep -rn "duration-\[[0-9]\|duration-1\|duration-2\|duration-3" --include="*.tsx" src/
grep -rn "_MS = [0-9]" --include="*.tsx" src/

机器口径:npm run check:motion(scripts/check-motion.mjs)把上面三条变成可执行检查,按行扫、不跨行匹配, 覆盖面比上面三条 grep 更宽——.astro 里的 duration-*(DemoChrome / page-home / pages ×2 共 4 处)与 WINDOW_MS = 3000(弹簧测量视窗,按本文口径不进时长体系)都被计入。 本文的 33 个声明点是 tsx/css 口径,脚本口径是 38(CSS/内联 19 + Tailwind duration 16 + JS 常量 3), 另有 4 处内置 ease-* 工具类单列。以脚本输出为准,本文数字在脚本落地后按基线更新。

3. 曲线族

标准:内置 ease-out 太弱,故意的动效用强曲线;ease-in 禁止出现在 UI 上。 两条流派本站都出现过——Emil 系 cubic-bezier(0.23,1,0.32,1),Material 系 standard = (0.2,0,0,1)。

实测——我们只有三条,且其中一条正是 Material 的 standard:

曲线 处数 族属(已核实出处) 语义命名
cubic-bezier(0.16, 1, 0.3, 1) 5 强减速族。与 Emil 的 --ease-out 同族且更极端,也与 M3 emphasized-decelerate (.05,.7,.1,1) 同族 --ease-out
cubic-bezier(0.2, 0, 0, 1) 4 M3 motionEasingStandardInterpolator 逐字相同 --ease-standard
cubic-bezier(0.3, 0, 0.4, 1) 2 轻缓收尾,用在 dockBounce 与 segmented stretch 两处物理感场景 --ease-settle

还有第四条曲线我们一直在用而不自知:Tailwind ease-out 4 处,它是内置的 cubic-bezier(0, 0, 0.2, 1)。 其中一处是 Switch.tsx 的旋钮平移与轨道配色——按 Emil 的规则,"故意的动效"不该停在内置曲线上。

所以问题从来不是"要不要换成标准曲线"——(0.2,0,0,1) 已经是 Material 的 standard。真正的问题是三条: 这三条没有名字;(0.16,1,0.3,1) 这条比两家都更极端的选择是有意还是偶然; 以及内置与自定义混用——同一个"平移"动作,Segmented 用自定义、Switch 用内置。

动作:三条曲线起名为上表的 token;(0.16,1,0.3,1) 保留现值但标记为未经慢放复核的继承选择, 用 DevTools 把 fadeUp 拉到 5× 时长看一次收尾有没有急停;Switch.tsx 的两处 ease-out 换 --ease-standard, 与 Segmented 对齐。 检查项:出现 cubic-bezier( 字面量即报警(允许的字面量只在 token 定义处); TSX 里出现 ease-out / ease-in-out 工具类即提示改用命名曲线。

4. 物理性

标准:不许 scale(0) 入场(真实世界没有从无到有),起点 scale(0.9–0.97) + opacity: 0; 锚定在触发元素上的浮层,transform-origin 要跟触发点,模态除外(它居中呈现)。

实测:scale(0) 与 scale(0.x) 小值入场 0 命中 ✓;transform-origin 相关:0 处(我们还没有锚定型浮层, modal 与 toast 都是居中/贴边,暂不适用)。

动作:无需改动,但这条要在 P3 补 dropdown / popover 组件时生效——现在写下来正是为了那时不忘记。 检查项:grep -rn "scale(0)\b" src/ 必须为空。

5. 可打断与退场

标准:可能被连续触发的元素(通知、开关、快速切换)用 transition 而非 keyframes—— transition 从当前值重新定向,keyframes 每次都从零重启。弹簧在被打断时保留速度,关键帧不保留。 退场要沿入场的路径原路返回。

实测:⚠️ 一处结构性冲突。

  • .stagger-in 与 .anim-fade-up 是 keyframes(fadeUp ... both)。anim-fade-up 用在工作台切组件上—— 用户连点侧栏时会反复从零重启,且 both 会把动画状态钉住。这正是标准点名要避免的形态
  • toast 走的是逐层 transition(400ms 时序、320ms 退场)✓ 符合;退场沿 translateY(120%) 原路返回 ✓

动作:.anim-fade-up 改为 transition + @starting-style 的入场写法,或至少去掉 both 的钉住。 检查项:role=status / 通知类组件内不得出现 animation: 关键字,一律 transition:。

6. 可动属性白名单

标准:只动 transform 与 opacity(跳过 layout 与 paint,走合成器);clip-path 是受许可的第四项。 width/height/margin/padding/top/left 三阶段全触发。will-change 不许常挂。

实测:transition: all 0 命中 ✓;will-change 0 命中(既没乱用也没用,中性)。 两处违规(脚本 npm run check:motion 查出来的,本文初稿只抓到第一处):

  • global.css 的 .col-handle::before 写 transition: opacity 200ms ease, height 200ms ease,hover 时 height: 24px → 36px。 缓解因素是它绝对定位且是个 2px 宽伪元素,布局代价被限制住了——但等价写法是免费的。
  • Dock.tsx:122 的 transition-[width,height] duration-300 ease-out:放大动画直接动宽高, 触发布局;且它同时用了内置 ease-out(见 §3)。

动作:前者改 transform: scaleY(1.5) + transform-origin: center,视觉结果相同、走合成器; 后者改成 transform: scale() 驱动(Dock 本来就有缩放态),或至少把宽高动画换成不触发布局的等价写法。 检查项:transition / animation 声明里出现 height|width|margin|padding|top|left 即报警, clip-path 与 opacity transform 豁免。

7. 降级与门控

标准:prefers-reduced-motion 和动效同批交付,不是后续补(animate 硬规则 4)。 降级是"更少更温和"而非清零:保留帮助理解的 opacity/颜色过渡,去掉位移与形变。 基于 hover 的位移动效须用 @media (hover: hover) and (pointer: fine) 门控——触屏会在点击后留下假悬停态。 条款依据:SC 2.2.2(自动运动 >5s 须可暂停)、SC 2.3.3(交互触发的动画须可禁用)。

实测:这是本站最大的一处成片缺口。

  • prefers-reduced-motion 只出现在 2 个组件(spring-ios、text-decode),island-ios 作为动效组件没有降级
  • global.css 里零条 reduced-motion 规则——而那里有 5 组 keyframes(fadeUp、dockBounce、islandEq、segmentedStretch、stagger-in),其中 islandEq 是 infinite 循环,正撞 SC 2.2.2
  • hover 位移门控 0 处:.glass-hover:hover 带 transform、dock 放大、spotlight 光斑全部门控缺失

动作:global.css 加一段全局 reduce 闸门(关掉 infinite 循环与 transform 分量,保留 opacity); island-ios 补 JS 侧 matchMedia 检测;hover 位移统一加 pointer 门控。 检查项:grep -c "prefers-reduced-motion" src/styles/global.css ≥ 1; 每个含 animation: 的文件都能对应到一处 reduce 分支;@media (hover: hover) 命中数 ≥ 位移型 hover 的数量。

8. 一致性与 stagger

标准:成组入场要 stagger,间隔 30–80ms,更长就显拖沓;stagger 是装饰,播放期间不得阻塞交互; 动效气质要贴组件性格(sonner 用 ease 而非 ease-out 显得慢而雅,是有意的不协调消除)。

实测:stagger-in 的用法注释写的是 animationDelay: i * 80ms——正好卡在上限, 但它是硬编码在使用侧而非 token 化,第二处 stagger 很可能飘到 100ms。 islandEq 循环的 900ms 与 dockBounce 的 780ms 分属两类,不需要一致,但需要各自有名。

动作:立 --stagger: 60ms(取区间中值而非上限);使用侧改成 calc(var(--stagger) * var(--i))。 检查项:出现 animationDelay 字面量乘数即提示改用 token。

9. notes 里的数字要有代码保证

标准:53ac764 已经把"灵感来源"改成 notes 单一真值。那么 notes 里的关键参数同样成真值—— 后面的人会拿它当依据,而它现在没有任何东西保证与代码一致。

实测:抓到一处三重的硬矛盾。 switch/glass-apple/notes.md:8 写 "250ms cubic-bezier(0.2, 0, 0, 1) 平移,和分段控件同一条曲线", 而实现是 Switch.tsx:46 与 Switch.tsx:59 的 duration-200 ease-out。 三处不实:值(250 → 200)、曲线(自定义 → 内置)、"和分段控件同一条曲线"为假 (Segmented 用 (0.2,0,0,1),Switch 用内置 ease-out,两者不同)。 另有一处轻度漂移:dock notes 的"~750ms"对应实现 780ms。

动作:notes 的关键参数改成引用 token 名(--dur-base、--ease-standard),只在暂无 token 时写裸值; P2 落 token 后回头修这两处。分段控件与开关是同族控件,本就该共用一档时长一条曲线——顺便把真值对齐。

检查项:从 notes 抽取 ms 数字与 cubic-bezier 字面量,与同目录代码的声明集合比对,报不匹配项。 这条做不全自动化(散文表述难以解析),但**"notes 声明的值在代码里找不到同值来源"这一条可以精确报警**—— 上面两处矛盾都属于此类。

10. 路由切换(2026-10-31 落地,同日修订)

标准:顶栏是常驻 chrome,跨路由不重挂;内容交换不加入场动画。 导航是高频动作,换页必须跟手——实测内容交换 50–90ms(/design 约 260ms), 再叠 300ms 入场,点击到看清内容要 350–560ms,判为慢。

编排(global.css 的 ::view-transition-*):

  • root 与 top-nav 的 group / old / new 六个伪元素统一 animation: none: Astro 仍走 startViewTransition(换页免整页刷新),但不再付伪元素动画的时长; UA 默认的根交叉淡入也显式关掉,否则它仍会跑 250ms。
  • 顶栏 transition:persist 加 transition:name="top-nav"。只写 persist 不够: 没有名字它就留在根快照里,跟着内容一起对穿——中间帧实测图标与文字重影。 有了名字它单独成组,新旧几何相同 → 视觉上静止。
  • 导航链接 data-astro-prefetch="hover":悬停即取目标 HTML,点击只剩余交换。
  • reduced-motion:Astro 自带闸门保留;本页已无动画,两者结论一致。

为什么不挂 100ms 的轻淡入:淡入从第一帧就压住内容,等于给每次点击加固定税。 换页本身已经够短,再藏起来那一下只会让"已经好了"看起来像"还在加载"。

实测(headless Chrome + CDP,1920×1080,生产构建,点击顶栏到 astro:after-swap):

目标 点击 → 内容可见 备注
首页 / 页面 50–66ms —
工作台 78–87ms 首访带 268KB JS
设计 约 260ms 195KB HTML + 3313 节点,快照开销

取 HTML 在 before-swap 前占 30–40ms,悬停预取可吸收。旧版 300ms 入场把总时延推到 350–560ms。

检查项:grep -n "transition:name" src/layouts/BaseLayout.astro 必须命中顶栏; global.css 的路由伪元素块只能出现 animation: none,不得有 keyframes 引用。


Never Ship(本站适用版)

逐条对当前代码跑过,右侧是实测状态:

禁止 本站实测(基线 982e282)
transition: all ✓ 0 命中
scale(0) 入场 ✓ 0 命中
UI 上用 ease-in ✓ 0 命中
故意的动效用内置 ease-out ✓ 0 处——曲线进 --ease-* token,工具类也取本站曲线
键盘动作 / 100+ 次每天的动作加动效 ⚠️ 高频面两处(见 §1),未动
无因由的 >300ms 转场 ⚠️ --dur-enter 400ms 使用处须在 notes 写明理由
锚定浮层用 transform-origin: center — 暂无此类组件
高频元素用 keyframes ✓ .anim-fade-up 改 transition + @starting-style(见 §5)
动 width/height/margin/padding/top/left ✓ 0 处——.col-handle::before 改 transform;Dock 的放大是逐帧改尺寸(macOS 式推开邻居),不是 CSS 过渡
未门控的 hover 位移 ✓ 已加 @media (hover: hover) and (pointer: fine) 闸门
缺 prefers-reduced-motion ✓ global.css 有全局闸门,island-ios 与 Dock 在 JS 侧也降级
全体同时入场 ✓ 间隔走 --stagger
notes 写死数值、与代码脱节 ✗ 两处待修(见 §9,其中 Switch 是三重的)

六条 ✗ 的优先级:§7 的降级缺口是唯一会直接造成用户不适的——前庭障碍用户面对 infinite 循环无从关闭, 建议 P2 第一批改动落在那儿。其次是 §9 的 notes 失真,它会让后面的人按错值建判据。 §3 / §5 / §6 属手感与性能,可与 token 落地同批处理。

这份标准落成 token 的样子

不在本次改动范围(属 P2),但先给出形状,避免届时另起一套:

:root {
  /* --- 动效 token:类别即命名空间 --- */
  --dur-press:   120ms;   /* 按压反馈,Emil 100–160 */
  --dur-fast:    200ms;   /* hover / 颜色 / 小浮层 / 内容切换 */
  --dur-base:    250ms;   /* 下拉 / 分段切换 / 开关平移 */
  --dur-exit:    180ms;   /* 退场:比入场短,离开不该让人等 */
  --dur-slow:    300ms;   /* 模态与抽屉入场 */
  --dur-enter:   400ms;   /* 仅页面级入场与退场,使用须注明理由 */
  --loop-eq:     900ms;   /* 灵动岛均衡器循环 */
  --dwell-toast: 3500ms;  /* 通知停留计时器,非动画 */
  --seq-dock:    780ms;   /* Dock 启动弹跳 */
  --seq-decode:  900ms;   /* 标题解码序列 */
  --stagger:      60ms;   /* 组入场间隔,取区间中值 */
  --ease-out:      cubic-bezier(0.16, 1, 0.3, 1);    /* 待慢放复核 */
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);       /* = M3 standard */
  --ease-settle:   cubic-bezier(0.3, 0, 0.4, 1);     /* 物理感收尾 */
}

另需注意:spring-ios 的 dt ≤ 32ms 与 dock 折射滤镜的 120ms 防抖不进这套 token—— 它们是数值积分步长与事件节流,套上 --dur- 前缀反而误导读者。

本文的自查(也要守"数值诚实")

这份草稿的第一版数字是错的,错法值得记下来,因为它就是本站"裸值散落"造成的同类事故:

我最初 grep -ro "[0-9]\+ms" src/ 得到"13 种时长 / 35 处",据此写下"把 27 处裸值替换掉"。三个错叠加:

  1. 混算了散文与代码——35 处里有 22 处在 notes.md 的文字描述中,不是声明点
  2. 漏了两个声明面——Tailwind duration-200 和 JS const ENTER_MS = 400 都不含 ms 后缀,grep 抓不到,漏掉 14 处
  3. 基线已漂移——53ac764 把 global.css 从 598 行扩到 683 行并新增 --text-scale,我最初的测量发生在它之前

修正后的口径是三个声明面共 33 个声明点、8 个转场值(改造前)。§2 的检查项命令已改成三面分别扫描, 正是为了防止同一个盲区再犯——那 33 个声明点现在已全部 token 化,见下。

落地状态

本文的标准已落地(commit 982e282,2026-10-30):

  • 五个 --dur-*、三个 --ease-*、--loop-eq / --seq-dock / --seq-decode / --dwell-toast / --stagger 全部进 token
  • 三个声明面的裸值清零:CSS/内联 0 处、Tailwind duration-* / ease-* 0 处、JS 常量只剩 WINDOW_MS(弹簧测量视窗,按口径不进时长体系)
  • 降级闸门:global.css 的 prefers-reduced-motion 段 + island-ios / Dock 的 JS 侧降级 + hover 门控
  • 检查脚本:npm run check:motion(scripts/check-motion.mjs),已转 --strict,不达标即拦住提交

本文的"实测"小节记录的是落地前的审计结果,作为改造依据留着; 判断现状看 npm run check:motion 的输出,不要拿这里的数字当现状。


搬到新项目要花多少代价

要搬的 在哪 量
动效 token 13 个 + 3 条曲线 global.css「动效 token」+ @theme 曲线 约 22 行
降级闸门 + hover 门控 global.css 的 prefers-reduced-motion 与 @media (hover: hover) 约 30 行
JS 侧读 token / 浮层进出 src/lib/motion.ts + src/lib/overlay.ts 58 行
裸值扫描 scripts/check-motion.mjs 107 行
路由切换(仅 Astro) global.css 的 ::view-transition-* + 顶栏 persist 13 行
  • 零依赖。
  • 必须整块搬:token + 闸门 + 扫描脚本三件一起。只搬 token 没有闸门 = 无障碍欠账;只搬闸门没有 token = 每处还要自己编时长。
  • 可以剥掉:§10 路由转场(依赖 Astro ClientRouter,React 项目换成自己 router 的钩子);overlay.ts 只在 React 项目里需要。
  • 工时:约 20 分钟(React / 原生);Astro 转场另加 10 分钟。
  • 搬完怎么验:node scripts/check-motion.mjs --strict 全达标;系统开"减少动效"后位移消失、透明度过渡保留;transition: all 与 will-change 各 0 命中。

材质与阴影

材质分三级——玻璃只做悬浮层、内容面板一律不透明白底、新拟态是独立风格轴不是修饰
material.md

1. 材质分级

标准:本站只有三种材质,各有明确职责,不许互相顶替。

级别 材质 用在哪 判据
悬浮层 玻璃(--glass-* + backdrop-blur) 顶栏、命令面板、齿轮菜单、弹层、坞站、tooltip 浮在别的内容之上、底下有东西要看
内容层 不透明白底(--color-surface + --shadow-sm/md) 卡片、表格、面板、表单 内容本身,要读要读要选
风格轴 新拟态(--neuro-*) 尚未实现(neumorphic 是 P3 的第三条轴) 独立配方,不是"玻璃的另一种写法"

为什么玻璃不能下沉到内容层:玻璃是增透的——它靠背后有东西才成立(--glass-bg 是 28% 的 surface 叠在模糊上)。 铺满一屏的玻璃卡片等于没有底,用户读不到"这块内容属于哪",而且背景光斑会跟着文字一起抖。 这条 CHANGELOG 里改过三次才收敛,至今没有正式条文——就是本文。

实测(基线 982e282,2026-10-31 重测):玻璃表面 token(--glass-bg / blur / highlight / inset)出现在 19 个文件里(18 个组件 + global.css 的定义处),全部是悬浮层或展示型组件: 顶栏、齿轮菜单、命令面板、字体面板、坞站、模态、输入框、按钮、下拉、勾选、导航、骨架、标签页、玻璃卡片、演示工具条、复用卡与工作台两侧面板。页面层 0 处——原先 showcase 首页的玻璃 CTA / 提示条与页面预览缩略图里的玻璃底已改回 --color-surface,缩略图现在与真实页面同材质。 --glass-border 另有 44 个文件用作通用发丝线——它只是描边,不构成玻璃表面,不算违反。 站内结构(页面骨架、列表页、dashboard、设置页)一律是 bg-[var(--color-surface)] 的白底面板 ✓ 符合。

动作:

  • 新增浮层一律玻璃;新增内容容器一律白底。要混用时,先问"这块底下有没有东西"。
  • card/glass-apple 是展示品(复现对象),它可以违反这条——但它不能成为站内页面的做法。

检查项:

# 内容层不该出现玻璃表面:页面与骨架文件里不应有 bg / blur / highlight / inset
grep -rnE "var\(--glass-(bg|blur|highlight|inset)\)" src/pages src/layouts src/components/PageShell.astro | wc -l   # 应为 0

# 玻璃表面只在组件层出现(浮层与展示品)
grep -rlE "var\(--glass-(bg|blur|highlight|inset)\)" src/components | wc -l

2. 阴影三档

标准:阴影只表达高度,不表达颜色。三档对应三种高度,用错就是噪声。

token 用途 判据
--shadow-sm 贴着表面的东西:卡片、按钮、输入框 元素不离开自己的位置
--shadow-md 浮起一层的面板:工作台中栏、模态、坞站 元素有明确的前后关系
--shadow-lg 压住全屏的东西:遮罩下的弹层、命令面板 元素是当前焦点

标准:

  • 阴影色是冷紫灰(--base-shadow-tint: 56 50 120),不是纯黑——纯黑会让画面发脏。
  • 一层里只用一个阴影档;需要两个就是两个元素,不要在一个元素上叠 shadow-sm + shadow-md。
  • 玻璃面本身已有 --glass-inset(边缘光学),再叠大阴影会显脏——玻璃浮层用 --shadow-md 即可。

实测:--shadow-sm 14 处、--shadow-md 9 处、--shadow-lg 6 处,分布与上表一致 ✓; --glass-inset 与大阴影同层出现的地方只有玻璃浮层本身 ✓。

动作:新面板先问"它浮不浮"。贴着 = sm,浮一层 = md,压屏 = lg。

检查项:

# 同一个元素上不该同时出现两档阴影
grep -rn "shadow-\[var(--shadow-" src | grep -c "shadow-\[var(--shadow-md)\].*shadow-\[var(--shadow-sm)\]"

搬到新项目要花多少代价

要搬的 在哪 量
材质层约 22 个变量(玻璃 / 新拟态 / 阴影) global.css「玻璃 / 新拟态」「阴影 token」 22 行
三条风格配方 global.css「风格轴配方」 40 行
玻璃实际生效的全局规则 global.css 的 [class*='backdrop-blur'] 4 行
  • 零依赖:backdrop-filter 是浏览器原生,不引任何模糊库。
  • 必须整块搬:搬 token 时把分级判据一起搬——玻璃只给悬浮层、内容面板一律不透明白底。只搬 token 不搬这条,玻璃会从浮层蔓延到卡片,分级就没了。
  • 可以剥掉:新拟态配方(除非真要那条轴);--glass-inset 的边缘光学可先不搬(没有它玻璃也能看,只是薄一点)。
  • 工时:约 15 分钟。
  • 搬完怎么验:把 --glass-blur 改成 0 再改回 16px,看所有浮层是否同步变化(说明没有硬编码模糊);悬浮层底下必须有内容,空底玻璃看不出厚度。

字号节奏与排版

字号只有一套阶梯 + 一条缩放倍率,档位按"角色"选不按"好看"选
typography.md

1. 阶梯

标准:本站只有一条字号阶梯(--text-*),外加一条全局缩放倍率 --text-scale(小 0.92 / 标准 1 / 大 1.12,由「字体与字号」面板控制)。

档 值 角色 实测用量
--text-2xs 10px 极小 label、图标角标 27
--text-xs 12px 副标、tag、时间戳 56
--text-sm 13px 元数据、表格、说明文字 57
--text-base 14px 正文 10
--text-md 16px 强调正文 0
--text-lg 20px 小标题 9
--text-xl 24px 大标题 7
--text-2xl 28px 特大标题 3
--text-3xl…7xl 30–72px 页面级标题(Tailwind 原生档,跟倍率走) 9

标准:

  • 只用 Tailwind 工具类(text-sm、text-2xs),不写 text-[length:var(--text-sm)]。 两种写法指向同一个 token,但混用会让"这一档到底用在哪"查不出来——本站两种形式都出现过(任意值形式 24 处)。
  • 档位按角色选,不按"我觉得小一点更好":正文 13–14、说明 12、元数据 10–12。 相邻两档不要跳着选(比如 12 直接跳 16),阶梯的意义就是连续。
  • 标题与正文的对比:至少两档。全站只有页头允许用大标题(text-2xl 起),组件内部标题不超过 text-lg。

实测(基线 982e282,2026-10-31 重测):命名档位工具类 361 处,text-[length:...] 任意值 97 处——两套写法并存,这是本文要清掉的第一件事。 --text-md(16px 强调正文)定义了就没人用:要么补上用法,要么删掉这一档(阶梯里的空档也是噪声)。

动作:

  1. 把 97 处任意值形式换成工具类形式(text-[length:var(--text-2xs)] → text-2xs)。
  2. --text-md 二选一:删掉,或在"强调正文"处统一使用。

检查项:

# 不该再有任意值形式的字号
grep -rn "text-\[length:var(--text-" src | wc -l

2. 行长、行距与中英混排

标准(library.md 第四节,clreq §7.1 / Butterick):

  • 中文正文行距 = 字号的 1.5–2.0 倍(--text-base 14px → 行高 21–28px)。
  • 正文行长:中文 17–40 字/行(硬上限 48),西文 45–90 字符。
  • 中西文混排间距可固定 ¼ 字宽;line-break: strict 用于中文正文,短标签用 loose。
  • 中文标点避头尾(GB/T 15834)——正文里不能出现行首句读符号。

实测(2026-10-31 更新):行高已收成一条阶梯,覆写 Tailwind 的 --leading-*, 所以 20 处 leading-relaxed 都跟着一个数走:

档 值 用在哪
--leading-tight 1.15 页头 / 分区标题(leading-tight)
--leading-snug 1.375 单行说明、副标(leading-snug)
--leading-normal 1.5 默认(Tailwind 原值)
--leading-relaxed 1.65 正文(leading-relaxed,原 1.625;落在 clreq 1.5–2.0)
--leading-code 1.55 代码块(等宽字不需要正文那份呼吸)

例外:复现页(page-home / dashboard 等"海报")保留 leading-[1.1] / leading-none 这类自己的行高—— 它们不是站内结构,按 layout.md 的 counter_example 同一口径处理;首页 Hero 是单行展示标题,直接用 leading-none。

行长也已落地:--measure: 40em(≈40 汉字 / 80 西文字符,同时落在 clreq 17–40 与 Butterick 45–90 内), .prose-notes 的段落 / 列表 / 引文统一封顶。宽屏下有两种合法去处: 分栏(.prose-columns,≥1800px 起 columns: 34em——栏宽本身就是行长,栏内段落不再二次收窄), 以及表格与代码块——它们不跟行长走,表格按内容宽度左对齐(拉满会把长文本列撑成 150 字一行, 见 layout.md §2),代码块用满容器宽。

动作:新增标题用 leading-tight、正文用 leading-relaxed,不写 leading-[1.x] 字面量; 新增正文容器一律走 .prose-notes,别再手写 max-w-[68ch] 一类的第二套行长。

检查项:

# 站内结构不该再有任意值行高(复现页除外)
grep -rn "leading-\[" src/pages src/components | grep -v "showcase/demo" | wc -l   # 应为 0
# 行长与行高各只有一处真值
grep -rn -- "--measure\|--leading-relaxed" src/styles/global.css

3. 字重

标准:只有四档字重,取自 OpenType wght 轴:--fw-normal 400 / --fw-medium 500 / --fw-semibold 600 / --fw-bold 700。 字重只用数字,不用 light / bold 关键字(CSS Fonts 4 + OpenType 注册轴)。 层级靠字号 + 颜色拉开,字重只在"强调"和"标题"两处用:--fw-semibold 给标题,--fw-medium 给选中态。

实测:--fw-bold 定义了没人用——同上,二选一(删掉,或在真正需要强调的地方用)。


搬到新项目要花多少代价

要搬的 在哪 量
字号阶梯 9 档 + --text-scale 两行 global.css「字号阶梯」 14 行
行高阶梯 4 档 + 代码块行高 global.css 的 @theme 与「行高」段 8 行
--measure 与正文规则 global.css 的 --measure + .prose-notes 4 行
正文字族 + 等宽字族 public/fonts/ + @font-face 6 个家族约 1.0MB;只带 2 个约 480KB
字体切换面板(可选) src/components/workbench/FontSwitcher.tsx 264 行(含 EN / ZH / 字号 / 等宽四维)
  • 零依赖:字体是静态文件;不想管文件就换 fontsource 的 npm 包。
  • 必须整块搬:字号阶梯要整条搬(9 档齐全),东拼西凑会出现"14 和 15 都在用";--measure 与正文规则一起搬,否则宽屏会把行拉到 90 字符以上。
  • 可以剥掉:三家族等宽切换(省 3 个字体包与开关 UI);中文字族只留一个。
  • 工时:阶梯 + 两个字族约 15 分钟;连字体切换面板约 40 分钟。
  • 搬完怎么验:grep -rn "text-\[length:var(--text-" src | wc -l 证明字号全走 token;量一段中文正文是否落在 40 字/行上下;站内结构里 leading-[ 任意值应为 0。

状态色、交互反馈与指针

状态色只表达"事情成没成";按压与焦点是可用性底线;光标是操作提示,不是装饰
interaction.md

1. 状态色

标准:三个语义前景色 + 一套底色/描边,只表达事情的状态,不表达品牌。

token 值(浅色) 用在哪 实测对比度(白底 / 页底)
--color-success-fg #1c815c 成功提示、涨跌正向 4.83 / 4.51
--color-warning-fg #b65700 危险操作、待办、"计划中" 4.83 / 4.50
--color-info-fg #2777ac 进行中、中性提示 4.86 / 4.53

标准:

  • 前景色只当文字色或小圆点用,且必须过 SC 1.4.3 的 4.5:1(正文尺寸)。 底色与描边(--color-*-bg / --color-*-bd)从前景色 mix 出来,自动跟着前景色走。
  • 大面积底色用 -bg(10% 混色),描边用 -bd(25% 混色)——不要自己再调 rgba。
  • 状态不能只靠颜色表达:圆点旁边要有文字("已复现 / 复现中 / 计划中"),色盲用户也要读得到。

实测:语义前景色在代码里用于 notes 链接、dashboard 涨跌、settings 删除按钮、列表页状态点 ✓。 对比度已按 SC 1.4.3 修正(修正前 warning-fg 在页底只有 2.97,连 3:1 都不够)。 --color-text-subtle 同步修正为 #717179(4.84 / 4.51)——它之前是 2.76,被当小字用着。

动作:新增状态一律从这三个 token 出,不要写新的色值。

检查项:

# 不该有裸写的状态色
grep -rnE "#(059669|d97706|0284c7|9b9ba3)" src | wc -l

2. 按压反馈

标准:所有可点元素都要有按压反馈,用缩放表达,不用颜色——颜色变化会和 hover 撞。

  • 通用做法:给元素加 .press-feedback(transform: scale(0.97) + --dur-press 过渡)。
  • 缩放量统一:全站一个值,不许这里 0.97 那里 0.95。
  • 按压是瞬时的:--dur-press 120ms(Emil 100–160ms 带内)。长按不做长反馈。

实测(基线 982e282 之后):原先有三套缩放值并存——.press-feedback 的 0.97、 玻璃按钮的 hover:scale-[1.02] active:scale-[0.98]、灵动岛按钮的 hover:scale-105 active:scale-95。 现已收敛成两处定义(在 global.css,不在组件里;Toast 入场另有 scale(0.92),属动效入场,不在此列):

class 行为 用在哪
.press-feedback 按压 → scale(0.97) 所有可点元素(chip、按钮、图标、开关、通知关闭)
.press-hover 悬停 → scale(1.02),按压 → scale(0.97) 按钮型元素(CTA、玻璃按钮)

禁用态不缩放(.press-feedback:disabled:hover { transform: none })——这条必须留在层外, 否则压不过 Tailwind 的 layered 工具类。

悬停位移要门控:触屏没有真悬停,group-hover:translate-x-* 会在点完后留下假悬停态。 位移型悬停一律用 .group-shift-right / .group-shift-down(包在 @media (hover: hover) and (pointer: fine) 里)。

检查项:

# 缩放值只应出现在 global.css 的两处定义里
grep -rn "scale-\[0\.9\|scale-\[1\.0" src | grep -v global.css | wc -l
# 位移型 hover 不该再用 Tailwind 的 group-hover:translate
grep -rn "group-hover:translate" src | wc -l

3. 焦点与目标尺寸

标准(这是可用性底线,不是修饰):

  • 焦点环必须可见:SC 2.4.13 要求等效周长 ≥2px,本站 *:focus-visible 用 2px 实线 + 2px 偏移。
  • 指针目标 ≥ 24×24 CSS px(SC 2.5.8)。轨道、图标、关闭按钮都要够。 空间不够时可以靠间距补偿,但不要靠"看起来够大"。
  • 键盘可达:Tab 能走到,Enter/Space 能触发;浮层打开时焦点进去、关闭后归还。
  • 浮层不能压住下层的焦点(SC 2.4.11)。
  • 输入框只能有一套焦点指示:用 focus:outline-none + 自己的环(拟态用 2px shadow 环, 玻璃 / 极简用描边提亮)替代全局环。全局规则放在 @layer base, 否则层外规则会压过工具类,出现「外环 + 内环」两层焦点。

实测(基线 982e282,修正后):

  • 焦点环 1.5px → 2px ✓(原值低于 SC 2.4.13)。
  • Switch sm 轨道 20px → 24px ✓(md 26px 本来就合规)。
  • Segmented 是完整的 APG radiogroup(方向键 / Home / End / roving tabindex)✓。
  • Dropdown 是 APG Select-Only Combobox:aria-activedescendant 指向高亮项,焦点留在触发器; ↑↓ / Home / End 移动、Enter / 空格选中、Esc / Tab 收起、点外关闭,禁用项跳过 ✓。
  • Combobox 是 APG list-filtering combobox:焦点同样留在输入框,↑↓ 在关闭时先打开、 Enter 选中并阻止表单提交、Esc 只收弹层、禁用项跳过,结果数用 aria-live 播报 ✓。 与 Dropdown 的分工是能不能自由输入:只能选一个已有值用 Dropdown,输入即过滤用 Combobox。
  • Tabs 是 APG Tabs(automatic activation):roving tabindex、方向键同时移动焦点与选中、 Home / End 到两端、禁用页签跳过,面板可聚焦 ✓。
  • Choice 是 APG Checkbox + Radio Group:原生 input 保留空格切换与方向键单选, input sr-only 后焦点环用 peer-focus-visible 画在可视框上,组级与单项禁用都可见 ✓。
  • Switch 是 role="switch" + aria-checked,底色 <button> 自带 Space/Enter ✓。
  • Modal 补齐了焦点陷阱、关闭后焦点归还、aria-labelledby ✓。
  • 缺口:Dock 的图标按钮尺寸随放大变化,静止时 52px ✓;但 topnav 的字号/图标类 chip 与 CommandPalette 的行高仍需逐个核对是否达到 24px。

动作:新增可点元素先量尺寸(DevTools 或 getBoundingClientRect),不够就加 padding。

检查项:

# 焦点环不能退回 1.5px
grep -rn "outline: 1.5px" src | wc -l

4. 拖拽与指针手势

标准:

  • 双通道:指针能做的,键盘也必须能做——拾起 / 移动 / 放下 / 取消四步缺一不可。
  • 指针拖拽用 Pointer Events + setPointerCapture,不用 HTML5 drag-and-drop(触屏与样式都不可控)。
  • 只在把手上开始拖拽,行本身保留滚动;把手设 touch-action: none。
  • 拖动中只表达"拿起了"与"会落到哪",松手才提交新顺序;落点量布局位(offsetTop),不量被 translate 的 rect。
  • 读屏要播报:拾起 / 移到第 n 项 / 放下 / 取消,写进 aria-live="polite"。
  • 降级:prefers-reduced-motion 下可去掉过渡,但顺序改变与焦点位置不能丢。

两种手势,两种判据

同一个"拖",动机不同,规则也不同。混着用就会把一种的正确做法搬到另一种上。

跟手型 拉动型
例子 拖拽排序、滑动删除、滑块、抽屉 拽住往外拉、拉够远就触发(如"拉飞便签")
位移关系 按住期间 1:1,不许打折 阻尼就是交互本身
提交时机 松手判定(滑块例外:连续提交) 越过触发线即提交,之后不可撤销
判错的样子 加了缓动或多乘一个系数 → 读成"卡顿 / 拖不动" 改成 1:1 → 失去"拉"的张力

跟手型手势的四段链

1:1 跟手  →  越界阻尼  →  阈值提交  →  吸附落位
 track        damp         commit       settle
  • 跟手:按住期间位移必须与指针相等。任何缓动、任何系数都会读成延迟;拖拽期间不挂 transition,松手才挂。
  • 越界:越过边界后仍跟手,但那部分只跟 30% 左右。硬停像 bug,无限位移像没有边界。
  • 提交:速度与距离双条件,不看单一位移。「放在那里」和「甩过去」是两种意图;测速只取最后约 120ms 的位移,全程平均会被起手那一下拖偏。
  • 吸附:落位用 token 时长;reduced-motion 下不画过程,直接到终态。

轴锁:横向 / 纵向意图不明时先看起手方向,纵向就让位给滚动(touch-action: pan-y),不要抢滚动手势。

本站实现:

  • drag/sortable-list——跟手型,指针与键盘共用同一份 moveItem(items, from, boundary);
  • drag/swipe-to-dismiss——跟手型,四段齐全,速度线 0.5px/ms;键盘 Delete / Backspace 等价,结果进 aria-live;
  • slider/*——跟手型但连续提交,不涉及阈值与吸附。

检查项:

# 拖拽组件必须捕获指针,并带播报
grep -rn "setPointerCapture" src/components | wc -l
grep -rn "aria-live" src/components/drag | wc -l

5. 指针与光标

标准:光标是操作提示,不是装饰。按三层处理,越往后越谨慎。

A 层 · 原生状态关键字(首选,零 JS):状态用系统关键字表达,不用图片光标。

状态 关键字 用在哪
默认可交互 pointer 按钮、链接、可点卡片
文本编辑 text 输入框、可选文本
可拖拽 / 拖拽中 grab / grabbing 把手、可拖行
不可用 not-allowed 禁用控件
缩放 / 列宽 / 移动 zoom-in/zoom-out、col-resize、move 画布、表格、可拖动面板
  • 光标图片(cursor: url(...))只在必须时用:PNG 或带自然尺寸的静态 SVG, 建议 ≤32×32(Firefox / Chromium 默认上限 128×128,超限会被静默忽略), 必须带末尾关键字兜底;热区 x y 相对图片左上角。
  • 光标不参与状态色体系:状态不能只靠光标表达,颜色与文案仍是主信号。

C 层 · 自定义光标替身(仅 demo,不做站点级):四条硬规则,缺一条就不做。

  1. 不全局隐藏:禁止在 html / body 上写 cursor: none;原生光标始终保留。
  2. 只精确指针:只在 (hover: hover) and (pointer: fine) 下启用,触屏与粗指针不加载。
  3. 输入区让位:文本、输入框、可选中区域上不画替身,避免遮挡插入点与选区。
  4. 动效降级:prefers-reduced-motion 下关闭跟随与拖尾,只留静态提示或直接不启用。
  • 替身不能是唯一反馈:键盘、读屏、触屏用户必须仍能完成同一操作。
  • mix-blend-mode 只做点缀,不保证任何背景下的对比度;它还会创建独立堆叠上下文。

B 层 · 元素响应指针(推荐):光标不动,元素响应——磁吸、聚光、放大。

  • 写 transform,不写 left/top 与宽高;指针移动期间不挂过渡,离开再补间回位。
  • 同时门控 (hover: hover) and (pointer: fine) 与 prefers-reduced-motion;触屏不响应。
  • 指针操作在 up-event 完成或可中止(SC 2.5.2);指针目标 ≥24×24(SC 2.5.8)。

本站实现:cursor/magnetic(矩阵「其他」列)——元素朝指针偏移、离开回位,光标保持原生; cursor/contextual 作为 C 层的局部 demo——只在虚线框内跟随、按 data-cursor-state 换形、 输入区让位,原生光标不动;原生关键字已散落在各组件(pointer / grab / grabbing / not-allowed)。

检查项:

# 不做站点级光标隐藏;磁吸只给精确指针
grep -rn "cursor: none" src | wc -l            # 应为 0
grep -rn "pointer: fine" src | wc -l           # ≥1
grep -rn "prefersReducedMotion" src/components/cursor | wc -l  # ≥1

搬到新项目要花多少代价

要搬的 在哪 量
状态色(3 前景 + 3 对底/描边 + 分隔线) global.css 语义层 14 行
全局焦点环 global.css 的 *:focus-visible 8 行
按压 / 悬停缩放(全站唯一定义) .press-feedback / .press-hover + hover 门控 约 30 行
光标关键字对照与自定义光标四条硬规则 本文 §5 0 行代码,1 张表
对比度重算脚本(改色时才要) scripts/pick-colors.mjs 138 行
  • 零依赖。
  • 必须整块搬:焦点环与 24×24 目标尺寸是可用性底线,不能只搬颜色;.press-feedback 全站只留一处定义,搬过去之后把目标项目里其它 scale(...) 全删掉。
  • 可以剥掉:pick-colors.mjs(它依赖本站的 OKLCH 取色流程,只在你也要改色时才需要)。
  • 工时:约 10 分钟(换色另算:用脚本把新色重算到 4.5:1 约 20 分钟)。
  • 搬完怎么验:纯键盘 Tab 走一遍,焦点环可见且不被浮层遮住(SC 2.4.11);按钮点击面积 ≥24×24(SC 2.5.8);开"减少动效"后按压缩放消失、颜色变化保留。

状态矩阵

每个组件按七态验收;哪些状态"不许缺"由 APG 有没有专条决定
states.md

1. 七态

标准:每个组件按七态验收,缺哪一态要说得出理由。

状态 判据 缺了会怎样
默认 静止时的样子 ——
悬停 指针在目标上 用户不知道能不能点
按压 目标被按下 不知道点击是否生效
焦点 键盘可见焦点 键盘用户直接不可用(SC 2.4.7)
禁用 不可用但可见 用户不知道为什么不能操作
加载 数据在路上 不知道是卡住了还是没反应
错误 输入非法 / 操作失败 用户不知道自己错在哪
空 没有数据 页面看着像坏了

2. 哪些不许缺

标准(决策 8:APG 状态闸门——APG 有专条的,状态与键位不许缺):

type 必缺即不合规 依据
switch 焦点、禁用、按下 APG Switch pattern
segmented(radio group) 焦点、禁用、方向键遍历 APG Radio Group pattern
modal(dialog) 焦点陷阱、Esc、焦点归还、关闭后焦点归还给触发元素 APG Dialog (Modal) pattern
dock(toolbar) 焦点、Home/End APG Toolbar pattern
input 焦点、禁用、错误 WCAG 1.4.11 + 3.3.1
dropdown 焦点、禁用、按下、展开/收起 APG Combobox (Select-Only) + Listbox
tabs 焦点、禁用、按下、方向键 / Home / End APG Tabs pattern
choice(checkbox / radio) 焦点、禁用、方向键(radio) APG Checkbox + Radio Group
button 焦点、禁用、按下 WCAG 2.4.7 / 4.1.2

没专条的(card / nav / motion)自定,但焦点与禁用不许没有——那是所有可交互元素的底线。

Skeleton 是 loading 态的载体,不做七态:只验 role=status + aria-busy、 占位块 aria-hidden,以及版式是否与最终内容一致。

3. 怎么查

实测口径:demo 是状态矩阵的载体,所以"demo 里有没有这个状态"就是覆盖度。 npm run check:states 扫 demo 源码里的状态证据(disabled / aria-invalid / aria-busy / focus-visible / 空 / loading 等),对照上表出缺口——缺口就是 P4 的优先级,不靠新鲜感排。

动作:补状态时先补"必缺"那几类;demo 里要让状态看得见(不是只加一个 prop), 否则它不算被演示。

demo 里的文字只有一类该留在舞台内:怎么操作这一屏(键位、触发方式),一行以内。 "为什么这样设计 / 材质怎么叠"这类解释写在 notes 与复用卡(右侧本来就有), 舞台是看效果的地方,不是读说明的地方。

检查项:

npm run check:states

搬到新项目要花多少代价

要搬的 在哪 量
必缺状态表 src/data/state-requirements.json 24 行
覆盖率脚本 scripts/check-states.mjs 95 行
notes frontmatter 约定 AGENTS.md + 各组件的 notes.md 每组件一个字段块
  • 零依赖,但依赖机制:矩阵能被执行的前提是"每个组件都有带 frontmatter 的 notes"。没有 registry / frontmatter 体系时,这一篇只能降级成一张人工对照表。
  • 必须整块搬:表 + 脚本 + frontmatter 约定三件一起;只搬表就是抄了一份没人执行的清单。
  • 可以剥掉:APG 专条判定(目标项目不要求无障碍达标时,可只留默认 / 悬停 / 按压 / 禁用四态)。
  • 工时:已有 frontmatter 体系约 30 分钟;没有的话先建约定,约 2–3 小时。
  • 搬完怎么验:node scripts/check-states.mjs --strict 全绿;故意删掉某个组件的 disabled 演示,脚本必须报出来。

页面骨架与间距

全站页面固定视口高度、页头不滚、正文内滚;间距走 4px 网格,圆角走一条阶梯
layout.md

1. 固定视口 + 内滚

标准(ROADMAP 决策 5):站内页面不整体上下滚。

┌ TopNav(fixed,56px)──────────────────────────┐
├ PageShell(高度 = 100dvh - 顶栏高度)─────────┤
│  ┌ PageBar(不滚)标题 + 说明 + 工具栏 ─────┐  │
│  └─────────────────────────────────────────┘  │
│  ┌ PageScroll(overflow-y-auto)正文 ───────┐  │
│  └─────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘
  • 视口高度 = calc(100dvh - var(--nav-h)),--nav-h 由 body.has-topnav 给(56px),复现页为 0。
  • 页头(.page-bar)不参与滚动,只靠一条发丝线(--color-divider)与正文划界。
  • 正文滚在自己的容器里(.page-scroll),overscroll-behavior: contain——滚到底不带动文档。
  • 只有整页场景(营销复现页)例外:它没有页头,内容从上到下铺开。

为什么:整页滚动时顶栏和页头会跟着走,用户每次要操作都得滚回去;固定视口让"页头是常驻的"这件事成立。

实测(基线 982e282,2026-10-31 重测):src/components/PageShell.astro 已落地,9 个页面走它; 有页头的 7 页(使用概览 / 组件详情 / 整页场景 / 组件列表 / 设置 / 矩阵 / 页面总览)全部走 PageShell; 营销复现页(page-home)走 PageShell 但无页头;index 与 workbench 不走 PageShell——前者是落地页,后者是固定视口工作台 ✓。

动作:新增页面一律用 PageShell;不要自己写 min-h-screen + 文档滚动。

检查项:

# 页面里不该再有文档级滚动容器
grep -rn "min-h-screen\|min-h-\[calc(100vh" src/pages | wc -l

2. 屏宽基准

标准:同一个屏宽基准 → 同一套内边距 → 同一个容器。 全站的宽度只有一处真值,容器、内边距、贴边定位都从它算:

:root {
  --frame-max: clamp(1560px, calc(100vw - 440px), 3600px); /* 中间项 = 两侧留白固定 220 */
  --frame-gutter: 94vw;  /* 窄屏两侧留白 */
  --frame-pad: 16px;     /* 框内左右内边距(≥640px 时 32px) */
  --measure: 40em;       /* 行长上限:管读的东西,不管框 */
  --page-col: 96rem;     /* 分区页的内容列:框内的第二档,单页可用 colWidth 覆写 */
}

基准本身随屏宽增长(2026-10-31 两轮):固定 1560 在 4K 上两侧各留 1140px,用户实测"留白很不舒服"。 现在的规则是留白固定 220px:2000px 以下与最早一致(1560),往上框按 1:1 吃掉额外宽度—— 2560 与 3840 两档实测两侧都是 220px,3600 封顶。增长只写在 token 定义里: 别处再写一个 calc(100vw - …) 就是第二套基准,check:frame 会拦。

宽度被列数吃掉,而不是被行长吃掉:框长了之后,段落有两种合法去处—— 分栏流动,或留在 --measure 内(40em ≈ 40 汉字 / 80 西文字符,见 typography.md §2):

类 用途
.container-page 只定宽与居中(不带内边距)
.page-frame 居中框 + padding-inline: var(--frame-pad)——页面内容区与顶栏用它
.page-col 分区页(设置 / 详情 / 向导)的内容列:在框内再收一档到 --page-col 并居中,页头与正文同用一条列
.wb-frame 工作台三栏的框,与全站同一个基准
.doc-grid 文档墙:≥1800px 起 repeat(auto-fill, minmax(34rem, 1fr)),窄屏单列;展开的那篇 span 2
.prose-columns 正文分栏:≥1800px 起 columns: 34em,标题 / 表格 / 代码块 / 引用 column-span: all 整宽跨栏
.glossary-columns 条目墙:columns: 26rem,宽度换列数(字典页排法)
.glossary-columns-wide 行更宽的一档(组件清单/术语表):≥2600px 才分列,常规屏保持单列表格

表格不拉满:≥1800px 时表格回到真实 table 盒(display: table; width: auto; max-width: 100%), 按内容宽度左对齐、放不下就换行。拉满卡片会把"管什么"这种长文本列撑成 150 字一行—— 宁可让表格右侧留白,也不牺牲列内的行长。

为什么这条是硬规则:2026-10-31 之前的"分段感"几乎全是基准不统一造成的——顶栏 1560 居中、 工作台满宽、logo 绝对定位到屏幕边缘。于是 logo、导航、控制簇、三栏各用各的宽度, 在 3848px 屏上两端能差 3800px。改基准比改样式有效:统一之后,1920 与 3848 两档实测 logo 左边 212 / 1176、齿轮右边 1696 / 2660,两端始终落在框上。

实测(2026-10-31,headless Chrome 真像素 + CDP 量盒): 1920 框 1560(两侧 180)、2560 框 2120(两侧 220)、3840 框 3400(两侧 220); /design 文档墙 1920 两列 / 3840 四列,判断层术语 1920 三列 / 3840 六列, 资料库正文 2560 三栏、3840 五栏,表格按内容宽(3840 实测 2081px,最后一个字列 915px); 工作台舞台在超宽下把 demo 卡垂直居中(my-auto),画布留白成为构图而不是空洞。

检查项:

npm run check:frame   # 写死宽度 + 容器后又写 px-,两处都拦

3. 间距

标准:间距只用 4px 网格(Refactoring UI:间距只用 4 的倍数)。本站间距 token:

token 值 用途
--space-1 4px 图标与文字之间
--space-2 8px 紧凑内边距
--space-3 12px 控件内边距
--space-4 16px 常规间距
--space-5 24px 区块内边距
--space-6 32px 区块之间
--space-7 48px 大块留白
--space-8 64px 页面级留白

实测:--space-* 定义了就没人用——全站的间距实际由 Tailwind 的 p-4 / gap-3 这类工具类承担, 而且这些工具类的值不是 4 的倍数(Tailwind 的 spacing 步长是 0.25rem = 4px ✓,但 gap-2.5/p-3.5 这类半档会破格)。

动作:二选一——① 把 Tailwind 的间距步长也纳入 token 契约(在 @theme 里定义 --spacing); ② 承认间距由 Tailwind 负责,删掉本站的 --space-*(它们现在是"定义了没人用"的假契约)。 推荐 ①:既然字号已经用 --text-scale 管住了倍率,间距同理可以收口。

检查项:

# 半档间距会破 4px 网格
grep -rhoE "(p|m|gap|w|h|top|left|right|bottom)-[0-9]+\.5" src | sort | uniq -c

4. 圆角

标准:圆角是一条阶梯,值来自 @theme 的 --radius-*(Tailwind 的 rounded-* 工具类直接取用):

档 值 用途
--radius-sm 4px 小标签
--radius-md 6px 输入框、chip
--radius-lg 8px 按钮、卡片
--radius-xl 12px 大卡片
--radius-2xl 16px 面板
--radius-3xl 24px 弹层、坞站托盘

阶梯 = 8px 基准的倍数(0.5 / 0.75 / 1 / 1.5 / 2 / 3)。值沿用原状—— 先把真值收进 token,改档位是 P3 配方表的事(换轴时半径应该跟着轴一起换)。

实测:101 处 rounded-* 工具类,全部自动取用 token ✓(收进 @theme 之前是散落的默认值, 而 Tailwind 的默认值恰好与本站阶梯一致,所以视觉没有变化)。

动作:新增容器从这六档里选;需要"随手一个圆角"时先想清楚它属于哪一档。


搬到新项目要花多少代价

要搬的 在哪 量
屏宽基准 4 个变量(含 --measure) global.css「屏宽基准」 12 行
三个容器类 global.css「布局工具」 17 行
页面骨架 src/components/PageShell.astro 32 行
守门脚本 scripts/check-frame.mjs 86 行(改 SKELETON 路径即可)
文档墙 / 条目墙 / 正文分栏(按需) global.css 的 .doc-grid / .glossary-* / .prose-columns 约 60 行
  • 零依赖:纯 CSS + 一个原生 Node 脚本。
  • 必须整块搬:token → 容器 → 页面骨架 → 守门脚本,四件按序。少了脚本,三个月后一定会冒出第二套宽度——那正是本站 2026-10-31 之前"分成好几截"的成因。
  • 可以剥掉:.wb-*(工作台三栏);多列容器(信息密度不够时单列更稳)。
  • 工时:约 20 分钟——10 分钟拷,10 分钟把 --frame-max 的下限/封顶换成目标设计稿的宽度,再跑 1920 与 3840 两档。
  • 搬完怎么验:两档宽度下量 logo、页头标题、内容左边缘是否落在同一条竖线上;node scripts/check-frame.mjs 报 0 违规。

图标

通用 UI 图标统一走 Lucide 24 网格与 2px 线宽;品牌 Logo 与图形 demo 例外;激活态只改颜色不改线宽
icons.md

1. 唯一来源

标准:通用 UI 图标一律来自 lucide-react,并通过 src/components/Icon.tsx 渲染。业务组件不直接手写 <svg> 表达箭头、勾选、关闭、面板、设置这类通用含义。

动作:

  • 新增通用图标:先在 Lucide 里找,再通过 Icon 使用。
  • 类型图标:只改 src/components/icons.ts 的 TYPE_ICON 映射,不在 TypeGlyph 里画路径。
  • 品牌 Logo、滤镜图形、曲线图可以保留自绘 SVG,但不能顺手新增装饰性内联图标。

检查项:

grep -Rn "<svg" src --include="*.tsx" --include="*.astro"

2. 网格、尺寸与线宽

标准:

项 规则
网格 统一 viewBox="0 0 24 24"
渲染尺寸 14 / 16 / 18 / 20 / 24,不再出现 15、17、22 这类临时值
默认线宽 2
小尺寸勾选 14px 勾用 2.25,12px 勾用 3,避免缩下去后断线
端点 Lucide 默认 round cap / join,不混 square
激活态 只改颜色、底衬或尺寸,不改 strokeWidth

为什么:图标是否舒服,第一眼看的是光学重量,不是形状。线宽随激活状态变化会让图标“跳一下”;14 / 20 / 24 三种网格同时存在,会让一排图标看起来像从三个系统里借来的。

3. 类型图标

标准:16 个组件类型各有唯一图标,名称与语义来自 TYPE_ICON:

类型 Lucide 图标
button MousePointerClick
card SquareStack
input TextCursorInput
dropdown ChevronsUpDown
choice SquareCheck
nav Waypoints
tabs LayoutPanelTop
modal AppWindow
dock Dock
segmented SlidersHorizontal
switch ToggleRight
drag GripVertical
cursor MousePointer2
motion Activity
toast BellRing
skeleton Rows3

动作:新增类型时,TYPE_LABEL、TYPE_ORDER、TYPE_ICON 三处一起补;缺图标会落回 Circle,但这不应成为常态。

4. 无障碍

标准:

  • 图标旁边有文字时,图标 aria-hidden="true"。
  • 图标是按钮唯一内容时,按钮必须有 aria-label,图标仍保持装饰性。
  • 颜色不能作为图标状态的唯一表达;禁用、激活、错误要同时有形状、文字或焦点态。

搬到新项目要花多少代价

要搬的文件:src/components/Icon.tsx(13 行)、src/components/icons.ts(44 行)、package.json 里的 lucide-react。

零耦合:Icon 只包 Lucide,不读 registry、不读站点 token。换项目时可以直接复制这两个文件。

可以剥掉的部分:TYPE_ICON 是本站组件类型专用的,新项目可以删掉映射,只保留 Icon 包装。

不能直接搬的情况:如果项目不允许外部依赖,就把用到的 Lucide SVG path 复制进 icons.ts;Lucide 是 ISC 许可,但复制后需要自己维护更新。

工时:接入约 10 分钟;把现有内联图标全部替换约 1 小时;补图标预览页约 1 小时。

搬完怎么验:

grep -Rn "strokeWidth=\"1.8\|strokeWidth=\"2.2\|viewBox=\"0 0 20 20\|viewBox=\"0 0 14 14" src

期望 0 处。

页面模式

页面头只留一个主操作在右上;索引页的工具栏紧贴数据;设置里文本显式保存、开关即时生效;流程失败先给可聚焦的错误摘要
pages.md

组件回答"这个零件长什么样",页面模式回答"零件按什么顺序摆"。 下面每条都来自至少两家系统的同一结论;只有一家主张的,标了出处不作硬规则。

1. 页面头

标准:H1 是资源名或任务名;一句说明只补 H1 没说的;主操作只有一个,放在右上。

位置 放什么
左 返回 / 面包屑(二选一)→ H1 → 一句说明
右 主操作(动词 + 名词)→ 次要操作 → 溢出菜单
永不 第二个同权重的主操作;把主操作沉到内容底部
  • 主操作是"创建 / 查看日志 / 提交"这类推进任务的动作;"矩阵视图 / 列表视图"是视图切换,走次要按钮。
  • 单任务页例外(向导 / 认证 / 问卷):主操作就是这条流程的提交,留在表单末尾; 页头不再放第二个提交,也不放"下一步"的副本。页头右上只留给跨页面的动作(退出向导、找帮助)。
  • 错误分两层:页面级 banner 说后果与恢复方式,字段级 errorMessage 说这一格该填什么。两层各说一件事,不互相复述。
  • 窄屏时操作区换到标题下一行,主操作仍排最后(视觉上靠右)。
  • 没有创建能力就不放创建按钮,不摆一个禁用按钮占位。

本站落点:PageHeader 的 actions 槽。dashboard(筛选)、list(矩阵视图)已用; record 的三个操作原先沉在正文底部,与"页头承担页面级操作"不一致,已迁入页头。 分区页(PageShell col)的页头与正文共用 .page-col 这一条内容列——标题不会一个人留在框边上。 单任务页:create(48rem)与 auth(30rem)用 colWidth 收窄并在正文区垂直居中。

反例:page-home 没有页头——它是品牌页,主操作在 hero 里,跟着内容走。

2. 索引页

标准:工具栏紧贴数据上方;计数与结果同区;分页只在超过一页时出现。

  • 搜索、筛选、排序放进同一个面板的顶部条,不要分两处;它们只影响下方列表。
  • 计数写成结果范围 + 总数(1–12 条,共 89 条),放在列表页脚;不要只写总数,用户不知道当前看到哪一段。
  • 筛选或搜索变化时回到第 1 页;只有一个分页时不渲染分页条。
  • 行整行可点就整行是链接;不可用的行保持不可点,不假装可点。

本站落点:ComponentList。工具栏、计数、分页、回第 1 页都已按此实现。

3. 表格(只读也要语义)

标准:文本左对齐、数字右对齐,列头跟数据同向;数字用 tabular-nums;只读表格同样需要 caption 与 scope。

  • 行高默认 40px 一档,比卡片行更紧;不为稀疏数据放大行高。
  • 窄屏不要硬压列:优先减少次要列,其次横向滚动,最后才换卡片列表。
  • 排序状态要能被看见(当前列 + 方向),不能只靠悬停提示。

本站落点:/showcase/matrix 已用 th scope;只读表已复现为 table 三材质,list 场景用 framed=false 嵌进面板(/showcase/demo/list 的「表格」视图)。排序 / 筛选 / 选择 / 固定列都还没做,逐项放行。

4. 设置页

标准:分组用 fieldset + legend;一行 = 标签 + 一句说明 + 控件;文本编辑显式保存,开关/滑块即时生效。

  • 宽屏两列:左栏是分组导航(锚点),右栏是卡片列(Polaris 的 2fr / 5fr)。 导航出现时,卡片内同名的分组标题收成 sr-only——同一屏不画两遍分组名,语义仍在。
  • 说明只写用户不知道的后果("上次更新 3 个月前"),不重复标签。
  • 文本字段有脏状态时给出保存 / 放弃,并说明"未保存";保存后给一次 aria-live 确认。
  • 不要每个卡片一套独立保存;同页只保留一套保存模型。
  • 危险操作单独成组(账户删除、回滚),不与日常开关并排;是否需要二次确认按决策 17 的可撤销性判断。

本站落点:page-settings。开关与滑块即时;个人资料改为显式保存(保存 / 放弃 + 状态播报); ≥1024px 起左栏分组导航 + 右栏卡片,窄屏退回单列并让卡片标题重新出现。

5. 详情页

标准:键值事实用 dl;行级操作必须带视觉隐藏上下文;所有行都去同一个地方时,用卡片级的一个操作。

  • 状态徽标、负责人、时间放在身份区附近,不要散到正文各处。
  • 主操作一个;次要操作与溢出菜单往后排。
  • 缺失的可选值写"未填写",不省略整行。

本站落点:record 用 Descriptions(dl)+ 面包屑;操作移到页头与第 1 条对齐; 宽屏两列(2fr / 1fr)——左=身份与变更摘要,右=版本 / 环境 / 提交 / 负责人 / 回滚点,窄屏按 身份 → 摘要 → 元数据 单列下来。

6. 流程页

标准:线性且 ≥3 步才用步骤条;可乱序、可跨会话的任务用任务清单,两者不混用(GOV task list / USWDS step indicator)。

  • 校验在提交时发生;失败后顶部错误摘要放在 H1 之下、表单之上,并把焦点移过去。
  • 摘要标题说明有几处问题;每条链接到出错字段,摘要文案与行内错误逐字一致。
  • 不因为失败清空输入;返回上一步要保留已填内容。
  • 核对页分组展示答案,能回到对应步骤修改;未填写的可选项显示"未填写"。

本站落点:create 三步步进器;本次补错误摘要、焦点与字段链接。宽屏时步骤条转到左侧成竖排导航,表单列仍按 colWidth 收到 48rem——表单不跟着框拉长。

7. 页面级状态

状态 规则 本站
加载 先给结构再给内容;骨架与最终版式同形 组件级 skeleton 已有;页面级载体未定
空 说清什么空了 + 一句下一步 + 一个主操作 empty-state 三材质已落地,list 已接
错误 说明影响与恢复方式,不用"出错了"兜底 banner 三材质已落地(warning 走 role="alert",info / success 走 role="status"),auth 已接;页面级"服务不可用"载体仍未定
404 / 500 H1 直说问题;给一条回去的路;不显示技术细节 仍是框架默认页

8. 来源与不照抄的部分

规则 本地快照 官方
索引页主操作右上、工具栏紧贴列表 polaris/.../patterns/resource-index-layout/variants/default.mdx Polaris(存档)
设置两列分组 + 保存栏 polaris/.../patterns/app-settings-layout/variants/default.mdx Polaris(存档)
分页只在需要时出现、过滤回第 1 页 govuk/src/components/pagination/index.md GOV.UK
数字右对齐、只读表格语义 govuk/src/components/table/index.md · uswds/_components/table/guidance/usability.md GOV.UK · USWDS
摘要列表的行级操作要带隐藏上下文 govuk/src/components/summary-list/index.md GOV.UK
错误摘要置顶、聚焦、逐条链接 govuk/src/components/error-summary/index.md · govuk/src/patterns/validation/index.md GOV.UK
步骤条只用于线性多步 uswds/_components/step-indicator/guidance/when-to-use.md USWDS
图表不靠悬停才能读、给数据表替代 uswds/_components/data-visualizations/data-visualizations.md USWDS

不照抄:

  • GOV 禁止"返回链接 + 面包屑"同时出现——本站 create 用返回、record 用面包屑,分场景成立。
  • GOV 的绿/红系统色不直接搬;语义色走本站 token。
  • Polaris 的 contextual save bar 已标 deprecated;本站不引那套组件,只借"脏状态才出现保存"的时机。
  • 破坏性操作不一律二次确认(决策 17);可撤销的用即时执行 + 恢复入口。
  • Polaris 的两列分组只在宽屏成立;窄屏退回单列,此时卡片内的分组标题重新出现(左栏导航隐藏后它是唯一标题)。

搬到新项目要花多少代价

要搬的 在哪 量
页面头槽位 src/components/PageHeader.astro 40 行
索引页工具栏 + 计数 + 分页 ComponentList.tsx + pagination/* 约 1 个页面
错误摘要 CreateWizard.tsx 内的局部实现 约 40 行
只读表格语义 matrix.astro 的 table 写法 1 个示例
  • 零依赖:全部是结构规则,不带库。
  • 必须整块搬:页面头槽位 + 索引页计数/分页 + 错误摘要,三件一起才形成一致的任务路径。
  • 可以剥掉:Polaris 两列设置、Gov 任务清单——站点没有对应场景就不搬。
  • 搬完怎么验:四个场景各走一遍"进入 → 主操作 → 失败 → 恢复";错误摘要能用键盘逐条跳到字段。

整站带走

一段话给 AI,一块 CSS 给工程——两者都由本站的 token 与标准生成

复用「UI Echo 设计系统」:
- 材质分级:玻璃只做悬浮层;内容面板一律不透明白底 + 软阴影;新拟态是独立风格轴
- 风格轴:Apple Liquid Glass (visionOS)(液态玻璃):半透明底 + 1px 细边框 + 背景模糊 = 玻璃厚度
Flat Minimal UI(极简):没有浮起、没有模糊、没有大阴影
Neumorphism(新拟态):同色双阴影浮起:亮侧高光、暗侧投影
Border Glow + Inner Spotlight(聚光):外层渐变边框跟随鼠标,白底盖住中心只露 1px 光边
- 色彩:base(原值)/ semantic(引用)两层;前景色 ≥4.5:1(WCAG SC 1.4.3)
- 圆角:4 / 6 / 8 / 12 / 16 / 24(8px 基准的倍数)
- 动效:--dur-press 120 / fast 200 / base 250 / slow 300 / enter 400ms;曲线 --ease-out / --ease-standard / --ease-settle
- 字号:8 档阶梯 + --text-scale(0.92 / 1 / 1.12)
- 骨架:视口高度固定,页头不滚,正文在容器内滚
- 无障碍:焦点环 ≥2px、目标 ≥24×24、动效带 prefers-reduced-motion 降级
参考:/design(本站标准全文)
展开 token 变量块(143 行)
:root {
  color-scheme: light dark;

  /* --- base 层:原值(改这里就够) --- */
  --base-bg:          #f6f7f9;
  --base-surface:     #ffffff;
  --base-text:        #1d1d1f;
  --base-text-strong: #000000;
  --base-text-muted:  #6e6e73;
  --base-text-subtle: #717179;
  --base-accent:      #7755ef;
  --base-success:     #1c815c;
  --base-warning:     #b65700;
  --base-info:        #2777ac;
  /* 玻璃的"墨水":描边、凹陷、遮罩、滚动条、点阵都从它取,色相翻转只改这一处 */
  --base-ink:         #1d1f26;
  /* 阴影的冷紫灰(rgb 三元组,方便逐层调透明度) */
  --base-shadow-tint: 56 50 120;
  --base-neuro-bg:    #eef0f4;
  --base-neuro-light: #ffffff;
  --base-neuro-dark:  #d5d9e2;

  /* --- semantic 层:只引用 base --- */
  --color-bg:          var(--base-bg);
  --color-surface:     var(--base-surface);
  --color-text:        var(--base-text);
  --color-text-strong: var(--base-text-strong);
  --color-text-muted:  var(--base-text-muted);
  --color-text-subtle: var(--base-text-subtle);
  --color-accent:      var(--base-accent);
  --color-success-fg:  var(--base-success);
  --color-warning-fg:  var(--base-warning);
  --color-info-fg:     var(--base-info);
  /* 状态底色与描边从前景色 mix 出来:前景色改了,底色自动跟上 */
  --color-success-bg: color-mix(in srgb, var(--base-success) 10%, transparent);
  --color-success-bd: color-mix(in srgb, var(--base-success) 25%, transparent);
  --color-warning-bg: color-mix(in srgb, var(--base-warning) 10%, transparent);
  --color-warning-bd: color-mix(in srgb, var(--base-warning) 25%, transparent);
  --color-info-bg:    color-mix(in srgb, var(--base-info) 10%, transparent);
  --color-info-bd:    color-mix(in srgb, var(--base-info) 25%, transparent);
  /* 分隔线:面板之间的界,比 --glass-border 更轻,只用于"分区"不用于"描边" */
  --color-divider:    color-mix(in srgb, var(--base-ink) 4.5%, transparent);

  /* 顶栏高度:页面骨架据此算固定高度,复现页为 0 */
  --nav-h: 0px;
     容器、内边距、贴边定位都从这里算,别处不再写死宽度。
     2000px 以下固定 1560(与最早一致),往上按 1:1 吃掉额外宽度:
     两侧留白稳定在 220px(2560 与 3840 实测都是 220),到 3600 封顶。
     文字可读性不靠框管,靠 --measure 与 ≥1800px 的正文分栏(.prose-columns)。 */
  --frame-max: clamp(1560px, calc(100vw - 440px), 3600px);
  --frame-gutter: 94vw;  /* 窄屏两侧留白 */
  --frame-pad: 16px;     /* 框内左右内边距(窄屏) */
     只管正文段落与列表,表格和代码块照旧用满容器宽。 */
  --measure: 40em;
     框会长到 3400px,卡片与行不该被拉着铺满——96rem 是"一屏看得完"的上限。 */
  --page-col: 96rem;

  /* --- 玻璃 / 新拟态:玻璃是增透的,不是遮罩 --- */
  --glass-bg:        color-mix(in srgb, var(--base-surface) 28%, transparent);
  --glass-border:    color-mix(in srgb, var(--base-ink) 7%, transparent);
  --glass-blur:      16px;
  --glass-highlight: color-mix(in srgb, var(--base-surface) 85%, transparent);
  --glass-recess:    color-mix(in srgb, var(--base-ink) 5%, transparent);
  --glass-switch-off: color-mix(in srgb, var(--base-ink) 15%, transparent);
  /* 边缘光学:白色辉光环 + 顶部定向高光 + 底部微阴(厚度感) */
  --glass-inset:     inset 0 0 0 1px color-mix(in srgb, var(--base-surface) 32%, transparent),
                     inset 0 1px 1px color-mix(in srgb, var(--base-surface) 90%, transparent),
                     inset 0 -1px 2px color-mix(in srgb, var(--base-ink) 5%, transparent);
  --neuro-bg:        var(--base-neuro-bg);
  --neuro-light:     var(--base-neuro-light);
  --neuro-dark:      var(--base-neuro-dark);
  --neuro-shadow:    8px 8px 16px var(--neuro-dark), -8px -8px 16px var(--neuro-light);

  /* --- 阴影 token(带背景色调的冷紫灰,轻而不黑) --- */
  --shadow-sm: 0 1px 2px rgb(var(--base-shadow-tint) / 0.05), 0 3px 10px rgb(var(--base-shadow-tint) / 0.06);
  --shadow-md: 0 2px 4px rgb(var(--base-shadow-tint) / 0.04), 0 10px 30px rgb(var(--base-shadow-tint) / 0.10);
  --shadow-lg: 0 2px 6px rgb(var(--base-shadow-tint) / 0.06), 0 24px 60px rgb(var(--base-shadow-tint) / 0.16);
  --shadow-scrim: color-mix(in srgb, var(--base-ink) 18%, transparent);

  /* --- 字号阶梯(单一真值:单调递增,rem 基准,系统字号变了也跟着走) --- */
  /* --text-scale 由「字体与字号」面板控制:小 0.92 / 标准 1 / 大 1.12 */
  --text-scale: 1;
  --text-2xs: calc(0.625rem  * var(--text-scale)); /* 10 极小 label / icon */
  --text-xs:  calc(0.75rem   * var(--text-scale)); /* 12 副标 / tag */
  --text-sm:  calc(0.8125rem * var(--text-scale)); /* 13 元数据 / 元 */
  --text-base: calc(0.875rem * var(--text-scale)); /* 14 正文 */
  --text-md:  calc(1rem      * var(--text-scale)); /* 16 强调正文 */
  --text-lg:  calc(1.25rem   * var(--text-scale)); /* 20 小标题 */
  --text-xl:  calc(1.5rem    * var(--text-scale)); /* 24 大标题 */
  --text-2xl: calc(1.75rem   * var(--text-scale)); /* 28 特大标题 */

  /* 行高只给正文之外的独立角色;其余走 @theme 的 --leading-* 阶梯 */
  --leading-code: 1.55; /* 代码块:等宽字不需要正文那份呼吸 */

  /* --- 动效 token:类别即命名空间(见 docs/standards/motion.md) --- */
  --dur-press:   120ms;  /* 按压反馈 */
  --dur-fast:    200ms;  /* hover / 颜色 / 小浮层 / 内容切换 */
  --dur-base:    250ms;  /* 下拉 / 分段切换 / 开关平移 */
  --dur-exit:    180ms;  /* 退场:比入场短,离开不该让人等(浮层卸载计时器也读它) */
  --dur-slow:    300ms;  /* 模态与抽屉入场 */
  --dur-enter:   400ms;  /* 仅页面级入场与退场,使用须在 notes 里写明理由 */
  --loop-eq:     900ms;  /* 灵动岛均衡器循环 */
  --dwell-toast: 3500ms; /* 通知停留计时器,不是动画 */
  --dwell-tooltip: 350ms; /* 悬停多久才出提示;键盘聚焦不看它 */
  --seq-dock:    780ms;  /* Dock 启动弹跳 */
  --seq-decode:  900ms;  /* 标题解码序列 */
  --stagger:      60ms;  /* 组入场间隔,取区间中值而不是上限 */

  /* --- 字重 token --- */
  --fw-normal:   400;
  --fw-medium:   500;
  --fw-semibold: 600;
  --fw-bold:     700;

  /* --- 间距 token(4px 网格) --- */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  /* --- 字体家族(EN/ZH 两个独立 dim) --- */
  /* EN:真字体 + 最小兜底,不让 web font 被截胡 */
  --font-stack-en-geist:    'Geist', sans-serif;
  --font-stack-en-inter:    'Inter', sans-serif;
  --font-stack-en-pingfang: 'PingFang SC', 'Hiragino Sans GB', sans-serif;
  --font-stack-en-misans:   'MiSans', sans-serif;

  /* ZH:每个选项都是真切的字形,不混入 -apple-system 兜底 */
  --font-stack-zh-pingfang: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei';
  --font-stack-zh-noto:     'Noto Sans SC Variable', 'Noto Sans SC';
  --font-stack-zh-serif:    'Noto Serif SC Variable', 'Noto Serif SC';
  --font-stack-zh-system:   'PingFang SC', 'Microsoft YaHei';

  --font-en-active: var(--font-stack-en-geist);
  --font-zh-active: var(--font-stack-zh-pingfang);
  /* 等宽:代码与数字。默认 Source Code Pro(人味足、字腔高),面板可切 Cascadia / JetBrains */
  --font-mono:      var(--font-mono-active);
  --font-mono-active: 'Source Code Pro', 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
}

判断层术语

标准里的判断也拆成术语——按术语搜,比按「别乱加动画」搜得到。

快进慢出 Fast-out, slow-in
退场约为进场的 0.45 倍,曲线更陡;出场不许比入场慢
motion.md
进出场成对 Paired enter & exit
浮层进场与退场必须成对,不许啪地消失
motion.md
降级是更少更温和 Reduced, not off
降级保留颜色与透明度过渡,只去掉位移与形变
motion.md
动效与降级同批交付 Motion ships with its fallback
新增动效必须同时写降级分支,否则闸门白做
motion.md
只动 transform 与 opacity Compositor-only properties
width/height/margin/padding/top/left 会触发布局重算,不许过渡
motion.md
高频面少动效 Less motion on frequent surfaces
每天上百次的动作永远不动;hover 只做几乎察觉不到的微动
motion.md
材质三分级 Three material tiers
悬浮层用玻璃 / 内容层用不透明白底 / 风格轴另算(新拟态)
material.md
玻璃只做悬浮层 Glass is for overlays
玻璃靠背后有东西才成立;内容面板一律白底 + 软阴影
material.md
阴影只表高度 Shadows mean elevation
sm 贴着表面 / md 浮起一层 / lg 压住全屏;一层只用一个档
material.md
同一个屏宽基准 One width basis
全站宽度只有一处真值(--frame-max / --frame-gutter / --frame-pad),容器与贴边定位都从它算
layout.md
圆角一条阶梯 One radius scale
4 / 6 / 8 / 12 / 16 / 24,8px 基准的倍数,不随手取值
layout.md
间距只用 4 的倍数 4px spacing grid
所有内外边距落在 4px 网格上
layout.md
字号按角色选 Pick type by role
正文 13–14、说明 12、元数据 10–12;标题与正文至少差两档
typography.md
行长 17–40 字 Measure 17–40 CJK chars
中文正文每行 17–40 字(硬上限 48),西文 45–90 字符
typography.md
行距 1.5–2.0 倍 Leading 50–100% of size
行距取字号的 50%–100%(clreq §7.1)
typography.md
焦点环 ≥2px Visible focus ring
键盘焦点必须可见,等效周长 ≥2px(SC 2.4.13)
interaction.md
指针目标 ≥24×24 24px hit target
可点元素不小于 24×24 CSS px(SC 2.5.8)
interaction.md
状态不只靠颜色 Never color alone
色点旁必须有文字,色盲用户也要读得到
interaction.md
状态矩阵 State matrix
七态验收;APG 有专条的状态与键位不许缺
states.md
术语驱动 Term-driven
复现先说术语再给参数——照着术语就能找到现成规范
reuse.md
单一真值 Single source of truth
一个事实只写一遍;代码与文档不一致时以生成的那份为准
layout.md

术语表

每个组件的正式叫法——按术语去搜规范,比按「玻璃卡片」搜得到得多。

玻璃头像 无图或 onError 回落首字母:中文取前两字,西文取首字母缩写,不重试加载 Avatar with fallback 复用卡 → 极简头像 同一份 props(name / src / size / status) Avatar with fallback 复用卡 → 拟态头像 同一份 props(name / src / size / status) Avatar with fallback 复用卡 → 玻璃徽标 胶囊只用文字色表达语义(neutral / accent / success / warning),底色始终中性 Status Badge 复用卡 → 极简徽标 同一份 props(variant / dot) Status Badge 复用卡 → 拟态徽标 同一份 props(variant / dot) Status Badge 复用卡 → 玻璃提示条 页面级状态:常驻、不自动消失,处理完或手动关闭才走 Notification Banner 复用卡 → 极简提示条 同一份 props 与 role 分配 Notification Banner 复用卡 → 拟态提示条 同一份 props 与 role 分配 Notification Banner 复用卡 → 玻璃面包屑 nav aria-label + ol/li;分隔符 aria-hidden,当前位置 aria-current=page 且不可点 Breadcrumb 复用卡 → 极简面包屑 同一份 props(items) Breadcrumb 复用卡 → 拟态面包屑 同一份 props(items) Breadcrumb 复用卡 → 玻璃按钮 半透明底 + 1px 细边框 + 背景模糊,做出玻璃厚度 Liquid Glass Button 复用卡 → 极简按钮 实底 + 一条发丝边线 + 一档软阴影,没有模糊与浮起 Flat Minimal Button 复用卡 → 拟态按钮 控件与页面同色,只靠亮侧高光 + 暗侧投影浮出表面 Neumorphic Button 复用卡 → 玻璃卡片 玻璃面 + 边缘光学(内高光 + 顶部定向光) Liquid Glass Card 复用卡 → 极简卡片 无浮起、无模糊、无阴影 Flat Minimal Card 复用卡 → 拟态卡片 同色底 + 一组外双阴影浮起,没有边框与模糊 Neumorphic Card 复用卡 → 聚光卡片 外层渐变边框跟随鼠标位置,白底盖住中心只露 1px 光边 Border Glow + Inner Spotlight Card 复用卡 → 玻璃勾选 原生 checkbox / radio + fieldset / legend;单选组同 name,方向键与空格交给浏览器 Checkbox & Radio Group (APG) 复用卡 → 极简勾选 原生 checkbox / radio + fieldset / legend;单选组同 name,方向键与空格交给浏览器 Checkbox & Radio Group (APG) 复用卡 → 拟态勾选 原生 checkbox / radio + fieldset / legend;单选组同 name,方向键与空格交给浏览器 Checkbox & Radio Group (APG) 复用卡 → 玻璃可搜索下拉 input role=combobox + aria-expanded / aria-controls / aria-autocomplete=list / aria-activedescendant List-filtering Combobox 复用卡 → 极简可搜索下拉 同一份 props 与行为(~/lib/combobox) List-filtering Combobox 复用卡 → 拟态可搜索下拉 同一份 props 与行为(~/lib/combobox) List-filtering Combobox 复用卡 → 上下文光标 只在自身范围内跟随指针,原生光标始终保留,不写 cursor: none Contextual Cursor Ring (local demo) 复用卡 → 磁吸指针 指针靠近时元素向指针偏移,光标保持原生 Magnetic Hover 复用卡 → 玻璃描述列表 原生 dl / dt / dd,读屏按标签—值成对播报 Description List 复用卡 → 极简描述列表 同一份 props(items / columns) Description List 复用卡 → 拟态描述列表 同一份 props(items / columns) Description List 复用卡 → 放大坞站 余弦平方衰减的放大:峰值在鼠标处,半径外平滑归零 macOS-style Dock with Magnification 复用卡 → 极简坞站 余弦平方衰减的放大:峰值在鼠标处,半径外平滑归零 macOS-style Dock, Flat Tray 复用卡 → 拟态坞站 余弦平方衰减的放大:峰值在鼠标处,半径外平滑归零 macOS-style Dock, Neumorphic Tray 复用卡 → 拖拽排序 Pointer Events + setPointerCapture,只在把手按下时开始拖拽 Sortable List (Pointer + Keyboard) 复用卡 → 滑动删除 四段链:1:1 跟手 → 越界阻尼 30% → 阈值 96px / 速度 0.5px/ms 双判 → 吸附或飞出 Swipe Actions 复用卡 → 玻璃抽屉 role=dialog + aria-modal + 标题命名;打开移入首个可聚焦,Tab 首尾循环 Side Sheet / Drawer 复用卡 → 极简抽屉 同一份 props(open / onClose / title / side / children / footer) Side Sheet / Drawer 复用卡 → 拟态抽屉 同一份 props(open / onClose / title / side / children / footer) Side Sheet / Drawer 复用卡 → 玻璃下拉 触发器 role=combobox,弹层 role=listbox,aria-activedescendant 指向高亮项 Select-Only Combobox (APG) 复用卡 → 极简下拉 触发器 role=combobox,弹层 role=listbox,aria-activedescendant 指向高亮项 Select-Only Combobox (APG) 复用卡 → 拟态下拉 触发器 role=combobox,弹层 role=listbox,aria-activedescendant 指向高亮项 Select-Only Combobox (APG) 复用卡 → 玻璃空态 三件套:正面措辞的标题 + 一句说明 + 单个主操作;零数据不是错误,不用 warning 色 Empty State 复用卡 → 极简空态 同一份 props(title / description / icon / action) Empty State 复用卡 → 拟态空态 同一份 props(title / description / icon / action) Empty State 复用卡 → 玻璃输入框 标签在框上方,不做浮动标签动画 Floating Label Input 复用卡 → 极简输入框 标签在框上方,白底 + 发丝边 + 一档软阴影 Flat Minimal Input 复用卡 → 拟态输入框 输入区是同色凹槽:左上暗、右下亮两条 inset 阴影 Neumorphic Input 复用卡 → 玻璃菜单 触发器 aria-haspopup=menu + aria-expanded + aria-controls;菜单 role=menu,项 role=menuitem Menu Button (APG) 复用卡 → 极简菜单 同一份 props(label / items / placement) Menu Button (APG) 复用卡 → 拟态菜单 同一份 props(label / items / placement) Menu Button (APG) 复用卡 → 玻璃模态 role=dialog + aria-modal + aria-labelledby Sheet / Modal Dialog (APG) 复用卡 → 极简模态 role=dialog + aria-modal + aria-labelledby Flat Minimal Dialog (APG) 复用卡 → 拟态模态 role=dialog + aria-modal + aria-labelledby Neumorphic Dialog (APG) 复用卡 → 灵动岛形变 紧凑⇄展开两态之间做弹簧插值(阻尼比 ≈0.78) Dynamic Island Morph 复用卡 → 弹簧曲线 基准参数:response 0.5s、dampingFraction 0.825 Spring Timing Explorer 复用卡 → 文字解码 字符逐个替换成随机字符再落回 Text Decode / Scramble 复用卡 → 玻璃导航 固定顶栏 + 背景模糊,滚动不跟随 Top Navigation Bar 复用卡 → 极简导航 白底 + 发丝边外壳,圆角收到 --radius-md Flat Top Navigation 复用卡 → 拟态导航 外壳同色浮起(一组外双阴影),无边框无模糊 Neumorphic Top Navigation 复用卡 → 玻璃验证码输入 一个真输入框 + 画的格子:逐格 input 会打断 autocomplete=one-time-code 自动填充 One-Time Code Input 复用卡 → 极简验证码输入 同一份行为与语义(单输入框 + 画的格子) One-Time Code Input 复用卡 → 拟态验证码输入 同一份行为与语义(单输入框 + 画的格子) One-Time Code Input 复用卡 → 玻璃分页 nav aria-label + ul/li;当前页 aria-current=page;每个页码 aria-label=第 n 页 Pagination 复用卡 → 极简分页 同一份 props(page / pageCount / onChange) Pagination 复用卡 → 拟态分页 同一份 props(page / pageCount / onChange) Pagination 复用卡 → 玻璃浮层 触发器 aria-haspopup=dialog + aria-expanded + aria-controls;浮层 role=dialog 以标题命名 Non-modal Popover 复用卡 → 极简浮层 同一份 props(trigger / title / children / placement) Non-modal Popover 复用卡 → 拟态浮层 同一份 props(trigger / title / children / placement) Non-modal Popover 复用卡 → 分段控件 APG radiogroup:方向键遍历 + roving tabindex Segmented Control (iOS) 复用卡 → 极简分段控件 APG radiogroup:方向键遍历 + roving tabindex Flat Segmented Control 复用卡 → 拟态分段控件 APG radiogroup:方向键遍历 + roving tabindex Neumorphic Segmented Control 复用卡 → 玻璃骨架 role=status + aria-busy,占位块全部 aria-hidden,读屏只播一句加载状态 Skeleton Screen 复用卡 → 极简骨架 role=status + aria-busy,占位块全部 aria-hidden,读屏只播一句加载状态 Skeleton Screen 复用卡 → 拟态骨架 role=status + aria-busy,占位块全部 aria-hidden,读屏只播一句加载状态 Skeleton Screen 复用卡 → 玻璃滑块 role=slider + aria-valuenow/min/max,← → 单步、PageUp/Down 十步、Home/End 到端 UISlider 复用卡 → 极简滑块 4px 发丝线轨道,无边框无内阴影 Minimal Range 复用卡 → 拟态滑块 轨道压入(inset 双阴影)、旋钮浮起(外双阴影),方向相反 Neumorphic Range 复用卡 → 玻璃开关 role=switch + aria-checked,Space / Enter 切换 iOS UISwitch 复用卡 → 极简开关 role=switch + aria-checked,Space / Enter 切换 Flat Minimal Switch 复用卡 → 拟态开关 role=switch + aria-checked,Space / Enter 切换 Neumorphic Switch 复用卡 → 玻璃数据表 table + caption(视觉隐藏);表头 scope=col,首列当行头 scope=row Data Table 复用卡 → 极简数据表 同一份 props(caption / columns / rows / className) Data Table 复用卡 → 拟态数据表 同一份 props(caption / columns / rows / className) Data Table 复用卡 → 玻璃标签页 tablist / tab / tabpanel 三件套,aria-selected + aria-controls 绑定面板 Tabs (APG, automatic activation) 复用卡 → 极简标签页 tablist / tab / tabpanel 三件套,aria-selected + aria-controls 绑定面板 Tabs (APG, automatic activation) 复用卡 → 拟态标签页 tablist / tab / tabpanel 三件套,aria-selected + aria-controls 绑定面板 Tabs (APG, automatic activation) 复用卡 → 玻璃标签输入 回车 / 逗号 / 失焦提交;粘贴逗号分隔的多个值会拆开并去重 Tag Input (chips) 复用卡 → 极简标签输入 同一份 props(value / defaultValue / onChange / max / disabled) Tag Input (chips) 复用卡 → 拟态标签输入 同一份 props(value / defaultValue / onChange / max / disabled) Tag Input (chips) 复用卡 → 玻璃多行输入 label 在框上方;校验契约与单行输入一致(aria-describedby + aria-invalid + role=alert) Autosizing Textarea 复用卡 → 极简多行输入 白面 + 发丝边 + shadow-sm,没有模糊与 inset Autosizing Textarea 复用卡 → 拟态多行输入 同色凹槽:左上暗、右下亮两条 inset 阴影,没有边框 Autosizing Textarea 复用卡 → 玻璃通知 新通知在前,后排逐层缩小上移 Toast Stack (sonner-style) 复用卡 → 极简通知 宽 356px,牌堆每层缩 5%、抬 14px Flat Toast Stack (sonner-style) 复用卡 → 拟态通知 宽 356px,牌堆每层缩 5%、抬 14px Neumorphic Toast Stack (sonner-style) 复用卡 → 玻璃提示 悬停等 --dwell-tooltip(350ms)才出,键盘聚焦立即出;移出 / 失焦 / Esc 立即关 Tooltip (WCAG 1.4.13) 复用卡 → 极简提示 同一份 props(content / children / placement) Tooltip (WCAG 1.4.13) 复用卡 → 拟态提示 同一份 props(content / children / placement) Tooltip (WCAG 1.4.13) 复用卡 → 玻璃上传 三个入口:拖放、点选择文件、键盘聚焦按钮后 Enter 打开选择器 Dropzone 复用卡 → 极简上传 同一份 props(accept / multiple / maxSizeMb / files / onChange) Dropzone 复用卡 → 拟态上传 同一份 props(accept / multiple / maxSizeMb / files / onChange) Dropzone 复用卡 →

组件

101 个已复现组件,元数据来自各自的 notes frontmatter。

玻璃头像 iOS 联系人头像 / Linear 成员头像(图 + 首字母回落 + 状态点) avatar · glass-apple 极简头像 Refactoring UI(实底圆 + 发丝边) avatar · minimal 拟态头像 Neumorphism(同色浮起的圆形) avatar · neumorphic 玻璃徽标 GitHub Primer / Linear 的状态徽标(胶囊 + 状态点 + 语义色文字) badge · glass-apple 极简徽标 Refactoring UI(发丝边 + 文字色做语义) badge · minimal 拟态徽标 Neumorphism(同色浮起的小胶囊) badge · neumorphic 玻璃提示条 GOV.UK notification banner / Polaris banner / USWDS alert banner · glass-apple 极简提示条 GOV.UK notification banner / USWDS alert banner · minimal 拟态提示条 GOV.UK notification banner / USWDS alert banner · neumorphic 玻璃面包屑 Carbon Breadcrumb / GOV.UK breadcrumbs breadcrumb · glass-apple 极简面包屑 Refactoring UI(文字层级 + 下划线反馈) breadcrumb · minimal 拟态面包屑 Neumorphism(悬停压成凹槽) breadcrumb · neumorphic 玻璃按钮 Apple Vision Pro / iOS 26 Liquid Glass button · glass-apple 极简按钮 Refactoring UI(层次靠排版与留白,不靠材质) button · minimal 拟态按钮 Neumorphism(同色双阴影:亮侧高光 + 暗侧投影) button · neumorphic 玻璃卡片 visionOS / Linear 浮动卡片 card · glass-apple 极简卡片 去浮起、去模糊,靠排版和留白呼吸 card · minimal 拟态卡片 Neumorphism(同色浮起 + 一组双阴影) card · neumorphic 聚光卡片 Vercel Templates 的边框流光 + Linear 官网的内光斑(实测调研) card · spotlight 玻璃勾选 APG Checkbox / Radio Group + iOS 设置勾选 choice · glass-apple 极简勾选 APG Checkbox / Radio Group + iOS 设置勾选 choice · minimal 拟态勾选 APG Checkbox / Radio Group + iOS 设置勾选 choice · neumorphic 玻璃可搜索下拉 APG list-filtering combobox / Polaris autocomplete combobox · glass-apple 极简可搜索下拉 APG list-filtering combobox / Polaris autocomplete combobox · minimal 拟态可搜索下拉 APG list-filtering combobox / Polaris autocomplete combobox · neumorphic 上下文光标 Figma 多人光标的上下文标签 + Pointer Events(局部演示,不接管全站) cursor · contextual 磁吸指针 Luma / Cuberto 的磁吸按钮(元素朝指针偏移,光标保持原生) cursor · magnetic 玻璃描述列表 Apple HIG 详情视图 / Salesforce record detail(键值成对、标签压小、值给正文) descriptions · glass-apple 极简描述列表 Refactoring UI(实底面板 + 发丝线) descriptions · minimal 拟态描述列表 Neumorphism(同色浮起的面板) descriptions · neumorphic 放大坞站 macOS Dock 的 magnification dock · glass-apple 极简坞站 macOS Dock 的 magnification + Refactoring UI 的平面托盘 dock · minimal 拟态坞站 macOS Dock 的 magnification + Neumorphism 的同色浮起 dock · neumorphic 拖拽排序 Linear / Notion 的拖拽排序(指针)+ APG listbox 的方向键模型(键盘) drag · sortable-list 滑动删除 iOS 邮件 / 通知列表的左滑删除(UITableView swipe actions) drag · swipe-to-dismiss 玻璃抽屉 HIG Sheets / M3 Navigation drawer(复用 modal 的焦点纪律) drawer · glass-apple 极简抽屉 Refactoring UI(实底面板 + 发丝线) drawer · minimal 拟态抽屉 Neumorphism(同色面板 + 单侧投影) drawer · neumorphic 玻璃下拉 APG Select-Only Combobox + macOS Pop-up Button dropdown · glass-apple 极简下拉 APG Select-Only Combobox + macOS Pop-up Button dropdown · minimal 拟态下拉 APG Select-Only Combobox + macOS Pop-up Button dropdown · neumorphic 玻璃空态 Carbon empty states pattern / Polaris empty state empty-state · glass-apple 极简空态 Refactoring UI(图标块 + 文字层级) empty-state · minimal 拟态空态 Neumorphism(同色浮起的小图标块) empty-state · neumorphic 玻璃输入框 visionOS / iOS 26 浮层输入 input · glass-apple 极简输入框 Refactoring UI(实底 + 发丝线,不用材质做深度) input · minimal 拟态输入框 Neumorphism(同色凹槽:暗侧内投影 + 亮侧内高光) input · neumorphic 玻璃菜单 WAI-ARIA Menu Button(含 APG 键位) menu · glass-apple 极简菜单 Refactoring UI(实底 + 发丝边) menu · minimal 拟态菜单 Neumorphism(同色浮起的菜单块) menu · neumorphic 玻璃模态 visionOS 浮层 + Apple system sheet modal · glass-apple 极简模态 Refactoring UI(用留白与尺寸分层,不靠模糊与光晕) modal · minimal 拟态模态 Neumorphism(外圈浮起、内圈压入,共用一组同色双阴影) modal · neumorphic 灵动岛形变 iOS 16 Dynamic Island motion · island-ios 弹簧曲线 iOS UIKit / SwiftUI 的 spring 动画(UISpringTimingParameters) motion · spring-ios 文字解码 Vercel / Linear hero 的标题解码动效 motion · text-decode 玻璃导航 visionOS 浮层 + Linear 顶部 nav nav · glass-apple 极简导航 Linear 顶部 nav 的平面化处理(去材质,留结构) nav · minimal 拟态导航 Neumorphism(外壳同色浮起,内部靠文字与激活色区分) nav · neumorphic 玻璃验证码输入 Apple 短信验证码自动填充实践 / GOV.UK passwords otp · glass-apple 极简验证码输入 Apple 短信验证码自动填充实践 / GOV.UK passwords otp · minimal 拟态验证码输入 Apple 短信验证码自动填充实践 / GOV.UK passwords otp · neumorphic 玻璃分页 Carbon Pagination / GOV.UK pagination pagination · glass-apple 极简分页 Refactoring UI(实底页码 + 发丝边) pagination · minimal 拟态分页 Neumorphism(同色页码,悬停压成凹槽) pagination · neumorphic 玻璃浮层 Radix Popover 的焦点行为 + Apple HIG 的浮层材质 popover · glass-apple 极简浮层 Refactoring UI(实底面板 + 发丝边) popover · minimal 拟态浮层 Neumorphism(同色浮起的面板) popover · neumorphic 分段控件 iOS UISegmentedControl / 控制中心切换器 segmented · glass-apple 极简分段控件 Refactoring UI(用墨色块与反白文字分层,不用滑块光影) segmented · minimal 拟态分段控件 Neumorphism(轨道压入、滑块浮起,共用一组同色双阴影) segmented · neumorphic 玻璃骨架 Skeleton Screen 模式 + 本站材质配方 skeleton · glass-apple 极简骨架 Skeleton Screen 模式 + 本站材质配方 skeleton · minimal 拟态骨架 Skeleton Screen 模式 + 本站材质配方 skeleton · neumorphic 玻璃滑块 iOS 设置页的 UISlider(音量 / 亮度) slider · glass-apple 极简滑块 Refactoring UI(细轨道 + 墨色方块旋钮,不靠光影分层) slider · minimal 拟态滑块 Neumorphism(轨道压入、旋钮浮起,同一组同色双阴影) slider · neumorphic 玻璃开关 iOS 设置页的 UISwitch switch · glass-apple 极简开关 Refactoring UI(开启 = 实心前景轨,关闭 = 描边空槽) switch · minimal 拟态开关 Neumorphism(轨道压入、圆钮浮起,开启时圆钮点亮) switch · neumorphic 玻璃数据表 GOV.UK table / Polaris data-table table · glass-apple 极简数据表 GOV.UK table / USWDS usa-table table · minimal 拟态数据表 GOV.UK table / USWDS usa-table table · neumorphic 玻璃标签页 APG Tabs pattern (automatic activation) + macOS segmented tabs tabs · glass-apple 极简标签页 APG Tabs pattern (automatic activation) + macOS segmented tabs tabs · minimal 拟态标签页 APG Tabs pattern (automatic activation) + macOS segmented tabs tabs · neumorphic 玻璃标签输入 GitHub issue labels / Linear 过滤器(芯片 + 文本输入) tags · glass-apple 极简标签输入 Refactoring UI(实底 + 发丝边) tags · minimal 拟态标签输入 Neumorphism(凹槽 + 同色芯片) tags · neumorphic 玻璃多行输入 visionOS / iOS 26 浮层输入(延续玻璃输入框的凹陷材质) textarea · glass-apple 极简多行输入 Refactoring UI(实底 + 发丝线,延续极简输入框) textarea · minimal 拟态多行输入 Neumorphism(同色凹槽,延续拟态输入框) textarea · neumorphic 玻璃通知 sonner(Emil Kowalski)。源码级调研,非目测 toast · glass-apple 极简通知 sonner(Emil Kowalski)的堆叠公式 + Refactoring UI 的平面分层 toast · minimal 拟态通知 sonner(Emil Kowalski)的堆叠公式 + Neumorphism 的同色浮起 toast · neumorphic 玻璃提示 Apple HIG Tooltips / WAI-ARIA Tooltip pattern tooltip · glass-apple 极简提示 Refactoring UI(实底 + 发丝边) tooltip · minimal 拟态提示 Neumorphism(同色浮起的小块) tooltip · neumorphic 玻璃上传 Dropzone 的拖放模型 + Apple HIG 的虚线投放区 upload · glass-apple 极简上传 Refactoring UI(实底虚线框) upload · minimal 拟态上传 Neumorphism(同色凹槽投放区) upload · neumorphic

机器可读出口

设计、提示词与实现导出成 registry 条目;底盘全部派生,check:export 是第十一道闸。

条目清单与安装命令 /r/registry.json /llms.txt 材质 101 条 · 整页场景 7 条 · 另有底盘与整套条目

资料库

书籍与规范的可调用性分级——哪条能变成 CSS 变量,哪条只能内化为取舍。

L0 原料层的第二块:书、官方规范、参考实现。与 .agents/skills/(给判断)和 design-systems/(给规范全文)平级。

核实日期:2026-10-30。所有条目由四路调研代理检索并实际打开官方页取得,再由我方独立复验: Material 时长与曲线数值逐条比对官方仓库原文、Apple ToU 关键句逐字复核、WCAG 2.2 版本日期与 SC 2.5.8 阈值另开官方页确认、 本站 token 对比度用 WCAG 公式在 Node 里重算(与代理结果逐项吻合,并补测了两个未覆盖的 token)。

标注规则

  • 出处:一律给可点开的官方页(出版社 / 作者站 / W3C / Unicode / 平台文档 / 一手仓库),不接受聚合站。
  • 优先级:先读(能直接改写本站某个 token 或验收项)/ 该读(成体系但需转化)/ 备查(按需查)/ 暂不(已评估,收益不足)。
  • 可调用性:硬约束 = 能变成 CSS 声明、token 值或机器检查项;判断 = 只能内化为取舍,无法机器校验。 这一列决定条目进 P2 的验收标准还是留在资料库,是本项目最关心的分层。
  • 数字未经复核不进正表,进第六节「待核实」。

一、书籍

书名 版次·年份 出处 优先级 可调用 管什么 · 我们怎么用
Refactoring UI
中译《重构 UI》
自出版,官方页无出版日期(页数自述 218) refactoringui.com 先读 硬约束 间距阶、灰阶、层级对比。抄成规则:间距只用 4 的倍数、不用纯黑纯白、相邻层级 ≥1.5 倍
About Face 4
中译《About Face 4:交互设计精髓(纪念版)》倪卫国等译,电子工业 2020-04
2014·第4版 Wiley wiley.com 先读 硬约束 控件状态数。用来给每个组件定"状态矩阵":默认/hover/press/focus/disabled/loading/error 缺一不过——正是 roadmap 未决问题 2 的判据来源
WCAG 2.2(规范非书) W3C Recommendation 2024-12-12 w3.org/TR/WCAG22 先读 硬约束 见第二节条款表,本站验收项的唯一硬依据
The Elements of Typographic Style 2012·第4版(Hartley & Marks)
繁中《字體的技藝》呂奕欣譯,臉譜 2020;简体无译本
wikipedia 书目 先读 硬约束 行长与空间模数:行长 = 正文字母表宽的 2–3 倍、half-em 间距。落成 --measure / --leading token
Butterick's Practical Typography 第2版(仅网络版,年份待核实) practicaltypography.com 先读 硬约束 行长 45–90 字符、行距 115–120%、句号后一个空格。写成 CSS 与 lint 判据
The Non-Designer's Design Book
中译《写给大家看的设计书(第4版)》苏金国等译,人邮 2015-11
2014-11·第4版 Peachpit peachpit 先读 判断 CRAP 四则(亲密/对齐/重复/对比),写成 notes 里的四问自查
clreq 中文排版需求(规范非书) W3C Group Note Draft 2026-10-07(非 endorsed) w3.org/TR/clreq 先读 硬约束 §7.1 行距 = 字号 50%–100%、正文 17–40 字/行、横排 ≤48 字;§6.2 中西间距可固定 ¼ 字宽
Microinteractions
中译《微交互:细节设计成就卓越产品》李松峰译,人邮 2013
2013·初版(Full Color 版 2013-11) oreilly 该读 判断 触发-规则-反馈-循环-模式五段结构,给 toast / switch / motion 组件补"循环与模式"字段,替代现在散落的 34 处时长值
Designing Interfaces 2020·第3版 O'Reilly(e-book 2019-12-18) openlibrary 该读 判断 模式适用条件。建"dock / segmented / modal 各在什么场景赢"的取舍表,防同一页堆两种同类模式
The Design of Everyday Things
中译《设计心理学1:日常的设计》中信 2015-05
2013·修订扩展版 jnd.org 作者页 该读 判断 示能/指示物。给 glass-apple 的透明层写"可点性指示物"判据:看不出能点 = 不合格
Thinking with Type
繁中新译《圖解字型思考指南》林庭如譯,商周 2026-03(据第3版)
2024-03-12·第3版(PA Press,256p) papress 该读 判断 字号对比、字重层级、混排规则。按第3版而非 2010 年第2版;网上大量引用已作废
Grid Systems in Graphic Design
中译《平面设计中的网格系统》上海人民美术 2016
1981·英文版(年份待核实) niggli.ch 该读 判断 论证"同一风格轴内模数必须统一",据此审 global.css 字号/间距是否同基
Graphic Design: The New Basics(无中译) 2015·第2版修订扩展 PA Press papress 该读 判断 给"视觉层级"下定义时引它的 modulation 概念,替掉现在含糊的"层级更强"
Detail in Typography
繁中《歐文排印細部法則》網路與書 2021
2015·英文版 Éditions B42 slanted.de 备查 判断 复核 input / nav 的字距与标点挤压
The Anatomy of Type(无中译) 2012·Harper Design openlibrary 备查 判断 切 Geist / Inter / Noto Sans SC 时给"选这组的理由",防凭印象
Universal Principles of Design 2023·第3版(扩到 200 条) quarto.com 备查 判断 L1 原则层的条目池。两本中译只覆盖旧版 125 条,别按中译建判据
Making and Breaking the Grid 2023·第3版 Rockport quarto.com 备查 判断 审 spotlight 那条轴:破格要成体系地破
Don't Make Me Think, Revisited 2013-12-24·第3版 New Riders peachpit 备查 判断 只当"每屏一个主意图"的口头禅校验 demo 页。第4版 rev 4 2026-11-30 才上市(ISBN 9780135958728),现无中译
Laws of UX 2020·第1版 / 2024-01·第2版 lawsofux.com/book 暂不 判断 免费网站版更全,书无可调用增量

二、官方规范(条款级)

条款 版本·日期 出处 对应我们什么 可调用判据
SC 1.4.3 Contrast (Minimum) AA WCAG 2.2 · 2024-12-12 #contrast-minimum --color-accent、三个状态色 fg 正文 ≥4.5:1,大字 ≥3:1
SC 1.4.11 Non-text Contrast AA 同上 #non-text-contrast --glass-border、--glass-switch-off 轨道、状态圆点 非文本 UI ≥3:1
SC 2.4.7 Focus Visible AA 同上 #focus-visible global.css:515 的全局 focus 环 焦点必须可见
SC 2.4.11 Focus Not Obscured (Min) AA 2.2 新增 同上 #focus-not-obscured-minimum Toast / Dock 悬浮层压住 Switch·Segmented 焦点 焦点框 ≥25% 面积不被遮
SC 2.4.13 Focus Appearance AAA 2.2 新增 同上 #focus-appearance 现焦点环 1.5px ≥2px 周长等效面积 + 状态间 3:1
SC 2.5.8 Target Size (Minimum) AA 2.2 新增 同上 #target-size-minimum Switch sm 20px / md 26px 轨道 原文:"at least 24 by 24 CSS pixels",含 5 项例外(间距补偿/等效控件/内联/UA 控件/必需呈现)
SC 2.5.7 Dragging Movements AA 2.2 新增 同上 #dragging-movements Dock 拖拽、Segmented 滑块、Island 形变 拖拽须有单点非拖拽替代
SC 1.4.13 Content on Hover or Focus AA 同上 #content-on-hover-or-focus Dock 悬停放大、Spotlight、tooltip 可关 / 可指 / 不遮
APG Switch pattern APG 活文档(TR 末次 NOTE 2021-11-29,非规范性) patterns/switch switch/glass-apple/Switch.tsx role=switch + aria-checked + Space/Enter
APG Radio Group(分段控件唯一对应模式,APG 无 segmented 专条) 同上 patterns/radio segmented/glass-apple/Segmented.tsx radiogroup/radio + 方向键遍历 + roving tabindex
APG Dialog (Modal) 同上 patterns/dialog-modal modal/glass-apple/Modal.tsx 焦点陷阱、Esc、关闭后焦点归还
APG Toolbar(Dock 最近似,无 dock 专条) 同上 patterns/toolbar dock/glass-apple/Dock.tsx role=toolbar + Home/End
UAX #14 Line Breaking Revision 57 · Unicode 18.0.0 · 2026-09-01(稳定,可规范引用) tr14 中文正文换行 WJ/CL/OP/CP/ID/NS/GL 类;CJ 作 NS = CSS strict
CSS Text Level 3 CR Draft 2026-08-14(hanging-punctuation at-risk) css-text-3 混排换行 line-break: strict(中文正文)/ loose(短标签);word-break: break-word 已标废弃
OpenType 注册轴 wght OpenType 1.9.1(页 updated 2024-05-31) microsoft learn 字重 token 合法 1–1000,400 必等 Regular。字重 token 只用数字,不用 light/bold 关键字
CSS Fonts Level 4 Working Draft 2026-10-22(未 endorsed) css-fonts-4 中英混排字重 混排加 font-synthesis-weight: none,防合成假粗
CSS Color Level 4 Candidate Recommendation Draft 2026-10-29 css-color-4 渐变与色彩空间 §9.4 oklch()/oklab();§13.2 原文:视觉等距用 Oklab,避免混色发灰用 OkLCh
CSS Color Adjustment Level 1 Candidate Recommendation Snapshot 2025-12-16 css-color-adjust-1 暗色第一步 color-scheme: light dark 决定 UA 表单/滚动条/根画布
Media Queries Level 5 §12.5 prefers-color-scheme Working Draft 2026-02-19(位阶仅 WD,引用须注明) mediaqueries-5 暗色入口 @media (prefers-color-scheme: dark);Baseline 跨浏览器自 2020-01
Understanding SC 2.3.3 Animation from Interactions W3C WAI,updated 2025-09-16 animation-from-interactions 全站动效降级 降级三选一:不做 / 给开关 / 响应 reduce(C39 / SCR40 技法)
MDN prefers-reduced-motion Last modified 2026-06-10 MDN 降级写法 只写 reduce 分支;降级 = 换 opacity 过渡而非全删
MDN will-change Last modified 2026-10-07 MDN Dock / Island 图层提升 禁止常挂 will-change,只允许事件前后切换
Stick to Compositor-Only Properties web.dev · Paul Lewis · 2015-03-20 article 可动属性白名单 只允许 transform / opacity;主线程预算 4–5ms/帧
Design Tokens Format Module Draft Community Group Report 2026-09-08(自述"不要当权威引用",非 W3C 标准) designtokens.org token 源文件结构 $value/$type、别名 {a.b}、$extends。命名分层无 spec,需自定并文档化
GB/T 15834-2011 标点符号用法 2011-12-30 发布 / 2012-06-01 实施(现行) openstd 避头尾的上游依据 clreq 明确引用它;验收:行首不得出现句读符号

规范层的两个空洞(重要,别到处找依据了):中英混排的行高与基线对齐无成文规范——clreq §6.4 无混排数值,gap 分析 §7.4 条目为空;色彩对比算法只有 WCAG 的 relative luminance 是硬要求,APCA 仅存在于 WCAG 3.0 Working Draft(2026-09-10)的 developing 章节,W3C Silver 子组明说"not approved… not normative",只能参考、不能写成检查项。这两处只能自定规则并写进 docs/standards/。

三、许可与再分发边界

资料 条款原文(关键句) 出处 能否离线入库 做法
Apple HIG 正文 "no part of the Site and no Content may be copied, reproduced, republished, uploaded, posted, publicly displayed, encoded, translated, transmitted or distributed in any way (including 'mirroring') to any other computer, server, Web site or other medium",需 Apple "express prior written consent";且禁止 "any 'deep-link', 'page-scrape', 'robot', 'spider' or other automatic device" 抓取。唯一非商业口子限 "your personal, non-commercial informational purpose",同句要求 "not copy or post such information on any networked computer" apple.com ToU(HIG 各页页脚直链) 不能。 commit 进 git / 推 GitHub 恰好落在"张贴到联网计算机"这一禁止项内 notes 逐条链到具体条款页;用我们自己的话写"检查项 ↔ HIG 条款"对照表,摘引控制在一两句并注明出处。"个人学习/非营利"不构成镜像例外
Apple HIG 抓取可行性 robots.txt 未 Disallow /design/;且 HIG 页面自附面向自动化与辅助工具的 .md 端点 robots.txt robots 允许 ≠ 条款允许 以 ToU 为准,不因 robots 放行就镜像
Apple Design Resources(UI Kit / SF Symbols / 字体) 页面无许可正文,仅版权行 + ToU 与 "Agreements and Guidelines" 链接 design/resources 再分发无授权 → 不 commit 本地开发用,仓库只登记链接与版本
Material Design 3 站点 站点壳层与 Wayback 原始 HTML 均无任何许可声明;CC 句只见于 Google 开发者站通则 m3.material.io/robots.txt、site-policies 站点内容是否被 CC BY 4.0 覆盖 未证实 → 不做整站快照 走下面的官方仓库
material-components/material-web "Apache License Version 2.0…TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION","Copyright 2018-2023 Google, Inc." LICENSE 可以(Apache 2.0 允许再分发) 与现有 Polaris/antd 同法:目录留 LICENSE + NOTICE,不引入 Google 商标(商标不在授权内)
Android / Compose 无障碍文档 文档与代码 Apache 2.0,其余内容 CC BY 2.5,"proprietary trademarks…not included" developer.android.com/license 可以 保留许可副本
W3C 规范原文(WCAG / APG / clreq) "Permission to copy, and distribute the contents of this document…in any medium for any purpose and without fee",须附原文 URI + 版权声明 + STATUS;"No right to create modifications or derivatives"(实现用途例外,须加注来源于该文档) copyright-documents 可原样快照 我们的检查项文档属衍生:逐条标 SC 编号 + 原文 URI,且不得自称规范
nikdelvin/liquid-glass "MIT License / Copyright (c) 2025 Nikita Stadnik";GitHub API license.spdx_id: MIT(2026-10-30 独立核实,非引用 CHANGELOG) LICENSE · api 可以 只登记技法来源(Dock 折射滤镜、纯 CSS/SVG filter 实现 iOS 26 Liquid Glass),不引代码——见 ROADMAP §2 决策 7 的边界

四、可直接抄成 token 的数值表

这一节是 P2 判断层的直接输入。每条都有已核实出处。

类别 数值 出处
时长阶梯(16 档) Short 50/100/150/200 · Medium 250/300/350/400 · Long 450/500/550/600 · ExtraLong 700/800/900/1000 ms MDC-Android docs/theming/Motion.md(Apache-2.0,数值已逐条比对原文)
缓动曲线 standard .2,0,0,1 · standard-decelerate 0,0,0,1 · standard-accelerate .3,0,1,1 · emphasized-decelerate .05,.7,.1,1 · emphasized-accelerate .3,0,.8,.15 · linear 0,0,1,1 · emphasized 为双段 path 同上
Emil 系时长档 按压反馈 100–160 · tooltip/小浮层 125–200 · 下拉 150–250 · 模态/抽屉 200–500;转场不过 300ms;stagger 30–80ms .agents/skills/review-animations/STANDARDS.md §Duration §Stagger
Emil 系曲线 --ease-out: .23,1,.32,1 · --ease-in-out: .77,0,.175,1 · iOS 抽屉 .32,.72,0,1 同上 §Easing
系统弹簧基准 response 0.5s、dampingFraction 0.825、blendDuration 0(1 = 临界阻尼,<1 回弹) SwiftUI Animation.spring(iOS 13.0+)
弹簧阻尼判据 无惯性入场用 100% damping、带惯性滑动 80%;iOS 手势迟滞约 10pt;bounce 保持 0.1–0.3,多数 UI 不用回弹 WWDC 2018 Session 803《Designing Fluid Interfaces》(页面含全文 transcript)
无障碍硬值 自动运动 >5s 且并行呈现须可暂停(SC 2.2.2,A 级);交互触发动画须可禁用(SC 2.3.3,AAA);指针目标 ≥24×24 CSS px(SC 2.5.8);iOS 控件默认 44×44pt、最小 28×28;Android 触控目标 ≥48dp WCAG 2.2 · Apple HIG Accessibility(在线,不入库)· Android
中文行距与行长 line-height = 字号的 50%–100%(即 1.5–2.0);正文 17–40 字/行,最小 10,横排上限 48;中西文间距可固定 ¼ 字宽 clreq §7.1 与 §6.2(DNOTE 2026-10-07,非规范,作数值参考)
西文行长与行距 45–90 字符/行;行距 115–120%;行长 = 字母表宽度 2–3 倍 Butterick(在线版)· Bringhurst 第4版
可动属性白名单 仅 transform 与 opacity 不触发 layout/paint;clip-path 为受许可的第四项(STANDARDS.md);主线程预算 4–5ms/帧 web.dev 2015 + STANDARDS.md §Performance

五、本站实测:已核实的不合规项

用自己的 WCAG 公式重算,非引用他人结论。底色分别取 --color-surface #ffffff 与页面底色 --color-bg #f6f7f9。

token 值 vs #ffffff vs #f6f7f9 判定 实际用处
--color-text #1d1d1f 16.83 15.70 通过 正文
--color-text-muted #6e6e73 5.07 4.73 通过但余量薄(12–13px 非大字,须按 4.5:1 算) 副标、元数据
--color-accent #7c5cf6 4.45 4.15 不过 SC 1.4.3 用作文字:global.css:398 .prose-notes a 链接色
--color-text-subtle #9b9ba3 2.76 2.57 不过 4.5,也不过 3:1 分隔点(纯装饰,可豁免);不可用于文字或功能性描边
--color-success-fg #059669 3.77 3.52 不过 4.5;够 3:1 dashboard.astro:14 用作 ↑ 文字 → 违规
--color-warning-fg #d97706 3.19 2.97 连 SC 1.4.11 的 3:1 都差一点 page-settings.astro:66 删除按钮文字 + 列表页"计划中"圆点
--color-info-fg #0284c7 4.10 3.82 不过 4.5;够 3:1 列表页"复现中"圆点

结论:三个语义前景色 + 主色全部不能直接当文字色,在 --color-surface 与 --color-bg 上都不够 4.5:1。修法是沿同色相降 oklch 的 L(CSS Color 4 §9.4 有出处),而不是加粗或提字号——后者只对 ≥24px 或 ≥18.66px bold 才降到 3:1。

其余三项实测缺口(非色彩,已在前轮审计确认):动效零 token(3 曲线 / 13 时长 / 34 处)、--radius 命中 0 次、island-ios 缺 prefers-reduced-motion。

六、待核实(不入正表)

条目 卡在哪
Refactoring UI 出版年 官方页无日期,"2018"来自 Goodreads 二手记录;页数官方 218 与 Goodreads 252 不一致
Butterick 2nd edition 年份 官方站只写"2nd edition"无日期,二手引注在 2014/2018/2019/2020 间互斥
《平面设计中的网格系统》1981 niggli.ch 产品页是 JS 壳抓不到正文;年份取自 Wikipedia 书目,且它与现行重印 ISBN 记在同一条,现印次年份未知
Noto DemiLight = usWeightClass 350 官方仓库只给 7 档文件名,数值表未取到(Source Han Sans FAQ 404)。结论"要中间字重必须用可变字体"成立,具体数字待核
GB/T 9704-2012 的"22 行 / 28 字" openstd 页只给元数据,正文数值来自高校转载 PDF,官方全文需授权
Penner easing 许可细节 主页面写 "BSD and MIT",terms_of_use 子页 403 未开
Val Head《Designing Interface Animation》第2版 出版社页 403,仅 Google Books 快照显示 2024-09-18
animate.css 许可证 README 称 Hippocratic License(非 MIT),GitHub SPDX 为 NOASSERTION。条款含"不做 harm",慎作长期规范依据
m3.material.io 站点内容许可 一手证据缺失(见第三节),数值一律改引官方仓库 Motion.md

七、下一步的取用顺序

  1. 第四节数值表 → docs/standards/motion.md 的时长档位与曲线族(这是当前 P0 未完项)
  2. 第五节实测 → P2 的色彩 token 修正与无障碍矩阵
  3. 先读 + 硬约束 的条目 → 每条要么变成 CSS 变量,要么变成 lint 判据,否则不进标准
  4. 待核实项:按第六节逐条补,补不上就长期留在候选区,不并入正表