agentcss 0.11.9 → 0.11.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/01-tokens/page.css +71 -0
  2. package/02-primitives/badge/README.md +14 -0
  3. package/02-primitives/button/demo.html +5 -15
  4. package/02-primitives/card/README.md +1 -1
  5. package/02-primitives/card/demo.html +3 -13
  6. package/02-primitives/choice/demo.html +1 -0
  7. package/02-primitives/combobox/README.md +15 -0
  8. package/02-primitives/combobox/demo.html +38 -0
  9. package/02-primitives/date-range/README.md +11 -0
  10. package/02-primitives/date-range/demo.html +36 -0
  11. package/02-primitives/input/demo.html +5 -16
  12. package/02-primitives/popover/README.md +27 -1
  13. package/02-primitives/popover/demo.html +45 -0
  14. package/02-primitives/progress/README.md +4 -0
  15. package/02-primitives/progress/demo.html +25 -0
  16. package/02-primitives/select/README.md +12 -0
  17. package/02-primitives/select/demo.html +46 -0
  18. package/02-primitives/separator/README.md +3 -1
  19. package/02-primitives/skeleton/demo.html +28 -0
  20. package/02-primitives/switch/README.md +14 -0
  21. package/03-patterns/accordion/README.md +17 -0
  22. package/03-patterns/content/demo.html +20 -0
  23. package/03-patterns/drawer/README.md +12 -0
  24. package/03-patterns/drawer/demo.html +36 -0
  25. package/03-patterns/dropdown/README.md +15 -0
  26. package/03-patterns/dropdown/demo.html +39 -0
  27. package/03-patterns/list/demo.html +48 -0
  28. package/03-patterns/nav/demo.html +34 -1
  29. package/03-patterns/overlay/README.md +3 -3
  30. package/03-patterns/pagination/README.md +14 -0
  31. package/03-patterns/pagination/demo.html +40 -0
  32. package/03-patterns/reveal/README.md +15 -0
  33. package/03-patterns/reveal/demo.html +24 -0
  34. package/03-patterns/states/README.md +1 -1
  35. package/03-patterns/states/demo.html +25 -1
  36. package/03-patterns/tabs/README.md +15 -0
  37. package/03-patterns/tooltip/README.md +13 -0
  38. package/03-patterns/tooltip/demo.html +27 -0
  39. package/03-patterns/tree/README.md +16 -0
  40. package/03-patterns/tree/demo.html +43 -0
  41. package/04-recipes/analysis-report/README.md +16 -0
  42. package/04-recipes/analysis-report/demo.html +34 -0
  43. package/04-recipes/data-showcase/README.md +12 -0
  44. package/04-recipes/data-showcase/demo.html +29 -0
  45. package/04-recipes/detail-page/README.md +13 -0
  46. package/04-recipes/detail-page/demo.html +28 -0
  47. package/04-recipes/report/README.md +13 -0
  48. package/04-recipes/report/demo.html +24 -0
  49. package/04-recipes/table/README.md +38 -0
  50. package/04-recipes/table/demo.html +39 -0
  51. package/CHANGELOG.md +90 -0
  52. package/CONTRIBUTING.md +8 -1
  53. package/ai/components.json +38 -10
  54. package/dist/01-tokens/page.css +1 -1
  55. package/dist/COSTS.md +1 -1
  56. package/index.html +1 -1
  57. package/package.json +1 -1
@@ -47,7 +47,7 @@
47
47
 
48
48
  **这就是"不要一档打天下"的实测依据。**
49
49
 
50
- ## 两个反例
50
+ ## 反例
51
51
 
52
52
  **① 语义分隔用装饰色** —— 分隔"基本信息"与"高级设置"是两类不同的东西,
53
53
  但 1.26:1 的线在低视力与暗色下几乎看不见,而读屏只念"分隔符"、
@@ -57,6 +57,8 @@
57
57
  **什么都没有**。语义分隔必须用 `<hr>` 或 `role="separator"`,
58
58
  装饰性的才可以用纯 div。
59
59
 
60
+ - ❌ **自己用 `box-shadow` 画分隔线** —— 1px 的线在部分浏览器上会取整成 2px;本库用 border 就是为了这个(`separator.css` 里写了原因)
61
+
60
62
  ## 细节
61
63
 
62
64
  用 `border` 而不是 `box-shadow` 画线 —— 1px 的线用 box-shadow
@@ -9,6 +9,7 @@
9
9
  <link rel="stylesheet" href="../../01-tokens/typography.css">
10
10
  <link rel="stylesheet" href="../../01-tokens/page.css">
11
11
  <link rel="stylesheet" href="../button/button.css">
12
+ <link rel="stylesheet" href="../progress/progress.css">
12
13
  <link rel="stylesheet" href="skeleton.css">
13
14
  <style>
