agentcss 0.16.1 → 0.24.1

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 (93) hide show
  1. package/01-tokens/focus-ring-demo.html +1 -1
  2. package/01-tokens/page.css +32 -0
  3. package/01-tokens/responsive-demo.html +1 -1
  4. package/01-tokens/scrollbar-demo.html +1 -1
  5. package/02-primitives/badge/demo.html +1 -1
  6. package/02-primitives/button/demo.html +1 -1
  7. package/02-primitives/card/demo.html +1 -1
  8. package/02-primitives/choice/choice.css +31 -2
  9. package/02-primitives/choice/demo.html +1 -1
  10. package/02-primitives/combobox/demo.html +1 -1
  11. package/02-primitives/date-range/demo.html +1 -1
  12. package/02-primitives/delta/demo.html +1 -1
  13. package/02-primitives/input/demo.html +1 -1
  14. package/02-primitives/input/input.css +14 -0
  15. package/02-primitives/metric/demo.html +1 -1
  16. package/02-primitives/popover/demo.html +1 -1
  17. package/02-primitives/progress/demo.html +1 -1
  18. package/02-primitives/progress/progress.css +56 -0
  19. package/02-primitives/select/demo.html +1 -1
  20. package/02-primitives/separator/demo.html +1 -1
  21. package/02-primitives/skeleton/demo.html +1 -1
  22. package/02-primitives/skeleton/skeleton.css +18 -0
  23. package/02-primitives/switch/demo.html +1 -1
  24. package/02-primitives/switch/switch.css +61 -2
  25. package/02-primitives/timeline/README.md +70 -0
  26. package/02-primitives/timeline/demo.html +161 -0
  27. package/02-primitives/timeline/timeline.css +152 -0
  28. package/02-primitives/upload/README.md +142 -0
  29. package/02-primitives/upload/demo.html +318 -0
  30. package/02-primitives/upload/upload.css +490 -0
  31. package/02-primitives/upload/upload.js +676 -0
  32. package/03-patterns/accordion/demo.html +1 -1
  33. package/03-patterns/content/demo.html +1 -1
  34. package/03-patterns/drawer/demo.html +1 -1
  35. package/03-patterns/drawer/drawer.js +30 -5
  36. package/03-patterns/dropdown/demo.html +1 -1
  37. package/03-patterns/form-validation/demo.html +1 -1
  38. package/03-patterns/list/demo.html +1 -1
  39. package/03-patterns/nav/demo.html +1 -1
  40. package/03-patterns/overlay/demo.html +1 -1
  41. package/03-patterns/pagination/demo.html +1 -1
  42. package/03-patterns/states/demo.html +1 -1
  43. package/03-patterns/tabs/demo.html +1 -1
  44. package/03-patterns/tooltip/demo.html +1 -1
  45. package/03-patterns/tree/demo.html +1 -1
  46. package/04-recipes/app-shell/README.md +272 -0
  47. package/04-recipes/app-shell/app-shell.css +588 -0
  48. package/04-recipes/app-shell/demo.html +326 -0
  49. package/04-recipes/app-shell/shell-nav.js +288 -0
  50. package/04-recipes/table/table.css +22 -0
  51. package/09-assets/bar/demo.html +1 -1
  52. package/09-assets/echarts-adapter/demo.html +1 -1
  53. package/09-assets/model-viewer/demo.html +1 -1
  54. package/09-assets/sparkline/demo.html +1 -1
  55. package/AGENTS.md +8 -0
  56. package/API.md +4 -0
  57. package/CHANGELOG.md +795 -0
  58. package/CONTRIBUTING.md +22 -1
  59. package/README.md +17 -5
  60. package/README.zh-CN.md +16 -6
  61. package/START-HERE.md +12 -7
  62. package/ai/START-HERE.md +37 -3
  63. package/ai/behaviors.json +32 -0
  64. package/ai/cli.js +183 -25
  65. package/ai/components.json +185 -7
  66. package/ai/components.meta.json +13 -0
  67. package/ai/pitfalls-writing.json +119 -0
  68. package/ai/pitfalls.json +193 -0
  69. package/dist/01-tokens/page.css +1 -1
  70. package/dist/02-primitives/choice/choice.css +1 -1
  71. package/dist/02-primitives/input/input.css +1 -1
  72. package/dist/02-primitives/progress/progress.css +1 -1
  73. package/dist/02-primitives/skeleton/skeleton.css +1 -1
  74. package/dist/02-primitives/switch/switch.css +1 -1
  75. package/dist/02-primitives/timeline/timeline.css +1 -0
  76. package/dist/02-primitives/upload/upload.css +1 -0
  77. package/dist/02-primitives/upload/upload.js +515 -0
  78. package/dist/03-patterns/drawer/drawer.js +14 -4
  79. package/dist/04-recipes/app-shell/app-shell.css +1 -0
  80. package/dist/04-recipes/app-shell/shell-nav.js +173 -0
  81. package/dist/04-recipes/table/table.css +1 -1
  82. package/dist/COSTS.md +12 -10
  83. package/docs/ACCESSIBILITY.md +20 -7
  84. package/docs/AI-CONTRACT.md +2 -0
  85. package/docs/BUDGET.md +179 -0
  86. package/docs/CONVENTIONS.md +3 -0
  87. package/docs/STABILITY.md +126 -0
  88. package/esm/index.mjs +2 -0
  89. package/esm/upload.mjs +7 -0
  90. package/index.html +5 -2
  91. package/llms-full.txt +153 -0
  92. package/llms.txt +55 -0
  93. package/package.json +4 -1
