agentcss 0.0.0-stage → 0.11.3

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 (274) hide show
  1. package/01-tokens/README.md +236 -0
  2. package/01-tokens/behavior/dismissable.js +132 -0
  3. package/01-tokens/behavior/emit.js +76 -0
  4. package/01-tokens/behavior/focus-return.js +81 -0
  5. package/01-tokens/behavior/roving.js +152 -0
  6. package/01-tokens/behavior/typeahead.js +124 -0
  7. package/01-tokens/focus-ring-demo.html +133 -0
  8. package/01-tokens/focus-ring.css +102 -0
  9. package/01-tokens/page.css +244 -0
  10. package/01-tokens/polyfill.js +116 -0
  11. package/01-tokens/responsive-demo.html +171 -0
  12. package/01-tokens/scrollbar-demo.html +194 -0
  13. package/01-tokens/scrollbar.css +179 -0
  14. package/01-tokens/theme-toggle.js +103 -0
  15. package/01-tokens/tokens.css +972 -0
  16. package/01-tokens/typography.css +156 -0
  17. package/02-primitives/badge/README.md +71 -0
  18. package/02-primitives/badge/badge.css +141 -0
  19. package/02-primitives/badge/demo.html +101 -0
  20. package/02-primitives/button/README.md +99 -0
  21. package/02-primitives/button/button.css +353 -0
  22. package/02-primitives/button/demo.html +200 -0
  23. package/02-primitives/card/README.md +145 -0
  24. package/02-primitives/card/card.css +246 -0
  25. package/02-primitives/card/demo.html +170 -0
  26. package/02-primitives/choice/README.md +161 -0
  27. package/02-primitives/choice/choice.css +395 -0
  28. package/02-primitives/choice/demo.html +277 -0
  29. package/02-primitives/combobox/README.md +79 -0
  30. package/02-primitives/combobox/combobox.css +223 -0
  31. package/02-primitives/combobox/combobox.js +485 -0
  32. package/02-primitives/combobox/demo.html +109 -0
  33. package/02-primitives/date-range/README.md +100 -0
  34. package/02-primitives/date-range/date-range.css +206 -0
  35. package/02-primitives/date-range/date-range.js +301 -0
  36. package/02-primitives/date-range/demo.html +104 -0
  37. package/02-primitives/input/README.md +170 -0
  38. package/02-primitives/input/demo.html +246 -0
  39. package/02-primitives/input/input.css +239 -0
  40. package/02-primitives/popover/README.md +80 -0
  41. package/02-primitives/popover/demo.html +150 -0
  42. package/02-primitives/popover/popover.css +187 -0
  43. package/02-primitives/popover/popover.js +331 -0
  44. package/02-primitives/progress/README.md +82 -0
  45. package/02-primitives/progress/demo.html +154 -0
  46. package/02-primitives/progress/progress.css +172 -0
  47. package/02-primitives/select/README.md +95 -0
  48. package/02-primitives/select/demo.html +121 -0
  49. package/02-primitives/select/select.css +151 -0
  50. package/02-primitives/select/select.js +549 -0
  51. package/02-primitives/separator/README.md +63 -0
  52. package/02-primitives/separator/demo.html +84 -0
  53. package/02-primitives/separator/separator.css +73 -0
  54. package/02-primitives/skeleton/README.md +69 -0
  55. package/02-primitives/skeleton/demo.html +110 -0
  56. package/02-primitives/skeleton/skeleton.css +96 -0
  57. package/02-primitives/switch/README.md +65 -0
  58. package/02-primitives/switch/demo.html +115 -0
  59. package/02-primitives/switch/switch.css +370 -0
  60. package/03-patterns/accordion/README.md +121 -0
  61. package/03-patterns/accordion/accordion.css +453 -0
  62. package/03-patterns/accordion/accordion.js +369 -0
  63. package/03-patterns/accordion/demo.html +223 -0
  64. package/03-patterns/content/README.md +103 -0
  65. package/03-patterns/content/content.css +286 -0
  66. package/03-patterns/content/demo.html +88 -0
  67. package/03-patterns/drawer/README.md +79 -0
  68. package/03-patterns/drawer/demo.html +101 -0
  69. package/03-patterns/drawer/drawer.css +129 -0
  70. package/03-patterns/drawer/drawer.js +258 -0
  71. package/03-patterns/dropdown/README.md +78 -0
  72. package/03-patterns/dropdown/demo.html +101 -0
  73. package/03-patterns/dropdown/dropdown.css +135 -0
  74. package/03-patterns/dropdown/dropdown.js +451 -0
  75. package/03-patterns/form-validation/README.md +147 -0
  76. package/03-patterns/form-validation/demo.html +280 -0
  77. package/03-patterns/form-validation/form-validation.css +263 -0
  78. package/03-patterns/list/README.md +150 -0
  79. package/03-patterns/list/demo.html +225 -0
  80. package/03-patterns/list/flip.js +201 -0
  81. package/03-patterns/list/list.css +162 -0
  82. package/03-patterns/nav/README.md +225 -0
  83. package/03-patterns/nav/demo.html +131 -0
  84. package/03-patterns/nav/nav.css +314 -0
  85. package/03-patterns/nav/nav.js +430 -0
  86. package/03-patterns/nav/toc.js +111 -0
  87. package/03-patterns/overlay/README.md +196 -0
  88. package/03-patterns/overlay/demo.html +241 -0
  89. package/03-patterns/overlay/overlay.css +319 -0
  90. package/03-patterns/overlay/overlay.js +563 -0
  91. package/03-patterns/pagination/README.md +114 -0
  92. package/03-patterns/pagination/demo.html +165 -0
  93. package/03-patterns/pagination/pagination.css +195 -0
  94. package/03-patterns/pagination/pagination.js +395 -0
  95. package/03-patterns/reveal/README.md +91 -0
  96. package/03-patterns/reveal/demo.html +106 -0
  97. package/03-patterns/reveal/reveal.css +93 -0
  98. package/03-patterns/reveal/reveal.js +172 -0
  99. package/03-patterns/states/README.md +141 -0
  100. package/03-patterns/states/demo.html +240 -0
  101. package/03-patterns/states/states.css +252 -0
  102. package/03-patterns/tabs/README.md +84 -0
  103. package/03-patterns/tabs/demo.html +139 -0
  104. package/03-patterns/tabs/tabs.css +172 -0
  105. package/03-patterns/tabs/tabs.js +307 -0
  106. package/03-patterns/tooltip/README.md +96 -0
  107. package/03-patterns/tooltip/demo.html +78 -0
  108. package/03-patterns/tooltip/tooltip.css +89 -0
  109. package/03-patterns/tooltip/tooltip.js +288 -0
  110. package/03-patterns/tree/README.md +94 -0
  111. package/03-patterns/tree/demo.html +138 -0
  112. package/03-patterns/tree/tree.css +113 -0
  113. package/03-patterns/tree/tree.js +456 -0
  114. package/04-recipes/analysis-report/README.md +80 -0
  115. package/04-recipes/analysis-report/analysis-report.css +187 -0
  116. package/04-recipes/analysis-report/demo.html +274 -0
  117. package/04-recipes/data-showcase/README.md +45 -0
  118. package/04-recipes/data-showcase/demo.html +247 -0
  119. package/04-recipes/detail-page/README.md +93 -0
  120. package/04-recipes/detail-page/demo.html +55 -0
  121. package/04-recipes/detail-page/detail-page.css +190 -0
  122. package/04-recipes/detail-page/full.html +60 -0
  123. package/04-recipes/detail-page/null.html +60 -0
  124. package/04-recipes/detail-page/partial.html +60 -0
  125. package/04-recipes/detail-page/verify-skeleton.py +19 -0
  126. package/04-recipes/longform/README.md +93 -0
  127. package/04-recipes/longform/agc-memory.svg +4 -0
  128. package/04-recipes/longform/content.html +610 -0
  129. package/04-recipes/longform/content.md +51 -0
  130. package/04-recipes/longform/longform.html +627 -0
  131. package/04-recipes/report/README.md +176 -0
  132. package/04-recipes/report/demo.html +355 -0
  133. package/04-recipes/report/report.css +364 -0
  134. package/04-recipes/table/README.md +103 -0
  135. package/04-recipes/table/demo.html +455 -0
  136. package/04-recipes/table/table.css +372 -0
  137. package/04-recipes/tierA-tokens.css +189 -0
  138. package/09-assets/README.md +74 -0
  139. package/09-assets/bar/README.md +94 -0
  140. package/09-assets/bar/bar.css +166 -0
  141. package/09-assets/bar/bar.js +320 -0
  142. package/09-assets/bar/demo.html +176 -0
  143. package/09-assets/echarts-adapter/demo.html +192 -0
  144. package/09-assets/echarts-adapter/echarts-adapter.js +277 -0
  145. package/09-assets/model-viewer/README.md +155 -0
  146. package/09-assets/model-viewer/demo.html +105 -0
  147. package/09-assets/model-viewer/model-viewer.css +143 -0
  148. package/09-assets/model-viewer/model-viewer.js +423 -0
  149. package/09-assets/scientific-plot/README.md +94 -0
  150. package/09-assets/scientific-plot/demo.html +252 -0
  151. package/09-assets/scientific-plot/scientific-plot.css +141 -0
  152. package/09-assets/scientific-plot/scientific-plot.js +610 -0
  153. package/09-assets/sparkline/README.md +153 -0
  154. package/09-assets/sparkline/demo.html +163 -0
  155. package/09-assets/sparkline/sparkline.css +158 -0
  156. package/09-assets/sparkline/sparkline.js +508 -0
  157. package/API.md +332 -0
  158. package/CHANGELOG.md +3138 -0
  159. package/CODE_OF_CONDUCT.md +64 -0
  160. package/CONTRIBUTING.md +89 -0
  161. package/ERRORS.md +1658 -0
  162. package/LICENSE +21 -0
  163. package/README.md +497 -2
  164. package/SECURITY.md +72 -0
  165. package/START-HERE.md +164 -0
  166. package/SUPPORT.md +109 -0
  167. package/adapters/presentation/README.md +113 -0
  168. package/adapters/presentation/check.js +101 -0
  169. package/adapters/presentation/deck.schema.json +74 -0
  170. package/adapters/presentation/to-doc.js +138 -0
  171. package/ai/START-HERE.md +104 -0
  172. package/ai/behaviors.json +380 -0
  173. package/ai/behaviors.schema.json +61 -0
  174. package/ai/capabilities.json +56 -0
  175. package/ai/cli.js +387 -0
  176. package/ai/components.json +1944 -0
  177. package/ai/components.meta.json +168 -0
  178. package/ai/components.schema.json +220 -0
  179. package/ai/contract.schema.json +147 -0
  180. package/ai/deprecations.json +55 -0
  181. package/ai/deprecations.schema.json +85 -0
  182. package/ai/motion-policy.json +22 -0
  183. package/ai/patch.js +323 -0
  184. package/ai/patch.schema.json +63 -0
  185. package/ai/profiles.json +92 -0
  186. package/ai/tokens.audience.json +55 -0
  187. package/ai/tokens.audience.schema.json +73 -0
  188. package/ai/tokens.json +762 -0
  189. package/ai/tokens.schema.json +58 -0
  190. package/ai/tokens.tree.json +752 -0
  191. package/ai/tokens.tree.schema.json +56 -0
  192. package/ai/validate.js +383 -0
  193. package/dist/01-tokens/behavior/dismissable.js +60 -0
  194. package/dist/01-tokens/behavior/emit.js +21 -0
  195. package/dist/01-tokens/behavior/focus-return.js +31 -0
  196. package/dist/01-tokens/behavior/roving.js +77 -0
  197. package/dist/01-tokens/behavior/typeahead.js +61 -0
  198. package/dist/01-tokens/focus-ring.css +1 -0
  199. package/dist/01-tokens/page.css +1 -0
  200. package/dist/01-tokens/polyfill.js +78 -0
  201. package/dist/01-tokens/scrollbar.css +1 -0
  202. package/dist/01-tokens/theme-toggle.js +61 -0
  203. package/dist/01-tokens/tokens.css +1 -0
  204. package/dist/01-tokens/typography.css +1 -0
  205. package/dist/02-primitives/badge/badge.css +1 -0
  206. package/dist/02-primitives/button/button.css +1 -0
  207. package/dist/02-primitives/card/card.css +1 -0
  208. package/dist/02-primitives/choice/choice.css +1 -0
  209. package/dist/02-primitives/combobox/combobox.css +1 -0
  210. package/dist/02-primitives/combobox/combobox.js +439 -0
  211. package/dist/02-primitives/date-range/date-range.css +1 -0
  212. package/dist/02-primitives/date-range/date-range.js +267 -0
  213. package/dist/02-primitives/input/input.css +1 -0
  214. package/dist/02-primitives/popover/popover.css +1 -0
  215. package/dist/02-primitives/popover/popover.js +257 -0
  216. package/dist/02-primitives/progress/progress.css +1 -0
  217. package/dist/02-primitives/select/select.css +1 -0
  218. package/dist/02-primitives/select/select.js +449 -0
  219. package/dist/02-primitives/separator/separator.css +1 -0
  220. package/dist/02-primitives/skeleton/skeleton.css +1 -0
  221. package/dist/02-primitives/switch/switch.css +1 -0
  222. package/dist/03-patterns/accordion/accordion.css +1 -0
  223. package/dist/03-patterns/accordion/accordion.js +219 -0
  224. package/dist/03-patterns/content/content.css +1 -0
  225. package/dist/03-patterns/drawer/drawer.css +1 -0
  226. package/dist/03-patterns/drawer/drawer.js +229 -0
  227. package/dist/03-patterns/dropdown/dropdown.css +1 -0
  228. package/dist/03-patterns/dropdown/dropdown.js +365 -0
  229. package/dist/03-patterns/form-validation/form-validation.css +1 -0
  230. package/dist/03-patterns/list/flip.js +117 -0
  231. package/dist/03-patterns/list/list.css +1 -0
  232. package/dist/03-patterns/nav/nav.css +1 -0
  233. package/dist/03-patterns/nav/nav.js +322 -0
  234. package/dist/03-patterns/nav/toc.js +76 -0
  235. package/dist/03-patterns/overlay/overlay.css +1 -0
  236. package/dist/03-patterns/overlay/overlay.js +398 -0
  237. package/dist/03-patterns/pagination/pagination.css +1 -0
  238. package/dist/03-patterns/pagination/pagination.js +273 -0
  239. package/dist/03-patterns/reveal/reveal.css +1 -0
  240. package/dist/03-patterns/reveal/reveal.js +138 -0
  241. package/dist/03-patterns/states/states.css +1 -0
  242. package/dist/03-patterns/tabs/tabs.css +1 -0
  243. package/dist/03-patterns/tabs/tabs.js +242 -0
  244. package/dist/03-patterns/tooltip/tooltip.css +1 -0
  245. package/dist/03-patterns/tooltip/tooltip.js +213 -0
  246. package/dist/03-patterns/tree/tree.css +1 -0
  247. package/dist/03-patterns/tree/tree.js +383 -0
  248. package/dist/04-recipes/analysis-report/analysis-report.css +1 -0
  249. package/dist/04-recipes/detail-page/detail-page.css +1 -0
  250. package/dist/04-recipes/report/report.css +1 -0
  251. package/dist/04-recipes/table/table.css +1 -0
  252. package/dist/04-recipes/tierA-tokens.css +1 -0
  253. package/dist/09-assets/bar/bar.css +1 -0
  254. package/dist/09-assets/bar/bar.js +222 -0
  255. package/dist/09-assets/echarts-adapter/echarts-adapter.js +185 -0
  256. package/dist/09-assets/model-viewer/model-viewer.css +1 -0
  257. package/dist/09-assets/model-viewer/model-viewer.js +320 -0
  258. package/dist/09-assets/scientific-plot/scientific-plot.css +1 -0
  259. package/dist/09-assets/scientific-plot/scientific-plot.js +568 -0
  260. package/dist/09-assets/sparkline/sparkline.css +1 -0
  261. package/dist/09-assets/sparkline/sparkline.js +265 -0
  262. package/dist/COSTS.md +56 -0
  263. package/docs/AI-CONTRACT.md +197 -0
  264. package/docs/BACKLOG.md +127 -0
  265. package/docs/BENCHMARK.md +252 -0
  266. package/docs/GUIDANCE.md +154 -0
  267. package/docs/INVARIANT.md +372 -0
  268. package/docs/LAYER-BASELINE.md +158 -0
  269. package/docs/RELEASING.md +311 -0
  270. package/docs/RFC-THEME-SCOPE.md +141 -0
  271. package/docs/ROADMAP.md +182 -0
  272. package/index.html +82 -0
  273. package/package.json +107 -4
  274. package/types/index.d.ts +310 -0