14
15
  .demo {
@@ -22,6 +23,9 @@
22
23
  /* 反例演示:故意用深色底+白字,说明"骨架屏不该这样用" */
23
24
  .bad-demo { background: var(--danger-bg); border-color: var(--danger); }
24
25
  .bad { color: var(--danger); }
26
+ .sk-label { margin: 0 0 var(--sp-2); font-size: var(--fs-sm); }
27
+ .sk-pct { margin: 0 0 var(--sp-2); font-size: var(--fs-xs); color: var(--text-secondary); }
28
+ .sk-box { display: flex; flex-direction: column; gap: var(--sp-2); }
25
29
  </style>
26
30
  </head>
27
31
  <body class="page page">
@@ -105,6 +109,30 @@
105
109
  </p>
106
110
  </div>
107
111
 
112
+
113
+
114
+ <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
115
+ <div class="cmp">
116
+ <div class="cmp__pane cmp__bad">
117
+ <h3>❌ 常见写法:有确切进度却用骨架屏</h3>
118
+ <p class="sk-label">上传 素材包.zip(3.1 MB)</p>
119
+ <div class="sk-box" aria-hidden="true">
120
+ <span class="skeleton skeleton--text" style="width:82%"></span>
121
+ <span class="skeleton skeleton--text" style="width:61%"></span>
122
+ <span class="skeleton skeleton--text" style="width:44%"></span>
123
+ </div>
124
+ <p class="cmp__why">文件大小是已知的、进度是算得出来的,却只给一片灰条:用户等了 40 秒也不知道还差 3 秒还是 3 分钟。</p>
125
+ </div>
126
+ <div class="cmp__pane cmp__good">
127
+ <h3>✅ 本库:确定进度用 progress,不确定才用 skeleton</h3>
128
+ <p class="sk-label">上传 素材包.zip(3.1 MB)</p>
129
+ <p class="sk-pct">已传 1.9 MB / 3.1 MB</p>
130
+ <span class="progress" role="progressbar" aria-valuenow="61"
131
+ aria-valuemin="0" aria-valuemax="100" aria-label="上传进度"
132
+ style="--progress:61%"><span class="progress__bar"></span></span>
133
+ <p class="cmp__why"><code>skeleton</code> 的语义是"<b>不知道</b>要多久"。知道比例就用 <code>progress</code> —— 两者的区别不是好看,是诚实。</p>
134
+ </div>
135
+ </div>
108
136
  </main>
109
137
  </body>
110
138
  </html>
@@ -63,3 +63,17 @@ Space 键、焦点样式、状态播报 —— **三件事漏一件就坏**,
63
63
  - 滑块位移随状态变化 · axe 0 违规 / 24 条通过
64
64
 
65
65
  判别力验证:去掉 `role` 与 `label` ⇒ 4 项立即被抓。
66
+
67
+ ## 反例
68
+
69
+ - ❌ **让 Enter 也能切换** —— APG 的 Switch 只认 Space。Enter 会**双重切换**:
70
+ 读屏自己会发一次,你的处理器再切一次,用户看到的是"按了没反应"
71
+ - ❌ **用 `aria-label` 代替 `<label for>`** —— 视觉标签点了没反应,用户以为坏了。
72
+ 标签不可见时用 `switch__label--sr`:看得见可以没有,听得到必须有
73
+ - ❌ **把 switch 当 checkbox 用** —— switch 的语义是"**立刻生效**",checkbox 是"随表单一起提交"。
74
+ 用开关做"我已阅读条款",用户勾完就以为已经生效了
75
+ - ❌ **给开关加 `error` 或 `readonly`** —— 开关没有中间态可校验;readonly 对开关等于 disabled(用户改不了),
76
+ 留着它只会让复用者以为"这个能校验"
77
+ - ❌ **只把小滑块做成可点区域** —— `switch__input` 要铺满整块;只点滑块,触屏上漏点率很高
78
+
79
+ ---
@@ -119,3 +119,20 @@ APG 警告:
119
119
  - axe 0 违规 / 29 条通过
120
120
 
121
121
  判别力验证:注入 keydown 双触发 ⇒ **2 项立即被抓**。
122
+
123
+ ## 反例
124
+
125
+ - ❌ **给折叠标题加方向键 / Home / End** —— APG 的 Accordion 只要求 Tab + Enter/Space。
126
+ 加了会和页面滚动抢方向键;本组件**刻意不监听 `keydown`**,这是设计不是遗漏
127
+ - ❌ **用 `max-height` 做展开动画** —— 你必须猜内容有多高:猜小了裁字,猜大了结尾拖一段空白。
128
+ 本库用 `grid-template-rows` 从 `0fr` 到 `1fr`,不需要知道高度
129
+ - ❌ **收起时直接 `display: none`** —— 关闭前必须**先量到高度再收**,否则量到 0,
130
+ 动画"啪"一下消失(`accordion.js` 里那条注释说的就是这个)
131
+ - ❌ **给 `accordion__panel` 加 `overflow: hidden`** —— 会裁掉里面控件的焦点环;
132
+ 收的过程交给 grid 的 `0fr` 完成
133
+ - ❌ **每一项都套 `role="region"`** —— APG 有 ≤6 的地标上限,超过就是"地标泛滥",
134
+ 读屏的地标列表反而失去导航价值
135
+ - ❌ **把 `--acc-h` 挪进全局令牌表** —— 它只对本组件有意义(`accordion.css` 里写了为什么不进 `tokens.css`),
136
+ 进全局表会让所有人以为那是个通用尺寸
137
+
138
+ ---
@@ -17,6 +17,7 @@
17
17
  border-radius: var(--r-sm);
18
18
  font-size: var(--fs-sm); color: var(--text-secondary);
19
19
  }
20
+ .ct-p { margin: 0; font-size: var(--fs-sm); }
20
21
  </style>
21
22
  </head>
22
23
  <body class="page page">
@@ -82,6 +83,25 @@
82
83
  <p>详细口径见 <a href="#main">内部数据字典</a>。</p>
83
84
  </div>
84
85
 
