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
|
@@ -261,14 +261,39 @@
|
|
|
261
261
|
setBackgroundInert(true);
|
|
262
262
|
lockScroll();
|
|
263
263
|
|
|
264
|
-
/*
|
|
264
|
+
/* 初始焦点:优先指定项,其次第一个**真的能聚焦**的元素,最后容器本身。
|
|
265
|
+
------------------------------------------------------------------
|
|
266
|
+
🔴 0.19.0 修:原来写的是 `if (t) t.focus(); else (f[0] || d).focus();`
|
|
267
|
+
—— "找到节点"被当成了"聚焦成功"。可这两件事不等价:
|
|
268
|
+
|
|
269
|
+
· 节点存在但 `display: none`(本库真实踩到:app-shell 的侧栏克隆
|
|
270
|
+
继承了窄屏的 `display: none`)⇒ `focus()` **静默无效**
|
|
271
|
+
· 节点存在但 `disabled` / `inert` ⇒ 同样无效
|
|
272
|
+
|
|
273
|
+
后果不是"焦点位置不理想",是**模态框关不掉**:
|
|
274
|
+
焦点留在 `<body>` 上,而 Esc 的监听挂在抽屉元素上,
|
|
275
|
+
事件根本冒泡不过去。键盘用户 Tab 一下也出不去(背景已 inert)。
|
|
276
|
+
鼠标用户点遮罩还能关,所以这个 bug 只在键盘下现形。
|
|
277
|
+
|
|
278
|
+
⇒ 判据改成"焦点**真的到了**"(比对 activeElement),没到就兜底;
|
|
279
|
+
兜底时也跳过隐藏项,否则兜底同样落空。 */
|
|
265
280
|
var f = d.querySelectorAll(FOCUSABLE);
|
|
281
|
+
var landed = false;
|
|
266
282
|
if (opt.initialFocus) {
|
|
267
283
|
var t = d.querySelector(opt.initialFocus);
|
|
268
|
-
if (t)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
284
|
+
if (t) {
|
|
285
|
+
try { t.focus(); } catch (e) { /* 极端情况下 focus 会抛,按没落位处理 */ }
|
|
286
|
+
landed = document.activeElement === t;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!landed) {
|
|
290
|
+
var first = null;
|
|
291
|
+
for (var fi = 0; fi < f.length; fi++) {
|
|
292
|
+
if (f[fi].offsetParent === null) continue; /* 隐藏项不作为落点 */
|
|
293
|
+
first = f[fi];
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
(first || d).focus();
|
|
272
297
|
}
|
|
273
298
|
|
|
274
299
|
/* ① Esc 与「点抽屉之外关闭」走 dismissable 核(0.14.0 收编)。
|
|
@@ -40,7 +40,7 @@ p.small { font-size: var(--fs-xs); color: var(--text-tertiary); line-height: 1.6
|
|
|
40
40
|
由 05-audit/css-imports.py 报出(它抓到过这个真问题) -->
|
|
41
41
|
<link rel="stylesheet" href="../../02-primitives/button/button.css">
|
|
42
42
|
</head>
|
|
43
|
-
<body class="page
|
|
43
|
+
<body class="page">
|
|
44
44
|
<main class="wrap">
|
|
45
45
|
|
|
46
46
|
<h1>表单校验</h1>
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# app-shell · 应用外壳
|
|
2
|
+
|
|
3
|
+
> 工具型页面的骨架:左边一列常驻导航、上面一条工具栏、右边是主区。
|
|
4
|
+
> 本库已有的四个 recipe 都是**内容型**版式(读 / 查 / 写 / 比),
|
|
5
|
+
> 缺的是**工具型**。
|
|
6
|
+
|
|
7
|
+
## 文件
|
|
8
|
+
|
|
9
|
+
| 文件 | 作用 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `app-shell.css` | 三段式布局 + 两个变体 + 窄屏退化 |
|
|
12
|
+
| `shell-nav.js` | 窄屏导航:菜单按钮 + 把侧栏克隆进 `03-patterns/drawer`。**必需**(理由见下) |
|
|
13
|
+
| `demo.html` | 六个形态 + 一个对比块 |
|
|
14
|
+
|
|
15
|
+
> 布局本身零 JS。`shell-nav.js` 不是"给布局加动效",它补的是一个**入口**:
|
|
16
|
+
> 没有它,窄屏上侧栏收走之后一个导航链接都没有。
|
|
17
|
+
> 所以 START-HERE 里它归「必需」,不是"可选增强"。
|
|
18
|
+
|
|
19
|
+
## 用法
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<link rel="stylesheet" href="app-shell.css">
|
|
23
|
+
|
|
24
|
+
<div class="shell">
|
|
25
|
+
<header class="shell__topbar">
|
|
26
|
+
<h1 class="shell__title">应付账款 · 2025 年度</h1>
|
|
27
|
+
<div class="shell__topbar-end">
|
|
28
|
+
<button>保存</button>
|
|
29
|
+
</div>
|
|
30
|
+
</header>
|
|
31
|
+
|
|
32
|
+
<nav class="shell__side" aria-label="主导航">
|
|
33
|
+
<a class="shell__brand" href="/">金标准 · 财务台</a>
|
|
34
|
+
<ul class="shell__side-group">
|
|
35
|
+
<li><a class="shell__link" href="/" aria-current="page">总览</a></li>
|
|
36
|
+
<li><a class="shell__link" href="/entities">实体</a></li>
|
|
37
|
+
</ul>
|
|
38
|
+
<p class="shell__side-label">数据</p>
|
|
39
|
+
<ul class="shell__side-group">
|
|
40
|
+
<li><a class="shell__link" href="/import">导入</a></li>
|
|
41
|
+
</ul>
|
|
42
|
+
</nav>
|
|
43
|
+
|
|
44
|
+
<main class="shell__main">
|
|
45
|
+
<div class="shell__content">
|
|
46
|
+
<!-- 你的内容 -->
|
|
47
|
+
</div>
|
|
48
|
+
</main>
|
|
49
|
+
</div>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
DOM 顺序与视觉顺序一致(顶栏 → 侧栏 → 主区),`grid-template-areas` 负责把侧栏
|
|
53
|
+
摆到左边。**不要**为了"看起来对"去改 DOM 顺序 —— 读屏用户就是按 DOM 顺序听的。
|
|
54
|
+
|
|
55
|
+
## 两个坑(这是本组件存在的主要理由)
|
|
56
|
+
|
|
57
|
+
### 坑 1 · 主区少了 `min-width: 0`
|
|
58
|
+
|
|
59
|
+
grid / flex 项的默认 `min-width` 是 `auto`(不小于内容的最小宽度)。
|
|
60
|
+
主区里只要有一个宽表格,它就被撑到 700px 以上,**整个 shell 横滚** ——
|
|
61
|
+
侧栏和顶栏一起被推走。
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
.shell__main { min-width: 0; } /* 这一行 */
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
加了之后,宽内容只能在自己的滚动容器里滚 —— 那个容器本库给了:
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<div class="shell__scroll" tabindex="0" role="region" aria-label="宽表格,可横向滚动">
|
|
71
|
+
<table>…</table>
|
|
72
|
+
</div>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
⚠️ 三个属性都不能省:`tabindex="0"` 让键盘用户能聚焦并滚动它
|
|
76
|
+
(不加就是 axe 的 `scrollable-region-focusable`:鼠标能拖、键盘滚不动),
|
|
77
|
+
`role="region"` 让它成为一个可识别的区块,`aria-label` 给它名字
|
|
78
|
+
(没有名字的 region,读屏只会说"区域")。
|
|
79
|
+
|
|
80
|
+
⚠️ 宽内容要放在 `.shell__content` **外面**:后者的 `max-width` 是正文栏宽,
|
|
81
|
+
放在里面宽度就被它限制住了,表格只能挤在 52rem 里。
|
|
82
|
+
|
|
83
|
+
判据:`app-shell-check.js` B1 / B1b(造一个 80rem 宽的块,分别量主区宽度与页面是否横滚)。
|
|
84
|
+
|
|
85
|
+
### 坑 2 · 高度用 `100vh` 而不是 `100dvh`
|
|
86
|
+
|
|
87
|
+
`100vh` 取的是**地址栏收起时**的视口高(最大那个)。
|
|
88
|
+
地址栏一展开,可视区域变小,元素仍按大的算 ⇒ 底部被裁一截,而且不出现滚动条。
|
|
89
|
+
iOS Safari 上最明显:最后一个按钮点不到。
|
|
90
|
+
|
|
91
|
+
```css
|
|
92
|
+
min-height: 100vh; /* 兜底:不认 dvh 的引擎 */
|
|
93
|
+
min-height: 100dvh; /* 正解 */
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
顺序不能反 —— 后写的优先。
|
|
97
|
+
|
|
98
|
+
## 侧栏为什么是 `sticky` 而不是 `fixed`
|
|
99
|
+
|
|
100
|
+
`fixed` 会把侧栏从 grid 流里摘出去,主区不知道它存在,于是要么被盖住,
|
|
101
|
+
要么手动补 `margin-inline-start`(然后在 RTL 里全错)。
|
|
102
|
+
|
|
103
|
+
`sticky` 留在流里:grid 照常算它的 15rem,滚到顶后固定住,RTL 自动正确
|
|
104
|
+
(因为用的是逻辑属性)。
|
|
105
|
+
|
|
106
|
+
⚠️ 两个前提缺一不可:
|
|
107
|
+
- `align-self: start` —— grid 项默认 `stretch`,撑满高度就无从"粘住"
|
|
108
|
+
- `max-height: 100dvh` + `overflow-y: auto` —— 条目多了侧栏自己滚,
|
|
109
|
+
而不是把主区一起滚走
|
|
110
|
+
|
|
111
|
+
## 窄屏导航(`shell-nav.js`)
|
|
112
|
+
|
|
113
|
+
```html
|
|
114
|
+
<link rel="stylesheet" href="app-shell.css">
|
|
115
|
+
<link rel="stylesheet" href="../drawer/drawer.css">
|
|
116
|
+
|
|
117
|
+
<div class="shell" data-shell-nav="主导航"> <!-- 值 = 抽屉标题 -->
|
|
118
|
+
<header class="shell__topbar">…</header> <!-- 按钮插在这里的第一个位置 -->
|
|
119
|
+
<nav class="shell__side" aria-label="主导航">…</nav>
|
|
120
|
+
<main class="shell__main">…</main>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<script src="../drawer/drawer.js"></script> <!-- 必需:抽屉的行为 -->
|
|
124
|
+
<script src="shell-nav.js"></script> <!-- 有 data-shell-nav 就自动初始化 -->
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- **opt-in**:不写 `data-shell-nav` 就一个按钮都不注入 —— 别人的顶栏布局不该被悄悄改。
|
|
128
|
+
- **只接线,不重写行为**:Esc、焦点陷阱、焦点归位、背景 `inert`、锁滚动五件事
|
|
129
|
+
全部来自 `drawer.js`。本层做的是"建按钮 / 克隆侧栏 / 判断何时该关"。
|
|
130
|
+
- **克隆,不移动**。桌面端还要用原节点;而抽屉是"用完即毁"的,
|
|
131
|
+
移动过去的话一关侧栏就永远回不来了。克隆里所有 `id` 会剥掉
|
|
132
|
+
(两份同 id 会让 `aria-labelledby` 指到隐藏那份上)。
|
|
133
|
+
- **断点不写在 JS 里**:判据是"菜单按钮还在不在"(`offsetParent === null`),
|
|
134
|
+
显隐由 CSS 的 `@media` 决定。写死数字的话 CSS 改一个值、JS 就静默错位。
|
|
135
|
+
- **缺 `drawer.js` 时拒绝初始化**并 warn —— 宁可没有按钮,也不要一个点了没反应的按钮。
|
|
136
|
+
窄屏下用户会反复点,然后认为整个页面坏了。
|
|
137
|
+
- **宽屏复位**:开着抽屉把窗口拉宽,抽屉自动关掉,否则两个导航同时在场。
|
|
138
|
+
- **rail 也能直接接**:侧栏是图标态时,抽屉里会自动变成「图标 + 文字」——
|
|
139
|
+
前提是文字留在 DOM 里,见下面「图标态的文字」。
|
|
140
|
+
|
|
141
|
+
### 为什么不用 `03-patterns/nav`
|
|
142
|
+
|
|
143
|
+
`nav.js` 做的是"横排导航 → 全屏抽屉":它的列表**常驻在文档里**,
|
|
144
|
+
抽屉和列表是同一个节点,只是移动位置。而这里的侧栏在窄屏是 `display: none`,
|
|
145
|
+
需要的是"另建一份显示出来" —— 生命周期正好相反(常驻 vs 即用即毁)。
|
|
146
|
+
两者共用同一套核(`emit` / `focus-return` / `dismissable`),只是不共用外壳。
|
|
147
|
+
|
|
148
|
+
## 变体
|
|
149
|
+
|
|
150
|
+
| 类 | 效果 | 用在哪 |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `.shell--rail` | 侧栏 4.5rem,只剩图标 | 工具栏型产品 |
|
|
153
|
+
| `.shell--flat` | 侧栏无边框、与主区同色 | 想要更轻的分层 |
|
|
154
|
+
| `.shell--scroll-main` | 侧栏与顶栏不动,只有主区滚 | 看板 / 监控墙 |
|
|
155
|
+
|
|
156
|
+
`.shell--rail` 下每个链接的名字必须来自**可见文字**,不要靠 `aria-label` 或 `title` ——
|
|
157
|
+
做法见下一节。
|
|
158
|
+
|
|
159
|
+
### 图标态的文字(`.shell__link-label`)
|
|
160
|
+
|
|
161
|
+
```html
|
|
162
|
+
<a class="shell__link" href="#" title="总览">
|
|
163
|
+
<svg … aria-hidden="true">…</svg>
|
|
164
|
+
<span class="shell__link-label">总览</span>
|
|
165
|
+
</a>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
rail 状态下 `.shell__link-label` 被**视觉隐藏**(1px 盒 + 裁切),但仍在可访问树里,
|
|
169
|
+
所以读屏念的是「总览」而不是「链接」。两条硬理由:
|
|
170
|
+
|
|
171
|
+
1. **名字只能有一个来源。** 删掉文字改用 `aria-label`,等于同一句话维护两遍:
|
|
172
|
+
改了可见文字却忘了改 `aria-label`,读屏念的还是旧的。
|
|
173
|
+
2. **抽屉里要靠它把文字补回来。** `shell-nav.js` 克隆的是侧栏节点,而抽屉把克隆
|
|
174
|
+
挂到 `<body>` 下 ⇒ 它**离开了 `.shell--rail` 的作用域** ⇒ 隐藏规则不再命中
|
|
175
|
+
⇒ 抽屉里自动变成「图标 + 文字」。不需要第二份标记,也没有 JS 分支。
|
|
176
|
+
|
|
177
|
+
⚠️ **不能**改成 `display: none`:不在可访问树里的内容不参与名字计算,链接会退化成
|
|
178
|
+
"链接、链接、链接"。门禁 `F12` 守名字;`G4` 把这条规则改成 `display:none` 后必须变红。
|
|
179
|
+
|
|
180
|
+
⚠️ `title` 只负责桌面端的鼠标提示(触屏没有 hover)。它还会**兜出**一个名字,
|
|
181
|
+
所以门禁 fixture 里故意不写 `title` —— 否则「把标签改成 display:none」这种破坏
|
|
182
|
+
会被 `title` 兜住,判据看起来仍然是绿的。
|
|
183
|
+
|
|
184
|
+
### 图标态的品牌与分组
|
|
185
|
+
|
|
186
|
+
4.5rem 放不下品牌名,也放不下「数据 / 校验」这种分组名。两者各有各的解法,
|
|
187
|
+
**都不是**"删掉了事"。
|
|
188
|
+
|
|
189
|
+
**品牌:标记 + 文字两段。**
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<a class="shell__brand" href="#">
|
|
193
|
+
<svg class="shell__brand-mark" …>…</svg>
|
|
194
|
+
<span class="shell__brand-text">金标准 · 财务台</span>
|
|
195
|
+
</a>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
rail 下 `.shell__brand-text` 与 `.shell__link-label` 同一套手法(1px 盒 + 裁切,
|
|
199
|
+
`display: none` 是**错的**):标记留在 72px 中央,品牌名仍在可访问树里,
|
|
200
|
+
宽屏回来时字还在。品牌名是直接文本的老写法会退化成「金…」——
|
|
201
|
+
不是坏,是提醒你补上两段结构。
|
|
202
|
+
|
|
203
|
+
标记用 `currentColor` 画(内联 `<svg>`),不用 `<img>`:强制色下 `currentColor`
|
|
204
|
+
会被换成 `CanvasText`,图形还在;`<img>` 里的颜色不受当前颜色影响。
|
|
205
|
+
|
|
206
|
+
**分组:让两个 `.shell__side-group` 相邻,不要用 `.shell__side-label`。**
|
|
207
|
+
|
|
208
|
+
```html
|
|
209
|
+
<ul class="shell__side-group">…</ul>
|
|
210
|
+
<ul class="shell__side-group">…</ul> <!-- `+` 会给出分隔线与间距 -->
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`.shell__side-group + .shell__side-group` 那条规则本来就是为"无标签分组"准备的。
|
|
214
|
+
rail 下硬塞一个 `.shell__side-label` 会被 CSS 藏掉(兜底,防止它折行挤散图标),
|
|
215
|
+
但那段标记从此永远不会被看见、也不会被维护 —— 所以门禁 `H3` 判的是
|
|
216
|
+
「rail 实例里**不许出现** `.shell__side-label`」,比 CSS 的容错更严。
|
|
217
|
+
|
|
218
|
+
门禁:`H1` 名字来自可见文字、`H2` 品牌两段、`H3` 分组不用标签 ——
|
|
219
|
+
三条都落在**demo 页的真实 DOM** 上,不只看 fixture
|
|
220
|
+
(教训:F12/F13/F14 验的是 fixture,而 demo 当时教的是相反写法,46 条全绿)。
|
|
221
|
+
|
|
222
|
+
## 令牌
|
|
223
|
+
|
|
224
|
+
| 变量 | 默认 | 说明 |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `--shell-side-w` | `15rem` | 侧栏宽。导航文字 + 图标不折行的下限附近 |
|
|
227
|
+
| `--shell-topbar-h` | `3.5rem` | 顶栏高 |
|
|
228
|
+
| `--shell-pad` | `var(--sp-4)` | 主区与顶栏的内边距(窄屏自动降到 `--sp-3`) |
|
|
229
|
+
| `--shell-bp` | `48rem` | 侧栏收起阈值 |
|
|
230
|
+
|
|
231
|
+
## 无障碍
|
|
232
|
+
|
|
233
|
+
- 侧栏是 `<nav aria-label="主导航">`:页面上可能还有第二个 `<nav>`(页内目录),
|
|
234
|
+
两个都没有名字时读屏只会说"导航、导航"。判据:`a11y-ax.js`。
|
|
235
|
+
- 当前页用 `aria-current="page"`,**不是** `class="active"` ——
|
|
236
|
+
前者是读屏能听到的状态,后者只是 CSS 钩子。
|
|
237
|
+
- 标记当前页用三条线索:竖条、底色、字重。灰度打印和色盲下都分得出来。
|
|
238
|
+
- 强制色下 `inset` 阴影会被整个移除,所以另有一段 media 把"当前项"换成实色边框;
|
|
239
|
+
所有项统一预留 3px 透明边框,避免当前项的文字比其它项右移。
|
|
240
|
+
- 顶栏与侧栏的链接都有 44px 命中区(`--sz-touch`)。
|
|
241
|
+
|
|
242
|
+
## 反例
|
|
243
|
+
|
|
244
|
+
- **❌ 侧栏 `position: fixed` + 主区 `margin-inline-start`** —— 看起来能用,但侧栏被摘出
|
|
245
|
+
grid 流,主区宽度靠**手抄的数字**维持(改侧栏宽就要改两处);`inset-inline-start`
|
|
246
|
+
与 `margin-inline-start` 只要有一处漏改,RTL 下侧栏跑到右边、主区仍在右边留白。**
|
|
247
|
+
|
|
248
|
+
正确的做法见上面「侧栏为什么是 `sticky` 而不是 `fixed`」。
|
|
249
|
+
|
|
250
|
+
- **❌ 窄屏把侧栏 `display: none` 之后就不管了** —— 导航消失了,用户在手机上
|
|
251
|
+
除了当前页哪儿也去不了。要么引 `shell-nav.js`(本目录就有),
|
|
252
|
+
要么明确接受"窄屏只有当前页"(只读看板可以接受,但那要是**决定**,不是疏忽)。
|
|
253
|
+
|
|
254
|
+
⚠️ 别自己写 `transform: translateX(-100%)` 的抽屉。看起来只是"滑出来",
|
|
255
|
+
实际要做的是**焦点陷阱 / 焦点归位 / 背景 `inert` / 锁滚动 / Esc** 五件事。
|
|
256
|
+
自己写必然漏两件,而漏掉的那两件在鼠标用户眼里完全正常 ——
|
|
257
|
+
只有键盘和读屏用户会被困在一个关不掉的抽屉里。
|
|
258
|
+
|
|
259
|
+
- **❌ 给 `.shell` 加 `overflow: hidden` 想"修好"横滚** —— 它确实不横滚了,
|
|
260
|
+
因为内容**被裁掉了**,而且滚动条也没了。主区被撑破的正确修法是
|
|
261
|
+
`min-width: 0` + `.shell__scroll`(见坑 1),不是把溢出的部分藏起来。
|
|
262
|
+
|
|
263
|
+
- **❌ 把宽表格放进 `.shell__content`** —— `.shell__content` 的 `max-width` 是
|
|
264
|
+
正文栏宽(`--measure-page`),表格会被限制在 52rem 里,右边几列挤成一团。
|
|
265
|
+
宽内容要放在 `.shell__content` **外面**、`.shell__main` 直下。
|
|
266
|
+
|
|
267
|
+
## 相关
|
|
268
|
+
|
|
269
|
+
- `03-patterns/nav` —— 页内目录 + 抽屉行为层。视觉上与侧栏条目对齐,但**不复用类**:
|
|
270
|
+
一个是"页面内的目录",一个是"应用级导航",语义不同。
|
|
271
|
+
- `03-patterns/drawer` —— 窄屏导航抽屉,Esc / 焦点归位 / 背景锁 / 变宽自关都已实现。
|
|
272
|
+
- `04-recipes/detail-page` / `analysis-report` —— 内容型版式,可以放进 `.shell__main`。
|