@@ -45,7 +45,7 @@ a { color: var(--accent); }
45
45
  .tip { font-size: var(--fs-xs); color: var(--text-tertiary); }
46
46
  </style>
47
47
  </head>
48
- <body class="page page">
48
+ <body class="page">
49
49
  <div class="wrap">
50
50
 
51
51
  <h1>焦点环</h1>
@@ -197,6 +197,38 @@
197
197
  border-radius: var(--r-xs);
198
198
  }
199
199
 
200
+ /* ============================================================================
201
+ 4b. 代码块的兜底:任何 <pre> 自己横向滚,不许把页面撑宽
202
+ ----------------------------------------------------------------------------
203
+ 🔴 为什么需要(0.24.1 实测,不是推演)
204
+ 在 `.page` 环境里放一条 220 字符的长行:
205
+
206
+ 裸 <pre> doc=2048 / vw=375 ❌ 撑破
207
+ <pre class="code"> doc=2211 / vw=375 ❌ 撑破 (没有 guide.css 时)
208
+ <div class="prose"><pre> doc=2048 / vw=375 ❌ 撑破 (没有 content.css 时)
209
+
210
+ 三种都是 `overflow-x: visible`、`white-space: pre`
211
+ ⇒ 长行直接顶到页面上,整页横向滚。
212
+
213
+ 现有 84 个页面之所以没红,是因为每个 pre 恰好都另有来源:
214
+ `.prose > pre`(content.css)、`pre.code`(guide.css)。**但那是巧合,不是契约**
215
+ —— 使用者照 page.css 写一页文档、贴一段长代码,就会踩到。
216
+
217
+ 🔴 为什么用 `:where(.page)` 而不是 `.page`
218
+ 特异性压到 0,0,1(只算 `pre`)⇒ 任何带类的规则都能盖掉它:
219
+ `.prose > pre` (0,1,1) 赢
220
+ `pre.code` (0,1,1) 赢
221
+ 这条的定位是**兜底**,不是"给所有 pre 定样式"。写成 `.page pre`(0,1,1)
222
+ 会与上面两条**同权重**,胜负取决于加载顺序 —— 那就成了一个随时会翻的脸。
223
+
224
+ ⚠️ 只写 `overflow-x`,不写 padding / background / font:
225
+ 那些是"代码块长什么样",属于 content.css / guide.css 的决定。
226
+ 这里只管"它不许撑破页面"。加了别的属性就会改动已有页面的观感。
227
+ ========================================================================= */
228
+ :where(.page) pre {
229
+ overflow-x: auto;
230
+ }
231
+
200
232
  /* ============================================================================
201
233
  5. 演示块 `.box`
202
234
  ----------------------------------------------------------------------------
@@ -29,7 +29,7 @@ p { margin: 0 0 var(--sp-4); }
29
29
  .swatch { height: var(--sp-4); background: var(--accent); border-radius: 3px; }
30
30
  </style>
31
31
  </head>
32
- <body class="page page">
32
+ <body class="page">
33
33
 
34
34
  <div id="meter">
35
35
  <span>视口 <b id="m-w">—</b></span>
@@ -46,7 +46,7 @@ td.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabu
46
46
  border: 1px solid var(--border-control); }
47
47
  </style>
48
48
  </head>
49
- <body class="page page">
49
+ <body class="page">
50
50
  <div class="wrap">
51
51
 
52
52
  <h1>滚动条</h1>
@@ -15,7 +15,7 @@
15
15
  padding: var(--sp-4) 0; }
16
16
  </style>
17
17
  </head>
18
- <body class="page page">
18
+ <body class="page">
19
19
  <main class="wrap">
20
20
  <h1>badge · 状态标记</h1>
21
21
  <p class="lede">
@@ -24,7 +24,7 @@
24
24
  .btn--naive:disabled { opacity: 0.5; }
25
25
  </style>
26
26
  </head>
27
- <body class="page page">
27
+ <body class="page">
28
28
  <main class="wrap">
29
29
 
30
30
  <h1>button · 七态矩阵</h1>
@@ -26,7 +26,7 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
26
26
  var(--text-tertiary) 0 1px, transparent 1px 8px); opacity: .5; margin-bottom: var(--sp-2); }
27
27
  </style>
28
28
  </head>
29
- <body class="page page">
29
+ <body class="page">
30
30
  <main class="wrap">
31
31
 
32
32
  <h1>card · hover 无布局跳动</h1>
@@ -234,8 +234,7 @@
234
234
  background: var(--accent);
235
235
  border-color: var(--accent);
236
236
  }
237
- .choice--radio .choice__input:checked ~ .choice__label .choice__mark::after {
238
- transform: scale(1);
237
+ .choice--radio .choice__input:checked ~ .choice__label .choice__mark::after { transform: scale(1);
239
238
  }
240
239
 
241
240
  /* ============================================================================
@@ -393,3 +392,33 @@
393
392
  它是 CSS Level 4,某项目 上 WebView 版本不确定。
394
393
  写这一行时已经违反了自己刚定的纪律,已改用相邻兄弟选择器提优先级。
395
394
  ========================================================================= */