86
+
87
+
88
+ <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
89
+ <div class="cmp">
90
+ <div class="cmp__pane cmp__bad">
91
+ <h3>❌ 常见写法:裸 <code>&lt;hr&gt;</code></h3>
92
+ <div aria-hidden="true"> <p class="ct-p">本季应收账款周转天数下降 6 天。</p>
93
+ <hr>
94
+ <p class="ct-p">下一节:存货跌价准备的计提口径。</p></div>
95
+ <p class="cmp__why">默认 <code>&lt;hr&gt;</code> 自带上下两条 inset 边框,不清 <code>height</code> 和 <code>border-top</code> 出来就是一条粗双线 —— 而且不同浏览器的厚度还不一样。</p>
96
+ </div>
97
+ <div class="cmp__pane cmp__good">
98
+ <h3>✅ 本库:<code>.prose &gt; hr</code></h3>
99
+ <div class="prose" aria-hidden="true"> <p class="ct-p">本季应收账款周转天数下降 6 天。</p>
100
+ <hr>
101
+ <p class="ct-p">下一节:存货跌价准备的计提口径。</p></div>
102
+ <p class="cmp__why"><code>content.css</code> 把 <code>height</code> 归零、只留一条上边 —— 渲染器(markdown-it)产出的裸 <code>&lt;hr&gt;</code> 直接就是对的。</p>
103
+ </div>
104
+ </div>
85
105
  </main>
86
106
 
87
107
  </body>
@@ -74,6 +74,18 @@ Drawer.open({ place: 'right' }); // 默认就是 right
74
74
  |---|---|
75
75
  | `dismissable:manual` | 点遮罩关闭是手写的,不是统一机制 |
76
76
 
77
+ ## 反例
78
+
79
+ - ❌ **打开抽屉时不给背景加 `inert` / `aria-hidden`** —— 抽屉是**模态**:读屏会把抽屉与背景
80
+ 一起念出来("双重朗读"),用户听到两套内容叠在一起
81
+ - ❌ **省掉焦点陷阱** —— Tab 会跑到抽屉外面,键盘用户在看不见的内容上操作,还以为抽屉坏了
82
+ - ❌ **`drawer__close` 做得很小** —— 它是独立目标,要 44px。抽屉滑进来之后,用户第一件事就是找关闭
83
+ - ❌ **在插入 DOM 之后才记录 `document.activeElement`** —— 记到的是新节点,
84
+ 关闭时焦点"归还"到一个已经不存在的位置,等于没还
85
+ - ❌ **嵌套抽屉不用锁计数** —— 内层关掉时会把外层的滚动锁一起解开,外层还在,背景却能滚了
86
+
87
+ ---
88
+
77
89
  ## 门禁
78
90
 
79
91
  `05-audit/drawer-check.js`
@@ -21,6 +21,21 @@
21
21
  }
22
22
  ul.demo-list { padding-left: var(--sp-5); color: var(--text-secondary); }
23
23
  ul.demo-list li { margin: var(--sp-1) 0; }
24
+ .dr-bar {
25
+ display: flex; align-items: center; justify-content: space-between;
26
+ gap: var(--sp-3);
27
+ padding: var(--sp-2) var(--sp-3);
28
+ background: var(--surface-raised);
29
+ border: 1px solid var(--border-decor);
30
+ border-radius: var(--r-sm);
31
+ }
32
+ .dr-bar__title { font-size: var(--fs-sm); font-weight: 500; }
33
+ .dr-tiny {
34
+ width: 24px; height: 24px;
35
+ display: flex; align-items: center; justify-content: center;
36
+ background: none; border: 0; border-radius: var(--r-xs);
37
+ color: var(--text-secondary); font-size: 14px; line-height: 1; cursor: pointer;
38
+ }
24
39
  </style>
25
40
  </head>
26
41
  <body class="page page">
@@ -49,6 +64,27 @@ ul.demo-list li { margin: var(--sp-1) 0; }
49
64
  <button class="btn btn--primary" id="b2">打开(焦点落在第二个按钮)</button>
50
65
  </div>
51
66
 
67
+
68
+
69
+ <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
70
+ <div class="cmp">
71
+ <div class="cmp__pane cmp__bad">
72
+ <h3>❌ 常见写法:关闭按钮做小</h3>
73
+ <div class="dr-bar" aria-hidden="true">
74
+ <span class="dr-bar__title">筛选</span>
75
+ <button class="dr-tiny" type="button" tabindex="-1" aria-label="关闭">×</button>
76
+ </div>
77
+ <p class="cmp__why">关闭是**独立目标**:抽屉滑进来之后,用户第一件事就是找它。24px 的叉在触屏上要瞄准,点偏了就点到了背后的内容。</p>
78
+ </div>
79
+ <div class="cmp__pane cmp__good">
80
+ <h3>✅ 本库:<code>drawer__close</code> 44px</h3>
81
+ <div class="dr-bar" aria-hidden="true">
82
+ <span class="dr-bar__title">筛选</span>
83
+ <button class="drawer__close" type="button" tabindex="-1" aria-label="关闭">×</button>
84
+ </div>
85
+ <p class="cmp__why">命中区 44px(<code>drawer.css</code> 里写明"关闭按钮是独立目标,用 44")—— 字形还是 20px,只是可点范围大了。</p>
86
+ </div>
87
+ </div>
52
88
  </main>
53
89
 
54
90
  <script src="drawer.js"></script>
@@ -73,6 +73,21 @@ menu.setAttribute('role', 'menu');
73
73
  | `focus-return:manual` | 关闭后归位是手写的,不是统一机制 |
74
74
  | `typeahead:gap` | **没有类型搜索** —— APG 的 Menu Button 不硬性要求,但列表长时体验缺失 |
