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
|
@@ -0,0 +1,322 @@
|
|
|
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>应用外壳 · app-shell</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="app-shell.css">
|
|
12
|
+
<!-- 抽屉的样式:只有第 6 节用到,但样式表要提前加载 -->
|
|
13
|
+
<link rel="stylesheet" href="../../03-patterns/drawer/drawer.css">
|
|
14
|
+
<style>
|
|
15
|
+
.demo {
|
|
16
|
+
background: var(--surface);
|
|
17
|
+
border: 1px solid var(--border-decor);
|
|
18
|
+
border-radius: var(--r-md);
|
|
19
|
+
padding: var(--sp-4);
|
|
20
|
+
margin-bottom: var(--sp-3);
|
|
21
|
+
}
|
|
22
|
+
/* demo 专用:给 shell 一个固定高度,否则 min-height: 100dvh 会把整页撑成一屏,
|
|
23
|
+
后面的章节要滚很久才看得到。真实页面**不要**加这个。 */
|
|
24
|
+
.shell-demo { min-height: 26rem; border-radius: var(--r-sm); overflow: hidden; }
|
|
25
|
+
.shell-demo--short { min-height: 15rem; }
|
|
26
|
+
.ph {
|
|
27
|
+
border: 1px dashed var(--border-decor);
|
|
28
|
+
border-radius: var(--r-sm);
|
|
29
|
+
padding: var(--sp-3);
|
|
30
|
+
color: var(--text-tertiary);
|
|
31
|
+
font-size: var(--fs-sm);
|
|
32
|
+
text-align: center;
|
|
33
|
+
}
|
|
34
|
+
.ph + .ph { margin-top: var(--sp-2); }
|
|
35
|
+
.wide { min-width: 30rem; } /* 对比块用:比栏宽更宽,足以撑破主区 */
|
|
36
|
+
.shell-note { font-size: var(--fs-xs); color: var(--text-tertiary); }
|
|
37
|
+
/* 320/393px 下对比块放不下两个 shell,宽内容会把整页撑出横滚。
|
|
38
|
+
这里收掉它 —— 那块对比在窄屏上失去意义是**可接受**的,
|
|
39
|
+
但让页面横滚不可接受(skel-lever / zoom-reflow 判这一条)。 */
|
|
40
|
+
@media (max-width: 30rem) {
|
|
41
|
+
.wide { min-width: 0; }
|
|
42
|
+
}
|
|
43
|
+
.shell__link svg { flex: none; }
|
|
44
|
+
</style>
|
|
45
|
+
</head>
|
|
46
|
+
<body class="page">
|
|
47
|
+
<main class="wrap">
|
|
48
|
+
|
|
49
|
+
<h1>应用外壳 · app-shell</h1>
|
|
50
|
+
<p class="lede">工具型页面的骨架:左边一列常驻导航、上面一条工具栏、右边是主区。
|
|
51
|
+
它存在的理由不是"少写几个 div",而是**两个每个人都会踩的坑** —— 见下面两节。</p>
|
|
52
|
+
|
|
53
|
+
<h2>1 · 标准三段式</h2>
|
|
54
|
+
<div class="demo">
|
|
55
|
+
<!-- ⚠️ 这里用 <div class="shell__main"> 而不是 <main>:本页已经有一个
|
|
56
|
+
<main class="wrap">(页面骨架),HTML 不允许嵌套 main。
|
|
57
|
+
真实项目里这一格就应该是 <main> —— 那时候 shell 是整个页面的容器。 -->
|
|
58
|
+
<div class="shell shell-demo">
|
|
59
|
+
<header class="shell__topbar">
|
|
60
|
+
<h2 class="shell__title">应付账款 · 2025 年度</h2>
|
|
61
|
+
<div class="shell__topbar-end">
|
|
62
|
+
<span class="shell-note">已同步 5 分钟前</span>
|
|
63
|
+
</div>
|
|
64
|
+
</header>
|
|
65
|
+
|
|
66
|
+
<nav class="shell__side" aria-label="主导航">
|
|
67
|
+
<a class="shell__brand" href="#">金标准 · 财务台</a>
|
|
68
|
+
<ul class="shell__side-group">
|
|
69
|
+
<li><a class="shell__link" href="#" aria-current="page">总览</a></li>
|
|
70
|
+
<li><a class="shell__link" href="#">实体</a></li>
|
|
71
|
+
<li><a class="shell__link" href="#">勾稽校验</a></li>
|
|
72
|
+
</ul>
|
|
73
|
+
<p class="shell__side-label">数据</p>
|
|
74
|
+
<ul class="shell__side-group">
|
|
75
|
+
<li><a class="shell__link" href="#">导入</a></li>
|
|
76
|
+
<li><a class="shell__link" href="#">审计日志</a></li>
|
|
77
|
+
</ul>
|
|
78
|
+
</nav>
|
|
79
|
+
|
|
80
|
+
<div class="shell__main">
|
|
81
|
+
<div class="shell__content">
|
|
82
|
+
<div class="ph">主区内容</div>
|
|
83
|
+
<div class="ph">主区内容</div>
|
|
84
|
+
<div class="ph">主区内容</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<p class="note">当前页靠<b>三条</b>线索标记:左侧 3px 竖条、底色、字重加一档 ——
|
|
90
|
+
不只靠底色,灰度打印也分得出来。而且它同时带了
|
|
91
|
+
<code>aria-current="page"</code>,读屏用户听到的是"总览,当前页"。</p>
|
|
92
|
+
|
|
93
|
+
<h2>2 · 坑之一:宽内容会撑破主区(两个保险)</h2>
|
|
94
|
+
<div class="cmp">
|
|
95
|
+
<!-- 反例栏抬到上一层:不然它溢出的那截会被右栏的底色盖住,"撑破"在截图里就看不见了(0.18.0 实测)。 -->
|
|
96
|
+
<div class="cmp__pane cmp__bad" data-bad=".shell__main|w|>420" style="position:relative;z-index:1">
|
|
97
|
+
<h3>❌ 常见写法:宽表格直接放主区</h3>
|
|
98
|
+
<!-- 反例 = 手动拆掉本库的两个保险(inline 覆盖,不改 app-shell.css):
|
|
99
|
+
· grid-template-columns 从 `minmax(0, 1fr)` 退回 `1fr`
|
|
100
|
+
· 主区的 min-width 从 0 退回默认的 auto
|
|
101
|
+
宽表格也没有自己的滚动容器。三样都不做,才是"每个项目自己糊一个外壳"的真实样子。 -->
|
|
102
|
+
<div class="shell shell-demo--short"
|
|
103
|
+
style="--shell-side-w:5rem;grid-template-columns:5rem 1fr">
|
|
104
|
+
<nav class="shell__side" aria-label="主导航(反例)">
|
|
105
|
+
<a class="shell__link" href="#" aria-current="page">总览</a>
|
|
106
|
+
</nav>
|
|
107
|
+
<!-- background:transparent 只是为了让溢出的那截**不遮住右栏的文字**,好让读者看见"越界"本身;撑破的机制与底色无关。 -->
|
|
108
|
+
<div class="shell__main" style="min-width:auto;background:transparent">
|
|
109
|
+
<div class="wide ph">一行 30rem 宽的表格式内容</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<p class="cmp__why">两件事都没做:<code>min-width</code> 保持默认的 <code>auto</code>
|
|
113
|
+
("不小于内容的最小宽度"),宽表格也没有自己的滚动容器。
|
|
114
|
+
于是 grid 轨道被内容顶开,<b>主区被撑到栏外</b> ——
|
|
115
|
+
侧栏、顶栏一起被推出可视区。</p>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="cmp__pane cmp__good" data-good=".shell__main|w|<=420">
|
|
118
|
+
<h3>✅ 本库:<code>min-width: 0</code> + <code>.shell__scroll</code></h3>
|
|
119
|
+
<div class="shell shell-demo--short" style="--shell-side-w:5rem">
|
|
120
|
+
<nav class="shell__side" aria-label="主导航(正例)">
|
|
121
|
+
<a class="shell__link" href="#" aria-current="page">总览</a>
|
|
122
|
+
</nav>
|
|
123
|
+
<div class="shell__main">
|
|
124
|
+
<div class="shell__scroll" tabindex="0" role="region" aria-label="宽表格,可横向滚动"><div class="wide ph">一行 30rem 宽的表格式内容</div></div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<p class="cmp__why">主区宽度锁在栏宽内(<code>min-width: 0</code>),宽内容放进
|
|
128
|
+
<code>.shell__scroll</code> 里自己横滚 —— 侧栏和顶栏不动,页面不横滚。</p>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<p class="note">这就是 <code>.shell__main { min-width: 0 }</code> 存在的全部理由。
|
|
132
|
+
它是整个组件里<b>最容易漏、后果最大</b>的一行:漏了以后页面在 1280px 屏幕上看着正常,
|
|
133
|
+
一旦数据表多两列就横滚 —— 而且你会先怀疑表格,不会怀疑外壳。</p>
|
|
134
|
+
|
|
135
|
+
<h2>3 · 坑之二:高度用 <code>100dvh</code>,不是 <code>100vh</code></h2>
|
|
136
|
+
<div class="demo">
|
|
137
|
+
<p class="note" style="margin-top:0">这一条<b>在桌面浏览器上看不出来</b>,只能在手机上看到,
|
|
138
|
+
所以没有截图可摆 —— 但它是本组件写 <code>min-height: 100vh; min-height: 100dvh</code> 两行的原因。</p>
|
|
139
|
+
<ul>
|
|
140
|
+
<li><code>100vh</code> 取的是<b>地址栏收起时</b>的视口高(最大那个)</li>
|
|
141
|
+
<li>地址栏一展开,可视区域变小,而元素仍然按大的算 ⇒ <b>底部被裁掉一截</b></li>
|
|
142
|
+
<li>iOS Safari 上最明显:页面最后一个按钮点不到,而且不出现滚动条</li>
|
|
143
|
+
</ul>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<h2>4 · 侧栏更窄:只有图标</h2>
|
|
147
|
+
<div class="demo">
|
|
148
|
+
<div class="shell shell--rail shell-demo--short">
|
|
149
|
+
<header class="shell__topbar">
|
|
150
|
+
<h2 class="shell__title">窄侧栏形态</h2>
|
|
151
|
+
</header>
|
|
152
|
+
<nav class="shell__side" aria-label="主导航(图标)">
|
|
153
|
+
<!-- 品牌在 72px 里放不下名字 ⇒ 只留标记,名字视觉隐藏(不是删掉)。
|
|
154
|
+
标记与文字分成两段是**契约要求**,见 README「图标态的父级元素」。 -->
|
|
155
|
+
<a class="shell__brand" href="#">
|
|
156
|
+
<svg class="shell__brand-mark" viewBox="0 0 16 16" width="20" height="20" fill="none"
|
|
157
|
+
stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" aria-hidden="true"><path d="M8 1.6l5.4 3.1v6.6L8 14.4 2.6 11.3V4.7z"/></svg>
|
|
158
|
+
<span class="shell__brand-text">金标准 · 财务台</span>
|
|
159
|
+
</a>
|
|
160
|
+
<!-- 分组标签在 72px 里放不下「数据 / 校验」这种词,折成两行会把三条图标挤散。
|
|
161
|
+
⇒ 图标态**不用** `.shell__side-label`,改让两个 `.shell__side-group` 相邻:
|
|
162
|
+
`+` 那条规则会自动给出一条分隔线与间距(见 README「图标态的分组」)。 -->
|
|
163
|
+
<ul class="shell__side-group">
|
|
164
|
+
<li>
|
|
165
|
+
<a class="shell__link" href="#" aria-current="page" title="总览">
|
|
166
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor"
|
|
167
|
+
stroke-width="1.4" aria-hidden="true"><path d="M2 2h5v5H2zM9 2h5v5H9zM2 9h5v5H2zM9 9h5v5H9z"/></svg>
|
|
168
|
+
<span class="shell__link-label">总览</span>
|
|
169
|
+
</a>
|
|
170
|
+
</li>
|
|
171
|
+
<li>
|
|
172
|
+
<a class="shell__link" href="#" title="实体">
|
|
173
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor"
|
|
174
|
+
stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><path d="M2.5 4h11M2.5 8h11M2.5 12h7"/></svg>
|
|
175
|
+
<span class="shell__link-label">实体</span>
|
|
176
|
+
</a>
|
|
177
|
+
</li>
|
|
178
|
+
</ul>
|
|
179
|
+
<ul class="shell__side-group">
|
|
180
|
+
<li>
|
|
181
|
+
<a class="shell__link" href="#" title="校验">
|
|
182
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor"
|
|
183
|
+
stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 8.5l3.5 3.5 7-8"/></svg>
|
|
184
|
+
<span class="shell__link-label">勾稽校验</span>
|
|
185
|
+
</a>
|
|
186
|
+
</li>
|
|
187
|
+
</ul>
|
|
188
|
+
</nav>
|
|
189
|
+
<div class="shell__main">
|
|
190
|
+
<div class="shell__content"><div class="ph">主区</div></div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
<p class="note">图标态的名字<b>必须</b>来自可见文字 <code>.shell__link-label</code> ——
|
|
195
|
+
它被视觉隐藏(1px 盒 + 裁切)但仍在可访问树里,读屏念的是「总览」而不是「链接」。
|
|
196
|
+
改用 <code>aria-label</code> 也能让读屏念对,但那是<b>第二份真值</b>:改了可见文字忘了改它,
|
|
197
|
+
念的还是旧的;更关键的是抽屉里那份克隆<b>补不回文字</b>(克隆离开
|
|
198
|
+
<code>.shell--rail</code> 作用域,隐藏规则失效,但 <code>aria-label</code> 不会变成可见文字)。
|
|
199
|
+
<code>title</code> 只负责鼠标悬停提示,不负责名字。</p>
|
|
200
|
+
|
|
201
|
+
<h2>5 · 主区自己滚(看板型)</h2>
|
|
202
|
+
<div class="demo">
|
|
203
|
+
<div class="shell shell--scroll-main shell-demo">
|
|
204
|
+
<header class="shell__topbar">
|
|
205
|
+
<h2 class="shell__title">只有主区滚动</h2>
|
|
206
|
+
</header>
|
|
207
|
+
<nav class="shell__side" aria-label="主导航(看板)">
|
|
208
|
+
<ul class="shell__side-group">
|
|
209
|
+
<li><a class="shell__link" href="#" aria-current="page">看板</a></li>
|
|
210
|
+
<li><a class="shell__link" href="#">报表</a></li>
|
|
211
|
+
</ul>
|
|
212
|
+
</nav>
|
|
213
|
+
<div class="shell__main">
|
|
214
|
+
<div class="shell__content">
|
|
215
|
+
<div class="ph">内容 1</div><div class="ph">内容 2</div><div class="ph">内容 3</div>
|
|
216
|
+
<div class="ph">内容 4</div><div class="ph">内容 5</div><div class="ph">内容 6</div>
|
|
217
|
+
<div class="ph">内容 7</div><div class="ph">内容 8</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
<p class="note"><code>.shell--scroll-main</code> 让侧栏与顶栏完全不动,只有主区滚 ——
|
|
223
|
+
适合看板、监控墙。⚠️ 用这个变体时<b>不要</b>再让页面本身滚:两个滚动容器会让
|
|
224
|
+
"滚到底"的行为不可预期(用户滚的是哪一个?)。</p>
|
|
225
|
+
|
|
226
|
+
<h2>6 · 窄屏:侧栏收走,顶栏出一个菜单按钮</h2>
|
|
227
|
+
<div class="demo">
|
|
228
|
+
<p class="note" style="margin-top:0">把浏览器窗口缩到 <b>768px 以下</b>。侧栏整列消失,
|
|
229
|
+
主区铺满,顶栏起始边出现一个 ☰ 按钮 —— 点它打开抽屉。
|
|
230
|
+
<b>Esc 关、点遮罩关、关闭后焦点回到按钮上</b>,这三件事来自
|
|
231
|
+
<code>03-patterns/drawer</code>,本组件只做接线(<code>shell-nav.js</code>)。</p>
|
|
232
|
+
|
|
233
|
+
<div class="shell shell-demo" data-shell-nav="主导航">
|
|
234
|
+
<header class="shell__topbar">
|
|
235
|
+
<h2 class="shell__title">应付账款 · 2025 年度</h2>
|
|
236
|
+
<div class="shell__topbar-end">
|
|
237
|
+
<span class="shell-note">已同步 5 分钟前</span>
|
|
238
|
+
</div>
|
|
239
|
+
</header>
|
|
240
|
+
<!-- ⚠️ 这一页有七个外壳,所以每个 <nav> 的 aria-label 必须互不相同:
|
|
241
|
+
同一页面出现两个同名 landmark 是 axe 的 landmark-unique 违规。
|
|
242
|
+
真实页面只有一个外壳,不会遇到 —— 这是 demo 页自己的问题。 -->
|
|
243
|
+
<nav class="shell__side" aria-label="主导航(窄屏演示)">
|
|
244
|
+
<a class="shell__brand" href="#">金标准 · 财务台</a>
|
|
245
|
+
<ul class="shell__side-group">
|
|
246
|
+
<li><a class="shell__link" href="#" aria-current="page">总览</a></li>
|
|
247
|
+
<li><a class="shell__link" href="#">实体</a></li>
|
|
248
|
+
<li><a class="shell__link" href="#">勾稽校验</a></li>
|
|
249
|
+
</ul>
|
|
250
|
+
<p class="shell__side-label">数据</p>
|
|
251
|
+
<ul class="shell__side-group">
|
|
252
|
+
<li><a class="shell__link" href="#">导入</a></li>
|
|
253
|
+
<li><a class="shell__link" href="#">审计日志</a></li>
|
|
254
|
+
</ul>
|
|
255
|
+
</nav>
|
|
256
|
+
<div class="shell__main">
|
|
257
|
+
<div class="shell__content">
|
|
258
|
+
<div class="ph">缩到 768px 以下,左上角会多出一个 ☰</div>
|
|
259
|
+
<div class="ph">抽屉里的列表是侧栏的<b>克隆</b>,当前页标记也一起带过去</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
<p class="note">上面第 1–5 节<b>没有</b>引 <code>shell-nav.js</code>,所以它们缩窄后没有按钮,
|
|
265
|
+
侧栏直接消失 —— 缩窄窗口看看就知道了:<b>一个入口都没有</b>。
|
|
266
|
+
所以这层脚本在 START-HERE 里归「必需」,不是"可选增强":
|
|
267
|
+
"宽屏没有它一切照旧"只对桌面成立,而用户在手机上就走不出去了。</p>
|
|
268
|
+
<p class="note">⚠️ 别在这里自己写一个滑出来的侧栏。抽屉要的不是"滑出来",
|
|
269
|
+
而是<b>焦点陷阱 / 焦点归位 / 背景 inert / 锁滚动 / Esc</b> 五件事。
|
|
270
|
+
自己写必然漏两件,而漏掉的那两件在鼠标用户眼里完全正常。</p>
|
|
271
|
+
|
|
272
|
+
<h2>7 · 窄侧栏 + 抽屉:图标在桌面,文字在抽屉</h2>
|
|
273
|
+
<div class="demo">
|
|
274
|
+
<p class="note" style="margin-top:0">把窗口缩到 <b>768px 以下</b>再点 ☰:
|
|
275
|
+
抽屉里是<b>图标 + 文字</b>。同一份标记 —— 不是第二份导航,也没有 JS 分支。</p>
|
|
276
|
+
|
|
277
|
+
<div class="shell shell--rail shell-demo--short" data-shell-nav="图标导航">
|
|
278
|
+
<header class="shell__topbar">
|
|
279
|
+
<h2 class="shell__title">图标导航</h2>
|
|
280
|
+
</header>
|
|
281
|
+
<!-- ⚠️ 本页第七个外壳,nav 名字必须唯一(landmark-unique) -->
|
|
282
|
+
<nav class="shell__side" aria-label="主导航(图标 · 抽屉演示)">
|
|
283
|
+
<a class="shell__link" href="#" aria-current="page" title="总览">
|
|
284
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor"
|
|
285
|
+
stroke-width="1.4" aria-hidden="true"><path d="M2 2h5v5H2zM9 2h5v5H9zM2 9h5v5H2zM9 9h5v5H9z"/></svg>
|
|
286
|
+
<span class="shell__link-label">总览</span>
|
|
287
|
+
</a>
|
|
288
|
+
<a class="shell__link" href="#" title="实体">
|
|
289
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor"
|
|
290
|
+
stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><path d="M2.5 4h11M2.5 8h11M2.5 12h7"/></svg>
|
|
291
|
+
<span class="shell__link-label">实体</span>
|
|
292
|
+
</a>
|
|
293
|
+
<a class="shell__link" href="#" title="校验">
|
|
294
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor"
|
|
295
|
+
stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 8.5l3.5 3.5 7-8"/></svg>
|
|
296
|
+
<span class="shell__link-label">勾稽校验</span>
|
|
297
|
+
</a>
|
|
298
|
+
</nav>
|
|
299
|
+
<div class="shell__main">
|
|
300
|
+
<div class="shell__content">
|
|
301
|
+
<div class="ph">桌面:侧栏只有图标(宽 4.5rem)</div>
|
|
302
|
+
<div class="ph">窄屏:点 ☰,抽屉里是图标 + 文字</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
<p class="note">文字包在 <code>.shell__link-label</code> 里:rail 状态下它被<b>视觉隐藏</b>,
|
|
308
|
+
但仍在可访问树里(读屏念「总览」,不是「链接」)。抽屉里的克隆被挂到
|
|
309
|
+
<code><body></code> 下,<b>离开了 <code>.shell--rail</code> 的作用域</b>,
|
|
310
|
+
隐藏规则自然不再命中 —— 文字自己就回来了。所以<b>不能</b>把文字删掉、
|
|
311
|
+
也不能用 <code>display: none</code>:前者抽屉里只剩一排图标,后者连名字都没了。</p>
|
|
312
|
+
<p class="note">只做纯图标栏、不接抽屉的话,第 4 节那种写法(图标 + <code>aria-label</code>)
|
|
313
|
+
也成立;但只要你会用 <code>data-shell-nav</code>,就把文字留在 DOM 里 ——
|
|
314
|
+
一份标记同时满足两种形态。</p>
|
|
315
|
+
|
|
316
|
+
</main>
|
|
317
|
+
<!-- shell-nav.js 依赖 Drawer(抽屉的行为层)。顺序不能反:
|
|
318
|
+
少了 drawer.js 时 shell-nav.js 会 warn 并跳过,不注入按钮。 -->
|
|
319
|
+
<script src="../../03-patterns/drawer/drawer.js"></script>
|
|
320
|
+
<script src="shell-nav.js"></script>
|
|
321
|
+
</body>
|
|
322
|
+
</html>
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shell-nav.js — app-shell 的窄屏导航(行为层)
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 它**不是**"可选增强",虽然我第一版是这么写的。判据很简单:
|
|
5
|
+
* 没有它时,`app-shell.css` 在 ≤47.99rem 把侧栏设成 `display: none`,
|
|
6
|
+
* 页面上就**一个导航入口都没有**。"宽屏没有它一切照旧"这句话只对桌面成立,
|
|
7
|
+
* 而"桌面看着能用、手机上走不出去"正是本库一直在治的那类缺陷。
|
|
8
|
+
* ⇒ START-HERE 里归「必需」,与 tabs / drawer / nav 同档。
|
|
9
|
+
*
|
|
10
|
+
* 语法刻意用 ES5(无箭头函数 / const / let / 模板字符串 / Element.closest)——
|
|
11
|
+
* 与 nav.js / tabs.js / accordion.js 一致,精简档 WebView 的版本不确定。
|
|
12
|
+
*
|
|
13
|
+
* ==========================================================================
|
|
14
|
+
* 🔴 为什么需要这一层
|
|
15
|
+
* --------------------------------------------------------------------------
|
|
16
|
+
* `app-shell.css` 在 ≤47.99rem 把 `.shell__side` 直接设成 `display: none`。
|
|
17
|
+
* 侧栏一收,用户**没有任何入口**去别的页面 —— 只读看板可以接受,
|
|
18
|
+
* 管理型后台不可以。
|
|
19
|
+
*
|
|
20
|
+
* 在此之前文档只写了一句"要抽屉就把 `.shell__side` 里的内容搬进 drawer 实例"。
|
|
21
|
+
* 但"搬"是六十行胶水:建按钮、克隆列表、断点判断、宽屏复位、
|
|
22
|
+
* 缺依赖时别装死。每个使用者抄一遍,抄出来的版本各有各的漏 ——
|
|
23
|
+
* 这正是 nav.js 当年被做出来的同一个理由。
|
|
24
|
+
*
|
|
25
|
+
* ==========================================================================
|
|
26
|
+
* 🔴 为什么不复用 03-patterns/nav 的行为层
|
|
27
|
+
* --------------------------------------------------------------------------
|
|
28
|
+
* `nav.js` 处理的是"横排导航 → 全屏抽屉":它的列表**常驻在文档里**
|
|
29
|
+
* (桌面端就是那条横排导航),抽屉和列表是**同一个节点**,只是移动位置。
|
|
30
|
+
*
|
|
31
|
+
* 而 app-shell 的侧栏在窄屏是 `display: none` —— 它不需要"移动",
|
|
32
|
+
* 需要的是"另建一份显示出来"。生命周期正好相反(常驻 vs 即用即毁)。
|
|
33
|
+
*
|
|
34
|
+
* ⇒ 用 `03-patterns/drawer`:它已经把 Esc、焦点陷阱、焦点归位、
|
|
35
|
+
* 背景 inert、锁滚动五件事做完了。本层只做**接线**,一行行为逻辑都不重写。
|
|
36
|
+
* nav.js 拒绝复用 drawer.js 的理由("不能开一次建一个")在这里不成立,
|
|
37
|
+
* 因为窄屏下那棵列表本来就看不见。
|
|
38
|
+
*
|
|
39
|
+
* ==========================================================================
|
|
40
|
+
* 🔴 断点不写在 JS 里
|
|
41
|
+
* --------------------------------------------------------------------------
|
|
42
|
+
* "什么时候该有抽屉"的唯一判据是**菜单按钮还在不在**
|
|
43
|
+
* (`btn.offsetParent === null`)。按钮的显隐由 CSS 决定,JS 只读结果。
|
|
44
|
+
*
|
|
45
|
+
* 写死 47.99rem 意味着 CSS 改一个数,JS 就静默错位 —— 而且错位的方式是
|
|
46
|
+
* "宽屏上抽屉还能打开、窄屏上反而打不开",从截图上完全看不出来。
|
|
47
|
+
* 这是 nav.js 契约 ⑥ 的同一个教训。
|
|
48
|
+
*
|
|
49
|
+
* ==========================================================================
|
|
50
|
+
* 契约(每条都能被测出来)
|
|
51
|
+
* --------------------------------------------------------------------------
|
|
52
|
+
* ① **opt-in**:只有 `.shell[data-shell-nav]` 才被接管(值 = 抽屉标题)。
|
|
53
|
+
* 没写这个属性 = 一个按钮都不注入 —— 别人的顶栏布局不该被悄悄改。
|
|
54
|
+
* ② 依赖缺失时**不注入按钮**:`window.Drawer` 不在就 warn 后跳过。
|
|
55
|
+
* 🔴 宁可没有按钮,也不要一个点了没反应的按钮:
|
|
56
|
+
* 窄屏下用户会反复点,然后认为整个页面坏了(比"没有导航"更糟)。
|
|
57
|
+
* ③ 按钮插在 `.shell__topbar` 的**第一个**子节点(贴着左边/起始边),
|
|
58
|
+
* 带 `aria-haspopup="dialog"`,名字是 `打开<标题>`。
|
|
59
|
+
* ④ 打开时**克隆**侧栏,不移动:桌面端还在用原节点;移动的话
|
|
60
|
+
* 抽屉一关(即用即毁)侧栏就永远回不来了。
|
|
61
|
+
* 克隆里所有 `id` 一律剥掉 —— 否则文档里出现两份同 id,
|
|
62
|
+
* `aria-labelledby` / `label[for]` 会指到克隆那份上。
|
|
63
|
+
* ⑤ 初始焦点落在 `[aria-current="page"]`(用户一眼看到自己在哪),
|
|
64
|
+
* 没有当前项时由 drawer 兜底到第一个可聚焦元素。
|
|
65
|
+
* ⑥ 点抽屉里的链接**先关**再走:SPA 里不关会留下一个盖住页面的遮罩,
|
|
66
|
+
* 而焦点还被锁在它里面 ⇒ 用户以为"点了没反应"。
|
|
67
|
+
* ⑦ 视口变宽到侧栏回来时**自动关掉**抽屉,否则两个导航同时存在。
|
|
68
|
+
* ⑧ 幂等:`root.shellNavInstance` 已存在就跳过(SPA 路由 / HMR / 重复引入)。
|
|
69
|
+
*
|
|
70
|
+
* ==========================================================================
|
|
71
|
+
* 用法
|
|
72
|
+
* --------------------------------------------------------------------------
|
|
73
|
+
* <div class="shell" data-shell-nav="主导航">
|
|
74
|
+
* <header class="shell__topbar">…</header> <!-- 按钮插在这里 -->
|
|
75
|
+
* <nav class="shell__side">…</nav>
|
|
76
|
+
* <main class="shell__main">…</main>
|
|
77
|
+
* </div>
|
|
78
|
+
*
|
|
79
|
+
* <link rel="stylesheet" href="drawer.css"> <!-- 抽屉的样子 -->
|
|
80
|
+
* <script src="drawer.js"></script> <!-- 抽屉的行为(必需) -->
|
|
81
|
+
* <script src="shell-nav.js"></script> <!-- 有 data-shell-nav 就自动初始化 -->
|
|
82
|
+
*
|
|
83
|
+
* 手动:new ShellNav(rootEl, { label: '打开菜单', place: 'left' })
|
|
84
|
+
* 事件:fl-shell-nav(detail = { open: true|false }),派发在 `.shell` 上
|
|
85
|
+
*/
|
|
86
|
+
(function (global) {
|
|
87
|
+
'use strict';
|
|
88
|
+
|
|
89
|
+
/* ==== BEHAVIOR INJECT BEGIN: emit ==== */
|
|
90
|
+
var flEmit = function (el, name, detail) {
|
|
91
|
+
if (!el || !el.dispatchEvent) return null;
|
|
92
|
+
var type = name.indexOf('fl-') === 0 ? name : 'fl-' + name;
|
|
93
|
+
var ev = null;
|
|
94
|
+
/* 老 WebView 没有 CustomEvent 构造函数 ⇒ 兜底走 createEvent(理由见文件头)*/
|
|
95
|
+
if (typeof window.CustomEvent === 'function') {
|
|
96
|
+
try {
|
|
97
|
+
ev = new window.CustomEvent(type, {
|
|
98
|
+
detail: detail || null, bubbles: true, cancelable: false
|
|
99
|
+
});
|
|
100
|
+
} catch (e) { ev = null; }
|
|
101
|
+
}
|
|
102
|
+
if (!ev) {
|
|
103
|
+
try {
|
|
104
|
+
ev = document.createEvent('CustomEvent');
|
|
105
|
+
ev.initCustomEvent(type, true, false, detail || null);
|
|
106
|
+
} catch (e2) { return null; }
|
|
107
|
+
}
|
|
108
|
+
el.dispatchEvent(ev);
|
|
109
|
+
return ev;
|
|
110
|
+
};
|
|
111
|
+
/* ==== BEHAVIOR INJECT END: emit ==== */
|
|
112
|
+
|
|
113
|
+
var ICON =
|
|
114
|
+
'<svg viewBox="0 0 16 16" width="18" height="18" fill="none" ' +
|
|
115
|
+
'stroke="currentColor" stroke-width="1.6" stroke-linecap="round" ' +
|
|
116
|
+
'aria-hidden="true" focusable="false"><path d="M2.5 4h11M2.5 8h11M2.5 12h11"/></svg>';
|
|
117
|
+
|
|
118
|
+
function warn(msg) {
|
|
119
|
+
if (global.console && global.console.warn) global.console.warn(msg);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 剥掉克隆里所有 id。
|
|
124
|
+
*
|
|
125
|
+
* ⚠️ 不能只剥根节点的:侧栏里的链接、分组、`aria-labelledby` 的目标都可能有 id。
|
|
126
|
+
* 两份同 id 的 DOM 里,`document.getElementById` 只会返回**文档里靠前的那份**,
|
|
127
|
+
* 于是抽屉里那些指向 id 的引用全部指到桌面端那份上 —— 而桌面端那份
|
|
128
|
+
* 在窄屏是 `display: none`,读屏读到的是一段不可见的文字。
|
|
129
|
+
*/
|
|
130
|
+
function stripIds(node) {
|
|
131
|
+
if (node.removeAttribute) node.removeAttribute('id');
|
|
132
|
+
var all = node.querySelectorAll ? node.querySelectorAll('[id]') : [];
|
|
133
|
+
for (var i = 0; i < all.length; i++) all[i].removeAttribute('id');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ShellNav(root, opts) {
|
|
137
|
+
if (!root) throw new Error('ShellNav: 缺少根元素');
|
|
138
|
+
var o = this.opts = opts || {};
|
|
139
|
+
var self = this;
|
|
140
|
+
|
|
141
|
+
this.root = root;
|
|
142
|
+
this.btn = null;
|
|
143
|
+
this.handle = null;
|
|
144
|
+
this.disabled = false;
|
|
145
|
+
|
|
146
|
+
var side = root.querySelector('.shell__side');
|
|
147
|
+
var topbar = root.querySelector('.shell__topbar');
|
|
148
|
+
|
|
149
|
+
if (!side || !topbar) {
|
|
150
|
+
this.disabled = true;
|
|
151
|
+
warn('shell-nav: 缺少 ' + (!side ? '.shell__side' : '.shell__topbar') +
|
|
152
|
+
',已跳过该元素(不改别人的布局)。');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ② 依赖检查放在最前:缺 Drawer 就什么都不做。
|
|
157
|
+
放在"建完按钮再检查"的位置,会留下一个点不动的按钮。 */
|
|
158
|
+
if (!global.Drawer || typeof global.Drawer.open !== 'function') {
|
|
159
|
+
this.disabled = true;
|
|
160
|
+
warn('shell-nav: 需要先加载 03-patterns/drawer/drawer.js。' +
|
|
161
|
+
'本层只做接线,抽屉的 Esc / 焦点陷阱 / 焦点归位 / 锁滚动都由它提供。' +
|
|
162
|
+
'已跳过,不注入菜单按钮。');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.side = side;
|
|
167
|
+
this.topbar = topbar;
|
|
168
|
+
|
|
169
|
+
/* ③ 按钮:插在顶栏第一个子节点(起始边)。
|
|
170
|
+
`aria-haspopup="dialog"` 告诉读屏"点了会出来一个对话框",
|
|
171
|
+
"导航菜单"这类措辞留给真正的 menu/menubar,那需要方向键语义。 */
|
|
172
|
+
var title = root.getAttribute('data-shell-nav') || '导航';
|
|
173
|
+
var btn = document.createElement('button');
|
|
174
|
+
btn.type = 'button';
|
|
175
|
+
btn.className = 'shell__menu';
|
|
176
|
+
btn.setAttribute('aria-label', o.label || ('打开' + title));
|
|
177
|
+
btn.setAttribute('aria-haspopup', 'dialog');
|
|
178
|
+
btn.innerHTML = ICON;
|
|
179
|
+
btn.addEventListener('click', function () { self.open(); });
|
|
180
|
+
topbar.insertBefore(btn, topbar.firstChild);
|
|
181
|
+
this.btn = btn;
|
|
182
|
+
|
|
183
|
+
/* ⑦ 宽屏复位。只在抽屉开着时才判断,避免无谓的 layout 读取。 */
|
|
184
|
+
this.onResize = function () {
|
|
185
|
+
if (!self.handle) return;
|
|
186
|
+
/* 判据 = 菜单按钮还在不在(CSS 决定),不是数字 —— 见文件头。 */
|
|
187
|
+
if (self.btn && self.btn.offsetParent === null) self.close();
|
|
188
|
+
};
|
|
189
|
+
global.addEventListener('resize', this.onResize);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
ShellNav.prototype = {
|
|
193
|
+
constructor: ShellNav,
|
|
194
|
+
|
|
195
|
+
/** 打开抽屉。已开着就直接返回现有句柄(重复点不该叠两层遮罩)。 */
|
|
196
|
+
open: function () {
|
|
197
|
+
if (this.disabled || this.handle) return this.handle;
|
|
198
|
+
var self = this;
|
|
199
|
+
var title = this.root.getAttribute('data-shell-nav') || '导航';
|
|
200
|
+
|
|
201
|
+
var content = this.side.cloneNode(true);
|
|
202
|
+
stripIds(content);
|
|
203
|
+
/* 🔴 克隆会**继承** `.shell__side` 的窄屏规则 `display: none` ——
|
|
204
|
+
也就是说,抽屉里那份内容原样是看不见的(0.19.0 实测:抽屉能打开、
|
|
205
|
+
但里面空无一物,焦点兜底到关闭按钮)。
|
|
206
|
+
加一个修饰类,让 CSS 把"抽屉里那份"重新显示出来。
|
|
207
|
+
⚠️ 不要在 JS 里写 style.display —— 那是把布局塞进脚本,
|
|
208
|
+
CSS 里的 display 一变(比如将来改成 grid),JS 就悄悄过时。 */
|
|
209
|
+
content.className += ' shell__side--drawer';
|
|
210
|
+
|
|
211
|
+
var handle = this.handle = global.Drawer.open({
|
|
212
|
+
place: this.opts.place || 'left',
|
|
213
|
+
title: title,
|
|
214
|
+
content: content,
|
|
215
|
+
/* ⑤ 焦点落在当前页:用户打开导航,第一个想看的就是"我在哪"。
|
|
216
|
+
没有 aria-current 时 drawer 会退到第一个可聚焦元素(关闭按钮)。 */
|
|
217
|
+
initialFocus: '[aria-current="page"]',
|
|
218
|
+
onClose: function () {
|
|
219
|
+
self.handle = null;
|
|
220
|
+
flEmit(self.root, 'fl-shell-nav', { open: false });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/* ⑥ 点链接先关。用事件委托而不是逐个绑:克隆里有多少链接是使用者决定的。
|
|
225
|
+
⚠️ 捕获 el 而不是每次读 self.handle —— 关闭后 handle 已是 null。 */
|
|
226
|
+
var el = handle.el;
|
|
227
|
+
el.addEventListener('click', function (e) {
|
|
228
|
+
var n = e.target;
|
|
229
|
+
while (n && n !== el) {
|
|
230
|
+
if (n.tagName === 'A') { self.close(); return; }
|
|
231
|
+
n = n.parentNode;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
flEmit(this.root, 'fl-shell-nav', { open: true });
|
|
236
|
+
return handle;
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
close: function () {
|
|
240
|
+
var h = this.handle;
|
|
241
|
+
if (!h) return false;
|
|
242
|
+
/* 先清空再调:drawer 的 onClose 里还会清一次,
|
|
243
|
+
顺序反了的话回调会重入到一个"正在关闭"的句柄上。 */
|
|
244
|
+
this.handle = null;
|
|
245
|
+
h.close();
|
|
246
|
+
return true;
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/** 拆掉本层加的东西(按钮、监听、开着的抽屉)。侧栏原节点一根手指都没动过。 */
|
|
250
|
+
destroy: function () {
|
|
251
|
+
this.close();
|
|
252
|
+
if (this.onResize) {
|
|
253
|
+
global.removeEventListener('resize', this.onResize);
|
|
254
|
+
this.onResize = null;
|
|
255
|
+
}
|
|
256
|
+
if (this.btn && this.btn.parentNode) this.btn.parentNode.removeChild(this.btn);
|
|
257
|
+
this.btn = null;
|
|
258
|
+
if (this.root && this.root.shellNavInstance === this) this.root.shellNavInstance = null;
|
|
259
|
+
this.disabled = true;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
global.ShellNav = ShellNav;
|
|
264
|
+
|
|
265
|
+
function init() {
|
|
266
|
+
var nodes = document.querySelectorAll('[data-shell-nav]');
|
|
267
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
268
|
+
/* ⑧ 幂等:SPA 路由切回、HMR、重复引入都会二次执行 init。
|
|
269
|
+
不加守卫会插第二个按钮(视觉上是"两个汉堡叠在一起")。 */
|
|
270
|
+
if (nodes[i].shellNavInstance) continue;
|
|
271
|
+
try {
|
|
272
|
+
nodes[i].shellNavInstance = new ShellNav(nodes[i]);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
/* 缺依赖是**作者错误**,但一个坏标记不该拖垮整页其余外壳 */
|
|
275
|
+
warn('shell-nav: 初始化失败,已跳过该元素 —— ' + (e && e.message));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// @autoinit: [data-shell-nav] —— 自动初始化;与 API.md「自动初始化」表由 autoinit-gate 双向校验
|
|
281
|
+
// ⚠️ 无 DOM 环境(SSR / 构建期 / Node 里 import)不自动初始化。
|
|
282
|
+
if (typeof document === 'undefined') return;
|
|
283
|
+
if (document.readyState === 'loading') {
|
|
284
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
285
|
+
} else {
|
|
286
|
+
init();
|
|
287
|
+
}
|
|
288
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -93,6 +93,28 @@
|
|
|
93
93
|
--sort-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M8 1.4 5.2 4.6h1.7v2.6h2.2V4.6h1.7z'/%3E%3Cpath d='M8 14.6 10.8 11.4h-1.7V8.8H6.9v2.6H5.2z'/%3E%3C/svg%3E");
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/* 🔴 强制色:mask 画的箭头会变成「白底上的白箭头」
|
|
97
|
+
---------------------------------------------------------------------------
|
|
98
|
+
0.17.0 实拍:列头只剩「公司」两个字,排序箭头一个像素都没有。
|
|
99
|
+
|
|
100
|
+
根因不是 mask —— mask **保留**(它不是颜色,强制色不管它)。
|
|
101
|
+
而是 `background-color: currentColor` 被替换成了 Canvas(白)
|
|
102
|
+
⇒ 白色形状 + 白色底 = 不可见。
|
|
103
|
+
|
|
104
|
+
⇒ 强制色下改用 `CanvasText`(系统前景色)。它是系统色关键字,
|
|
105
|
+
不会被再次强制替换。
|
|
106
|
+
⚠️ 这是**通用模式**,不是这张表的特例:凡是
|
|
107
|
+
`background-color: currentColor` + `mask` 画图标的地方
|
|
108
|
+
都在强制色下会消失。本库目前只有这里和 `field__error::before` 两处,
|
|
109
|
+
判据 A1(forced-colors-check.js)扫的就是这个模式。
|
|
110
|
+
--------------------------------------------------------------------------- */
|
|
111
|
+
@media (forced-colors: active) {
|
|
112
|
+
.table__sort::after {
|
|
113
|
+
background-color: CanvasText;
|
|
114
|
+
opacity: 1; /* .45 在黑字上仍可辨,但列头本来就小,别再加一层衰减 */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
/* 🔴🔴 排序指示符:三态必须**同时**用形状 + 颜色表达(WCAG 1.4.1)
|
|
97
119
|
--------------------------------------------------------------------
|
|
98
120
|
实测到的缺陷:
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# agentcss — for coding agents
|
|
2
|
+
|
|
3
|
+
- **Contract (machine-readable):** `ai/components.json` — 39 components with root class, slots, variants, states, ARIA and file paths. Query it; never guess class names.
|
|
4
|
+
- **Known failure modes:** `ai/pitfalls.json`, or `npx agentcss ai pitfalls --json --component=<id>`.
|
|
5
|
+
- **Query:** `npx agentcss ai components --json [--maturity=stable]` · `ai tokens --json [--set=dark]` · `ai tree --json`.
|
|
6
|
+
- **Validate:** `npx agentcss ai validate <file> --schema=components --json`. Every subcommand accepts `--json`; failures print `{"ok":false,…}` and exit non-zero.
|
|
7
|
+
- **Markup ground truth:** each component's `demo.html`. Restyle through tokens (CSS variables); never rewrite component selectors.
|
|
8
|
+
- **Docs are Chinese:** `ai/START-HERE.md` (navigation). English entry points: `README.md`, `llms.txt`, `llms-full.txt`.
|