395
+
396
+ /* ============================================================================
397
+ 强制色(Windows 高对比度)
398
+ ============================================================================
399
+ 0.17.0 全库扫描(forced-colors-check.js B3)在这里抓到 2 类 4 处,
400
+ 都是同一个根因,而且**checkbox 的勾恰好没事** —— 这个对比值得记下来:
401
+
402
+ · checkbox 的勾:用 `border: solid var(--text-on-accent)` 画
403
+ ⇒ 强制色把 border-color 换成 CanvasText ⇒ **白底黑勾,看得见**
404
+ · radio 的圆点:用 `background: var(--text-on-accent)` 画
405
+ ⇒ 强制色把 background-color 换成 Canvas ⇒ **白底白点,消失**
406
+ · indeterminate 的横线:用 `background: var(--accent)` 画
407
+ ⇒ 同上 ⇒ **白底白线,消失**
408
+
409
+ ⇒ 规律:**用 background-color 画"前景图形"必死,用 border/color 画则活。**
410
+ 强制色的换色规则是分属性的 —— 背景换背景色、前景换前景色。
411
+ 把图形画在"背景属性"上,就等于把它声明成了背景,自然被换成背景色。
412
+
413
+ 修法只有一处替换:把这两个 `background` 改成 `CanvasText`(系统前景色)。
414
+ ⚠️ 不要顺手把 checkbox 的勾也改了 —— 它现在是对的,
415
+ 改成 CanvasText 反而会在暗色高对比度主题下(CanvasText = 浅色)出错。
416
+ ============================================================================ */
417
+ @media (forced-colors: active) {
418
+ .choice--radio .choice__mark::after {
419
+ background: CanvasText;
420
+ }
421
+ .choice--checkbox .choice__input:indeterminate ~ .choice__label .choice__mark::after {
422
+ background: CanvasText;
423
+ }
424
+ }
@@ -30,7 +30,7 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
30
30
  .log { color: var(--text-tertiary); }
31
31
  </style>
32
32
  </head>
33
- <body class="page page">
33
+ <body class="page">
34
34
  <main class="wrap">
35
35
 
36
36
  <h1>checkbox / radio / select</h1>
@@ -30,7 +30,7 @@ label.f { display: block; margin-bottom: var(--sp-1); font-size: var(--fs-sm); }
30
30
  }
31
31
  </style>
32
32
  </head>
33
- <body class="page page">
33
+ <body class="page">
34
34
  <main class="wrap">
35
35
 
36
36
  <h1>combobox · 组合框 / 标签输入</h1>
@@ -22,7 +22,7 @@
22
22
  .dr-bad-field .drange__input { border-color: var(--danger); }