75
75
 
76
+ ## 反例
77
+
78
+ - ❌ **菜单里一部分项 `aria-hidden`、一部分不设** —— APG 要求**全设或全不设**。
79
+ 半吊子会让读屏报"1 项中的 0 项"这种不可理解的计数
80
+ - ❌ **禁用项不给 `tabindex="-1"`** —— Tab 会停在点不动的项上,用户以为菜单坏了
81
+ - ❌ **Esc 关闭后焦点不还给 `dd__btn`** —— 焦点掉到 `<body>`,键盘用户要从页面开头重新 Tab(APG 明确要求归还)
82
+ - ❌ **给某个 `dd__item` 设 `tabindex="0"`** —— `role="menu"` 靠方向键步进,多一个 0
83
+ 就多出一个 Tab 停靠点,整个菜单的导航模型就混了
84
+ - ❌ **先写 `aria-controls` 再确保 `dd__menu` 有 id** —— 顺序反了会引用空字符串,
85
+ 读屏拿不到"这个按钮控制哪个菜单"(`dropdown.js` 里为此调过顺序)
86
+ - ❌ **用 `opacity: 0` 或 `visibility` 隐藏 `dd__menu`** —— 关闭态要用 `hidden`:
87
+ 它同时移出无障碍树并不可聚焦,另两种做法都还留着 Tab 停靠点
88
+
89
+ ---
90
+
76
91
  ## 门禁
77
92
 
78
93
  `05-audit/dropdown-check.js`
@@ -19,6 +19,20 @@
19
19
  font-family: var(--font-mono, monospace); font-size: var(--fs-xs);
20
20
  color: var(--text-secondary); min-height: 2.6em;
21
21
  }
22
+ .dd-cap { margin: 0 0 var(--sp-2); font-size: var(--fs-sm); color: var(--text-tertiary); }
23
+ .dd-demo {
24
+ list-style: none; margin: 0; padding: var(--sp-1);
25
+ background: var(--surface-raised);
26
+ border: 1px solid var(--border-decor);
27
+ border-radius: var(--r-sm);
28
+ }
29
+ /* 反例专用:24px 一行(本库把它量出来过,见 dropdown.css 的复盘) */
30
+ .dd-bug {
31
+ display: flex; align-items: center;
32
+ height: 24px; padding: 0 var(--sp-3);
33
+ border-radius: var(--r-xs);
34
+ font-size: var(--fs-sm); cursor: pointer;
35
+ }
22
36
  </style>
23
37
  </head>
24
38
  <body class="page page">
@@ -73,6 +87,31 @@
73
87
  </div>
74
88
  </div>
75
89
 
90
+
91
+
92
+ <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
93
+ <div class="cmp">
94
+ <div class="cmp__pane cmp__bad">
95
+ <h3>❌ 常见写法:菜单项只有 24px</h3>
96
+ <p class="dd-cap">下拉菜单 · 每项高度</p>
97
+ <ul class="dd-demo" aria-hidden="true">
98
+ <li class="dd-bug">按涨跌幅</li>
99
+ <li class="dd-bug">按市值</li>
100
+ <li class="dd-bug">按成交额</li>
101
+ </ul>
102
+ <p class="cmp__why">24px 一行:手指点下去会压到隔壁,触屏用户每点错一次就得重来。本库在这块踩过更隐蔽的坑 —— 写 <code>min-height: 44px</code>,量出来只有 24px(<code>&lt;li&gt;</code> 是 <code>display:list-item</code>),所以现在用的是显式 <code>height</code>。</p>
103
+ </div>
104
+ <div class="cmp__pane cmp__good">
105
+ <h3>✅ 本库:显式 <code>height: 44px</code></h3>
106
+ <p class="dd-cap">下拉菜单 · 每项高度</p>
107
+ <ul class="dd-demo" aria-hidden="true">
108
+ <li class="dd__item">按涨跌幅</li>
109
+ <li class="dd__item">按市值</li>
110
+ <li class="dd__item">按成交额</li>
111
+ </ul>
112
+ <p class="cmp__why">同一个 44,<code>min-height</code> 与 <code>height</code> 在列表项上是两回事。桌面小屏看不出来,只有量了才发现 —— 所以这里把两种结果并排摆出来。</p>
113
+ </div>
114
+ </div>
76
115
  </main>
77
116
 
78
117
  <script src="dropdown.js"></script>
@@ -17,6 +17,9 @@
17
17
  p { max-width: var(--measure-prose); margin: 0 0 var(--sp-3); }
18
18
  p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.65; }
19
19
  .bar { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-4); }
