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,588 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
app-shell — 应用外壳(侧栏 + 顶栏 + 主区)
|
|
3
|
+
============================================================================
|
|
4
|
+
为什么需要它
|
|
5
|
+
---------------------------------------------------------------------------
|
|
6
|
+
本库已有的四个 recipe 都是**内容型**版式:
|
|
7
|
+
· analysis-report —— 报告正文 + 辅助栏(读)
|
|
8
|
+
· detail-page —— 实体详情(查)
|
|
9
|
+
· report —— 版式系统,三种排版取舍(写)
|
|
10
|
+
· table —— 数据表(比)
|
|
11
|
+
缺的是**工具型**骨架:左边一列常驻导航、上面一条工具栏、右边是主区。
|
|
12
|
+
每个后台项目都在手写它,而且每一版都会踩同样的两个坑(下面写清了)。
|
|
13
|
+
|
|
14
|
+
两个坑(这是本组件存在的**主要**理由)
|
|
15
|
+
---------------------------------------------------------------------------
|
|
16
|
+
① `height: 100vh` 在移动端是错的。
|
|
17
|
+
浏览器地址栏收起/展开时视口高度会变,`100vh` 取的是**最大**那一个
|
|
18
|
+
⇒ 底部永远被裁掉一截(iOS Safari 上最明显)。
|
|
19
|
+
正解是 `100dvh`(动态视口),并用 `100vh` 兜底给不支持的引擎。
|
|
20
|
+
|
|
21
|
+
② 侧栏 `position: fixed` 会把它从 grid 流里摘出去。
|
|
22
|
+
后果是主区不知道侧栏存在,内容自己往左铺 ⇒ 要么被盖住,要么手动加
|
|
23
|
+
`margin-inline-start`,然后在 RTL 里全错。
|
|
24
|
+
正解是 `position: sticky`:它留在流里(grid 照常算宽度),
|
|
25
|
+
滚到顶后又固定住。RTL 自动正确,因为用的是逻辑属性。
|
|
26
|
+
|
|
27
|
+
设计要点
|
|
28
|
+
---------------------------------------------------------------------------
|
|
29
|
+
① 三区域 grid,不是"三个浮动的盒子"
|
|
30
|
+
② 侧栏 sticky + 自己滚(`overflow-y: auto`),主区跟着页面滚
|
|
31
|
+
③ `min-width: 0` 落在主区上 —— 少了它,一个宽表格就能把整个 shell 撑出横滚
|
|
32
|
+
④ 窄屏退化:侧栏 `display: none`,主区全宽。**折叠成抽屉不是本组件的活**
|
|
33
|
+
—— 抽屉已经有 `03-patterns/drawer`,在这里再实现一遍就是第二份真值。
|
|
34
|
+
⑤ 本组件是**唯一**该用 `@media` 判宽度的:它本身就是"视口的容器",
|
|
35
|
+
没有"槽"可言。其余组件一律 `@container`(CONVENTIONS 第十一节)。
|
|
36
|
+
========================================================================== */
|
|
37
|
+
|
|
38
|
+
.shell {
|
|
39
|
+
/* 组件私有槽位 */
|
|
40
|
+
--shell-side-w: 15rem; /* 侧栏宽;240px 是导航文字 + 图标不折行的下限附近 */
|
|
41
|
+
--shell-topbar-h: 3.5rem; /* 顶栏高 */
|
|
42
|
+
--shell-gap: var(--sp-4);
|
|
43
|
+
--shell-pad: var(--sp-4);
|
|
44
|
+
--shell-bp: 48rem; /* 侧栏收起阈值:768px 以下是"手机/窄平板" */
|
|
45
|
+
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-columns: var(--shell-side-w) minmax(0, 1fr);
|
|
48
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
49
|
+
grid-template-areas:
|
|
50
|
+
"side topbar"
|
|
51
|
+
"side main";
|
|
52
|
+
/* 🔴 100dvh + 100vh 兜底。顺序不能反:后写的优先,
|
|
53
|
+
支持 dvh 的引擎用 dvh,不支持的退到 vh(底边被裁,但至少不塌成 0)。 */
|
|
54
|
+
min-height: 100vh;
|
|
55
|
+
min-height: 100dvh;
|
|
56
|
+
/* 🔴 底色铺在**外壳**上,而不是只给 `.shell__side`。
|
|
57
|
+
---------------------------------------------------------------------
|
|
58
|
+
侧栏是 `sticky` + `align-self: start`(见下),它的高度只到内容为止。
|
|
59
|
+
如果底色只铺在侧栏元素上,条目少的页面会看到"侧栏背景只到一半",
|
|
60
|
+
下面露一条纸色 —— 那看起来像渲染坏了,其实是高度自然。
|
|
61
|
+
|
|
62
|
+
解法不是给侧栏加 `min-height: 100dvh`(那会让它在 demo 的固定高度容器里
|
|
63
|
+
顶出去,也会让"内容不足一屏"的页面底部多一块死色),
|
|
64
|
+
而是把侧栏色铺在整个外壳上,再让主区盖回纸色:
|
|
65
|
+
|
|
66
|
+
.shell = 侧栏色
|
|
67
|
+
.shell__main = 纸色
|
|
68
|
+
|
|
69
|
+
这个分层不依赖任何方向,RTL 下自动正确。 */
|
|
70
|
+
background: var(--surface);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ============================================================================
|
|
74
|
+
1 · 侧栏
|
|
75
|
+
============================================================================ */
|
|
76
|
+
.shell__side {
|
|
77
|
+
grid-area: side;
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: var(--sp-1);
|
|
81
|
+
padding: var(--shell-pad) var(--sp-3);
|
|
82
|
+
/* 底色与分界线都**不**画在侧栏上:
|
|
83
|
+
· 底色 —— 侧栏高度只到内容为止(`align-self: start` 的必然结果),
|
|
84
|
+
画在这里会出现"侧栏背景只到一半"
|
|
85
|
+
· 右边框 —— 同理,会变成"只到一半的竖线",看起来像渲染断了
|
|
86
|
+
两者都交给 `.shell`(底色)与 `.shell__main`(左边框,全高)。 */
|
|
87
|
+
/* 🔴 sticky 而不是 fixed —— 见文件头坑 ②。
|
|
88
|
+
`align-self: start` 是必需的:grid 项默认 stretch,
|
|
89
|
+
stretch 之后元素和容器一样高,"粘住"就无从谈起。 */
|
|
90
|
+
position: sticky;
|
|
91
|
+
inset-block-start: 0;
|
|
92
|
+
align-self: start;
|
|
93
|
+
/* 🔴 自己滚:侧栏条目多的时候,主区不该跟着一起滚走。
|
|
94
|
+
高度取视口高(同样 dvh + vh 兜底)。 */
|
|
95
|
+
max-height: 100vh;
|
|
96
|
+
max-height: 100dvh;
|
|
97
|
+
overflow-y: auto;
|
|
98
|
+
/* 滚动链:侧栏滚到底后不要接着滚主区(iOS 上的橡皮筋会很难受) */
|
|
99
|
+
overscroll-behavior: contain;
|
|
100
|
+
/* 强制色预留:边框已存在,这条是为了让"侧栏边界"在强制色下更明确 */
|
|
101
|
+
outline: 1px solid transparent;
|
|
102
|
+
outline-offset: -1px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.shell__brand {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: var(--sp-2);
|
|
109
|
+
padding: var(--sp-2) var(--sp-2) var(--sp-3);
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
color: var(--text-primary);
|
|
112
|
+
text-decoration: none;
|
|
113
|
+
/* 品牌名过长时截断,不要把侧栏撑宽 */
|
|
114
|
+
min-width: 0;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
text-overflow: ellipsis;
|
|
117
|
+
white-space: nowrap;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* 品牌的两段:标记(图形)+ 文字。
|
|
121
|
+
🔴 为什么必须分成两段,而不是让品牌名当纯文本:
|
|
122
|
+
图标态(`.shell--rail`)侧栏只有 4.5rem,品牌名放不下。
|
|
123
|
+
纯文本只能截成「金…」,而 `display: none` 掉整个 `.shell__brand`
|
|
124
|
+
又等于没有品牌。分成两段后,rail 下只留标记、文字视觉隐藏
|
|
125
|
+
—— 与 `.shell__link-label` 同一套手法、同一个理由:
|
|
126
|
+
文字留在 DOM 与可访问树里,宽屏/抽屉里能原样回来,名字仍只有一份。
|
|
127
|
+
⚠️ `.shell__brand-mark` 用 `currentColor` 画(不用 <img>):
|
|
128
|
+
强制色下 `currentColor` 会被换成 `CanvasText`,图形还在;
|
|
129
|
+
`<img>` 里的颜色则完全不受当前颜色影响。 */
|
|
130
|
+
.shell__brand-mark {
|
|
131
|
+
flex: 0 0 auto;
|
|
132
|
+
display: inline-flex;
|
|
133
|
+
color: var(--accent);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* 文字自己截断:`.shell__brand` 上的 text-overflow 对 flex 子项不起作用 */
|
|
137
|
+
.shell__brand-text {
|
|
138
|
+
min-width: 0;
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
text-overflow: ellipsis;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.shell__side-group {
|
|
145
|
+
display: flex;
|
|
146
|
+
flex-direction: column;
|
|
147
|
+
gap: 2px;
|
|
148
|
+
margin: 0;
|
|
149
|
+
padding: 0;
|
|
150
|
+
list-style: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.shell__side-group + .shell__side-group {
|
|
154
|
+
margin-top: var(--sp-2);
|
|
155
|
+
padding-top: var(--sp-2);
|
|
156
|
+
border-top: 1px solid var(--border-decor);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.shell__side-label {
|
|
160
|
+
padding: var(--sp-2) var(--sp-2) var(--sp-1);
|
|
161
|
+
font-size: var(--fs-xs);
|
|
162
|
+
color: var(--text-tertiary);
|
|
163
|
+
letter-spacing: var(--tracking-normal);
|
|
164
|
+
text-transform: uppercase;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* 侧栏条目:与 nav 组件的 .nav__link 视觉一致,但**不复用它的类** ——
|
|
168
|
+
nav 是"页内目录 + 抽屉行为层",语义不同;复用类会让两边的改动互相牵连。
|
|
169
|
+
⚠️ 但间距/圆角/焦点环必须与 nav 对齐,否则同一页里两处导航长得不一样。 */
|
|
170
|
+
.shell__link {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: var(--sp-2);
|
|
174
|
+
/* 44px 命中区(CONVENTIONS 第八节) */
|
|
175
|
+
min-height: var(--sz-touch);
|
|
176
|
+
padding: var(--sp-2) var(--sp-3);
|
|
177
|
+
border-radius: var(--r-sm);
|
|
178
|
+
color: var(--text-secondary);
|
|
179
|
+
font-size: var(--fs-sm);
|
|
180
|
+
text-decoration: none;
|
|
181
|
+
/* 长条目名截断,不撑宽侧栏 */
|
|
182
|
+
min-width: 0;
|
|
183
|
+
overflow: hidden;
|
|
184
|
+
text-overflow: ellipsis;
|
|
185
|
+
white-space: nowrap;
|
|
186
|
+
transition: background var(--dur-fast) var(--ease-out),
|
|
187
|
+
color var(--dur-fast) var(--ease-out);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* 当前页:**两条线索**,不只靠底色。
|
|
191
|
+
左侧一条 3px 的竖条 + 文字变主色 + 字重加一档 —— 灰度打印也能分辨。 */
|
|
192
|
+
.shell__link[aria-current="page"] {
|
|
193
|
+
color: var(--text-primary);
|
|
194
|
+
background: var(--surface-sunken);
|
|
195
|
+
font-weight: 500;
|
|
196
|
+
/* 用 inset 阴影画竖条:不占布局,也不会被 overflow 裁掉 */
|
|
197
|
+
box-shadow: inset 3px 0 0 var(--accent);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* 焦点环(states 门禁 HIGH:缺 focus-visible 就是"键盘用户看不到自己停在哪")。
|
|
201
|
+
⚠️ 用 `:focus-visible` 而不是 `:focus` —— 后者会让鼠标点一下也留个框。 */
|
|
202
|
+
.shell__link:focus-visible {
|
|
203
|
+
outline: 2px solid var(--accent);
|
|
204
|
+
outline-offset: 2px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* 悬停:只改文字色,与 `03-patterns/nav` 的 `.nav__link` 保持一致 ——
|
|
208
|
+
两处导航在同一页里出现时,反馈方式不同会显得像两个库拼的。
|
|
209
|
+
⚠️ 门控 hover:触屏上"点一下"会先点亮 hover 再 click ⇒ 状态粘住。 */
|
|
210
|
+
@media (hover: hover) and (pointer: fine) {
|
|
211
|
+
.shell__link:hover {
|
|
212
|
+
color: var(--text-primary);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* RTL 下竖条换到右边。⚠️ box-shadow 的 x 偏移**不是**逻辑属性,
|
|
217
|
+
必须显式翻 —— 这是本文件里唯一需要方向判断的地方。 */
|
|
218
|
+
[dir="rtl"] .shell__link[aria-current="page"] {
|
|
219
|
+
box-shadow: inset -3px 0 0 var(--accent);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* 强制色:inset 阴影会被整个移除(0.17.0 实测),
|
|
223
|
+
所以在强制色下必须换一条**不靠颜色**的线索表达"当前项"。
|
|
224
|
+
|
|
225
|
+
🔴 0.22.1 修的是这里的第一版做法:当时用
|
|
226
|
+
`.shell__link { border-inline-start: 3px solid transparent }` 给所有项
|
|
227
|
+
"免费预留"3px,只给当前项换成实色。实拍发现**预留不是免费的** ——
|
|
228
|
+
强制色会把透明边框解析成实线(链接上实测是 LinkText 深蓝 rgb(0,0,159),
|
|
229
|
+
当前项是 CanvasText 黑),于是**五项全部长出一条 3px 竖线**,
|
|
230
|
+
当前项与其它项的差别只剩一个色相 —— 正是本库反对的"只靠颜色"。
|
|
231
|
+
|
|
232
|
+
⇒ 改成:不预留边框(零位移),当前项用**字重 + 行首圆点**两条非颜色线索。
|
|
233
|
+
圆点用系统色背景画(不是字符),位置用绝对定位 —— 不参与布局,也不进可访问名。 */
|
|
234
|
+
@media (forced-colors: active) {
|
|
235
|
+
.shell__link {
|
|
236
|
+
position: relative;
|
|
237
|
+
box-shadow: none; /* inset 阴影在强制色下会被整个移除,留着没意义 */
|
|
238
|
+
}
|
|
239
|
+
.shell__link[aria-current="page"] {
|
|
240
|
+
font-weight: 700;
|
|
241
|
+
}
|
|
242
|
+
/* 圆点用**系统色背景**画,不用字符。
|
|
243
|
+
实测:`content: "•"` 会被算进可访问名(读屏念成「圆点 总览」),
|
|
244
|
+
而 `content: ""` + `background: CanvasText` 两者都不发生 —— 空内容不进名字,
|
|
245
|
+
系统色背景不会被强制色替换(对比进度条的 `Highlight` 也是这么活的)。 */
|
|
246
|
+
.shell__link[aria-current="page"]::before {
|
|
247
|
+
content: "";
|
|
248
|
+
position: absolute;
|
|
249
|
+
inset-inline-start: 3px; /* 微几何,不是节奏间距 ⇒ 不走 --sp-* */
|
|
250
|
+
top: 50%;
|
|
251
|
+
width: 5px;
|
|
252
|
+
height: 5px;
|
|
253
|
+
margin-top: -2.5px;
|
|
254
|
+
border-radius: 50%;
|
|
255
|
+
background: CanvasText;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ============================================================================
|
|
260
|
+
2 · 顶栏
|
|
261
|
+
============================================================================ */
|
|
262
|
+
.shell__topbar {
|
|
263
|
+
grid-area: topbar;
|
|
264
|
+
display: flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: var(--sp-3);
|
|
267
|
+
min-height: var(--shell-topbar-h);
|
|
268
|
+
padding: 0 var(--shell-pad);
|
|
269
|
+
background: var(--surface);
|
|
270
|
+
border-block-end: 1px solid var(--border-decor);
|
|
271
|
+
/* 顶栏也粘住:长表单里用户滚到一半看不见"保存"是最常见的抱怨 */
|
|
272
|
+
position: sticky;
|
|
273
|
+
inset-block-start: 0;
|
|
274
|
+
z-index: var(--z-header);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* 窄屏导航按钮(由 shell-nav.js 注入,见该文件头)。
|
|
278
|
+
----------------------------------------------------------------------------
|
|
279
|
+
🔴 默认 `display: none`,只有在下面的窄屏 @media 块里才变 `inline-flex`。
|
|
280
|
+
这个"默认隐藏"不是样式选择,是**契约的一半**:
|
|
281
|
+
· JS 靠 `offsetParent === null` 判断"现在该不该有抽屉",
|
|
282
|
+
所以"什么时候显示"必须由 CSS 唯一决定,JS 一行数字都不写
|
|
283
|
+
· 桌面端即使有人手贱引了 shell-nav.js,也不会多出一个多余的汉堡按钮
|
|
284
|
+
|
|
285
|
+
⚠️ 没有 JS 时这条规则仍然生效(按钮根本不存在),所以基础形态不受影响。 */
|
|
286
|
+
.shell__menu {
|
|
287
|
+
display: none;
|
|
288
|
+
align-items: center;
|
|
289
|
+
justify-content: center;
|
|
290
|
+
flex: none; /* 标题很长时不要让按钮被压扁 */
|
|
291
|
+
width: var(--sz-touch); /* 44px 命中区(CONVENTIONS 第八节) */
|
|
292
|
+
height: var(--sz-touch);
|
|
293
|
+
/* 视觉上贴起始边:负 margin 只收视觉,热区仍是 44px */
|
|
294
|
+
margin-inline-start: calc(-1 * var(--sp-2));
|
|
295
|
+
padding: 0;
|
|
296
|
+
color: var(--text-secondary);
|
|
297
|
+
background: none;
|
|
298
|
+
/* 强制色预留:透明边框会被解析成 CanvasText 实线,不需要 media query */
|
|
299
|
+
border: 1px solid transparent;
|
|
300
|
+
border-radius: var(--r-sm);
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.shell__menu:focus-visible {
|
|
305
|
+
outline: 2px solid var(--accent);
|
|
306
|
+
outline-offset: 2px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@media (hover: hover) and (pointer: fine) {
|
|
310
|
+
.shell__menu:hover {
|
|
311
|
+
color: var(--text-primary);
|
|
312
|
+
background: var(--surface-sunken);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* 靠右的一组操作(头像/通知/保存) */
|
|
317
|
+
.shell__topbar-end {
|
|
318
|
+
display: flex;
|
|
319
|
+
align-items: center;
|
|
320
|
+
gap: var(--sp-2);
|
|
321
|
+
margin-inline-start: auto;
|
|
322
|
+
/* 🔴 不许被压缩。默认 `flex-shrink: 1` 时,窄屏上这组会被标题挤到
|
|
323
|
+
只剩一半宽,里面的文字**折成两行** —— 顶栏从 56px 长到 76px,
|
|
324
|
+
看起来像"手机上顶栏突然变厚了"(0.19.0 实拍)。
|
|
325
|
+
让它不收缩,标题自然会先截断(`.shell__title` 已经 `min-width: 0`)。 */
|
|
326
|
+
flex: none;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* ⚠️ 选择器带 `.shell__topbar` 前缀是为了压过页面骨架的 `.page h2`
|
|
330
|
+
((0,1,1) > (0,1,0))。不加的话标题的上下 margin 会把顶栏从 56px 撑到 99px ——
|
|
331
|
+
而"顶栏比设计高 43px"这种问题在截图里只像"留白多了点",很容易放过。 */
|
|
332
|
+
.shell__topbar .shell__title {
|
|
333
|
+
margin: 0;
|
|
334
|
+
font-size: var(--fs-md);
|
|
335
|
+
font-weight: 500;
|
|
336
|
+
color: var(--text-primary);
|
|
337
|
+
/* 标题长时截断,不要挤掉右侧操作区 */
|
|
338
|
+
min-width: 0;
|
|
339
|
+
overflow: hidden;
|
|
340
|
+
text-overflow: ellipsis;
|
|
341
|
+
white-space: nowrap;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ============================================================================
|
|
345
|
+
3 · 主区
|
|
346
|
+
============================================================================ */
|
|
347
|
+
.shell__main {
|
|
348
|
+
grid-area: main;
|
|
349
|
+
padding: var(--shell-pad);
|
|
350
|
+
/* 盖回纸色:外壳是侧栏色(见文件头分层说明) */
|
|
351
|
+
background: var(--paper);
|
|
352
|
+
/* 分界线画在**主区**上:它占满整个 grid 区(含"内容不足一屏"的部分),
|
|
353
|
+
所以这条线始终是通高的。画在侧栏上就只到侧栏内容为止。 */
|
|
354
|
+
border-inline-start: 1px solid var(--border-decor);
|
|
355
|
+
/* 🔴 `min-width: 0` —— 整个组件最容易漏、后果最大的一行。
|
|
356
|
+
grid 项默认 `min-width: auto`(= 内容的最小宽度),
|
|
357
|
+
一个 12 列的宽表格就能把主区撑到 2000px,然后整个 shell 横滚。
|
|
358
|
+
加了它,表格自己滚(配合 .shell__main 里的 overflow 容器)。 */
|
|
359
|
+
min-width: 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* 主区内容的宽度约束:正文别铺满 1920px。
|
|
363
|
+
⚠️ 用 `max-width` + `margin-inline: auto` 而不是 `padding` ——
|
|
364
|
+
后者在窄屏上会把内容挤窄两次(padding + 视口)。 */
|
|
365
|
+
.shell__content {
|
|
366
|
+
max-width: var(--measure-page);
|
|
367
|
+
margin-inline: auto;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* 🔴 宽内容的**唯一**正确容器。
|
|
371
|
+
-------------------------------------------------------------------------
|
|
372
|
+
`.shell__main { min-width: 0 }` 只保证"grid 轨道不被撑开",
|
|
373
|
+
它**不会**让溢出的内容自己滚 —— `overflow` 的默认值就是 `visible`,
|
|
374
|
+
于是 80rem 宽的表格照旧溢出到页面上,整页横滚。
|
|
375
|
+
|
|
376
|
+
第一版这里只写了"宽内容要在自己的 overflow 容器里滚",
|
|
377
|
+
却没给那个容器 ⇒ 契约是半截的(0.18.0 门禁 C2 抓到:375px 视口下
|
|
378
|
+
docScrollWidth 748)。现在把它做出来:
|
|
379
|
+
|
|
380
|
+
<div class="shell__scroll" tabindex="0" role="region" aria-label="宽表格,可横向滚动">
|
|
381
|
+
<table>…</table>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
🔴 `tabindex="0"` 与 `role="region"` 不是可选的(0.18.0 axe 实测报
|
|
385
|
+
`scrollable-region-focusable`):滚动容器不聚焦,键盘用户就**滚不动**它 ——
|
|
386
|
+
鼠标用户可以拖滚动条,键盘用户没有任何办法看到右边那几列。
|
|
387
|
+
`aria-label` 也不能省:`role="region"` 没有名字时读屏只会说"区域",
|
|
388
|
+
用户不知道这块是干什么的。
|
|
389
|
+
|
|
390
|
+
判据:app-shell-check.js B1(主区不被撑破)与 C2(窄屏不横滚)。 */
|
|
391
|
+
.shell__scroll {
|
|
392
|
+
overflow-x: auto;
|
|
393
|
+
/* 横向滚到头之后不要接着滚页面(触控板上很容易误触) */
|
|
394
|
+
overscroll-behavior-x: contain;
|
|
395
|
+
/* 滚动条不占位:内容区不会因为"多了一条滚动条"而重新排版 */
|
|
396
|
+
scrollbar-width: thin;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* ============================================================================
|
|
400
|
+
4 · 窄屏退化
|
|
401
|
+
============================================================================
|
|
402
|
+
🔴 这里是**唯一**用 @media 判宽度的地方,理由写在文件头 ⑤:
|
|
403
|
+
app-shell 就是"视口的容器",它没有外层槽可言。
|
|
404
|
+
|
|
405
|
+
🔴 纯 CSS 时:侧栏直接 `display: none`,页面上**没有**别的导航入口。
|
|
406
|
+
这是**故意**的降级 —— 只读看板可以接受;管理型后台请加下面这一行脚本。
|
|
407
|
+
|
|
408
|
+
🔴 要导航入口就引 `shell-nav.js`(行为层,同目录;START-HERE 归「必需」):
|
|
409
|
+
它把 `.shell__side` **克隆**进 `03-patterns/drawer`,于是 Esc 关闭、
|
|
410
|
+
焦点陷阱、焦点归位、背景 inert、锁滚动五件事全部来自 drawer,
|
|
411
|
+
本组件一行行为逻辑都不重写。
|
|
412
|
+
(`03-patterns/nav` 做的是同一件"汉堡 → 抽屉",但它的列表常驻在文档里、
|
|
413
|
+
靠移动位置,而这里的侧栏在窄屏是隐藏的 ⇒ 生命周期相反,不能共用。)
|
|
414
|
+
|
|
415
|
+
⚠️ 别在这里自己写一份抽屉。抽屉要的不是"滑出来",是那五件事 ——
|
|
416
|
+
自己写必然漏两件,而且漏掉的那两件(焦点归位 / 背景 inert)
|
|
417
|
+
在鼠标用户眼里完全正常。
|
|
418
|
+
========================================================================== */
|
|
419
|
+
@media (max-width: 47.99rem) {
|
|
420
|
+
.shell {
|
|
421
|
+
grid-template-columns: minmax(0, 1fr);
|
|
422
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
423
|
+
grid-template-areas:
|
|
424
|
+
"topbar"
|
|
425
|
+
"main";
|
|
426
|
+
--shell-pad: var(--sp-3);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.shell__side {
|
|
430
|
+
display: none;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* 侧栏收走 ⇒ 把导航入口交还给顶栏的菜单按钮。
|
|
434
|
+
🔴 这一行是 shell-nav.js 的**唯一**开关:JS 读 `offsetParent` 决定有没有抽屉。
|
|
435
|
+
(所以窄屏断点只有这一个数字,改这里就是改全部 —— JS 不会错位。) */
|
|
436
|
+
.shell__menu {
|
|
437
|
+
display: inline-flex;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* 🔴 抽屉里的那份侧栏必须**无视**上面那条 `display: none`。
|
|
442
|
+
----------------------------------------------------------------------------
|
|
443
|
+
克隆出来的节点带着 `.shell__side` 类 ⇒ 它同样命中窄屏那条规则
|
|
444
|
+
⇒ 抽屉能打开、遮罩在、关闭按钮在,但**内容是空的**(0.19.0 实拍)。
|
|
445
|
+
这不是"忘记加样式",是"同一条规则对两种容器说了相反的话":
|
|
446
|
+
|
|
447
|
+
· 桌面侧栏 在窄屏应该消失(顶栏有菜单按钮)
|
|
448
|
+
· 抽屉副本 在窄屏必须显示(它就是为了替代侧栏才存在的)
|
|
449
|
+
|
|
450
|
+
所以给副本加修饰类,用 0,2,0 的选择器压过 0,1,0 的那条 ——
|
|
451
|
+
不靠源码顺序,也不靠 `!important`。 */
|
|
452
|
+
.shell__side.shell__side--drawer {
|
|
453
|
+
display: flex;
|
|
454
|
+
/* 抽屉自己会滚(.drawer__body),别在里头再套一个滚动容器 */
|
|
455
|
+
max-height: none;
|
|
456
|
+
overflow-y: visible;
|
|
457
|
+
position: static;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* ============================================================================
|
|
461
|
+
5 · 变体
|
|
462
|
+
============================================================================ */
|
|
463
|
+
|
|
464
|
+
/* 侧栏更窄(只有图标):适合"工具条"型产品。
|
|
465
|
+
----------------------------------------------------------------------------
|
|
466
|
+
🔴 图标态的文字用 `.shell__link-label` 包起来,这里只做**视觉隐藏**:
|
|
467
|
+
|
|
468
|
+
<a class="shell__link" href="#" title="总览">
|
|
469
|
+
<svg … aria-hidden="true">…</svg><span class="shell__link-label">总览</span>
|
|
470
|
+
</a>
|
|
471
|
+
|
|
472
|
+
为什么不是"删掉文字 + 写 aria-label":
|
|
473
|
+
① 名字有两个来源就会漂 —— 改了可见文字忘了改 aria-label,读屏念的还是旧的
|
|
474
|
+
② 抽屉里那份是侧栏的**克隆**,克隆被挂在 `<body>` 下(drawer.js),
|
|
475
|
+
它**离开了 `.shell--rail` 的作用域** ⇒ 下面这条隐藏规则不再命中
|
|
476
|
+
⇒ 抽屉里自动变成"图标 + 文字"。文字删了就永远补不回来。
|
|
477
|
+
|
|
478
|
+
⚠️ 不能用 `display: none`:那样文字不参与可访问名计算,链接退化成
|
|
479
|
+
"链接、链接、链接"。门禁 F12 守名字、G4 守这条(改成 display:none 必须变红)。
|
|
480
|
+
⚠️ 仍建议给 `title`:44px 的图标对鼠标用户需要悬停提示。但它只是提示 ——
|
|
481
|
+
名字必须来自 `.shell__link-label`(`title` 会兜出名字,把上面的问题盖住)。 */
|
|
482
|
+
.shell--rail {
|
|
483
|
+
--shell-side-w: 4.5rem;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.shell--rail .shell__side {
|
|
487
|
+
padding-inline: var(--sp-1);
|
|
488
|
+
align-items: center;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.shell--rail .shell__link {
|
|
492
|
+
/* 绝对定位的标签以链接为包含块,不会被算到侧栏的滚动区外 */
|
|
493
|
+
position: relative;
|
|
494
|
+
justify-content: center;
|
|
495
|
+
padding-inline: 0;
|
|
496
|
+
width: var(--sz-touch);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* 视觉隐藏(保留在可访问树里)。这是 Web 上最老的一段样板之一,
|
|
500
|
+
每一行都有作用:1px 盒 + 裁切;`white-space: nowrap` 防止文字在
|
|
501
|
+
1px 宽里折成几十行(Safari 会因此把行高算得很夸张)。 */
|
|
502
|
+
.shell--rail .shell__link-label {
|
|
503
|
+
position: absolute;
|
|
504
|
+
width: 1px;
|
|
505
|
+
height: 1px;
|
|
506
|
+
margin: -1px;
|
|
507
|
+
padding: 0;
|
|
508
|
+
overflow: hidden;
|
|
509
|
+
clip-path: inset(50%);
|
|
510
|
+
white-space: nowrap;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/* 品牌:72px 里只留标记,名字视觉隐藏(与 `.shell__link-label` 同一手法)。
|
|
514
|
+
⚠️ 没有 `.shell__brand-text` 的老写法(品牌名是直接文本)会退化成
|
|
515
|
+
`.shell__brand` 自己的 ellipsis ⇒ 显示成「金…」。那不是坏,
|
|
516
|
+
是**提醒你补上两段结构** —— 门禁 H1 会对 rail 实例判这一条。 */
|
|
517
|
+
.shell--rail .shell__brand {
|
|
518
|
+
justify-content: center;
|
|
519
|
+
padding-inline: 0;
|
|
520
|
+
gap: 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.shell--rail .shell__brand-text {
|
|
524
|
+
position: absolute;
|
|
525
|
+
width: 1px;
|
|
526
|
+
height: 1px;
|
|
527
|
+
margin: -1px;
|
|
528
|
+
padding: 0;
|
|
529
|
+
overflow: hidden;
|
|
530
|
+
clip-path: inset(50%);
|
|
531
|
+
white-space: nowrap;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/* 分组标签:72px 放不下「数据 / 校验」这种词,折成两行会把三条图标挤散。
|
|
535
|
+
⇒ 图标态**不要**用 `.shell__side-label`:让两个 `.shell__side-group` 相邻,
|
|
536
|
+
`+` 那条规则会自动给出一条分隔线与间距(这才是分隔线的设计用途)。
|
|
537
|
+
这条规则是**兜底**:使用者直接沿用宽屏结构时,宁可标签消失,也不要它折行;
|
|
538
|
+
标签是纯视觉文本(`<p>`,不是标题、也不进任何元素的可访问名),
|
|
539
|
+
所以 `display: none` 不会让任何元素失去名字。 */
|
|
540
|
+
.shell--rail .shell__side-label {
|
|
541
|
+
display: none;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/* 图标态的分隔线要与图标同宽居中,否则在 72px 里看着像一条贴边的毛刺 */
|
|
545
|
+
.shell--rail .shell__side-group {
|
|
546
|
+
align-items: center;
|
|
547
|
+
width: 100%;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/* 侧栏无边框(与主区同色,靠留白分层) */
|
|
551
|
+
.shell--flat {
|
|
552
|
+
background: var(--paper); /* 外壳回到纸色 ⇒ 侧栏与主区同色 */
|
|
553
|
+
}
|
|
554
|
+
.shell--flat .shell__main {
|
|
555
|
+
border-inline-start-color: transparent; /* 无色差时也不画线:靠留白分层 */
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/* 主区自己滚(看板型:侧栏与顶栏完全不动,只有内容区滚动)。
|
|
559
|
+
⚠️ 用这个变体时**不要**再让页面滚 —— 两个滚动容器会让"滚到底"的行为不可预期。 */
|
|
560
|
+
.shell--scroll-main {
|
|
561
|
+
height: 100vh;
|
|
562
|
+
height: 100dvh;
|
|
563
|
+
min-height: 0;
|
|
564
|
+
overflow: hidden;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.shell--scroll-main .shell__side {
|
|
568
|
+
position: static;
|
|
569
|
+
max-height: none;
|
|
570
|
+
height: 100%;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.shell--scroll-main .shell__topbar {
|
|
574
|
+
position: static;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.shell--scroll-main .shell__main {
|
|
578
|
+
overflow-y: auto;
|
|
579
|
+
overscroll-behavior: contain;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* ============================================================================
|
|
583
|
+
6 · 减少动效
|
|
584
|
+
============================================================================
|
|
585
|
+
本组件的动效只有链接的底色/文字色过渡(颜色档),按 CONVENTIONS 第四节
|
|
586
|
+
第 3 档处理:可以缩短保留,不强制关。这里不写 media 块 ——
|
|
587
|
+
`--dur-fast` 本身已在 tokens 里被 reduce 档缩短。
|
|
588
|
+
========================================================================== */
|