23
23
  </style>
24
24
  </head>
25
- <body class="page page">
25
+ <body class="page">
26
26
  <main class="wrap">
27
27
 
28
28
  <h1>date-range · 日期区间</h1>
@@ -30,7 +30,7 @@
30
30
  color: var(--text-secondary); }
31
31
  </style>
32
32
  </head>
33
- <body class="page page">
33
+ <body class="page">
34
34
  <main class="wrap">
35
35
  <h1>delta · 变化量</h1>
36
36
  <p class="lede">
@@ -29,7 +29,7 @@
29
29
  .bad-err { color: red; font-size: 13px; }
30
30
  </style>
31
31
  </head>
32
- <body class="page page">
32
+ <body class="page">
33
33
  <main class="wrap">
34
34
 
35
35
  <h1>input · 五态矩阵</h1>
@@ -187,6 +187,20 @@
187
187
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 8v5M12 16.5v.5'/%3E%3C/svg%3E") center/contain no-repeat;
188
188
  }
189
189
 
190
+ /* 🔴 强制色:与 table__sort::after 同一个模式 —— mask + currentColor 会变成白图标
191
+ ---------------------------------------------------------------------------
192
+ 实拍(0.17.0):`.field__error` 只剩「请输入有效金额」,行首的叹号没了。
193
+ 文字本身已经表达了错误,所以这不是信息丢失,是**第二条线索**(给色盲用户的
194
+ 非颜色编码,WCAG 1.4.1)被强制色抹掉了 —— 恰好抹掉的是它存在的理由。
195
+
196
+ ⇒ 强制色下改用 CanvasText。判据见 forced-colors-check.js A1。
197
+ --------------------------------------------------------------------------- */
198
+ @media (forced-colors: active) {
199
+ .field__error::before {
200
+ background: CanvasText;
201
+ }
202
+ }
203
+
190
204
  /* ---------------------------------------------------------------- 成功态
191
205
  只在"校验有明确通过语义"时用(如"该用户名可用")。
192
206
  普通输入框加成功态是噪音。
@@ -17,7 +17,7 @@
17
17
  padding: var(--sp-5); background: var(--surface); }
18
18
  </style>
19
19
  </head>
20
- <body class="page page">
20
+ <body class="page">
21
21
  <main class="wrap">
22
22
  <h1>metric · 指标块</h1>
23
23
  <p class="lede">
@@ -44,7 +44,7 @@ th { font-weight: 600; }
44
44
  .pp-stage .popover__title { margin: 0; font-size: var(--fs-sm); font-weight: 500; }
45
45
  </style>
46
46
  </head>
47
- <body class="page page">
47
+ <body class="page">
48
48
  <main class="wrap">
49
49
  <!-- 🔴 0.8.9:axe 报 landmark-one-main(moderate)—— 本页原本没有任何 landmark 容器。
50
50
  加 <main> 是语义正确且不改布局的做法(<main> 默认不产生 box)。
@@ -27,7 +27,7 @@
27
27
  .pr-note { margin: var(--sp-2) 0 0; font-size: var(--fs-xs); color: var(--text-secondary); }
28
28
  </style>
29
29
  </head>
30
- <body class="page page">
30
+ <body class="page">
31
31
  <main class="wrap">
32
32
  <!-- 🔴 0.8.9:axe 报 landmark-one-main(moderate)—— 本页原本没有任何 landmark 容器。
33
33
  加 <main> 是语义正确且不改布局的做法(<main> 默认不产生 box)。
@@ -29,6 +29,22 @@
29
29
  background: var(--surface-sunken);
30
30
  border-radius: var(--r-full); /* 胶囊形 */
31
31
  overflow: hidden;
32
+ /* 🔴 强制色模式下的边界预留(0.17.0 实拍:没有它进度条整体消失)
33
+ ---------------------------------------------------------------------
34
+ Windows 高对比度下 `background` 被强制成 Canvas,轨道与填充条**同色**
35
+ ⇒ 735×6 的面积全白,用户完全看不出进度。
36
+
37
+ `outline: 1px solid transparent` 是这里的正解,不是笔误:
38
+ · 正常态:transparent ⇒ 看不见,且 outline **不占布局**,胶囊形不变
39
+ · 强制色下:浏览器把 transparent 替换成 CanvasText
40
+ ⇒ 自动显出一条 1px 实线,轨道因此有了轮廓
41
+ ⚠️ 别改成 `border: 1px solid transparent`:border 在 border-box 下会
42
+ 吃掉 2px 内容高,`--progress-h: 4px` 的细条就只剩 2px 了。
43
+ ⚠️ 也别用 `box-shadow: 0 0 0 1px transparent` 兜底 —— 实测无效,
44
+ box-shadow 在强制色下整个被移除,透明的也一样。
45
+ 判据见 05-audit/forced-colors-check.js A2/A3。 */
46
+ outline: 1px solid transparent;
47
+ outline-offset: -1px; /* 画在内侧,不与相邻元素抢位置 */
32
48
  /* ⚠️ 不用 <progress> 原生标签的原因:它在部分浏览器里无法设成胶囊形,
33
49
  且 ::-webkit-progress-value 的圆角行为不一致。 */