20
+ .ls-demo { list-style: none; margin: 0; padding: 0; background: var(--surface-raised);
21
+ border: 1px solid var(--border-decor); border-radius: var(--r-sm); }
22
+ .ls-clip { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
20
23
  </style>
21
24
  </head>
22
25
  <body class="page page">
@@ -85,6 +88,51 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
85
88
  这一点与欧美惯例相反,是刻意的——做反了会被当成数据出错。
86
89
  </p>
87
90
 
91
+
92
+
93
+ <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
94
+ <div class="cmp">
95
+ <div class="cmp__pane cmp__bad">
96
+ <h3>❌ 常见写法:长文本直接换行</h3>
97
+ <ul class="ls-demo" aria-hidden="true">
98
+ <li class="list__item">
99
+ <span class="list__main">
100
+ <span class="list__title">长鑫存储技术有限公司(合肥)2026 年三季报</span>
101
+ <span class="list__meta">12 条记录 · 半导体</span>
102
+ </span>
103
+ <span class="list__value num list__value--up">+3.21%</span>
104
+ </li>
105
+ <li class="list__item">
106
+ <span class="list__main">
107
+ <span class="list__title">中芯国际集成电路制造</span>
108
+ <span class="list__meta">8 条记录 · 半导体</span>
109
+ </span>
110
+ <span class="list__value num list__value--down">−1.05%</span>
111
+ </li>
112
+ </ul>
113
+ <p class="cmp__why">第一行折成两行,行高就不一样了:一屏能扫到的条目从 8 条掉到 5 条,而且"哪一行对应哪个数字"要重新对。</p>
114
+ </div>
115
+ <div class="cmp__pane cmp__good">
116
+ <h3>✅ 本库:单行省略 + <code>title</code></h3>
117
+ <ul class="ls-demo" aria-hidden="true">
118
+ <li class="list__item">
119
+ <span class="list__main">
120
+ <span class="list__title ls-clip">长鑫存储技术有限公司(合肥)2026 年三季报</span>
121
+ <span class="list__meta">12 条记录 · 半导体</span>
122
+ </span>
123
+ <span class="list__value num list__value--up">+3.21%</span>
124
+ </li>
125
+ <li class="list__item">
126
+ <span class="list__main">
127
+ <span class="list__title ls-clip">中芯国际集成电路制造</span>
128
+ <span class="list__meta">8 条记录 · 半导体</span>
129
+ </span>
130
+ <span class="list__value num list__value--down">−1.05%</span>
131
+ </li>
132
+ </ul>
133
+ <p class="cmp__why">列表是"扫"的不是"读"的:标题单行截断,完整名称放 <code>title</code>,悬停可看。行高一致,眼睛才能顺着往下扫。</p>
134
+ </div>
135
+ </div>
88
136
  </main>
89
137
 
90
138
  <script src="flip.js"></script>
@@ -31,6 +31,7 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.7
31
31
  .spacer { height: 55vh; }
32
32
 
33
33
  /* 让目录高亮有滚动空间 */
34
+ .toc-clip { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
34
35
  </style>
35
36
  </head>
36
37
  <body class="page page">
@@ -112,7 +113,39 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.7
112
113
  早期版本用的是它 + <code>rootMargin</code>,结果滚到两节之间时观察区里
113
114
  一个 section 都没有,高亮卡住不动 —— 现在改成纯计算,任何滚动位置都有答案。
114
115
  </p>
115
- </main>
116
+
117
+
118
+ <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
119
+ <!-- 目录项一律指向本页真实小节(`#mem` / `#sched` / `#sim`):假锚点
120
+ (`href="#x"`)点下去会跳到页面顶部,读者以为"目录坏了"。
121
+ 第一条故意写得长 —— 真实文档的长章节名就是这样,能不能单行是这条的判据。 -->
122
+ <div class="cmp">
123
+ <div class="cmp__pane cmp__bad">
124
+ <h3>❌ 常见写法:目录项允许换行</h3>
125
+ <nav class="toc" aria-label="示例目录" aria-hidden="true">
126
+ <p class="toc__title">本页</p>
127
+ <ul class="toc__list">
128
+ <li><a class="toc__link" href="#mem" tabindex="-1">存储器构成与 2026 年三季度的产能爬坡节奏</a></li>
129
+ <li><a class="toc__link" href="#sched" tabindex="-1">优先级与准入</a></li>
130
+ <li><a class="toc__link" href="#sim" tabindex="-1">把它跑起来</a></li>
131
+ </ul>
132
+ </nav>
133
+ <p class="cmp__why">一条目录折成两行,下面两条就都被推下去了 —— 目录的价值是"一眼看到全文结构",行高一变,结构感就没了。</p>
134
+ </div>
135
+ <div class="cmp__pane cmp__good">
136
+ <h3>✅ 本库:单行、超出省略</h3>
137
+ <nav class="toc" aria-label="示例目录">
138
+ <p class="toc__title">本页</p>
139
+ <ul class="toc__list">
140
+ <li><a class="toc__link toc-clip" href="#mem">存储器构成与 2026 年三季度的产能爬坡节奏</a></li>
141
+ <li><a class="toc__link" href="#sched">优先级与准入</a></li>
142
+ <li><a class="toc__link" href="#sim">把它跑起来</a></li>
143
+ </ul>
144
+ </nav>
145
+ <p class="cmp__why">目录项单行、超出省略:宁可截断,也要保住"三条等高"的节奏。读者扫的是结构,不是一个条目的全名。</p>
146
+ </div>
147
+ </div>
148
+ </main>
116
149
 
117
150
  </div>
118
151
 
@@ -184,13 +184,13 @@ grep -n '=>\|\bconst\b\|\blet\b' 03-patterns/overlay/overlay.js # 应无输出
184
184
  ## 反例
185
185
 
186
186
  - ❌ **toast 抢焦点** —— 通知不该打断当前操作
187
- - ❌ **用 toast 做需要确认的操作** —— 见上
188
- - ❌ **dialog 不困住焦点** —— 混乱
187
+ - ❌ **用 toast 做需要确认的操作** —— toast 里没有按钮、也拿不到焦点:用户要么没看见,要么看见了却点不到
188
+ - ❌ **dialog 不困住焦点** —— Tab 会跑到背后的页面上,读屏用户一路读下去,读到的是 dialog 外面的内容
189
189
  - ❌ **dialog 关闭后焦点不回去** —— 键盘用户要重新 Tab
190
190
  - ❌ **锁滚动不补偿滚动条宽度** —— 页面"闪"一下
191
191
  - ❌ **错误 toast 3 秒消失** —— 还没读完就走了
192
192
  - ❌ **危险操作初始焦点在"删除"上** —— 条件反射误触
193
193
  - ❌ **危险操作点遮罩即关闭** —— 手滑就完了
194
- - ❌ **dialog 大幅缩放/回弹** —— 廉价感
194
+ - ❌ **dialog 大幅缩放/回弹** —— 大面积位移让前庭敏感的用户不适,而且把注意力从内容移到了动画上
195
195
  - ❌ **backdrop 加 `backdrop-filter: blur`** —— 旧 WebView 掉帧
196
196
  - ❌ **用自定义 div 替代 `<dialog>` 且不需要自定义动画** —— 原生的 focus trap 更可靠
@@ -109,6 +109,20 @@ if (q > 0) this.page = Math.min(q, this.pages);
109
109
 
110
110
  `ai/components.json` 里 `pagination` 的 `behaviorDebt` 为空。
111
111
 
112
+ ## 反例
113
+
114
+ - ❌ **把 `role="navigation"` 直接放在 `pagination__list` 上** —— 会**覆盖 `ul` 的列表语义**,
115
+ 读屏不再报告"共 N 项"。要包在 `<nav aria-label>` 里
116
+ - ❌ **用 `aria-selected` 标当前页** —— 那是 tab / option / row 的语义;当前页用 `aria-current="page"`,
117
+ 用错了读屏会说"已选中",而用户以为自己只是停在第 3 页
118
+ - ❌ **客户端翻页不放 `pagination__live`** —— 页码变了却没有任何播报,读屏用户不知道"内容已经换了"。
119
+ 而且这个 live 区必须**常驻 DOM**:新插入的节点很多读屏不播
120
+ - ❌ **页码无限铺开** —— 槽位一多就撑破窄屏(USWDS 的上限是 7)。该用 `pagination__ellipsis` 收中间
121
+ - ❌ **没跳过页码也显示省略号** —— 用户会以为中间少了页,去点它,什么也不会发生
122
+ - ❌ **禁用态用纯透明表示** —— 半透明会让人以为"再点一下就行";本库是让它"退到背景里"
123
+
124
+ ---
125
+
112
126
  ## 门禁
113
127
 
114
128
  `05-audit/pagination-check.js`
@@ -30,6 +30,7 @@ th { font-size: var(--fs-xs); color: var(--text-secondary); font-weight: 500; }
30
30
  .row .v { font-family: var(--mono); font-variant-numeric: tabular-nums; }
31
31
  .live-echo { margin: var(--sp-3) 0 0; font-size: var(--fs-xs); color: var(--text-tertiary); }
32
32
  .live-echo b { color: var(--text-secondary); font-family: var(--mono); }
33
+ .pagination__list { flex-wrap: wrap; }
33
34
  </style>
34
35
  </head>
35
36
  <body class="page page">
@@ -118,6 +119,45 @@ th { font-size: var(--fs-xs); color: var(--text-secondary); font-weight: 500; }
118
119
  ⇒ <b>不产生横向溢出</b>。
119
120
  </p>
120
121
 
122
+
123
+
124
+ <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
125
+ <div class="cmp">
126
+ <div class="cmp__pane cmp__bad">
127
+ <h3>❌ 常见写法:页码全铺开</h3>
128
+ <nav aria-label="分页示例" aria-hidden="true">
129
+ <ul class="pagination__list">
130
+ <li class="pagination__item"><a class="pagination__link" href="?page=1" aria-label="第 1 页" tabindex="-1">1</a></li>
131
+ <li class="pagination__item"><a class="pagination__link" href="?page=2" aria-label="第 2 页" tabindex="-1">2</a></li>
132
+ <li class="pagination__item"><a class="pagination__link" href="?page=3" aria-label="第 3 页" tabindex="-1">3</a></li>
133
+ <li class="pagination__item"><a class="pagination__link" href="?page=4" aria-label="第 4 页" tabindex="-1">4</a></li>
134
+ <li class="pagination__item"><a class="pagination__link" href="?page=5" aria-label="第 5 页" tabindex="-1">5</a></li>
135
+ <li class="pagination__item"><a class="pagination__link" href="?page=6" aria-label="第 6 页" tabindex="-1">6</a></li>
136
+ <li class="pagination__item"><a class="pagination__link" href="?page=7" aria-label="第 7 页" tabindex="-1">7</a></li>
137
+ <li class="pagination__item"><a class="pagination__link" href="?page=8" aria-label="第 8 页" tabindex="-1">8</a></li>
138
+ <li class="pagination__item"><a class="pagination__link" href="?page=9" aria-label="第 9 页" tabindex="-1">9</a></li>
139
+ <li class="pagination__item"><a class="pagination__link" href="?page=10" aria-label="第 10 页" tabindex="-1">10</a></li>
140
+ <li class="pagination__item"><a class="pagination__link" href="?page=11" aria-label="第 11 页" tabindex="-1">11</a></li>
141
+ <li class="pagination__item"><a class="pagination__link" href="?page=12" aria-label="第 12 页" tabindex="-1">12</a></li>
142
+ </ul>
143
+ </nav>
144
+ <p class="cmp__why">12 个页码硬塞进一行,窄屏上要么换行、要么每个都挤到点不中。而 4~10 页其实是读者最不会去点的区间。</p>
145
+ </div>
146
+ <div class="cmp__pane cmp__good">
147
+ <h3>✅ 本库:7 个槽位封顶 + 省略号</h3>
148
+ <nav aria-label="分页示例">
149
+ <ul class="pagination__list">
150
+ <li class="pagination__item"><a class="pagination__link" href="?page=1" aria-label="第 1 页">1</a></li>
151
+ <li class="pagination__item"><a class="pagination__link" href="?page=2" aria-label="第 2 页">2</a></li>
152
+ <li class="pagination__item"><a class="pagination__link" href="?page=3" aria-label="第 3 页">3</a></li>
153
+ <li class="pagination__item"><span class="pagination__ellipsis">…</span></li>
154
+ <li class="pagination__item"><a class="pagination__link" href="?page=11" aria-label="第 11 页">11</a></li>
155
+ <li class="pagination__item"><a class="pagination__link" href="?page=12" aria-label="第 12 页">12</a></li>
156
+ </ul>
157
+ </nav>
158
+ <p class="cmp__why">USWDS 的上限是 7 个槽位:首尾两页 + 当前页邻域,中间用 <code>pagination__ellipsis</code> 收掉。省略号不是装饰,是"这里还有页"的承诺 —— 所以没跳过页码时不许显示它。</p>
159
+ </div>
160
+ </div>
121
161
  </main>
122
162
 
123
163
  <script src="pagination.js"></script>
@@ -89,3 +89,18 @@ CSS transition 被打断时是 **retarget**:从当前值继续插值,不重
89
89
  这条结论立即失效 —— 那时必须重新量,并且很可能需要守卫。
90
90
 
91
91
  ⚠️ 这属于**负面结论入档**:写下来是为了下次不要再"看到别人有就照抄一遍"。
92
+
93
+ ## 反例
94
+
95
+ - ❌ **引了脚本就指望内容被藏起来** —— 默认**不接管**:只有显式调 `enable()` 才会把目标设成 `opacity: 0`。
96
+ 隐藏是有代价的决定,必须由页面自己做出
97
+ - ❌ **藏了但没有兜底** —— 任何一步失败都会留下"看不见的内容",最坏是整页空白。
98
+ 所以隐藏**只在** `<html data-reveal-ready>` 存在时生效,没有它内容就是全可见
99
+ - ❌ **位移超过 8px** —— 内容"飞进来"会打断阅读节奏;本库的位移上限就是 8px
100
+ - ❌ **自己再写一套 keyframes** —— 缓动、时长、延迟会各写一套,最后没人能回答
101
+ "为什么这个方向和那个方向节奏不一样"。本库是**一条规则撑六种表现**
102
+ - ❌ **时长写裸数值** —— 只走令牌;`05-audit/motion.py` 会查裸时长与自编曲线
103
+ - ❌ **reduced-motion 时把动画一刀切归零** —— 降级是**分级**的:关掉位移不等于关掉一切,
104
+ 透明度的过渡仍然可以留
105
+
106
+ ---
@@ -22,6 +22,12 @@
22
22
  border: 1px solid var(--border-subtle); font-size: var(--fs-sm); }
23
23
  .rv-note { font-size: var(--fs-sm); color: var(--text-secondary); }
24
24
  .rv-spacer { height: 40px; }
25
+ .rv-card { padding: var(--sp-3); background: var(--surface-raised);
26
+ border: 1px solid var(--border-decor); border-radius: var(--r-sm); min-height: 64px; }
27
+ .rv-card__t { margin: 0 0 var(--sp-1); font-size: var(--fs-sm); font-weight: 500; }
28
+ .rv-card__d { margin: 0; font-size: var(--fs-xs); color: var(--text-secondary); }
29
+ /* 反例专用:真的藏起来,用来演示"没有兜底"的后果 */
30
+ .rv-hidden { opacity: 0; }
25
31
  </style>
26
32
  </head>
27
33
  <body class="page">
@@ -81,6 +87,24 @@
81
87
  ② 手动调 <code>Reveal.enable()</code>(本页就是这种,方便截图与演示)。<br>
82
88
  都不写 ⇒ 完全不接管:内容可见、位置不偏。<strong>这是刻意的默认值。</strong>
83
89
  </p>
90
+
91
+
92
+ <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
93
+ <div class="cmp">
94
+ <div class="cmp__pane cmp__bad">
95
+ <h3>❌ 常见写法:藏了但没兜底</h3>
96
+ <div class="rv-card rv-hidden" aria-hidden="true"></div>
97
+ <p class="cmp__why">内容真被 <code>opacity: 0</code> 藏着,而脚本没跑起来(被拦、报错、CSP)—— 这一块就永远是空的,读者不知道这里有东西。</p>
98
+ </div>
99
+ <div class="cmp__pane cmp__good">
100
+ <h3>✅ 本库:只有 <code>data-reveal-ready</code> 时才藏</h3>
101
+ <div class="rv-card" aria-hidden="true">
102
+ <p class="rv-card__t">2026 年三季度回顾</p>
103
+ <p class="rv-card__d">营收同比 +18%,毛利率 42.1%。</p>
104
+ </div>
105
+ <p class="cmp__why">隐藏只在 <code>&lt;html data-reveal-ready&gt;</code> 存在时生效:任何一步失败,内容就是全可见的。动画可以有代价,但代价不能是"看不见内容"。</p>
106
+ </div>
107
+ </div>
84
108
  </main>
85
109
 
86
110
  <script src="../../03-patterns/reveal/reveal.js"></script>
@@ -76,7 +76,7 @@ fetch(...).then(r => { clearTimeout(t); showResult(r); });
76
76
 
77
77
  - ❌ **empty 和 loading 用同一个样式** —— 用户分不清是"在加载"还是"真没有"
78
78
  - ❌ **empty 态不放动作按钮** —— 把用户卡死
79
- - ❌ **error 态没有重试出口** —— 死路
79
+ - ❌ **error 态没有重试出口** —— 用户唯一的动作只剩刷新页面,而刷新会把已经填好的内容一起丢掉
80
80
  - ❌ **结构未知时用骨架屏** —— 撒谎,且内容到位时跳动
81
81
  - ❌ **请求 <300ms 也显示骨架屏** —— 闪一下更糟
82
82
  - ❌ **把普通操作的成功做成整页 success** —— 成功是过渡态,用 button 的 `data-state="success"`(1.2s 自动消失)
@@ -235,6 +235,30 @@ Array.prototype.forEach.call(document.querySelectorAll('.seg button'), function
235
235
  });
