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.
- package/01-tokens/focus-ring-demo.html +1 -1
- package/01-tokens/page.css +32 -0
- package/01-tokens/responsive-demo.html +1 -1
- package/01-tokens/scrollbar-demo.html +1 -1
- package/02-primitives/badge/demo.html +1 -1
- package/02-primitives/button/demo.html +1 -1
- package/02-primitives/card/demo.html +1 -1
- package/02-primitives/choice/choice.css +31 -2
- package/02-primitives/choice/demo.html +1 -1
- package/02-primitives/combobox/demo.html +1 -1
- package/02-primitives/date-range/demo.html +1 -1
- package/02-primitives/delta/demo.html +1 -1
- package/02-primitives/input/demo.html +1 -1
- package/02-primitives/input/input.css +14 -0
- package/02-primitives/metric/demo.html +1 -1
- package/02-primitives/popover/demo.html +1 -1
- package/02-primitives/progress/demo.html +1 -1
- package/02-primitives/progress/progress.css +56 -0
- package/02-primitives/select/demo.html +1 -1
- package/02-primitives/separator/demo.html +1 -1
- package/02-primitives/skeleton/demo.html +1 -1
- package/02-primitives/skeleton/skeleton.css +18 -0
- package/02-primitives/switch/demo.html +1 -1
- 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/accordion/demo.html +1 -1
- package/03-patterns/content/demo.html +1 -1
- package/03-patterns/drawer/demo.html +1 -1
- package/03-patterns/drawer/drawer.js +30 -5
- package/03-patterns/dropdown/demo.html +1 -1
- package/03-patterns/form-validation/demo.html +1 -1
- package/03-patterns/list/demo.html +1 -1
- package/03-patterns/nav/demo.html +1 -1
- package/03-patterns/overlay/demo.html +1 -1
- package/03-patterns/pagination/demo.html +1 -1
- package/03-patterns/states/demo.html +1 -1
- package/03-patterns/tabs/demo.html +1 -1
- package/03-patterns/tooltip/demo.html +1 -1
- package/03-patterns/tree/demo.html +1 -1
- 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 +326 -0
- package/04-recipes/app-shell/shell-nav.js +288 -0
- package/04-recipes/table/table.css +22 -0
- package/09-assets/bar/demo.html +1 -1
- package/09-assets/echarts-adapter/demo.html +1 -1
- package/09-assets/model-viewer/demo.html +1 -1
- package/09-assets/sparkline/demo.html +1 -1
- package/AGENTS.md +8 -0
- package/API.md +4 -0
- package/CHANGELOG.md +795 -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/01-tokens/page.css +1 -1
- 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
|
@@ -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">
|
|
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
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# upload · 文件上传
|
|
2
|
+
|
|
3
|
+
> 本库表单控件一直是"缺一块"的状态:input / select / choice / switch 都有,
|
|
4
|
+
> 唯独没有"选文件"。于是每个项目自己糊一个 `<input type=file>`,
|
|
5
|
+
> 它长得和全站不一样,而且拖放区、多文件、失败原因全缺。
|
|
6
|
+
|
|
7
|
+
## 两层形态
|
|
8
|
+
|
|
9
|
+
| 形态 | 需要 JS | 得到什么 |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `.upload` | **不需要** | 原生 `<input type=file>` + 美化过的 `::file-selector-button`;原生文件名回显、表单提交、`accept` 过滤全走浏览器 |
|
|
12
|
+
| `.upload--drop` | 需要 | 在上面加一层拖放区 + 文件列表 + 大小/类型/数量校验 + 移除 |
|
|
13
|
+
|
|
14
|
+
`.upload--drop` 是**加**在基础形态上的一层,不是替代品。原生 `<input>` 始终在 DOM 里、
|
|
15
|
+
始终可聚焦、始终在表单里。
|
|
16
|
+
|
|
17
|
+
## 用法
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<!-- ① 基础:零 JS -->
|
|
21
|
+
<link rel="stylesheet" href="upload.css">
|
|
22
|
+
<input class="upload__input" type="file" id="f1" name="statement" accept=".csv,.xlsx">
|
|
23
|
+
<label class="field__label" for="f1">导入对账单</label>
|
|
24
|
+
|
|
25
|
+
<!-- ② 增强:拖放区 + 列表 -->
|
|
26
|
+
<link rel="stylesheet" href="upload.css">
|
|
27
|
+
<script src="upload.js"></script>
|
|
28
|
+
|
|
29
|
+
<div class="upload upload--drop" data-upload data-max-size="20971520" data-max-files="5">
|
|
30
|
+
<input class="upload__input" type="file" id="f2" name="files" multiple
|
|
31
|
+
accept=".pdf,.xlsx" aria-describedby="f2-hint">
|
|
32
|
+
<!-- 拖放区是 label for:点它 = 原生打开选择器,这一步不依赖 JS。
|
|
33
|
+
⚠️ 不要给它加 aria-hidden:加了它就从无障碍树里消失,
|
|
34
|
+
input 只能靠外面的 .field__label 拿名字 —— 少写一个就是 axe critical。
|
|
35
|
+
(0.18.0 的 axe 实测:对比块里漏了 .field__label ⇒ label 违规。) -->
|
|
36
|
+
<label class="upload__dropzone" for="f2">
|
|
37
|
+
<svg class="upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
38
|
+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
39
|
+
<path d="M8 11V2.6M4.4 6.2L8 2.6l3.6 3.6M2.6 11.4v1.2a.8.8 0 0 0 .8.8h9.2a.8.8 0 0 0 .8-.8v-1.2"/>
|
|
40
|
+
</svg>
|
|
41
|
+
<span class="upload__title">拖入文件,或 <span class="upload__browse">选择文件</span></span>
|
|
42
|
+
<span class="upload__hint">PDF / XLSX,单个不超过 20 MB</span>
|
|
43
|
+
</label>
|
|
44
|
+
<ul class="upload__list"></ul>
|
|
45
|
+
<span class="upload__status" role="status" aria-live="polite"></span>
|
|
46
|
+
</div>
|
|
47
|
+
<p class="field__hint" id="f2-hint">可以一次选多个</p>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 声明式属性(写在 `.upload` 根上)
|
|
51
|
+
|
|
52
|
+
| 属性 | 含义 | 默认 |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `data-upload` | 自动初始化标记 | — |
|
|
55
|
+
| `data-max-size` | 单文件上限(**字节**) | 0 = 不限 |
|
|
56
|
+
| `data-max-files` | 文件数上限 | 0 = 不限(`multiple` 时)/1(单选时) |
|
|
57
|
+
| `data-state` | **输出**:`dragover` 时存在 | — |
|
|
58
|
+
| `data-count` | **输出**:当前文件数(供"没选文件就不让提交"用) | `"0"` |
|
|
59
|
+
|
|
60
|
+
## JS API
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
var u = document.getElementById('f2').closest('.upload').uploadInstance;
|
|
64
|
+
// 或 new Upload(root, { maxSize: 20 * 1024 * 1024 })
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| 成员 | 说明 |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `add(files[, dirCount])` | 收下一批文件(= 拖放入口)。**走同一条校验**,返回是否至少收下一个 |
|
|
70
|
+
| `removeAt(i)` | 移除第 i 个;返回 false 表示浏览器不支持写 `input.files` |
|
|
71
|
+
| `setProgress(i, pct)` | `0–99` 上传中;`-1` 或 `>=100` 标记完成并去掉进度条 |
|
|
72
|
+
| `render()` | 从 `input.files` 重画列表(你手动改了 `files` 之后调它) |
|
|
73
|
+
| `destroy()` | 解绑全部监听 |
|
|
74
|
+
|
|
75
|
+
### 事件
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
document.addEventListener('fl-upload', function (e) {
|
|
79
|
+
e.target // .upload 根元素(认它,别认事件名 —— 嵌套组件会冒泡到一起)
|
|
80
|
+
e.detail // { files: File[], added: number, removed?: File, rejected: {file, reason, text}[] }
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`reason` 取值:`size` / `type` / `count` / `dir` / `unsupported`。
|
|
85
|
+
|
|
86
|
+
> ⚠️ **程序性改 `input.files` 不会触发原生 `change`**(这是规范,不是本库的选择)。
|
|
87
|
+
> 本组件不补发假 `change`(那会形成"渲染两遍"的循环),
|
|
88
|
+
> 所以 **HTML 表单校验请监听 `fl-upload`,不要监听 `change`**。
|
|
89
|
+
|
|
90
|
+
## 三条硬约定
|
|
91
|
+
|
|
92
|
+
### 1 · `input.files` 是唯一真值,列表是镜像
|
|
93
|
+
|
|
94
|
+
拖放不是"第二条通道":它把文件塞进 `input.files`,然后重画。
|
|
95
|
+
代价是**再次选择/拖入会替换整个列表**(与原生 `<input multiple>` 一致)。
|
|
96
|
+
不做"下一次追加"—— 否则同一个控件里存在两种累加规则,迟早打架。
|
|
97
|
+
|
|
98
|
+
### 2 · 校验只有一份实现
|
|
99
|
+
|
|
100
|
+
拖放与原生选择都调 `_validate`。写成两遍的后果不是"难看",是它们会**漂移**:
|
|
101
|
+
某天给拖放加了大小限制,忘了给原生选择加 ⇒ 同一页面上,
|
|
102
|
+
拖进去的被拒、点选进去的通过。这种 bug 只有用户会碰到。
|
|
103
|
+
|
|
104
|
+
`accept` 必须二次校验:它只约束**文件选择器**,拖放完全绕过它。
|
|
105
|
+
只写 `accept=".pdf"` 而拖入 `.exe` 时,浏览器不会拦 —— 这是规范行为。
|
|
106
|
+
|
|
107
|
+
### 3 · `DataTransfer` 不存在时,拖放明确拒绝
|
|
108
|
+
|
|
109
|
+
老 Safari(< 14.1)没有 `new DataTransfer()` ⇒ `input.files` 不可写。
|
|
110
|
+
此时照样渲染列表、照样派发事件,用户会看到"文件进去了",点提交却什么都没有,
|
|
111
|
+
**且不报错**。所以本组件直接提示"当前浏览器不支持拖放,请点选择文件",不接收。
|
|
112
|
+
|
|
113
|
+
## 无障碍
|
|
114
|
+
|
|
115
|
+
- **状态不只靠颜色**:左侧图形(空心圆 / 勾 / 叹号)+ 右侧文本("上传中 42%" / "2.4 MB" / "超过 20 MB")。
|
|
116
|
+
灰度打印、色盲、强制色三种条件下信息都不丢。
|
|
117
|
+
- **播报走单独的 `role="status"`**,不给 `<ul>` 加 `aria-live` ——
|
|
118
|
+
后者会把"进度 42% → 43%"每 200ms 打断一次。
|
|
119
|
+
同一句话连续播两次时读屏不再读,所以 `announce()` 先清空再延迟写入。
|
|
120
|
+
- **移除按钮的 `aria-label` 带文件名**("移除 2025年度审计报告.pdf"),
|
|
121
|
+
否则读屏用户听到一排"移除、移除、移除"。
|
|
122
|
+
- 拖放区是 `<label for>`,本身**不进 Tab 序列**(焦点属于 input);
|
|
123
|
+
聚焦时焦点环画在 dropzone 上,视觉上看到的是"这一块被选中了"。
|
|
124
|
+
- **可访问名字来自两处**:`.field__label`("上传审计底稿")与 dropzone 里的文字。
|
|
125
|
+
读屏会连着念,略啰嗦,但这是**安全的冗余** —— 只用 dropzone、不写 `.field__label`
|
|
126
|
+
的页面也有名字。反过来(dropzone 加 `aria-hidden`、靠 `.field__label` 给名)
|
|
127
|
+
看起来更干净,代价是少写一个 label 就变成 axe critical,而且没有任何门禁拦得住。
|
|
128
|
+
- 强制色模式下**不需要额外 media query**:边界全是 `border`(会自动变 CanvasText 实线),
|
|
129
|
+
图标全是内联 SVG + `currentColor`。这是 0.17.0 逐页实拍定下的两条规律。
|
|
130
|
+
|
|
131
|
+
## 何时**不该**用
|
|
132
|
+
|
|
133
|
+
- **需要断点续传 / 分片上传** ⇒ 这不是纯前端控件能解决的,本组件只负责"选与校验",
|
|
134
|
+
传输交给你的上传库。它不发送任何请求。
|
|
135
|
+
- **单个文件且没有校验** ⇒ 用基础形态(`.upload__input` 一行)就够了,不要套拖放区。
|
|
136
|
+
- **图片裁剪/预览** ⇒ 需要 canvas 预览与交互,超出本组件范围。
|
|
137
|
+
|
|
138
|
+
## 相关
|
|
139
|
+
|
|
140
|
+
- `progress` —— 列表里的进度条复用它;没引入时退化成空槽,不报错
|
|
141
|
+
- `field`(在 `input` 里)—— `field__label` / `field__hint` / `field__error` 的外层结构
|
|
142
|
+
- `form-validation` —— 提交前校验;错误提示用 `field__error` + `aria-describedby`
|