34
50
  }
@@ -49,6 +65,27 @@
49
65
  .progress--warning .progress__bar { background: var(--warning); }
50
66
  .progress--danger .progress__bar { background: var(--danger); }
51
67
 
68
+ /* 🔴 强制色:填充条必须换「系统高亮色」,不能靠作者色
69
+ ---------------------------------------------------------------------------
70
+ 强制色模式下所有作者写的 background-color 都被替换成 Canvas(白/黑)
71
+ ⇒ 填充条与轨道同色,进度归零。实测 735×6 全白,一个像素都不剩。
72
+
73
+ 为什么用 `Highlight` 而不是别的系统色:
74
+ · 它是**唯一**语义为"被选中的一段/重点区域"的系统色,正好对应填充条
75
+ · 系统色关键字**不被强制替换** —— 实测 `Highlight` 在模拟下解析为
76
+ rgb(55,0,110),`CanvasText` 为 rgb(0,0,0),都保持自己的身份
77
+ ⚠️ 但**别在判据里断言具体 RGB**:模拟色板与真实高对比度主题不是同一套
78
+ (实测 `GrayText` 在模拟下是 rgb(96,0,0) 深红,真实系统下是灰)。
79
+ 判据只能问"是不是等于 Canvas"。
80
+
81
+ 为什么选择器是 `.progress .progress__bar` 而不是 `.progress__bar`:
82
+ 上面三个变体是 0,2,0 优先级。用 0,1,0 会被它们压掉 ⇒ 危险态仍然不可见。
83
+ 这里必须同权或更高,且写在变体之后。
84
+ --------------------------------------------------------------------------- */
85
+ @media (forced-colors: active) {
86
+ .progress .progress__bar { background: Highlight; }
87
+ }
88
+
52
89
  /* 尺寸变体 */
53
90
  .progress--lg { --progress-h: 10px; }
54
91
  .progress--sm { --progress-h: 4px; }
@@ -90,6 +127,25 @@
90
127
  .progress-ring--success { background: conic-gradient(var(--success) calc(var(--progress-num, 0) * 1%), var(--surface-sunken) 0); }
91
128
  .progress-ring--danger { background: conic-gradient(var(--danger) calc(var(--progress-num, 0) * 1%), var(--surface-sunken) 0); }
92
129
 
