agentcss 0.11.9 → 0.11.10

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.
@@ -65,6 +65,20 @@
65
65
 
66
66
  高度 24px(默认)· 字号 12px —— 达 WCAG 2.2 的 24×24 最小目标。
67
67
 
68
+
69
+ ## 反例
70
+
71
+ - ❌ **用 `badge--danger` 当按钮** —— badge 不可点;要可点必须换成 `<button>` / `<a>` 并加 `badge--clickable`,
72
+ `div` + `onClick` 对读屏是"没有角色的按钮"(`badge.css` 里写明了这一条)
73
+ - ❌ **只用颜色区分状态、不加文字** —— WCAG 1.4.1 要求形状或文字参与区分;
74
+ 纯色块在灰度打印和色觉障碍下不传递任何信息。所以本库是「淡底 + 深字」而不是「饱和底 + 白字」
75
+ - ❌ **用 badge 装数字角标** —— 那是另一类设计:数字一变化读屏未必会念,用户不知道"3 变成 4"是发生了什么事
76
+ - ❌ **给不可点的 badge 写 hover 过渡** —— 光标和过渡都在暗示"我能点",用户去点,什么也不会发生
77
+ - ❌ **列表里每一行都挂 `badge--solid-warning`** —— 饱和底在长列表里会盖过正文;`solid` 那三档只留给"这一行必须被看见"的少数情况
78
+
79
+ ---
80
+
81
+
68
82
  ## 验证
69
83
 
70
84
  - `component-contrast.py`:5 组字/底组合两套配色全过
@@ -125,7 +125,7 @@ hover 只改 **`transform`** 和 **`box-shadow`**,这两个属性不触发回
125
125
  ## 反例 —— 什么时候不该用
126
126
 
127
127
  - ❌ **一排卡片全用 `--elevated`** —— 阴影互相打架,整页发灰。列表用 `--flat`
128
- - ❌ **hover 抬起 8px 以上** —— 游戏感
128
+ - ❌ **hover 抬起 8px 以上** —— 鼠标扫过列表时整页都在晃,注意力从内容跑到动画上;悬停只需要"轻微浮起"这一点点反馈
129
129
  - ❌ **整卡套 `div onclick`** —— 键盘不可达,见上
130
130
  - ❌ **给链接本身再画焦点环** —— 会变成两个框
131
131
  - ❌ **用 JS 截断描述文字** —— 会留下半个词或半个字,用 `-webkit-line-clamp`
@@ -73,6 +73,21 @@ if (!list.id) list.id = 'combo-list-' + seq;
73
73
  |---|---|
74
74
  | `dismissable:manual` | 点外部关闭是手写的,**不是统一机制** |
75
75
 
76
+ ## 反例
77
+
78
+ - ❌ **标签只能点小叉删除、不支持 Backspace** —— 键盘用户删不掉已选项,
79
+ 这是标签输入的事实标准交互(`combobox.js` 里 Backspace 删最后一个是刻意实现的)
80
+ - ❌ **过滤结果不高亮命中片段** —— 用户看到的是"顺序乱了",而不是"因为匹配才留下";
81
+ `combobox.js` 高亮命中片段就是为了回答"为什么这条匹配"
82
+ - ❌ **把焦点环画在 `combo__input` 上** —— 环画在 `combo__field:focus-within` 上;
83
+ 两处都画会出现双层框
84
+ - ❌ **`combo__tag-del` 只做视觉上的小叉、不补命中区** —— 命中区要 44px;手指点不中,等于删不掉
85
+ - ❌ **禁用时只让输入框变灰、`combo__tag-del` 还能点** —— 看着能点,点下去要么没反应要么出事;
86
+ 禁用态要把删除按钮一起变灰
87
+ - ❌ **在自定义匹配函数里做异步过滤** —— 它会被逐项同步调用,返回 Promise 等同于"全部不匹配"
88
+
89
+ ---
90
+
76
91
  ## 门禁
77
92
 
78
93
  - `05-audit/combobox-check.js`
