agentcss 0.16.0 → 0.24.0
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.
- package/02-primitives/choice/choice.css +31 -2
- package/02-primitives/input/input.css +14 -0
- package/02-primitives/progress/progress.css +56 -0
- package/02-primitives/skeleton/skeleton.css +18 -0
- package/02-primitives/switch/switch.css +61 -2
- package/02-primitives/timeline/README.md +70 -0
- package/02-primitives/timeline/demo.html +161 -0
- package/02-primitives/timeline/timeline.css +152 -0
- package/02-primitives/upload/README.md +142 -0
- package/02-primitives/upload/demo.html +318 -0
- package/02-primitives/upload/upload.css +490 -0
- package/02-primitives/upload/upload.js +676 -0
- package/03-patterns/drawer/drawer.js +30 -5
- package/04-recipes/app-shell/README.md +272 -0
- package/04-recipes/app-shell/app-shell.css +588 -0
- package/04-recipes/app-shell/demo.html +322 -0
- package/04-recipes/app-shell/shell-nav.js +288 -0
- package/04-recipes/table/table.css +22 -0
- package/AGENTS.md +8 -0
- package/API.md +4 -0
- package/CHANGELOG.md +763 -0
- package/CONTRIBUTING.md +22 -1
- package/README.md +17 -5
- package/README.zh-CN.md +16 -6
- package/START-HERE.md +12 -7
- package/ai/START-HERE.md +37 -3
- package/ai/behaviors.json +32 -0
- package/ai/cli.js +183 -25
- package/ai/components.json +185 -7
- package/ai/components.meta.json +13 -0
- package/ai/pitfalls-writing.json +119 -0
- package/ai/pitfalls.json +193 -0
- package/dist/02-primitives/choice/choice.css +1 -1
- package/dist/02-primitives/input/input.css +1 -1
- package/dist/02-primitives/progress/progress.css +1 -1
- package/dist/02-primitives/skeleton/skeleton.css +1 -1
- package/dist/02-primitives/switch/switch.css +1 -1
- package/dist/02-primitives/timeline/timeline.css +1 -0
- package/dist/02-primitives/upload/upload.css +1 -0
- package/dist/02-primitives/upload/upload.js +515 -0
- package/dist/03-patterns/drawer/drawer.js +14 -4
- package/dist/04-recipes/app-shell/app-shell.css +1 -0
- package/dist/04-recipes/app-shell/shell-nav.js +173 -0
- package/dist/04-recipes/table/table.css +1 -1
- package/dist/COSTS.md +12 -10
- package/docs/ACCESSIBILITY.md +20 -7
- package/docs/AI-CONTRACT.md +2 -0
- package/docs/BUDGET.md +179 -0
- package/docs/CONVENTIONS.md +3 -0
- package/docs/STABILITY.md +126 -0
- package/esm/index.mjs +2 -0
- package/esm/upload.mjs +7 -0
- package/index.html +5 -2
- package/llms-full.txt +153 -0
- package/llms.txt +55 -0
- package/package.json +4 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
普通输入框加成功态是噪音。
|
|
@@ -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
|
============================================================================
|
|
@@ -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
|
--------------------------------------------------------------------------- */
|
|
@@ -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
|
-
|
|
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` 就够。
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="data:,">
|
|
7
|
+
<title>timeline · 时间轴</title>
|
|
8
|
+
<link rel="stylesheet" href="../../01-tokens/tokens.css">
|
|
9
|
+
<link rel="stylesheet" href="../../01-tokens/typography.css">
|
|
10
|
+
<link rel="stylesheet" href="../../01-tokens/page.css">
|
|
11
|
+
<link rel="stylesheet" href="../../02-primitives/timeline/timeline.css">
|
|
12
|
+
<style>
|
|
13
|
+
.tl-demo { max-width: 36rem; }
|
|
14
|
+
/* 代码块在 320px 上会横向溢出(实测 scrollWidth 630 > 320)——
|
|
15
|
+
一行长标记就能把整页撑成横向滚动。这一条只属于 demo,不进组件。 */
|
|
16
|
+
.tl-code { overflow-x: auto; }
|
|
17
|
+
.tl-demo .timeline__title { font-size: var(--fs-base); }
|
|
18
|
+
/* 反例专用:把圆点画成**字符**。这是错的,只为对照演示。 */
|
|
19
|
+
.timeline--dotchar .timeline__item::after {
|
|
20
|
+
content: "●";
|
|
21
|
+
width: auto;
|
|
22
|
+
height: auto;
|
|
23
|
+
background: none;
|
|
24
|
+
color: var(--accent);
|
|
25
|
+
font-size: 9px;
|
|
26
|
+
line-height: 1;
|
|
27
|
+
inset-block-start: 6px;
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body class="page page">
|
|
32
|
+
<main class="wrap">
|
|
33
|
+
<h1>timeline · 时间轴</h1>
|
|
34
|
+
<p class="lede">
|
|
35
|
+
一串按时间排的事件:<strong>一个时点、一句标题、可选一段正文</strong>。
|
|
36
|
+
公告列表、大事记、版本记录都是它 —— 它们的共同点不是“有日期”,
|
|
37
|
+
而是<strong>顺序本身就是信息</strong>。
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<h2 class="rule-top">一个完整的时间轴</h2>
|
|
41
|
+
<ol class="timeline tl-demo">
|
|
42
|
+
<li class="timeline__item timeline__item--latest">
|
|
43
|
+
<time class="timeline__time num" datetime="2026-10-29">2026-10-29</time>
|
|
44
|
+
<h3 class="timeline__title">2026 年三季报披露</h3>
|
|
45
|
+
<p class="timeline__body">
|
|
46
|
+
营业总收入 1,294.06 亿元,同比 +12.40%;经营现金流净额本期未披露。
|
|
47
|
+
</p>
|
|
48
|
+
</li>
|
|
49
|
+
<li class="timeline__item">
|
|
50
|
+
<time class="timeline__time num" datetime="2026-08-27">2026-08-27</time>
|
|
51
|
+
<h3 class="timeline__title">2026 年半年报披露</h3>
|
|
52
|
+
<p class="timeline__body">营业总收入 842.31 亿元,同比 +9.87%。</p>
|
|
53
|
+
</li>
|
|
54
|
+
<li class="timeline__item">
|
|
55
|
+
<time class="timeline__time num" datetime="2026-05-18">2026-05-18</time>
|
|
56
|
+
<h3 class="timeline__title">2025 年度分红实施</h3>
|
|
57
|
+
<p class="timeline__body">每 10 股派 12.80 元(含税),除权除息日 2026-05-19。</p>
|
|
58
|
+
</li>
|
|
59
|
+
<li class="timeline__item">
|
|
60
|
+
<time class="timeline__time num" datetime="2026-04-02">2026-04-02</time>
|
|
61
|
+
<h3 class="timeline__title">2025 年年度报告披露</h3>
|
|
62
|
+
</li>
|
|
63
|
+
</ol>
|
|
64
|
+
<p class="small">
|
|
65
|
+
第一项带 <code>timeline__item--latest</code>:圆点从 8px 变成 12px、标题加重到 600。
|
|
66
|
+
<strong>两条都是非颜色线索</strong> —— 强制色模式会把所有作者色替换掉,
|
|
67
|
+
只靠“最新那条是蓝的”在那里等于没标。
|
|
68
|
+
<br>
|
|
69
|
+
连接线是伪元素画的,最后一条由 <code>:not(:last-child)</code> 收掉 ——
|
|
70
|
+
不是给最后一条设透明边框。<strong>透明边框在强制色里会显形</strong>,
|
|
71
|
+
本库为此吃过一次亏(0.22.1 的侧栏“当前项”,五条全都长出了竖线)。
|
|
72
|
+
</p>
|
|
73
|
+
|
|
74
|
+
<h2 class="rule-top">紧凑档:侧栏、卡片、弹层里的事记</h2>
|
|
75
|
+
<div class="box">
|
|
76
|
+
<ol class="timeline timeline--compact" style="max-width: 22rem">
|
|
77
|
+
<li class="timeline__item timeline__item--latest">
|
|
78
|
+
<time class="timeline__time num" datetime="2026-10-31">10-31</time>
|
|
79
|
+
<span class="timeline__title">0.23.0 发布</span>
|
|
80
|
+
</li>
|
|
81
|
+
<li class="timeline__item">
|
|
82
|
+
<time class="timeline__time num" datetime="2026-10-31">10-31</time>
|
|
83
|
+
<span class="timeline__title">0.22.1 修强制色当前项</span>
|
|
84
|
+
</li>
|
|
85
|
+
<li class="timeline__item">
|
|
86
|
+
<time class="timeline__time num" datetime="2026-10-31">10-31</time>
|
|
87
|
+
<span class="timeline__title">0.21.0 发现层</span>
|
|
88
|
+
</li>
|
|
89
|
+
</ol>
|
|
90
|
+
</div>
|
|
91
|
+
<p class="small">
|
|
92
|
+
<code>timeline--compact</code> 只做两件事:条目间距从 <code>--sp-4</code> 收到
|
|
93
|
+
<code>--sp-2</code>,时间挪回标题同一行。**它不改字号** ——
|
|
94
|
+
在侧栏里把字缩小,等于承认这块内容不重要,而事实往往相反。
|
|
95
|
+
</p>
|
|
96
|
+
|
|
97
|
+
<h2 class="rule-top">反例:拿字符画圆点</h2>
|
|
98
|
+
<div class="cmp">
|
|
99
|
+
<div class="cmp__pane cmp__bad" data-bad=".timeline--dotchar .timeline__item|glyphs|=10">
|
|
100
|
+
<h3>错:<code>content: "●"</code></h3>
|
|
101
|
+
<!-- ⚠️ 这一条**故意写成一行**:下面那条断言数的是 textContent 的字符数,
|
|
102
|
+
标签之间换行会带进缩进空白,数字就跟着排版走了。 -->
|
|
103
|
+
<ol class="timeline timeline--dotchar"><li class="timeline__item"><time class="timeline__time num" datetime="2026-10-29">10-29</time><span class="timeline__title">季报披露</span></li></ol>
|
|
104
|
+
<p class="small">
|
|
105
|
+
这一条的字形多 1 个:那个 <code>●</code> 是<strong>文字</strong>。
|
|
106
|
+
它会出现在 <code>textContent</code> 里、能被选中复制、
|
|
107
|
+
也会被算进链接名(读屏念“圆点 季报披露”)。
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="cmp__pane cmp__good" data-good=".timeline .timeline__item|glyphs|=9">
|
|
111
|
+
<h3>对:空内容 + 盒子</h3>
|
|
112
|
+
<ol class="timeline"><li class="timeline__item"><time class="timeline__time num" datetime="2026-10-29">10-29</time><span class="timeline__title">季报披露</span></li></ol>
|
|
113
|
+
<p class="small">
|
|
114
|
+
<code>content: ""</code> + 8×8 的圆角盒子:视觉一样,字形 0 个。
|
|
115
|
+
装饰不该占用“读得出来的字符”这个稀缺资源。
|
|
116
|
+
</p>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<p class="cmp__why">
|
|
120
|
+
两栏的字形数差 1 —— 那 1 个就是圆点。这不是洁癖:
|
|
121
|
+
<strong>伪元素里的字符会参与可访问名计算</strong>(本库 `internal/ERRORS.md` E189 记过,
|
|
122
|
+
侧栏当时被念成“圆点 总览”)。要画形状就用盒子,不要用字。
|
|
123
|
+
</p>
|
|
124
|
+
|
|
125
|
+
<h2 class="rule-top">什么时候不该用它</h2>
|
|
126
|
+
<ul>
|
|
127
|
+
<li>
|
|
128
|
+
<strong>状态要区分时</strong>(已披露 / 待披露 / 停牌):用 <code>badge</code> 配文字。
|
|
129
|
+
本组件<strong>故意不给状态配色</strong> —— 颜色在强制色、色盲、打印三种情况下会同时失效,
|
|
130
|
+
而状态文字不会。
|
|
131
|
+
</li>
|
|
132
|
+
<li>
|
|
133
|
+
<strong>要做进度条时</strong>:步骤条(steps)与时间轴不是一回事 ——
|
|
134
|
+
前者有“当前在第几步”的语义,本组件只表示“发生过”。
|
|
135
|
+
</li>
|
|
136
|
+
<li>
|
|
137
|
+
<strong>只有一条事件时</strong>:那是一个段落,不是时间轴。连接线和圆点在这里只会让人以为还有下文。
|
|
138
|
+
</li>
|
|
139
|
+
</ul>
|
|
140
|
+
|
|
141
|
+
<h2 class="rule-top">标记结构</h2>
|
|
142
|
+
<pre class="tl-code"><code><ol class="timeline">
|
|
143
|
+
<li class="timeline__item
|
|
144
|
+
timeline__item--latest">
|
|
145
|
+
<time class="timeline__time"
|
|
146
|
+
datetime="2026-10-29">
|
|
147
|
+
2026-10-29
|
|
148
|
+
</time>
|
|
149
|
+
<h3 class="timeline__title">
|
|
150
|
+
2026 年三季报披露
|
|
151
|
+
</h3>
|
|
152
|
+
</li>
|
|
153
|
+
</ol></code></pre>
|
|
154
|
+
<p class="small">
|
|
155
|
+
<code><ol></code> 不只是习惯:读屏会念“第 2 项,共 4 项”,
|
|
156
|
+
而 <code><div></code> 堆出来的时间轴在无障碍树里只是一串没有关系的段落。
|
|
157
|
+
<code><time datetime></code> 同理 —— 显示“10-29”时,机器仍拿到完整日期。
|
|
158
|
+
</p>
|
|
159
|
+
</main>
|
|
160
|
+
</body>
|
|
161
|
+
</html>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
Timeline — 时间轴(公告、大事记、版本记录)
|
|
3
|
+
============================================================================
|
|
4
|
+
一句话:**一串按时间排的事件**,每个事件有一个时点、一句标题、可选一段正文。
|
|
5
|
+
|
|
6
|
+
🔴 为什么它是组件,而不是「一个 ol + 几个 margin」
|
|
7
|
+
----------------------------------------------------------------------------
|
|
8
|
+
因为这四件事必须一起做对,少一件就会在真实页面里出错:
|
|
9
|
+
|
|
10
|
+
① **顺序就是内容** —— 用 `<ol>` 而不是 `<div>`。读屏会念「第 3 项,共 7 项」,
|
|
11
|
+
而 `<div>` 堆出来的时间轴在无障碍树里只是一串没有关系的段落。
|
|
12
|
+
CSS **只认类名、不认标签**(用 `<ul>` 也能跑),但 demo 与文档一律 `<ol>`。
|
|
13
|
+
② **连接线的收尾靠选择器,不靠颜色** —— 见下面「为什么不用 border」。
|
|
14
|
+
③ **状态不许只靠颜色** —— 见下面「为什么不给状态配色」。
|
|
15
|
+
④ **方向和间距全是逻辑属性** —— `padding-inline-start` 而不是 `padding-left`,
|
|
16
|
+
否则阿拉伯语页面里圆点会落在文字的右边(本库 40 页 RTL 基线会抓)。
|
|
17
|
+
|
|
18
|
+
🔴 为什么连接线用伪元素,不用 border
|
|
19
|
+
----------------------------------------------------------------------------
|
|
20
|
+
最省事的写法是给每个 item 加 `border-inline-start`,再用
|
|
21
|
+
`border-color: transparent` 收掉最后一条。**这条在本库已经炸过一次**:
|
|
22
|
+
强制色模式会把 transparent 边框解析成实线(0.21 的侧栏「当前项」,
|
|
23
|
+
五项全都长出竖线,见 CHANGELOG 0.22.1)。
|
|
24
|
+
|
|
25
|
+
⇒ 这里的收尾是**选择器**的事(`:not(:last-child)`),不是颜色的事。
|
|
26
|
+
伪元素要么在、要么不在,没有「在但看不见」这种中间态。
|
|
27
|
+
|
|
28
|
+
🔴 为什么不给状态配色(已披露 / 待披露 / 停牌)
|
|
29
|
+
----------------------------------------------------------------------------
|
|
30
|
+
因为「绿=已披露、灰=待披露」在三种情况下会同时失效:
|
|
31
|
+
强制色模式(所有作者色被替换成同一套系统色)、色盲用户、
|
|
32
|
+
以及打印。而**状态文字本身**不会失效。
|
|
33
|
+
|
|
34
|
+
⇒ 状态留给 `badge`(它带文字)。本组件只管「什么时候、发生了什么」。
|
|
35
|
+
|
|
36
|
+
🔴 为什么「最新一条」用尺寸 + 字重,而不是颜色
|
|
37
|
+
----------------------------------------------------------------------------
|
|
38
|
+
同一个道理:颜色在强制色里会消失。这里用**圆点更大 + 标题更重**,
|
|
39
|
+
两条都是非颜色线索,且都不依赖 `::before` 里的字符
|
|
40
|
+
(`content: "●"` 会被算进可访问名,读屏念「圆点 …」,见 internal/ERRORS.md E189)。
|
|
41
|
+
========================================================================= */
|
|
42
|
+
|
|
43
|
+
.timeline {
|
|
44
|
+
/* 三个几何量,全部从这里派生 —— 圆点大小一改,圆心、轴线、连接线一起跟着走,
|
|
45
|
+
不会出现「圆点变大了但线还在原地」这种只差半像素、却怎么看怎么歪的错位。 */
|
|
46
|
+
--tl-dot: 8px; /* 圆点直径(最新一条改成 12px) */
|
|
47
|
+
--tl-axis: 4px; /* 轴线:圆点中心与连接线中心都落在这条竖线上 */
|
|
48
|
+
|
|
49
|
+
list-style: none; /* ol 的序号对时间轴是噪声:时间本身就是序号 */
|
|
50
|
+
margin: 0; /* 组件不依赖 page.css 的重置 */
|
|
51
|
+
padding: 0;
|
|
52
|
+
min-width: 0; /* 放进 flex/grid 列时不撑破(长标题会) */
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.timeline__item {
|
|
56
|
+
position: relative;
|
|
57
|
+
padding-inline-start: var(--sp-5);
|
|
58
|
+
padding-block-end: var(--sp-4);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* 最后一条不留尾巴 —— 否则列表下方凭空多出一截空白,
|
|
62
|
+
而它看起来像「还有一个事件没加载出来」。 */
|
|
63
|
+
.timeline__item:last-child {
|
|
64
|
+
padding-block-end: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* 连接线:**从自己的圆心画到下一个的圆心**,两头各由圆点盖住。
|
|
68
|
+
不用 `inset-block: 0` —— 那样第一条上方会多出一小截悬空的线
|
|
69
|
+
(从条目顶端到圆点之间),看起来像「上面还有一个事件没渲染出来」。
|
|
70
|
+
两个端点都写成同一个表达式,圆点尺寸一改两边自动跟上。 */
|
|
71
|
+
.timeline__item:not(:last-child)::before {
|
|
72
|
+
content: "";
|
|
73
|
+
position: absolute;
|
|
74
|
+
inset-inline-start: calc(var(--tl-axis) - 1px);
|
|
75
|
+
inset-block-start: calc(var(--fs-xs) * var(--lh-base) / 2);
|
|
76
|
+
inset-block-end: calc(var(--fs-xs) * var(--lh-base) / -2);
|
|
77
|
+
width: 2px;
|
|
78
|
+
background: var(--border-decor-str);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* 圆点:**没有 border**,所以尺寸与 box-sizing 无关
|
|
82
|
+
(本库的 box-sizing 是选配,挂在 `.page *` 上;组件不能假设它存在)。
|
|
83
|
+
纵向对齐的是**第一行时间的行盒中心**,不是整块的中心 ——
|
|
84
|
+
正文长短不一时,后者会让圆点忽上忽下。 */
|
|
85
|
+
.timeline__item::after {
|
|
86
|
+
content: "";
|
|
87
|
+
position: absolute;
|
|
88
|
+
inset-inline-start: calc(var(--tl-axis) - var(--tl-dot) / 2);
|
|
89
|
+
inset-block-start: calc(var(--fs-xs) * var(--lh-base) / 2 - var(--tl-dot) / 2);
|
|
90
|
+
width: var(--tl-dot);
|
|
91
|
+
height: var(--tl-dot);
|
|
92
|
+
border-radius: var(--r-full);
|
|
93
|
+
background: var(--accent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* 最新一条:圆点 12px。圆心由上面的 calc 自动保持不动 ——
|
|
97
|
+
否则连接线会在这一条拐个弯,看起来像画错了。 */
|
|
98
|
+
.timeline__item--latest {
|
|
99
|
+
--tl-dot: 12px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.timeline__item--latest .timeline__title {
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.timeline__time {
|
|
107
|
+
display: block; /* <time> 默认是行内,不换行会跟标题黏在一起 */
|
|
108
|
+
line-height: var(--lh-base); /* 显式给:圆点对齐算的就是这个行盒 */
|
|
109
|
+
color: var(--text-tertiary);
|
|
110
|
+
font-size: var(--fs-xs);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ⚠️ 日期要等宽对齐(竖着扫)的话,**给它加 class="num"** —— 本库的等宽数字
|
|
114
|
+
只有一处定义(`typography.css` 的 `.num`),组件自己再声明一遍就会漂移
|
|
115
|
+
(`numeric-gate.py` 守这一条)。
|
|
116
|
+
本组件不默认加:`.timeline__time` 也可能放“第三季度”这种非数字文本。 */
|
|
117
|
+
|
|
118
|
+
.timeline__title {
|
|
119
|
+
margin: 0; /* 标题的 margin 由本组件说了算,不由浏览器默认值 */
|
|
120
|
+
color: var(--text-primary);
|
|
121
|
+
font-weight: 500;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.timeline__body {
|
|
125
|
+
margin: var(--sp-1) 0 0;
|
|
126
|
+
color: var(--text-secondary);
|
|
127
|
+
font-size: var(--fs-sm);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* 紧凑档:时间与标题同一行,用于侧栏、卡片内嵌、弹层里的事记。 */
|
|
131
|
+
.timeline--compact .timeline__item {
|
|
132
|
+
padding-block-end: var(--sp-2);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.timeline--compact .timeline__time {
|
|
136
|
+
display: inline;
|
|
137
|
+
margin-inline-end: var(--sp-2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* 强制色:圆点与连接线改用**系统色**。
|
|
141
|
+
作者色在这里会被统一替换掉 —— 若不显式给系统色,
|
|
142
|
+
整条时间轴会变成「一排悬空的文字」,而 DOM 顺序还在(信息没丢,
|
|
143
|
+
但读者看不出这是一个序列)。系统色关键字不会被替换。 */
|
|
144
|
+
@media (forced-colors: active) {
|
|
145
|
+
.timeline__item::after {
|
|
146
|
+
background: CanvasText; /* 主标记:最亮的系统前景色 */
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.timeline__item:not(:last-child)::before {
|
|
150
|
+
background: GrayText; /* 连接线是次要结构,用弱一档的系统色 */
|
|
151
|
+
}
|
|
152
|
+
}
|