130
+ /* 🔴 强制色:conic-gradient 整条被移除,环会"凭空不见"只剩一个数字
131
+ ---------------------------------------------------------------------------
132
+ 实测(0.17.0):`.progress-ring` 在 forced-colors 下 `background-image`
133
+ 变成 none ⇒ 只截到居中的 "72%",环一个像素都没有。
134
+ 信息本身没丢(中心 label 里就是百分比数字),丢的是"这是个环"的形状。
135
+
136
+ ⇒ 用 outline 补一个圈。**不用 border**:label 是
137
+ `calc(100% - var(--ring-thickness) * 2)`,border 会挤小它,
138
+ 在 --ring-size 较小时数字会被裁。
139
+ ⚠️ 也不能指望 `background: canvas` 之类的替换 —— 环是**渐变**,
140
+ 强制色下 gradient 不区分颜色,整条 background-image 直接丢弃。 */
141
+ @media (forced-colors: active) {
142
+ .progress-ring {
143
+ background: none;
144
+ outline: 2px solid CanvasText;
145
+ outline-offset: -2px; /* 内侧描边,外径不变 */
146
+ }
147
+ }
148
+
93
149
  /* ============================================================================
94
150
  3 · 不确定进度(不知道要多久)
95
151
  ============================================================================
@@ -38,7 +38,7 @@ label.f { display: block; margin-bottom: var(--sp-1); font-size: var(--fs-sm); }
38
38
  }
39
39
  </style>
40
40
  </head>
41
- <body class="page page">
41
+ <body class="page">
42
42
  <main class="wrap">
43
43
 
44
44
  <h1>select · 下拉选择</h1>
@@ -13,7 +13,7 @@
13
13
  .row { padding: var(--sp-2) 0; }
14
14
  </style>
15
15
  </head>
16
- <body class="page page">
16
+ <body class="page">
17
17
  <main class="wrap">
18
18
  <h1>separator · 分隔线</h1>
19
19
  <p class="lede">
@@ -28,7 +28,7 @@
28
28
  .sk-box { display: flex; flex-direction: column; gap: var(--sp-2); }
29
29
  </style>
30
30
  </head>
31
- <body class="page page">
31
+ <body class="page">
32
32
  <main class="wrap">
33
33
  <!-- 🔴 0.8.9:axe 报 landmark-one-main(moderate)—— 本页原本没有任何 landmark 容器。
34
34
  加 <main> 是语义正确且不改布局的做法(<main> 默认不产生 box)。
@@ -81,6 +81,24 @@
81
81
  .skeleton::after { animation: none; opacity: 0; }
82
82
  }
83
83
 
84
+ /* 🔴 强制色:骨架块会被抹成 Canvas,与页面同色 ⇒ 整个占位区消失
85
+ ---------------------------------------------------------------------------
86
+ 实拍(0.17.0):729×10 的骨架条在强制色下**完全空白**。
87
+ 危险之处在于它"看起来像加载完成了"—— 用户看到空行,会以为内容就是空的。
88
+
89
+ ⇒ 用 `GrayText`。它是系统色关键字(不被再次强制替换),语义正好是
90
+ "disabled / 占位 / 非正文",与骨架屏的含义一致。
91
+ ⚠️ 别断言它的具体 RGB:模拟色板下是 rgb(96,0,0) 深红,真实高对比度
92
+ 主题下是灰。判据只问"是否等于 Canvas"。
93
+ ⚠️ shimmer(`::after` 的 linear-gradient)在强制色下会被移除 ——
94
+ 这是**正确**行为,高对比度用户不需要动效,不需要补偿。
95
+ --------------------------------------------------------------------------- */
96
+ @media (forced-colors: active) {
97
+ .skeleton {
98
+ background: GrayText;
99
+ }
100
+ }
101
+
84
102
  /* ---------------------------------------------------------------------------
85
103
  组合件:列表骨架(最常用的形态)
86
104
  --------------------------------------------------------------------------- */
@@ -16,7 +16,7 @@
16
16
  color: var(--text-secondary); }
17
17
  </style>
18
18
  </head>
19
- <body class="page page">
19
+ <body class="page">
20
20
  <main class="wrap">
21
21
  <h1>switch · 开关</h1>
22
22
  <p class="lede">
@@ -227,7 +227,17 @@
227
227
  --surface 两套模式都有定义(浅色 #fff / 暗色 #1C2024),
228
228
  滑块作为「浮在轨道上的圆盘」用它是语义正确的。 */
229
229
  background: var(--surface);
230
- border: 0;
230
+ /* 🔴 原来是 `border: 0`。0.17.0 强制色实拍:滑块变成**完全不可见** ——
231
+ 白底(Canvas)+ 阴影被移除(box-shadow 在强制色下整个丢弃)+ 无边框,
232
+ 开关只剩一个空心胶囊,**开与关看不出区别**。
233
+
234
+ `1px solid transparent` 在这里同时满足两件事:
235
+ · 正常态:transparent ⇒ 与 `border: 0` 视觉等效(下面已标 box-sizing: border-box,
236
+ 外径仍是 --sw-thumb,滑块尺寸不变)
237
+ · 强制色下:transparent 被替换成 CanvasText ⇒ 自动获得 1px 描边
238
+ ⚠️ 不要为了"省一层"把箱模型改成 content-box —— 那会让滑块外径多 2px,
239
+ 与 `--sw-gap` 的推导值脱钩,圆圈又歪回去(真机踩过一次)。 */
240
+ border: 1px solid transparent;
231
241
  /* 阴影里的 rgba 是**阴影本身**的物理属性(不是主题色),
232
242
  它在浅色与暗色下都该是黑色 —— 换令牌反而会变灰。 */
233
243
  box-shadow: 0 1px 2px rgba(0, 0, 0, .28);
@@ -254,7 +264,11 @@
254
264
  - var(--sw-inset) * 2 - var(--sw-pad) * 2
255
265
  - (var(--sw-h) - var(--sw-inset) * 2
256
266
  - var(--sw-pad) * 2)));