@@ -95,6 +95,17 @@ if (msg) {
95
95
  契约里 `behaviorDebt` 为空 —— 清除、校验、preset 接线都有门禁覆盖
96
96
  (`05-audit/date-range-check.js`)。
97
97
 
98
+ ## 反例
99
+
100
+ - ❌ **不给 `drange__clear`** —— 日期填错要能一键清空;没有它,用户得逐格删六个数字
101
+ - ❌ **错误态只改颜色、不加图标** —— WCAG 1.4.1;本库的 `drange__error` 是「边框 + 图标」,颜色只是辅助
102
+ - ❌ **禁用态用 `opacity`** —— 透明度会把文字对比度一起拉低,禁用不等于允许不可读(`date-range.css` 里改用中性色)
103
+ - ❌ **自建日历替代 `<input type="date">`** —— 移动端原生选择器是系统级的:读屏、键盘、本地化都免费;
104
+ 自建的那一套要全部重做,而且一定做得更差
105
+ - ❌ **三个预设按钮只做鼠标点击** —— `drange__preset` 必须能 Tab 到,快捷选择对键盘用户才是"快捷"
106
+
107
+ ---
108
+
98
109
  ## 门禁
99
110
 
100
111
  `05-audit/date-range-check.js`
@@ -77,4 +77,15 @@ Popover.current; // 当前开着的实例(没有则null)
77
77
  ## 动画
78
78
 
79
79
  尊重 `prefers-reduced-motion` —— 用户在系统里关了动效,
80
- 组件的过渡也要跟着关。
80
+ 组件的过渡也要跟着关。
81
+
82
+ ## 反例
83
+
84
+ - ❌ **给 popover 加 focus trap** —— 不做 trap 是刻意的:它没有"确认/取消"来放行,
85
+ trap 住了人出不去。焦点**进入**浮层(键盘用户要能直接操作)就够了
86
+ - ❌ **关闭态只用 `pointer-events: none`** —— Tab 仍能走进去,焦点会跑到看不见的地方;
87
+ 必须是 `inert`(`popover.js` 的 `open()` 里写明了这一条)
88
+ - ❌ **同时开两个 popover** —— 两个叠着,用户不知道该关哪个,也不知道点空白处关的是哪一个
89
+ - ❌ **把箭头塞进带 `overflow: hidden` 的容器** —— 箭头会被裁掉;所以本组件不用 overflow 方式隐藏
90
+
91
+ ---
@@ -75,6 +75,10 @@ role="progressbar" aria-busy="true" aria-label="正在处理"
75
75
  - ❌ **代替"还剩多久"** ⇒ 百分比给的是"比例",不是"时间"
76
76
  - ❌ **环形用在极小尺寸**(< 32px)⇒ 环太细,读不出进度
77
77
  - ❌ **在一个页面里同时出现 3 个以上进度条** ⇒ 用户不知道该看哪个
78
+ - ❌ **用 `display: none` 隐藏进度条** ⇒ 会留下 `::after` 参与合成的空盒,位置还占着;改宽度或改状态,别改可见性
79
+ - ❌ **进度到 100% 就自动变绿** ⇒ 颜色的语义是"结果"不是"数值",100% 也可能是失败;`progress--success` 按业务给
80
+ - ❌ **把超过 100 的值喂给 `progress-ring`** ⇒ 弧长按百分比算,超过就是绕圈或错位
81
+ - ❌ **用 `progress-steps` 表达连续百分比** ⇒ 它按离散阶段设计(`progress-steps__seg--done` / `--current`),用户会以为"每段就是 25%"
78
82
 
79
83
  ## 相关
80
84
 
@@ -90,6 +90,18 @@ const sel = Select.create(元素, { multiple: false, value: 'b', label: '选项'
90
90
 
91
91
  接统一行为层前,别当`stable` 用。
92
92
 
93
+ ## 反例
94
+
95
+ - ❌ **不实现类型搜索** —— 原生 select 支持连续打字跳转;自建却丢掉这个,是**功能倒退**。
96
+ `select.js` 里做了 500ms 缓冲(超时清空,否则 "abc" 永远搜不到)
97
+ - ❌ **把焦点放进 `select__list`** —— 焦点必须常驻 `select__btn`,"当前在第几项"靠
98
+ `aria-activedescendant` 表达;焦点进了 list,读屏会开始念"列表"而不是"选项"
99
+ - ❌ **`select__opt` 命中区不足 44px** —— 本库按 AAA 要求做命中区;选项挤在一起,触屏必点错
100
+ - ❌ **把 `select__value--placeholder` 当已选值** —— 占位不是值:表单提交时它是空的,
101
+ 而用户以为自己选了
102
+
103
+ ---
104
+
93
105
  ## 门禁
94
106
 
95
107
  `05-audit/select-check.js` —— 覆盖键盘契约与 ARIA 属性同步。
@@ -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
@@ -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
+ ---
@@ -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`
@@ -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`
@@ -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`
@@ -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
+ ---
@@ -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 自动消失)
@@ -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` 里登记的唯一一道)——
@@ -89,6 +89,22 @@ const t = Tree.create(元素, { label: '目录' });
89
89
 
90
90
  `ai/components.json` 里 `tree` 的 `behaviorDebt` 为空。
91
91
 
92
+ ## 反例
93
+
94
+ - ❌ **↑↓ 只移动焦点、←→ 只展开不移动焦点** —— APG 是**两段式**:↑↓ 移动焦点,←→ 展开/收起
95
+ **并带着焦点走**。只做一半,键盘用户会觉得"按了没反应"
96
+ - ❌ **每个 `tree__item` 都 `tabindex="0"`** —— 整棵树只占**一个** Tab 位(roving tabindex);
97
+ 否则穿过 200 个节点要按 200 次 Tab
98
+ - ❌ **只靠旋转箭头表示展开/收起** —— 必须有**三态形状线索**(`tree__marker`):
99
+ 旋转只能看出"变过",看不出"现在是哪一态"
100
+ - ❌ **RTL 下收起箭头不翻向** —— 阿拉伯语用户看到的是一个指错方向的符号;
101
+ `tree.css` 里那条 `[dir="rtl"]` 就是为此
102
+ - ❌ **用 `margin` 做层级缩进** —— 缩进用 padding:`margin` 会把点击区一起缩窄,深层的节点点不中
103
+ - ❌ **把 ←→ 交给"左右移动"处理** —— 树里 ←→ 是展开/收起(APG 两段式),
104
+ 当成左右移动会被方向键库先吃掉
105
+
106
+ ---
107
+
92
108
  ## 门禁
93
109
 
94
110
  `05-audit/tree-check.js`
@@ -78,3 +78,19 @@ python -m http.server 8000 # 仓库根
78
78
 
79
79
  页面里的实验数据**全部是演示用示例数据**,不是真实实验结果。
80
80
  留出这条是因为:「长得像真实数据」的示例最容易被误引用。
81
+
82
+ ## 反例
83
+
84
+ - ❌ **栅格列写 `1fr` 而不是 `minmax(0, 1fr)`** —— 长表格、长代码会把列撑开,整页横向溢出。
85
+ 这是 Grid 的经典陷阱,`analysis-report.css` 里两处都写成 `minmax(0, 1fr)`
86
+ - ❌ **先做桌面并排、再用媒体查询缩回单栏** —— 默认就该是单栏,并排是宽屏**才发生**的事。
87
+ 反过来做,窄屏上得到的是"缩小的桌面版"
88
+ - ❌ **`position: sticky` 的辅助栏不配 `align-self: start`** —— 在 grid 里 sticky 不生效,
89
+ 那一栏会跟着页面一起滚走
90
+ - ❌ **再另造一个"侧栏卡片"** —— 直接复用 `card.css`;重复造会在换主题时多一个不一致的源头
91
+ - ❌ **用 inline style 写字号和颜色** —— inline 的优先级高于一切样式表(INVARIANT I-4),
92
+ 换肤时改不动,只能一路 `!important`
93
+ - ❌ **把组件 CSS 引在骨架之前** —— 顺序反了就要靠 `!important` 抢优先级;
94
+ "骨架在组件之前"这条是有门禁守着的
95
+
96
+ ---
@@ -43,3 +43,14 @@ python -m http.server 8000
43
43
 
44
44
  页面内所有数值均为**示例数据**,不构成任何业务或科研结论。
45
45
  留这句是因为「长得像真实数据」的示例最容易被误引用。
46
+
47
+ ## 反例
48
+
49
+ - ❌ **把这四个当成"同一种图表的四种皮肤"** —— 它们共享令牌与容器,但**不共享**坐标规则与数据格式,
50
+ 是四个用途。换皮肤换不来正确的表达
51
+ - ❌ **用 `table` 回答"涨还是跌"** —— 表格给的是具体值,趋势要看形状;
52
+ 那是 `sparkline` 的活,用表看趋势要一行行读数字
53
+ - ❌ **用完整坐标系画三五个点** —— 坐标轴、刻度、网格是为"值落在哪、误差多大"服务的;
54
+ 几个点的走向用迷你趋势图就够,画坐标系反而让人以为有精度
55
+
56
+ ---
@@ -91,3 +91,16 @@ python 05-audit/no-data-gate.py # 无数据态三条规则
91
91
  一旦把某个业务的字段写进通用库,库的边界就没了。
92
92
  - **不做图表**:指标卡只放数字。要图请用 `09-assets/` 的图表组件。
93
93
  - **不做标签页 / 折叠**:那是 `03-patterns/` 的组件,这里只给骨架。
94
+
95
+ ## 反例
96
+
97
+ - ❌ **缺的字段写成 `0`** —— `0` 是一个真实的值,用户会拿它去算同比。缺就是缺:
98
+ 写「未披露」+ 弱化色
99
+ - ❌ **缺的字段直接把节点删掉** —— 三个变体的 DOM 结构必须完全一致(节点数量与嵌套),
100
+ 删节点会让同一类页面越改越不像,最后没人能说清"这页该长什么样"
101
+ - ❌ **无数据时隐藏整块** —— 不隐藏、不冒充零,**必须写清原因**。
102
+ 空白让人以为是加载失败,反而会去刷新
103
+ - ❌ **三个变体各写一套类名** —— 允许变的是文案、数值、说明块;类名与语义标签不许变
104
+ - ❌ **换肤时改类名** —— 色对挂在 `section` 上,改类名等于换了一套结构,之后没法对比
105
+
106
+ ---
@@ -174,3 +174,16 @@ bash 05-audit/check-all.sh # 全量
174
174
  全部读**计算值/几何**而不是 CSS 文本 —— 因为"写了 ≠ 生效"在本系统上翻过车:
175
175
  `.rep h2 / .rep h3`(特异度 0,1,1)曾经压过 `.rep__title`(0,1,0),
176
176
  使 `--rep-title-fs` 静默失效、三套版式的标题被钉死在同一个字号。
177
+
178
+ ## 反例
179
+
180
+ - ❌ **拿它做分页 PDF / 印刷分页** —— 页边距、页眉页脚、页码、跨页控制**一律没有**,
181
+ 也不假装有。需要印刷排版,用专门的工具链
182
+ - ❌ **靠改选择器换版式** —— 换版式只改那六个变量(`--rep-measure` / `--rep-body-fs` /
183
+ `--rep-title-fs` / `--rep-lead-fs` / `--rep-body-lh` / `--rep-sec-num`),不改选择器
184
+ - ❌ **再发明第七种字体角色** —— 六个角色(眉题 / 元信息 / 图表说明 / 来源 / 编者注 / 风险披露)
185
+ 是**不多不少**的;多一种,读者就要重新学一遍"这行字是什么身份"
186
+ - ❌ **把它当 CMS 或模板引擎** —— 没有内容管理、没有模板继承、没有构建步骤,只有 CSS
187
+ - ❌ **自己写表格样式** —— 复用 `table.css`;两份表格样式在换主题时必然分叉
188
+
189
+ ---
@@ -101,3 +101,19 @@ var COLS = [
101
101
  python -m http.server 8000
102
102
  # http://127.0.0.1:8000/04-recipes/table/demo.html
103
103
  ```
104
+
105
+ ## 反例
106
+
107
+ - ❌ **靠数值大小猜语义** —— 不能用"小于 1 就乘 100"。`0.25` 是比例还是普通测量值,
108
+ 只能由列定义的 `format` 说
109
+ - ❌ **给"量"的正数加 `+`** —— 营收、成本、毛利率这类量没有 `+`;只有 `signed-percent`
110
+ 这类"变化量"才加。满屏 `+` 会让读者以为每个数都在变
111
+ - ❌ **负数用连字符 `-`** —— 用真减号 `−`;连字符在等宽数字里对不齐,也容易被当成项目符号
112
+ - ❌ **排序状态放在按钮上** —— 要放 `<th aria-sort="ascending|descending">`;
113
+ 放在按钮上读屏不会报告"这一列正按什么排序"
114
+ - ❌ **正负只靠红绿** —— 颜色之外必须有 `+` / `−` 符号(WCAG 1.4.1)
115
+ - ❌ **粘性表头配 `border-collapse: collapse`** —— `separate` 是 sticky 生效的前提,
116
+ 用 collapse 表头不会粘住
117
+ - ❌ **拿它当虚拟滚动表格用** —— 本组件不做虚拟滚动;超过 200 行请配分页
118
+
119
+ ---
package/CHANGELOG.md CHANGED
@@ -16,6 +16,28 @@
16
16
 
17
17
  (无)
18
18
 
19
+ ## [0.11.10] - 2026-10-29
20
+
21
+ ### Added
22
+
23
+ - **每个组件现在都写清了「什么时候不该用」**。此前 33 个组件里只有 11 个有反例章,
24
+ 其余只写"✅ 这么用"—— 一份只写正例的文档,读者学不到边界。新增 20 章、112 条
25
+ (全库现有 195 条),每条的依据都能在本库源码里指到具体位置:
26
+ `button.css` 的一条注释、`combobox.js` 的一个分支、某道门禁守的一条规则。
27
+ - `05-audit/do-dont-gate.py` —— 反例章的门禁。它**不判"理由对不对"**(那是判断题),
28
+ 只判两件机器能抓的事:章节在、条目够;以及**反例里提到的本库标识符必须真实存在**
29
+ (修饰符类名 / CSS 变量 / 文件路径)。编造的反例恰恰会在这里露馅:一个人可以凭想象
30
+ 写出流畅的理由,但写不出一个真实存在的类名。9 条自检,其中 5 条必须红。
31
+
32
+ ### Changed
33
+
34
+ - 章节标题按**语义**识别,不按字面:`## 反例` / `## 两个反例` / `## 反模式` /
35
+ `## 何时**不该**用` 都算。要求统一标题的后果,是让人为了过门禁去改文案。
36
+ - 已有章节里 6 条"只有结论、没有理由"的条目补全了理由
37
+ (`overlay` 的「见上」「混乱」「廉价感」、`states` 的「死路」、`card` 的「游戏感」)。
38
+ - `progress` 新增的四条并入原有的「何时不该用」,没有另开一章 ——
39
+ 同一个意思写两遍,读者会在两处看到两套说法。
40
+
19
41
  ## [0.11.9] - 2026-10-29
20
42
 
21
43
  **把「我们说做到的」和「能证明做到的」对齐**。这一版没有新组件,改的全是
package/CONTRIBUTING.md CHANGED
@@ -8,7 +8,7 @@ npm run gate # 全量:含浏览器类
8
8
  npm run gate:fast # 只跑静态层,不用装浏览器
9
9
  ```
10
10
 
11
- 全量是 **190 道门禁**(本机约 12 分钟)。
11
+ 全量是 **192 道门禁**(本机约 12 分钟)。
12
12
 
13
13
  环境依赖只有一套(Node + Chromium),缺了对应的门禁会**跳过**(跳过不算通过):
14
14
 
@@ -105,6 +105,12 @@ fix: <一句话说清改了什么、为什么>
105
105
  两者的差别就是问题的形状(这正是 E41 的发现方式)。
106
106
  5. **豁免要写清理由,并列举式。** 「这份代码的使用者控制不了它」是唯一的豁免理由;
107
107
  新增豁免必须显式列出来,否则会被抓到 —— 宁可多报一次,也不要默默放过。
108
+ 6. **新组件必须写「反例」章。** 至少 3 条,每条说清「不该怎么用 + 为什么」。
109
+ 反例里提到的类名、CSS 变量、文件名必须**真实存在** —— `do-dont` 门禁会逐个核。
110
+ 这条不是为了凑数:只写"✅ 这么用"的文档,读者学不到边界;
111
+ 而边界恰恰是组件库最该交付的东西。标题怎么写都行
112
+ (`## 反例` / `## 反模式` / `## 何时不该用` 都认),内容不能省。
113
+ **不要为了过门禁改文案**:判据认的是"这段话在讲不该怎么用",不是你用哪个标题。
108
114
 
109
115
  ## 提 PR 之前
110
116
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
3
  "generatedBy": "05-audit/gen-ai-contract.py",
4
- "libraryVersion": "0.11.9",
4
+ "libraryVersion": "0.11.10",
5
5
  "$comment": "机器可读的组件契约。slots = BEM 元素名(在本库里元素名就是插槽名),states.enum = 组件会在 DOM 上写出的 data-* 取值,states.legacyClass = 仍未收敛到 data 属性的历史状态类(INVARIANT I-8 未达标)。",
6
6
  "maturityCriteria": {
7
7
  "alpha": "缺 demo.html 或缺 05-audit 下的专属门禁 ⇒ 还没被机器验证过,不要用",
package/index.html CHANGED
@@ -92,7 +92,7 @@ li a:active { background: var(--surface-sunken); }
92
92
  </p>
93
93
  </main>
94
94
  <footer class="wrap">
95
- <p class="note">0.11.9 · 零第三方运行时依赖</p>
95
+ <p class="note">0.11.10 · 零第三方运行时依赖</p>
96
96
  </footer>
97
97
  </body>
98
98
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcss",
3
- "version": "0.11.9",
3
+ "version": "0.11.10",
4
4
  "description": "移动 Web 组件库:CSS + 可选行为脚本,零第三方运行时依赖。样式用 bundler 引入,交互行为用 script 标签引入。",
5
5
  "keywords": [
6
6
  "css",