判断层标准、风格轴配方与组件清单——判断从哪来、为什么这么定。
同一批 token 变量名 × 不同取值 = 一条风格轴。切配方即换轴,组件代码不动。
默认轴。半透明 + 细边框 + 模糊,靠背后有东西才成立。
没有浮起、没有模糊,阴影退化成一条发丝线。
同色浮起,没有边框也没有模糊。
每篇的结构一样:标准 / 实测 / 动作 / 检查项。数字都带基线 commit,过期就跑检查项重测。
本站动效的真实现状:零动效 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 档",会把积分步长和防抖延迟也收敛掉。
| 类别 | 定义 | 本站实例 | 受 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),不允许再出现裸值。
标准(animate 频率表 + HIG Motion 的节制原则):每天 100+ 次的键盘动作永远不动;
几十次的高频面(hover、列表导航)只做几乎察觉不到的微动;偶发(模态、抽屉、通知)用标准动效;
罕见/首次才允许花哨。合法目的只有五种:反馈、空间一致性、状态指示、防止突兀跳变、解释。
实测:⚠️ 两处与频率表冲突。
.anim-fade-up(fadeUp 200ms)——这是每天几十次的高频操作,属"微动或不动"档,勉强合规.glass-hover:hover 改 4 个属性 200ms)同样在高频面上动作:保留但降档——hover 的 transform 分量去掉,只留颜色与阴影变化。
检查项:新增动效时必须能一句话说出它属于五种目的里的哪一种;说不出就不加。
标准(转场类,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-* 工具类单列。以脚本输出为准,本文数字在脚本落地后按基线更新。
标准:内置 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 工具类即提示改用命名曲线。
标准:不许 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/ 必须为空。
标准:可能被连续触发的元素(通知、开关、快速切换)用 transition 而非 keyframes—— transition 从当前值重新定向,keyframes 每次都从零重启。弹簧在被打断时保留速度,关键帧不保留。 退场要沿入场的路径原路返回。
实测:⚠️ 一处结构性冲突。
.stagger-in 与 .anim-fade-up 是 keyframes(fadeUp ... both)。anim-fade-up 用在工作台切组件上——
用户连点侧栏时会反复从零重启,且 both 会把动画状态钉住。这正是标准点名要避免的形态translateY(120%) 原路返回 ✓动作:.anim-fade-up 改为 transition + @starting-style 的入场写法,或至少去掉 both 的钉住。
检查项:role=status / 通知类组件内不得出现 animation: 关键字,一律 transition:。
标准:只动 transform 与 opacity(跳过 layout 与 paint,走合成器);clip-path 是受许可的第四项。
width/height/margin/padding/top/left 三阶段全触发。will-change 不许常挂。
实测:transition: all 0 命中 ✓;will-change 0 命中(既没乱用也没用,中性)。
两处违规(脚本 npm run check:motion 查出来的,本文初稿只抓到第一处):
.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 豁免。
标准: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.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 的数量。
标准:成组入场要 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。
标准: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 声明的值在代码里找不到同值来源"这一条可以精确报警**——
上面两处矛盾都属于此类。
标准:顶栏是常驻 chrome,跨路由不重挂;内容交换不加入场动画。 导航是高频动作,换页必须跟手——实测内容交换 50–90ms(/design 约 260ms), 再叠 300ms 入场,点击到看清内容要 350–560ms,判为慢。
编排(global.css 的 ::view-transition-*):
animation: none:
Astro 仍走 startViewTransition(换页免整页刷新),但不再付伪元素动画的时长;
UA 默认的根交叉淡入也显式关掉,否则它仍会跑 250ms。transition:persist 加 transition:name="top-nav"。只写 persist 不够:
没有名字它就留在根快照里,跟着内容一起对穿——中间帧实测图标与文字重影。
有了名字它单独成组,新旧几何相同 → 视觉上静止。data-astro-prefetch="hover":悬停即取目标 HTML,点击只剩余交换。为什么不挂 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 引用。
逐条对当前代码跑过,右侧是实测状态:
| 禁止 | 本站实测(基线 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 落地同批处理。
不在本次改动范围(属 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 处裸值替换掉"。三个错叠加:
notes.md 的文字描述中,不是声明点duration-200 和 JS const ENTER_MS = 400 都不含 ms 后缀,grep 抓不到,漏掉 14 处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 全部进 tokenduration-* / 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 行 |
overlay.ts 只在 React 项目里需要。node scripts/check-motion.mjs --strict 全达标;系统开"减少动效"后位移消失、透明度过渡保留;transition: all 与 will-change 各 0 命中。标准:本站只有三种材质,各有明确职责,不许互相顶替。
| 级别 | 材质 | 用在哪 | 判据 |
|---|---|---|---|
| 悬浮层 | 玻璃(--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
标准:阴影只表达高度,不表达颜色。三档对应三种高度,用错就是噪声。
| 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 是浏览器原生,不引任何模糊库。--glass-inset 的边缘光学可先不搬(没有它玻璃也能看,只是薄一点)。--glass-blur 改成 0 再改回 16px,看所有浮层是否同步变化(说明没有硬编码模糊);悬浮层底下必须有内容,空底玻璃看不出厚度。标准:本站只有一条字号阶梯(--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 |
标准:
text-sm、text-2xs),不写 text-[length:var(--text-sm)]。
两种写法指向同一个 token,但混用会让"这一档到底用在哪"查不出来——本站两种形式都出现过(任意值形式 24 处)。text-2xl 起),组件内部标题不超过 text-lg。实测(基线 982e282,2026-10-31 重测):命名档位工具类 361 处,text-[length:...] 任意值 97 处——两套写法并存,这是本文要清掉的第一件事。
--text-md(16px 强调正文)定义了就没人用:要么补上用法,要么删掉这一档(阶梯里的空档也是噪声)。
动作:
text-[length:var(--text-2xs)] → text-2xs)。--text-md 二选一:删掉,或在"强调正文"处统一使用。检查项:
# 不该再有任意值形式的字号
grep -rn "text-\[length:var(--text-" src | wc -l
标准(library.md 第四节,clreq §7.1 / Butterick):
--text-base 14px → 行高 21–28px)。line-break: strict 用于中文正文,短标签用 loose。实测(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
标准:只有四档字重,取自 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 / 字号 / 等宽四维) |
--measure 与正文规则一起搬,否则宽屏会把行拉到 90 字符以上。grep -rn "text-\[length:var(--text-" src | wc -l 证明字号全走 token;量一段中文正文是否落在 40 字/行上下;站内结构里 leading-[ 任意值应为 0。标准:三个语义前景色 + 一套底色/描边,只表达事情的状态,不表达品牌。
| 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 |
标准:
--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
标准:所有可点元素都要有按压反馈,用缩放表达,不用颜色——颜色变化会和 hover 撞。
.press-feedback(transform: scale(0.97) + --dur-press 过渡)。--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
标准(这是可用性底线,不是修饰):
*:focus-visible 用 2px 实线 + 2px 偏移。focus:outline-none + 自己的环(拟态用 2px shadow 环,
玻璃 / 极简用描边提亮)替代全局环。全局规则放在 @layer base,
否则层外规则会压过工具类,出现「外环 + 内环」两层焦点。实测(基线 982e282,修正后):
sm 轨道 20px → 24px ✓(md 26px 本来就合规)。aria-activedescendant 指向高亮项,焦点留在触发器;
↑↓ / Home / End 移动、Enter / 空格选中、Esc / Tab 收起、点外关闭,禁用项跳过 ✓。aria-live 播报 ✓。
与 Dropdown 的分工是能不能自由输入:只能选一个已有值用 Dropdown,输入即过滤用 Combobox。sr-only 后焦点环用 peer-focus-visible 画在可视框上,组级与单项禁用都可见 ✓。role="switch" + aria-checked,底色 <button> 自带 Space/Enter ✓。aria-labelledby ✓。Dock 的图标按钮尺寸随放大变化,静止时 52px ✓;但 topnav 的字号/图标类 chip 与
CommandPalette 的行高仍需逐个核对是否达到 24px。动作:新增可点元素先量尺寸(DevTools 或 getBoundingClientRect),不够就加 padding。
检查项:
# 焦点环不能退回 1.5px
grep -rn "outline: 1.5px" src | wc -l
标准:
setPointerCapture,不用 HTML5 drag-and-drop(触屏与样式都不可控)。touch-action: none。offsetTop),不量被 translate 的 rect。aria-live="polite"。prefers-reduced-motion 下可去掉过渡,但顺序改变与焦点位置不能丢。同一个"拖",动机不同,规则也不同。混着用就会把一种的正确做法搬到另一种上。
| 跟手型 | 拉动型 | |
|---|---|---|
| 例子 | 拖拽排序、滑动删除、滑块、抽屉 | 拽住往外拉、拉够远就触发(如"拉飞便签") |
| 位移关系 | 按住期间 1:1,不许打折 | 阻尼就是交互本身 |
| 提交时机 | 松手判定(滑块例外:连续提交) | 越过触发线即提交,之后不可撤销 |
| 判错的样子 | 加了缓动或多乘一个系数 → 读成"卡顿 / 拖不动" | 改成 1:1 → 失去"拉"的张力 |
1:1 跟手 → 越界阻尼 → 阈值提交 → 吸附落位
track damp commit settle
transition,松手才挂。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
标准:光标是操作提示,不是装饰。按三层处理,越往后越谨慎。
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,不做站点级):四条硬规则,缺一条就不做。
html / body 上写 cursor: none;原生光标始终保留。(hover: hover) and (pointer: fine) 下启用,触屏与粗指针不加载。prefers-reduced-motion 下关闭跟随与拖尾,只留静态提示或直接不启用。mix-blend-mode 只做点缀,不保证任何背景下的对比度;它还会创建独立堆叠上下文。B 层 · 元素响应指针(推荐):光标不动,元素响应——磁吸、聚光、放大。
transform,不写 left/top 与宽高;指针移动期间不挂过渡,离开再补间回位。(hover: hover) and (pointer: fine) 与 prefers-reduced-motion;触屏不响应。本站实现: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 行 |
.press-feedback 全站只留一处定义,搬过去之后把目标项目里其它 scale(...) 全删掉。pick-colors.mjs(它依赖本站的 OKLCH 取色流程,只在你也要改色时才需要)。标准:每个组件按七态验收,缺哪一态要说得出理由。
| 状态 | 判据 | 缺了会怎样 |
|---|---|---|
| 默认 | 静止时的样子 | —— |
| 悬停 | 指针在目标上 | 用户不知道能不能点 |
| 按压 | 目标被按下 | 不知道点击是否生效 |
| 焦点 | 键盘可见焦点 | 键盘用户直接不可用(SC 2.4.7) |
| 禁用 | 不可用但可见 | 用户不知道为什么不能操作 |
| 加载 | 数据在路上 | 不知道是卡住了还是没反应 |
| 错误 | 输入非法 / 操作失败 | 用户不知道自己错在哪 |
| 空 | 没有数据 | 页面看着像坏了 |
标准(决策 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,以及版式是否与最终内容一致。
实测口径: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 |
每组件一个字段块 |
node scripts/check-states.mjs --strict 全绿;故意删掉某个组件的 disabled 演示,脚本必须报出来。标准(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
标准:同一个屏宽基准 → 同一套内边距 → 同一个容器。 全站的宽度只有一处真值,容器、内边距、贴边定位都从它算:
: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-,两处都拦
标准:间距只用 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
标准:圆角是一条阶梯,值来自 @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 行 |
.wb-*(工作台三栏);多列容器(信息密度不够时单列更稳)。--frame-max 的下限/封顶换成目标设计稿的宽度,再跑 1920 与 3840 两档。node scripts/check-frame.mjs 报 0 违规。标准:通用 UI 图标一律来自 lucide-react,并通过 src/components/Icon.tsx 渲染。业务组件不直接手写 <svg> 表达箭头、勾选、关闭、面板、设置这类通用含义。
动作:
Icon 使用。src/components/icons.ts 的 TYPE_ICON 映射,不在 TypeGlyph 里画路径。检查项:
grep -Rn "<svg" src --include="*.tsx" --include="*.astro"
标准:
| 项 | 规则 |
|---|---|
| 网格 | 统一 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 三种网格同时存在,会让一排图标看起来像从三个系统里借来的。
标准: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,但这不应成为常态。
标准:
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 处。
组件回答"这个零件长什么样",页面模式回答"零件按什么顺序摆"。 下面每条都来自至少两家系统的同一结论;只有一家主张的,标了出处不作硬规则。
标准:H1 是资源名或任务名;一句说明只补 H1 没说的;主操作只有一个,放在右上。
| 位置 | 放什么 |
|---|---|
| 左 | 返回 / 面包屑(二选一)→ H1 → 一句说明 |
| 右 | 主操作(动词 + 名词)→ 次要操作 → 溢出菜单 |
| 永不 | 第二个同权重的主操作;把主操作沉到内容底部 |
banner 说后果与恢复方式,字段级 errorMessage 说这一格该填什么。两层各说一件事,不互相复述。本站落点:PageHeader 的 actions 槽。dashboard(筛选)、list(矩阵视图)已用;
record 的三个操作原先沉在正文底部,与"页头承担页面级操作"不一致,已迁入页头。
分区页(PageShell col)的页头与正文共用 .page-col 这一条内容列——标题不会一个人留在框边上。
单任务页:create(48rem)与 auth(30rem)用 colWidth 收窄并在正文区垂直居中。
反例:page-home 没有页头——它是品牌页,主操作在 hero 里,跟着内容走。
标准:工具栏紧贴数据上方;计数与结果同区;分页只在超过一页时出现。
1–12 条,共 89 条),放在列表页脚;不要只写总数,用户不知道当前看到哪一段。本站落点:ComponentList。工具栏、计数、分页、回第 1 页都已按此实现。
标准:文本左对齐、数字右对齐,列头跟数据同向;数字用 tabular-nums;只读表格同样需要 caption 与 scope。
本站落点:/showcase/matrix 已用 th scope;只读表已复现为 table 三材质,list 场景用 framed=false 嵌进面板(/showcase/demo/list 的「表格」视图)。排序 / 筛选 / 选择 / 固定列都还没做,逐项放行。
标准:分组用 fieldset + legend;一行 = 标签 + 一句说明 + 控件;文本编辑显式保存,开关/滑块即时生效。
sr-only——同一屏不画两遍分组名,语义仍在。本站落点:page-settings。开关与滑块即时;个人资料改为显式保存(保存 / 放弃 + 状态播报);
≥1024px 起左栏分组导航 + 右栏卡片,窄屏退回单列并让卡片标题重新出现。
标准:键值事实用 dl;行级操作必须带视觉隐藏上下文;所有行都去同一个地方时,用卡片级的一个操作。
本站落点:record 用 Descriptions(dl)+ 面包屑;操作移到页头与第 1 条对齐;
宽屏两列(2fr / 1fr)——左=身份与变更摘要,右=版本 / 环境 / 提交 / 负责人 / 回滚点,窄屏按 身份 → 摘要 → 元数据 单列下来。
标准:线性且 ≥3 步才用步骤条;可乱序、可跨会话的任务用任务清单,两者不混用(GOV task list / USWDS step indicator)。
本站落点:create 三步步进器;本次补错误摘要、焦点与字段链接。宽屏时步骤条转到左侧成竖排导航,表单列仍按 colWidth 收到 48rem——表单不跟着框拉长。
| 状态 | 规则 | 本站 |
|---|---|---|
| 加载 | 先给结构再给内容;骨架与最终版式同形 | 组件级 skeleton 已有;页面级载体未定 |
| 空 | 说清什么空了 + 一句下一步 + 一个主操作 | empty-state 三材质已落地,list 已接 |
| 错误 | 说明影响与恢复方式,不用"出错了"兜底 | banner 三材质已落地(warning 走 role="alert",info / success 走 role="status"),auth 已接;页面级"服务不可用"载体仍未定 |
| 404 / 500 | H1 直说问题;给一条回去的路;不显示技术细节 | 仍是框架默认页 |
| 规则 | 本地快照 | 官方 |
|---|---|---|
| 索引页主操作右上、工具栏紧贴列表 | 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 |
不照抄:
| 要搬的 | 在哪 | 量 |
|---|---|---|
| 页面头槽位 | src/components/PageHeader.astro |
40 行 |
| 索引页工具栏 + 计数 + 分页 | ComponentList.tsx + pagination/* |
约 1 个页面 |
| 错误摘要 | CreateWizard.tsx 内的局部实现 |
约 40 行 |
| 只读表格语义 | matrix.astro 的 table 写法 |
1 个示例 |
一段话给 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(本站标准全文):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;
}标准里的判断也拆成术语——按术语搜,比按「别乱加动画」搜得到。
每个组件的正式叫法——按术语去搜规范,比按「玻璃卡片」搜得到得多。
101 个已复现组件,元数据来自各自的 notes frontmatter。
设计、提示词与实现导出成 registry 条目;底盘全部派生,check:export 是第十一道闸。
书籍与规范的可调用性分级——哪条能变成 CSS 变量,哪条只能内化为取舍。
L0 原料层的第二块:书、官方规范、参考实现。与 .agents/skills/(给判断)和 design-systems/(给规范全文)平级。
核实日期:2026-10-30。所有条目由四路调研代理检索并实际打开官方页取得,再由我方独立复验: Material 时长与曲线数值逐条比对官方仓库原文、Apple ToU 关键句逐字复核、WCAG 2.2 版本日期与 SC 2.5.8 阈值另开官方页确认、 本站 token 对比度用 WCAG 公式在 Node 里重算(与代理结果逐项吻合,并补测了两个未覆盖的 token)。
先读(能直接改写本站某个 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 的边界 |
这一节是 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 |
docs/standards/motion.md 的时长档位与曲线族(这是当前 P0 未完项)先读 + 硬约束 的条目 → 每条要么变成 CSS 变量,要么变成 lint 判据,否则不进标准