257
- border-color: var(--accent);
267
+ /* ⚠️ 原来是 `var(--accent)`。那时 border 是 0,这条**从来没生效过**。
268
+ 0.17.0 给滑块补了 `border: 1px solid transparent` 之后它会突然显形,
269
+ 在蓝底上多出一圈品牌色描边 ⇒ 必须显式写 transparent,
270
+ 让「正常态无描边 / 强制色下自动变 CanvasText」这条规则只有一个真相来源。 */
271
+ border-color: transparent;
258
272
  }
259
273
 
260
274
  /* 焦点环在**看得见**的元素上(原生 outline 被 clip 了,看不见)。
@@ -368,3 +382,48 @@
368
382
  - (var(--sw-h) - var(--sw-inset) * 2
369
383
  - var(--sw-pad) * 2)));
370
384
  }
385
+
386
+ /* ============================================================================
387
+ 强制色(Windows 高对比度)
388
+ ============================================================================
389
+ 0.17.0 实拍:加上滑块的 transparent 边框后,滑块已经**看得见**了,
390
+ 但"开"与"关"仍然只能靠滑块的左右位置分辨 —— 因为开态轨道原本铺的是
391
+ `--switch-on` 品牌色,强制色下同样被抹成 Canvas。
392
+
393
+ ⇒ 开态改用系统 `Highlight`。这样:
394
+ 关 = 白底胶囊 + 黑描边 + 左侧空心圆
395
+ 开 = 高亮底胶囊 + 右侧实心圆
396
+ 两重线索(底色 + 位置),不依赖任何作者色。
397
+
398
+ ⚠️ 选择器必须与上面开态规则**完全同权**(0,3,0)并写在它之后,
399
+ 否则会被 `--switch-on` 压掉。
400
+ ============================================================================ */
401
+ @media (forced-colors: active) {
402
+ .switch__input:checked ~ .switch__track {
403
+ background: Highlight;
404
+ }
405
+
406
+ /* 🔴 禁用态必须与「可用的关态」不同 —— 否则等于骗人
407
+ ------------------------------------------------------------------------
408
+ 实拍(0.17.0):修好滑块之后,禁用态与正常关态在强制色下**完全一致**
409
+ (track 白 + 1px 黑边、thumb 白 + 1px 黑边、都在左侧)。
410
+ 高对比度用户会以为这个开关还能点。
411
+
412
+ 原来靠 `--surface-sunken`(灰底)与 `--text-tertiary`(灰字)表达禁用,
413
+ 这两者在强制色下都被抹成 CanvasText/Canvas。
414
+
415
+ ⇒ 用 `GrayText`:它的系统语义就是 "disabled / 不可操作",
416
+ 在真实高对比度主题下是灰。三处一起换,保持"轨道-滑块-文字"同一套语义。
417
+ ⚠️ 模拟色板里 GrayText 解析为 rgb(96,0,0) 深红,看着像"危险"不像"禁用"。
418
+ 这是 Chromium 模拟的偏差,不是这条规则的问题 —— 判据只判"≠ Canvas"。
419
+ ------------------------------------------------------------------------ */
420
+ .switch__input:disabled ~ .switch__track {
421
+ border-color: GrayText;
422
+ }
423
+ .switch__input:disabled ~ .switch__track .switch__thumb {
424
+ border-color: GrayText;
425
+ }
426
+ .switch__input:disabled ~ .switch__label {
427
+ color: GrayText;
428
+ }
429
+ }
@@ -0,0 +1,70 @@
1
+ # timeline · 时间轴
2
+
3
+ 一串按时间排的事件:**一个时点、一句标题、可选一段正文**。
4
+ 公告列表、大事记、版本记录都是它 —— 共同点不是"有日期",而是**顺序本身就是信息**。
5
+
6
+ ```html
7
+ <link rel="stylesheet" href="02-primitives/timeline/timeline.css">
8
+
9
+ <ol class="timeline">
10
+ <li class="timeline__item timeline__item--latest">
11
+ <time class="timeline__time num" datetime="2026-10-29">2026-10-29</time>
12
+ <h3 class="timeline__title">2026 年三季报披露</h3>
13
+ <p class="timeline__body">营业总收入 1,294.06 亿元,同比 +12.40%。</p>
14
+ </li>
15
+ <li class="timeline__item">
16
+ <time class="timeline__time num" datetime="2026-08-27">2026-08-27</time>
17
+ <h3 class="timeline__title">2026 年半年报披露</h3>
18
+ </li>
19
+ </ol>
20
+ ```
21
+
22
+ ## 类
23
+
24
+ | 类 | 作用 |
25
+ |---|---|
26
+ | `.timeline` | 根。`<ol>`(顺序即信息),用 `<ul>` 或 `<div>` 也能渲染 |
27
+ | `.timeline__item` | 一条事件。`position: relative` + 左侧留出 `--sp-5` 的槽 |
28
+ | `.timeline__time` | 时点。默认块级、tertiary 色、`--fs-xs` |
29
+ | `.timeline__title` | 标题。`margin: 0`,不受浏览器默认标题边距影响 |
30
+ | `.timeline__body` | 正文,可选 |
31
+ | `.timeline__item--latest` | 最新一条:圆点 8 → 12px、标题 600 |
32
+ | `.timeline--compact` | 紧凑档:条目间距 `--sp-4` → `--sp-2`,时间回到标题同一行 |
33
+
34
+ ## 三条硬规矩
35
+
36
+ 1. **用 `<ol>`,不是 `<div>`**。读屏会念「第 2 项,共 4 项」;
37
+ `<div>` 堆出来的时间轴在无障碍树里只是一串没有关系的段落。
38
+ 时间用 `<time datetime="…">`:屏幕显示"10-29",机器仍拿到完整日期。
39
+ 2. **状态不在这里表达**。要区分「已披露 / 待披露 / 停牌」,用 `badge` 配文字。
40
+ 本组件**故意不给状态配色** —— 绿色和灰色在强制色模式、色盲、打印三种情况下会同时失效,
41
+ 而状态文字不会。
42
+ 3. **"最新一条"靠尺寸和字重,不靠颜色**。`timeline__item--latest` 只改两件非颜色的事:
43
+ 圆点直径与标题字重。颜色在强制色里会被替换掉,只靠"最新那条是蓝的"等于没标。
44
+
45
+ ## 反例
46
+
47
+ - **拿字符画圆点** `content: "●"` 视觉上看不出区别,但那是一个**文字**:它会进 `textContent`、能被选中复制、也会被算进链接名(读屏念「圆点 季报披露」)。用 `content: ""` + 盒子画。
48
+ - **最后一条也画连接线** 列表下方会多出一截悬空的线,看起来像「还有一个事件没加载出来」。收尾靠选择器(`:not(:last-child)`),不靠给最后一条设透明边框 —— 透明边框在强制色里会显形。
49
+ - **连接线用 `inset-block: 0`** 第一条的线会从条目顶端开始,在圆点上方留一小截 —— 同样像"上面还有内容"。线应该从**自己的圆心**画到下一个的圆心。
50
+ - **圆点对齐整块的中心** 正文一长,圆点就往下滑,同屏几条圆点高低不齐。要对齐的是**首行行盒**的中心。
51
+ - **只给一条事件也套时间轴** 那不是时间轴,是一段话。连接线和圆点只会让人以为还有下文。
52
+
53
+ ## 为什么它值得是一个组件
54
+
55
+ 四个必须一起做对的量,交给调用方"记得做"必然漏:
56
+
57
+ | 量 | 错了会怎样 |
58
+ |---|---|
59
+ | 圆心 / 连接线中心 / 首行中心 **三心合一** | 差 1–2px,说不清哪里不对,但一眼就是歪的 |
60
+ | 连接线**只画到下一个圆心** | 首尾各多一小截,像"内容没加载完" |
61
+ | 最后一条**不留底距、不画线** | 列表下方凭空多一块空白 |
62
+ | 圆点**不用 border 画** | 本库 `box-sizing` 是选配(挂在 `.page *` 上)⇒ 有它时圆点缩水一半 |
63
+
64
+ 前三条由 `timeline-check.js` 的 B 组守着(含反向控制),第四条是 `--tl-dot` 只作用于宽高。
65
+
66
+ ## 什么时候不该用它
67
+
68
+ - **要表达"进行到第几步"**:那是步骤条,有"当前"和"未到"的语义;本组件只表示"发生过"。
69
+ - **事件之间有嵌套/分支**:时间轴是线性的。要分支用树,要并行用表格。
70
+ - **只是"更新时间"一行字**:不需要圆点和连接线,一行 `small` 就够。