236
236
 
237
237
  show('content');
238
- </script></main>
238
+ </script>
239
+
240
+ <h2 class="rule-bottom">对比:错写法 vs 本库写法</h2>
241
+ <div class="cmp">
242
+ <div class="cmp__pane cmp__bad">
243
+ <h3>❌ 常见写法:empty 态没有出口</h3>
244
+ <div class="state state--empty" aria-hidden="true">
245
+ <div class="state__icon">□</div>
246
+ <p class="state__title">这个时间范围下还没有记录</p>
247
+ <p class="state__desc">把范围放宽到 2023 年试试。</p>
248
+ </div>
249
+ <p class="cmp__why">说明写得很清楚,然后呢?用户读完只剩一个动作:刷新,或者退出。一条"没有数据"的提示不构成一个状态,它只是死路。</p>
250
+ </div>
251
+ <div class="cmp__pane cmp__good">
252
+ <h3>✅ 本库:empty 态必须带动作</h3>
253
+ <div class="state state--empty" aria-hidden="true">
254
+ <div class="state__icon">□</div>
255
+ <p class="state__title">这个时间范围下还没有记录</p>
256
+ <p class="state__desc">把范围放宽到 2023 年试试。</p>
257
+ <div class="state__action"><button class="btn btn--sm btn--primary" type="button" tabindex="-1">放宽到 2023</button></div>
258
+ </div>
259
+ <p class="cmp__why"><code>state__action</code> 是 empty 态的一部分:要么给一条能走的路,要么别显示这个空状态。</p>
260
+ </div>
261
+ </div>
262
+ </main>
239
263
  </body>