@@ -0,0 +1,372 @@
1
+ # 不变式(Invariant)
2
+
3
+ > **这一层里写的不是「给 AI 定的规矩」,是「客观事实」。**
4
+ > 因此它**不会**因为模型变强而失效,也不需要"以后放宽"。
5
+
6
+ ---
7
+
8
+ ## 🔴 先读这条:写在这里,**不等于**它就是事实
9
+
10
+ **位置不代表正确性。** 一条内容进了这份文件,只说明写下它的那一刻,
11
+ 我们认为它经得起下面这一问("无限强的模型成立吗")。
12
+
13
+ 判据就两条,任一条不成立 ⇒ 改它,或把它挪到它该去的层:
14
+
15
+ 1. 它描述的是 **CSS / DOM / API 的机制**,还是**某一类体系的做法**?只有前者属于这层。
16
+ 后者要么有自己的作用域(写清楚是哪类体系),要么属于 Contract / Guidance。
17
+ 2. 它的**作用域**写清楚了吗?「在某类结构里成立」必须写明是哪类。
18
+ 不写 ⇒ 会被读成普遍禁令;而大多数被抱怨"这条太绝对"的内容,
19
+ 缺的从来不是结论,是作用域。
20
+
21
+ 最近一次复核据此处理了四条:事实部分保留,把推论中"对所有情况都成立"的那部分
22
+ 改成**带作用域**的说明(改动痕迹见各自的「作用域 / ⚠️」段落)。
23
+
24
+ ⇒ 发现这里写错了,**就地改**。「它在 INVARIANT 里」不是不去核的理由 ——
25
+ 恰恰相反,正因为别处的规则都引用它,错的代价才最大。
26
+
27
+ ---
28
+
29
+ ## 为什么要单独分出这一层
30
+
31
+ 一个真实的架构悖论:
32
+
33
+ > 今天为了让 AI 少犯错而加的约束,明天可能变成更强的 AI 的天花板。
34
+
35
+ 这个悖论之所以成立,是因为**大多数"约束"其实是给当前模型能力打的补丁**:
36
+ 模型不会规划 ⇒ 那就规定它必须按步骤走;模型不会看全局 ⇒ 那就禁止它改全局。
37
+
38
+ 模型一旦变强,这些补丁就从"护栏"变成"牢笼"。
39
+
40
+ **解法不是"以后再说",是分层。** 只有把「事实」和「补丁」分开,
41
+ 补丁才能被安全地摘掉,而事实一个字都不用动。
42
+
43
+ | 层 | 是什么 | 会不会变 | 谁负责 |
44
+ |---|---|---|---|
45
+ | **Invariant**(本文件) | 客观事实 | **永不变**(除非 CSS 规范变了) | 门禁 |
46
+ | **Contract**(`ai/contract.schema.json`) | 版本化的接口约定 | 随版本变,**向后兼容** | 校验器 |
47
+ | **Guidance**(`docs/GUIDANCE.md`) | 当前模型下的工作建议 | **随时可变,可整份推翻** | 人 |
48
+
49
+ ⇒ **判断一条内容该放哪层的唯一标准**:
50
+ > 如果明天出现一个无限强的模型,这句话还成立吗?
51
+ > 成立 ⇒ Invariant。不成立 ⇒ Guidance。
52
+ > 是"我们约定好接口长这样" ⇒ Contract。
53
+
54
+ ---
55
+
56
+ ## 一、作用域类事实
57
+
58
+ ### I-1 裸标签选择器的作用域是**整个文档**
59
+
60
+ ```css
61
+ a { color: red } /* 命中文档里每一个 <a>,无论它属于谁 */
62
+ ```
63
+
64
+ 这是 CSS 的定义,不是我们的规定。
65
+
66
+ **推论**:在一个"我们不拥有整个文档"的库里写裸标签选择器,
67
+ 等价于**修改使用者的页面**。与谁在写这行代码无关,与模型强弱无关。
68
+
69
+ - 门禁:`bleed-gate.py`
70
+ - 已发生的事故:`03-patterns/content/content.css` 曾有 `a { color: var(--accent) }`,
71
+ 把宿主全站链接染色。已修。
72
+
73
+ ### I-2 页面级 override 链的复杂度是**单调递增**的
74
+
75
+ ```css
76
+ .slide-3 h1 { ... } /* 第 1 次局部调整 */
77
+ .slide-4 h1 { ... } /* 第 2 次 */
78
+ .slide-3 .card h1 { ... }/* 发现冲突,加码 */
79
+ ```
80
+
81
+ 每加一层特异性,就**只能再加一层**来覆盖它。
82
+ 这不是"AI 容易犯的错",是**特异性机制的必然结果**。
83
+
84
+ **推论(作用域限定)**:在**存在唯一汇聚点**的体系里,改汇聚点优于在页面里逐处加特异性 ——
85
+ 因为前者一处生效、全量跟随,后者每一处都要再同步一次。
86
+
87
+ ⚠️ 这条**不**等价于"任何页面都不许有局部样式"。
88
+ 在不拥有汇聚点、或改动本就只影响一个页面的场景里,局部样式是直接表达意图的方式;
89
+ 它付出的代价是"改一处时这一处不跟随",而不是"这么写就错了"。
90
+
91
+ ### I-3 未分层样式**永远**赢过已分层样式
92
+
93
+ `@layer` 的层序优先于选择器特异性。
94
+ (这也是为什么 Web Awesome 把全局样式分层——让使用者的 CSS 自动赢,不需要 `!important`。)
95
+
96
+ - 本库未使用 `@layer`。理由、切换前提与就绪判据见 `docs/BENCHMARK.md` 的「级联分层的决定」一节。
97
+
98
+ ### I-4 inline style 的优先级高于**一切**样式表
99
+
100
+ `element.style.setProperty('--surface', x)` 会压掉使用者样式表里的 `--surface`,
101
+ 使用者只能再加 `!important` 反击。
102
+
103
+ - 过去的代价:强制暗色曾靠 JS 往 `:root` 写 30 条 inline 属性实现,
104
+ 等于把整个配色层抬到样式表之上。
105
+ - 现已消除:暗色由 CSS 表达(`[data-theme="dark"]`),JS 不持有任何色值。
106
+
107
+ ---
108
+
109
+ ## 二、令牌类事实
110
+
111
+ ### I-5 绕过令牌的颜色在令牌切换时**必然**失效
112
+
113
+ ```css
114
+ .card { background: #FFFFFF } /* 令牌切换对它没有任何作用 */
115
+ ```
116
+
117
+ 不是"可能不好看",是**切换机制对它没有任何作用**——它根本没引用令牌。
118
+
119
+ **作用域**:这条只在**颜色由令牌驱动切换**的系统里成立。
120
+ 一个没有令牌切换机制的页面里写死颜色,不受这条约束 ——
121
+ 它付出的代价是"将来接入切换机制时这一处不跟随",不是"现在就错了"。
122
+
123
+ - 门禁:`hardcode-gate.py`
124
+ - 例外判据(写死在门禁里):**判断"这是内容色还是效果色"**。
125
+ 阴影是光影效果,不是主题内容 ⇒ 允许裸值。内容底色 ⇒ 必须令牌。
126
+ 已发生的事故:把 `switch.css` 的阴影也改成令牌,反而是错的。
127
+ - ⇒ 反过来说:**并非所有固定颜色都错**。只有"本该跟随主题的内容色"才必须引用令牌;
128
+ 效果色、数据可视化里承载语义的色标、品牌固定色,都有各自的固定取值理由。
129
+
130
+ ### I-6 `tokens.css` 是全库配色/间距/字号的**唯一汇聚点**
131
+
132
+ 改它的影响面 = 全库。这是依赖拓扑的结果。
133
+
134
+ - 门禁:`impact-report.py` —— 它把"改了什么 ⇒ 必须重跑什么"机械化,
135
+ **不靠人判断**。
136
+
137
+ ### I-7 令牌有第二份真值来源 ⇒ 必然漂移
138
+
139
+ 两份手写的值**都能各自自洽**,所以**测不出来**,只会慢慢错开。
140
+
141
+ - 实证:`theme-toggle.js` 第一版手写的 20 个暗色令牌,17 个与 tokens.css 不一致,
142
+ 连名字都编错(`--warn` / `--danger-soft`,真名 `--warning` / `--danger-bg`)。
143
+ - 缓解:真值只有一份,其余**全部生成**(`theme-sync.py`)+ `--check` 门禁。
144
+ - 已根治的实例:暗色不再由 JS 持有值——CSS 自己表达,JS 只翻属性。
145
+
146
+ ---
147
+
148
+ ### I-16 自定义属性的 `var()` 在**声明它的元素**上就求值完毕,后代拿到的是结果
149
+
150
+ 这条是 P1(容器级主题)首跑实测撞出来的,和 I-7 是同一个根的两面:
151
+
152
+ ```css
153
+ :root { --raw-paper: #f6f7f8; --paper: var(--raw-paper); }
154
+
155
+ .dark { --raw-paper: #14171A; } /* 只覆盖 L1 */
156
+ ```
157
+
158
+ `.dark` 里的 `--paper` **仍然是 `#f6f7f8`**。原因不是特异性、不是顺序:
159
+ `--paper` 的声明在 `:root` 上,它的 `var(--raw-paper)` 在**那里**就替换完了,
160
+ 后代继承的是替换后的值。覆盖 `--raw-*` 只影响"在本元素上求值"的那些声明。
161
+
162
+ - 实证:改造前实测,暗区容器 `--paper` 与区外一字不差(`#f6f7f8`),
163
+ 按钮底色也完全没变(`rgb(27,77,143)` 两边相同)。
164
+ 补上 `--paper: var(--raw-paper)` 到作用域块之后,才变成 `#14171A` /
165
+ `rgb(122,169,222)`。
166
+ - 推论一:**"两层令牌 + 一处覆盖"不会自动获得作用域能力**。
167
+ 每多一个作用域,L2 别名就要跟着重声明一次 —— 这是 CSS 的规定动作,不是实现选择。
168
+ - 推论二:这也意味着 L1/L2 分层的收益(换肤只改一处、暗色不会漏)**只在
169
+ `:root` 上成立**;一旦要局部换肤,那份"重声明"就必然出现第三份真值
170
+ ⇒ 只能交给生成器 + 门禁(I-7 的老结论)。
171
+ - 推论三:`revert` / `unset` 都**不能**"回到上一层作用域"。
172
+ 自定义属性在 UA 样式里不存在 ⇒ `revert` 得到"未定义"(`var()` 走兜底或失效),
173
+ `unset` 对自定义属性等同 `inherit` ⇒ 拿到的还是上一层的值。
174
+
175
+ ## 三、状态表达类事实
176
+
177
+ ### I-8 写在 DOM 属性上的状态,其**存在不依赖 JS 执行过**
178
+
179
+ | 写法 | 谁能读到 | 改坏了 JS 之后 |
180
+ |---|---|---|
181
+ | `data-state="closed"` | 任何人、任何工具、任何时刻 | 值还在那儿,可被断言、可被门禁检查 |
182
+ | 状态类名(由 JS add/remove) | class 本身可读、可被选择器命中、可被测试断言 —— 但要等 JS 执行过才存在 | class 消失,CSS 规则还在 ⇒ **静默错乱,无报错** |
183
+
184
+ ⚠️ 两者的区别**不是**"class 不可读 / 不可测试":class 完全可读、可被选择器命中、
185
+ 可被断言,这一点没有争议。真正的区别是**状态的存在是否依赖 JS 曾经正确执行过** ——
186
+ 属性写进 HTML 就在那儿,类名要等 JS 跑。
187
+ ⇒ 因此风险的性质是**耦合**(状态与 JS 实现绑在一起),不是"不可观测"。
188
+
189
+ **这是 PPT 事故的技术根因**:状态不在 DOM 里 ⇒ 无法从文档反推当前状态 ⇒
190
+ 每次局部修改都是在看不见状态的情况下盲改 ⇒ 改 A 坏 B。
191
+
192
+ **实证(比"理论上有风险"更硬)**:本库曾长期两套并存——CSS 同时写
193
+ `.tooltip` 的状态类与 `[data-state="open"]`,但 JS 只加前者。后果有两层:
194
+
195
+ 1. 文档告诉使用者"请改用 `data-state`",而**这个分支从来不会被设上** ⇒
196
+ 承诺了一个不工作的 API;
197
+ 2. 一次半自动改写把 `.a.is-disabled .b` 拆成 `.a.is-disabled, .a[data-state] .b`
198
+ ⇒ 前半截**丢了后代选择器**,规则落到容器自己身上,真正该变灰的 `.b`
199
+ 再也没有这条样式。**这个 bug 已随发布版本出去了**,且不报错、页面照常显示。
200
+
201
+ - 已整改:`is-*` 归零,状态一律走属性(详见 `API.md` 的迁移表)。
202
+ - 教训:**两套表达不是"冗余",是"其中一套必然是死的,而且没人看得出来"。**
203
+
204
+ ### I-9 枚举字符串能表达的状态数**严格多于**布尔
205
+
206
+ `data-state="open" | "closed"` 可以表达第三态(`closing`——动画中)。
207
+ `isOpen: boolean` 不能。
208
+
209
+ ⇒ 这不是风格偏好,是**表达能力的客观差异**。
210
+
211
+ ⚠️ 但不能反推成"用布尔就是错的":二态场景(开/关、有/无、选中/未选中)
212
+ 用 boolean 本身不是技术错误,它只是**在需要中间态时不够用**。
213
+ ⇒ 判据是**这个状态有没有第三个取值**(如"动画中""加载中""部分选中"),
214
+ 而不是"一律不许用布尔"。
215
+
216
+ ---
217
+
218
+ ## 四、验证类事实
219
+
220
+ ### I-10 一个"报告通过但什么都没检查"的门禁,比没有门禁更危险
221
+
222
+ 没有门禁 ⇒ 你知道要自己看。
223
+ 假绿门禁 ⇒ 你以为有人在看。
224
+
225
+ - 实证 1:`es5-gate.py` 传 `--dir` 时扫 **0 个文件**,却报 PASS。
226
+ 它源码第 98–103 行自己就写了这个警告,但调用方一直传 `--dir`。
227
+ - 实证 2:`shot-baseline.py` 一张图都没截到,报 `OK 23 / 0 个页面观感一致`。
228
+ - 实证 3:`api-snapshot.py` 注入 `Select.destroy()` 幻觉 API,仍报 OK。
229
+ - 实证 4:16 个门禁用 `--dir .`,扫进了 `node_modules`。
230
+ - 实证 5:**代码说自己会检查,但没有任何门禁跑它**。
231
+ `adapters/presentation/check.js` 声称守五条呈现判据(版面登记 / 单页上限 /
232
+ 必须有标题 / 层级不跳级 / 禁页面级选择器),而 `core-boundary` 只验了
233
+ `to-doc.js` 的往返 ⇒ `check.js` **一次都没被执行过**。
234
+ 读 README 的人会以为有人在看,实际上没人看 —— 这就是假绿的一种,
235
+ 而且是最难发现的一种:**它不产出任何报告**。
236
+ ⇒ 修法:`presentation` 门禁对合规 / 违规两份 fixture 实跑,
237
+ 要求五条判据**各命中一次**,并带反向控制(弄瘫判据 ⇒ 门禁必须认出它失效)。
238
+ - 实证 6:**门禁在测另一棵树**。视觉回归用固定端口起静态服务,
239
+ 端口被占时子进程**立刻静默退出**,而 `Popen` 不报错、返回值正常
240
+ ⇒ 截图全部打到了另一个目录的残留服务上。
241
+ 之所以被发现,是**修好了 CSS 之后 diff 数字一字未变**。
242
+ ⇒ 修法:改用操作系统分配端口(bind 0)+ canary 校验(拉文件与磁盘字节比对)。
243
+ - 实证 7:**门禁的取文件清单只取"已跟踪"的文件**。
244
+ 新生成的产物目录还没进索引时,六道内容门禁**一个文件都看不见**
245
+ ⇒ 本地全绿 ⇒ 提交 ⇒ 干净克隆里跑 ⇒ 立刻红。
246
+ 未跟踪文件恰恰是最需要被看的(那是刚写完的新代码),却被排除在视野外。
247
+ ⇒ 修法:取文件清单 = 已跟踪 ∪ 未跟踪且未被忽略(见共享模块的取文件函数)。
248
+ ⇒ 反向控制:新建一个**未跟踪**的违规文件(旧视野下它 0 命中、新视野下必须被抓住)。
249
+ - 实证 8:**判据只数了"初始状态",没制造状态变化**。
250
+ 给树新加一条"整棵树只能有一个 `tabindex="0"`",第一版只数**页面加载完那一刻**;
251
+ 把实现里"移动前把其余置 -1"那行删掉后,它**依然全绿**(因为那一行只在按键后才起作用)。
252
+ ⚠️ 最迷惑的地方:它不是"判据写错了",是**判据根本没跑到被测的那段代码** ——
253
+ 报告里照样写着 PASS,和真绿长得一模一样。
254
+ ⇒ 修法:判据**自己先把状态跑起来**(移动 → 收起 → 再移动 → 再展开),再断言。
255
+ ⇒ 反向控制:删掉那一行 ⇒ 实测 1 个 0 变成 7 个 0,判据红了。
256
+ ⇒ 推论:**凡是"检查某个不变量"的判据,都要先问一句"我有没有真的走到会破坏它的那条路径"**。
257
+
258
+ - 实证 9:**"跟源码一致"本身可以是假的** —— 契约文件是**生成**出来的。
259
+ 注入 emit 核之后,生成器用 `window\.([A-Z]\w*)\s*=` 抽组件暴露的全局名,
260
+ 而核里有 `typeof window.CustomEvent === 'function'`
261
+ ⇒ `===` 的**第一个** `=` 被当成赋值 ⇒ **10 个组件的 global 全变成 `CustomEvent`**。
262
+ ⚠️ 为什么全绿:这道门禁的判据是「契约 == 生成器重跑的结果」,
263
+ 而生成器**正是**那个写错的东西 ⇒ 自己跟自己对答案,永远一致。
264
+ 干净克隆跑完全套门禁也是 EXIT=0(条数不写死 —— 由 `doc-facts-gate.py`
265
+ 从 `check-all.sh` 实算,见它自己打印的注册数)。
266
+ ⇒ 教训:**生成出来的契约,不能只跟"生成器再跑一遍"比** —— 那没有引入任何新信息。
267
+ 它必须挂在**另一根独立的桩**上(本例:浏览器实测的全局名单)。
268
+ ⇒ 修法:正则改 `=(?!=)`;抽取前先摘掉 `BEHAVIOR INJECT` 块(核不是组件的源码);
269
+ 并新增判据「声明的 global 必须在浏览器里真探得到」(`api-form` 判据 4,
270
+ 含反向控制,`CustomEvent` 是它的**事故真值反例**)。
271
+
272
+ - 实证 10:**反向控制自己也可能"没跑到被测的那段代码"**。
273
+ 给 `api-form` 加判据 5(注释里承诺的 API 必须真实存在)时,
274
+ 反向控制只喂了**判定函数**合成数据 ⇒ 四条全绿;
275
+ 而**扫描函数**第一版的别名正则用了非捕获组 `(?:Select|Pagination)`
276
+ ⇒ `m.group(2)` 直接 `IndexError` —— 真跑起来必崩,自检却一点没提。
277
+ ⚠️ 形状与实证 8 完全一致,只是这次"没跑到"的是**反向控制自己**:
278
+ 反向控制绿 ⇒ 大家以为判据有牙,实际它一跑就崩。
279
+ ⇒ 修法:把被测函数拆成**纯函数**(不读文件、不起浏览器),
280
+ 反向控制直接喂合成输入 ⇒ 扫描与判定**各自**都有反例。
281
+ ⇒ 推论:**写完反向控制,要再问一句"它有没有真的调用到被测的那个函数"**。
282
+
283
+ ⇒ **因此本库每一道新门禁都必须做反向控制**:注入反例,证明它会红。
284
+ **没有反向控制的门禁不算门禁。**
285
+ ⇒ 另外:**门禁依赖的每一份输入都要能被证明是"本次的"** ——
286
+ 端口、临时目录、缓存文件,任何一个指向别处,报告再漂亮也是假的。
287
+
288
+ ### I-11 解析 CSS/HTML 时,正则不是解析器
289
+
290
+ - 实证:`prefix-build.py` 第一版把 `this.tablist` 当成选择器 `.tablist` 改写 ⇒ `SyntaxError`。
291
+ - 缓解:改写后对所有 JS 跑 `node --check`;选择器规则加 `(?<![\w$])` 前缀约束。
292
+
293
+ ### I-12 浏览器不认识的声明、悬空的 `var()` 引用 ⇒ **静默失效**
294
+
295
+ 写错属性名**不报错、不告警、不影响后续解析**:整条声明被丢弃,其余照常生效。
296
+ `var(--x)` 里的 `--x` 从未定义且没有兜底 ⇒ 该声明在**计算值阶段**无效,整条丢弃。
297
+
298
+ - 实证:全库 19 处写了 `border-inset-inline-start` / `-end` ——
299
+ 这不是一个存在的 CSS 属性(正确的是 `border-inline-start` / `-end`)。
300
+ 机械验证:`CSS.supports()` 两种形式与 `element.style.setProperty()` 三处一致
301
+ 返回 `false`。⇒ 这些边框**从来没有被渲染过**;而配套的
302
+ `border-left-color` 只是给一条 0 宽、style 为 none 的边上色 ⇒ 全线不可见。
303
+ - 为什么别的门禁抓不到:它不影响解析、不抛异常、不改变任何可断言的行为,
304
+ 只是**观感**缺了一块;而观感只有视觉回归能验。
305
+ - 门禁:`css-validity`(判据交给浏览器自己,不手写属性白名单 ——
306
+ 手写的那份是第二份真相源,见 I-7)。
307
+
308
+ ### I-13 `npm install file:` 不做打包,会连未发布目录一起软链进来
309
+
310
+ - 实证:一度以为 `npm pack` 泄漏内部文档。实测证伪——真实 `npm pack` + 安装是干净的。
311
+ - 教训:**用真实 tarball 安装验证,不要用 `file:` 的假象下结论。**
312
+ - 门禁:`pack-smoke.sh`。
313
+
314
+ ### I-14 同一语义的样式若在每个使用点各写一份,"改一处"在物理上不成立
315
+
316
+ - 实证:一次成套多页制作中,想改一处"全局"的东西,整批页面立刻崩。
317
+ 机械测量(39 个使用点):页面框、内容列、标题阶梯、行内代码、说明块
318
+ 这几样"全局"的东西,各自出现在 21–36 个使用点、同一个选择器有 7–16 种写法,
319
+ 其中若干**已经存在共享定义**,使用点仍在重抄。
320
+ - 反方向的机械证据:把改动之前的版本取出,用同一把尺子量
321
+ "改一个全局变量,这个使用点跟不跟着变" ⇒ 36 处不跟随。
322
+ - 推论:**"全局可改"不是写法问题,而是"存在唯一汇聚点"这一事实的结果。**
323
+ 没有汇聚点,改动的完整性就只能靠逐点同步,而逐点同步必然漏。
324
+ - 推论二:一个已被收编的选择器若在使用点仍被书写,会被更高特异性的定义
325
+ **静默盖掉**,成为死声明 —— 不报错、不影响解析,只在"改了却没变"时才暴露。
326
+ - 门禁:`shell`(禁止使用点重抄骨架)+ `skel-lever`(在真实浏览器里要求
327
+ 每一个使用点都跟随同一全局变量的改动,并含"不引骨架的使用点不得被影响"的反向控制)。
328
+
329
+ ### I-15 判据绑在**内容**上才稳定;绑在**字节**或**位置**上必然自相矛盾
330
+
331
+ 同一份内容在两个环境下给出两个结论 ⇒ 判据一定绑错了东西。
332
+ 这类缺陷不表现为"检查失灵",而表现为"换个环境结论就变",
333
+ 因此极难复现:维护者本地永远绿,只有陌生人一上来就撞上。
334
+
335
+ - 实证 A(绑在字节上):新鲜度判据按磁盘字节算散列。
336
+ 源码在提交库里换行符是混合状态(实测 LF 17 个、CRLF 32 个);
337
+ 别人克隆后,签出时的换行符自动转换把那 17 个改成 CRLF
338
+ ⇒ 内容一字未改,17 个文件全部被判"产物不新鲜"。
339
+ ⇒ 修法:散列先把 CRLF 归一成 LF 再算(产物本就是按文本读的,不含 CR),
340
+ 并把"只换换行符仍然绿"写成常驻自检。
341
+ - 实证 B(绑在位置上):产物目录里的文件与它的源码同源同内容,
342
+ 但两道门禁的例外/豁免是按路径前缀判的 ⇒ 源码侧放行、产物侧报错。
343
+ 同一份内容,两个相反结论 —— 错的不是产物,是判据把规则写在了位置而不是内容上。
344
+ ⇒ 修法:判例前先把产物路径映射回源路径,规则只写一份。
345
+ - 推论:**凡出现"内容相同却判定不同",先怀疑判据绑错了东西,而不是改内容去凑判据。**
346
+ 后者会把门禁逼成一个说谎的尺子。
347
+
348
+ ---
349
+
350
+ ## 五、这一层**不**包含什么
351
+
352
+ 明确排除,避免这一层被污染后失去"永不变"的性质:
353
+
354
+ | 不写在这里 | 应该去哪 | 理由 |
355
+ |---|---|---|
356
+ | "改之前先跑 impact-report.py" | `GUIDANCE.md` | 这是工作建议。更强的模型自己就能判断影响面 |
357
+ | "AI 应该先读 README" | `GUIDANCE.md` | 同上 |
358
+ | "本版有 N 个组件"(**写具体数字**的那种) | `ai/components.json`(Contract) | 会随版本变 |
359
+ | "某档位只允许某成熟度的组件" | `ai/profiles.json`(Contract) | 这是**约定**,不是事实,可以改 |
360
+ | "不要写 magic number" | `GUIDANCE.md` | 这是品味建议,不是物理事实 |
361
+
362
+ ---
363
+
364
+ ## 六、怎么保证这一层不被污染
365
+
366
+ `05-audit/invariant-gate.py` 会检查:
367
+
368
+ 1. 本文件中**不出现**任何组件名、令牌名、版本号、档位名(出现 ⇒ 那是 Contract 的内容)
369
+ 2. 本文件中**不出现** "应该 / 建议 / 推荐 / 最好" 这类措辞(出现 ⇒ 那是 Guidance 的内容)
370
+ 3. `ai/contract.schema.json` 里声明的 `invariants` 条目,与本文件的锚点**一一对应**
371
+
372
+ ⇒ 门禁先于人情。想往这层塞东西,得先说服门禁。
@@ -0,0 +1,158 @@
1
+ # `@layer` 的 WebView 基线(W12)
2
+
3
+ > **当前状态:部分验证(7 行),且已出现 1 个阻塞样本。**
4
+ > 已有 7 行真机实测。**`@layer` 6 行通过、1 行不通过**:
5
+ >
6
+ > | 入口 | 结果 |
7
+ > |---|---|
8
+ > | iOS 18.7 · Safari | 15 / 15 |
9
+ > | Android 14 · 微信 8.0.47(XWEB / Chromium 146) | 14 / 15,缺 `scrollbar-width`(有私有回退) |
10
+ > | iOS 26.6.2 · 微信 8.0.79(WKWebView) | 15 / 15 |
11
+ > | iOS 18.7 · 企业微信 5.0.12(wxwork) | 15 / 15 |
12
+ > | iPad · Safari 26.6.1(桌面版 UA) | 15 / 15 |
13
+ > | iPhone · QQ 浏览器 20.6.6 | 15 / 15 |
14
+ > | **Android 14 · 百度浏览器 13.51(Chromium 97)** | **11 / 15,缺 `@layer`** 🔴 |
15
+ >
16
+ > ⚠️ **百度浏览器那一行是决定性的**:它证明"国产 App 内置浏览器长期停在老内核"
17
+ > 不是猜测 —— Chromium 97(2022 年初)不认识 `@layer`,切层后**那一层样式会整段消失**。
18
+ >
19
+ > 按下面第三节的判据:**已有一行 `@layer` 不通过 ⇒ 当前不能无条件切层**,
20
+ > 只能走 `@supports at-rule(@layer)` 双写,或者不切。
21
+
22
+ ---
23
+
24
+ ## 〇、数据是怎么来的
25
+
26
+ 不是手抄的,也不是推测的:探针页把结果 **POST 回采集端**,落成 JSON 后原样贴进表格。
27
+ 手机访问 `http://<电脑内网地址>:端口` 属于**非安全上下文**,
28
+ `navigator.clipboard` 在那里根本不存在 —— 所以页面提供的是"发送到电脑"(同源 POST)作为主路径,
29
+ 剪贴板只作备用(详见 `ERRORS.md` E47)。
30
+
31
+ ---
32
+
33
+ ## 一、为什么切 `@layer` 之前必须先有这个基线
34
+
35
+ `@layer` 与其它 CSS 特性有一个**性质上的区别**:
36
+
37
+ | 特性 | 不支持时的表现 |
38
+ |---|---|
39
+ | `:has()` / `clamp()` / `dvh` / `aspect-ratio` | **单条声明或单条规则失效** —— 局部退化,页面还能看 |
40
+ | `@layer` | 引擎不认识 `@layer { … }` 这个**块**,整块规则被当作非法内容丢弃 —— **整段样式消失** |
41
+
42
+ ⇒ 不支持 `@layer` 的引擎上,不是"层叠顺序不对",而是"**这一层里的所有样式都没了**"。
43
+ `cascade-layer-gate.py` 现在只证明"我们**有能力**切层"(`!important` 都能归类、层数可控),
44
+ 它证明不了"切完之后**目标浏览器**还认识它"。
45
+
46
+ ⇒ 基线就是这个"目标浏览器认识不认识"的证据。没有它,切层是一次**没有安全网的改动**。
47
+
48
+ ---
49
+
50
+ ## 二、怎么测(三条路径,任选一条)
51
+
52
+ 探针页:`10-review/webview/capability-probe.html`
53
+ (自包含单文件:不引本库 CSS、不引外部资源,拷到手机上直接能开)
54
+
55
+ 1. **局域网**(最省事)
56
+ 在仓库根目录起静态服务(`npx serve` / `python -m http.server`),
57
+ 手机与电脑连同一个无线局域网,访问 `http://<电脑内网地址>:<端口>/10-review/webview/capability-probe.html`
58
+ 2. **拷文件**
59
+ 把这个 HTML 单独发到手机(文件传输 / 数据线),用系统浏览器或目标 App 的 WebView 打开
60
+ 3. **云真机**
61
+ 需要覆盖 iOS WKWebView 与 Android System WebView 两类内核时用
62
+
63
+ 页面会输出一段 JSON(点「复制结果」按钮)⇒ 整段贴进下面表格的「结果」列即可。
64
+
65
+ ⚠️ 一定要说明**从哪个入口打开的**:
66
+ Safari 里打开 ≠ WKWebView 里打开(同一内核,但 `WebKit` 的宿主配置会关掉部分能力),
67
+ Android 上 Chrome ≠ 系统 WebView ≠ 各家 App 内置 WebView。**入口写不清楚,数据就没法用。**
68
+
69
+ ---
70
+
71
+ ## 三、判据:什么时候才有资格切层
72
+
73
+ 🔴 **先把两类缺失分开**(这一点是被真机数据纠正的):
74
+ `@layer` 能不能用,与"某一项别的能力缺失"是**两件事**。
75
+ 第一版判据把两者混在一起,于是微信缺一个 `scrollbar-width` 就会被判成"不能切层" ——
76
+ 而那个缺失跟层叠毫无关系,且本库有私有回退。
77
+
78
+ | 类别 | 判据 | 动作 |
79
+ |---|---|---|
80
+ | **阻塞切层** | 任一行 `@layer` 实测**不通过** | 只能走 `@supports at-rule(@layer)` 双写,或者不切 |
81
+ | **不阻塞切层** | 除 `@layer` 外的其它能力缺失 | 不影响层叠决策;但必须逐条判定"有没有回退、降级后是否可接受",并把结论写进「缺失能力」列 |
82
+ | **可以无条件切** | 目标受众内**每一行** `@layer` 都通过 | 直接切 |
83
+
84
+ ⚠️ "每一行"是**字面意思**:目标受众里还有一台设备不过,就不能无条件切。
85
+ `docs/INVARIANT.md` 的口径是"约束要么成立、要么被写进合同",不存在"大部分成立"。
86
+
87
+ **当前判定:不能无条件切层。**
88
+ 5 行里 **百度浏览器(Chromium 97)不支持 `@layer`** —— 而 `@layer` 不支持时,
89
+ **整层规则会被丢弃**(不是"层序不对",是"那一层的样式没了")。
90
+ ⇒ 要么 `@supports at-rule(@layer)` 双写,要么不切。
91
+
92
+ ⚠️ 决策还差一步:**百度浏览器在本库的目标受众里占多少**。
93
+ 这一条数据只能回答"支不支持",回答不了"值不值得为它放弃切层" ——
94
+ 那是产品判断,不该由基线表替使用者下结论。
95
+
96
+ | 已有 | 缺 |
97
+ |---|---|
98
+ | iPhone Safari、iPad Safari、iPad 微信、iPad 企微、iPhone QQ 浏览器、Android 微信(XWEB)、Android 百度浏览器(Chromium 97) | Android 系统 WebView(Chrome)、其它 App 内置内核(抖音 / 小红书 / 支付宝…) |
99
+
100
+ 另有一条**方法论**上的收获,值得单独记下来:
101
+
102
+ > `scrollbar-width` 在微信 XWEB 里 `CSS.supports` 返回 `false`,
103
+ > 但本库的滚动条样式**照样生效** —— 因为 `01-tokens/scrollbar.css` 同时写了
104
+ > `::-webkit-scrollbar` 私有实现。
105
+ > ⇒ **「探针说这一项不支持」不等于「这个能力在本库里没有效果」。**
106
+ > 判读基线表时必须回到源码看一眼有没有第二条路,否则会把"可接受降级"误判成"缺陷"。
107
+
108
+ ---
109
+
110
+ ## 四、记录表
111
+
112
+ <!--
113
+ 填写规则(门禁 webview-probe-gate.py 会核对):
114
+ · 每一行 8 列都必须填,不许留 `?` / `TBD` / `待填` / `TODO`
115
+ · 「状态」只能是 `UNVERIFIED` 或 `VERIFIED`
116
+ · 标成 `VERIFIED` 的行,UA 必须是**从页面 JSON 里复制出来的完整 UA**(不是"iPhone 15"这种简称)
117
+ · 「结果」列写 `支持 N / 15`,缺失项写在「缺失能力」列
118
+ 当前 7 行(iPhone Safari / iPad 微信 / iPad 企微 / iPad Safari / iPhone QQ 浏览器 / Android 微信 XWEB / Android 百度浏览器)。**Android 系统 WebView 仍没有 —— 那是缺口,不是"没问题"。**
119
+ 「测试日期」不写死年月日:这个文件在公开仓里,写死日期等于把内部时间线留在仓库里
120
+ (`leak-scan` 的「工作日期戳」判据就是为了这个)。日期以**提交时间**为准,精确到秒且无法伪造。
121
+ 「UA」里凡是**身份标识**(如微信的 `MMWEBID`)一律脱敏成 `<已脱敏>` ——
122
+ 基线需要的是技术信息(内核与版本),不是谁测的。
123
+ -->
124
+
125
+ | 设备 / 系统 | 入口 | UA | 结果 | 缺失能力 | 测试日期 | 记录人 | 状态 |
126
+ |---|---|---|---|---|---|---|---|
127
+ | iPhone · Safari(UA 自述;430×932 是 iPhone 15/16 Pro Max 逻辑尺寸) | Safari(系统浏览器) | `Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.1 Mobile/15E148 Safari/604.1` | 15 / 15 | 无 | 见采集所在提交(git log) | 项目所有者(真机) | VERIFIED |
128
+ | Android 14(视口 400×787 @2.7x) | 微信 8.0.47 内置 WebView(XWEB 内核,Chromium 146) | `Mozilla/5.0 (Linux; Android 14; PHZ110 Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/146.0.7680.178 Mobile Safari/537.36 XWEB/1460249 MMWEBSDK/20231202 MMWEBID/<已脱敏> MicroMessenger/8.0.47.2560 WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64` | 14 / 15 | `scrollbar-width` —— **标准 API 缺失,但本库有私有回退**:`01-tokens/scrollbar.css` 同时写了 `::-webkit-scrollbar` 伪元素族,XWEB 是 Chromium 内核,私有实现仍在 ⇒ 观感可接受降级,不是缺陷 | 见采集所在提交(git log) | 项目所有者(真机) | VERIFIED |
129
+ | iPad · 微信(**用户口述为平板**;UA 与视口均为 iPhone ⇒ iPad 上以 iPhone 兼容模式运行) | 微信 8.0.79 内置 WebView(WKWebView) | `Mozilla/5.0 (iPhone; CPU iPhone OS 26_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.79(0x18004f2b) NetType/WIFI Language/zh_CN` | 15 / 15 | 无 | 见采集所在提交(git log) | 项目所有者(真机) | VERIFIED |
130
+ | iPad · 企业微信(**用户口述为平板**;UA 与视口均为 iPhone ⇒ 兼容模式运行) | 企业微信 5.0.12 内置 WebView(wxwork) | `Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 wxwork/5.0.12 MicroMessenger/7.0.1 Language/zh ColorScheme/Light wwmver/3.26.511.377` | 15 / 15 | 无 | 见采集所在提交(git log) | 项目所有者(真机) | VERIFIED |
131
+ | Android 14(视口 360×672 @3x) | 百度浏览器 13.51(baiduboxapp,Chromium 97 内核) | `Mozilla/5.0 (Linux; Android 14; PHZ110 Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/97.0.4692.98 Mobile Safari/537.36 T7/13.51 SP-engine/2.89.0 baiduboxapp/13.51.0.10 (Baidu; P1 14) NABar/1.0` | 11 / 15 | 🔴 **`@layer` —— 阻塞项**:Chromium 97 太老,分层不可用 ⇒ **不能无条件切层**。另缺三项,均非阻塞:`dvh`(`page.css` 先写 `100vh` 再写 `100dvh`,老内核丢后者留前者);`scrollbar-width`(有 `::-webkit-scrollbar` 私有回退);原生 Popover 属性(**本库未使用**,只有自研类名 `.popover`,与本库无关) | 见采集所在提交(git log) | 项目所有者(真机) | VERIFIED |
132
+ | iPad · Safari(**用户口述为平板**;UA 是桌面版 = iPad 请求桌面网站,视口 834×1210 与 iPad Pro 11" 一致) | Safari 26.6.1(桌面版 UA) | `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6.1 Safari/605.1.15` | 15 / 15 | 无 | 见采集所在提交(git log) | 项目所有者(真机) | VERIFIED |
133
+ | iPhone · QQ 浏览器(**用户口述为手机**;视口 430×736 @3x) | QQ 浏览器 20.6.6(MQQBrowser,WKWebView) | `Mozilla/5.0 (iPhone; CPU iPhone OS 26_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 MQQBrowser/20.6.6 Mobile/15E148 Safari/604.1 QBWebViewUA/2 QBWebViewType/1 WKType/1` | 15 / 15 | 无 | 见采集所在提交(git log) | 项目所有者(真机) | VERIFIED |
134
+
135
+ ---
136
+
137
+ ## 五、刻意不写的东西
138
+
139
+ **本文件不写任何引擎版本号。**
140
+
141
+ 「iOS 16.4 才支持某个特性」这类说法在本文件里没有位置,原因很直接:
142
+ 我没有在真机上验证过它,写下来就会被下一个人当成事实引用
143
+ (`ERRORS.md` E13 记过一类同源事故:文档里的结论没人回来复核,最后整份文档都在骗人)。
144
+
145
+ ⇒ 需要版本结论时,**跑一次探针页**,用那一次的 UA 与结果说话。
146
+ UA 是证据,版本传闻不是。
147
+
148
+ ---
149
+
150
+ ## 六、与 `10-review/ios/` 的分工
151
+
152
+ | 文件 | 回答什么 | 谁来填 |
153
+ |---|---|---|
154
+ | `10-review/ios/index.html` | **观感与交互**在 Safari 下对不对(按钮、键盘弹起、暗色) | 人眼看,逐条对 |
155
+ | `10-review/webview/capability-probe.html` | **能力**在目标 WebView 里有没有(`@layer` 等 15 项) | 机器判定,人只负责贴 JSON |
156
+
157
+ 两者不能互相替代:能力全支持而观感崩了,和观感正常但 `@layer` 被整段丢弃,
158
+ 是两种完全不同的事故。