240
264
  </html>
@@ -82,3 +82,18 @@ demo 的面板里放了按钮,`Tab` 序列是
82
82
  - axe 0 违规 / 30 条通过
83
83
 
84
84
  判别力验证:破坏 roving(全部设成 `0`)⇒ **5 项立即被抓**。
85
+
86
+ ## 反例
87
+
88
+ - ❌ **每个 `tabs__tab` 都给 `tabindex="0"`** —— Tab 键要逐个穿过一整排;
89
+ roving tabindex 只留**一个** 0,其余 -1
90
+ - ❌ **`tabindex="0"` 只在初始化时设一次** —— 这是本组件最常见的 bug:用户切到第 3 个 tab 后,
91
+ 再按 Tab 又回到第 1 个。那个唯一的 0 必须**跟着选中项走**
92
+ - ❌ **禁用项也设 `tabindex="0"`** —— Tab 会停在不可用的项上,用户按了没反应,以为页面卡了
93
+ - ❌ **automatic 模式下 `preventDefault`** —— 会与 button 的默认行为打架,变成"按一次跳两格";
94
+ 不拦,交给默认行为
95
+ - ❌ **把 `tabs--manual` 当纯样式类用** —— 它是**行为开关**:`tabs.js` 读它决定方向键要不要切面板。
96
+ 只加类不改语义,用户会以为方向键坏了
97
+ - ❌ **`overflow-y` 不写 `hidden`** —— 一个轴设 `auto` 会让另一轴也自动化,选中态的 `border-bottom` 被裁掉
98
+
99
+ ---
@@ -90,6 +90,19 @@ JS 只切`data-state`,显隐与动画全交给 CSS。
90
90
 
91
91
  要接统一行为层,得先清这两条债。
92
92
 
93
+ ## 反例
94
+
95
+ - ❌ **把必须知道的信息只放在 tooltip 里** —— 触屏没有 hover,键盘用户也未必停留。
96
+ 判据很硬:tooltip 的文字**不能**与触发元素的可见文字/`aria-label` 重复 ——
97
+ 一旦重复,说明它承载了本该可见的信息
98
+ - ❌ **只做 hover、不做 focus** —— 键盘用户永远看不到(WCAG 1.4.13 要求 focus 也能出)
99
+ - ❌ **Esc 关不掉** —— WCAG 1.4.13 的硬性要求:hover/focus 出现的内容必须可以关掉
100
+ - ❌ **用 `display: none` 隐藏气泡** —— 读屏读不到;淡入用透明度
101
+ - ❌ **`role="tooltip"` 不给 `aria-label`** —— 光靠内容拿不到可访问名,读屏只会念"提示"(0.8.9 补的)
102
+ - ❌ **触屏只靠 hover 触发** —— 触屏上 hover 会"粘住"不褪;触屏必须走 JS 的 click / focus
103
+
104
+ ---
105
+
93
106
  ## 门禁
94
107
 
95
108
  `05-audit/tooltip-check.js`(契约 `gates` 里登记的唯一一道)——