agentcss 0.0.0-stage → 0.11.2
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/README.md +236 -0
- package/01-tokens/behavior/dismissable.js +132 -0
- package/01-tokens/behavior/emit.js +76 -0
- package/01-tokens/behavior/focus-return.js +81 -0
- package/01-tokens/behavior/roving.js +152 -0
- package/01-tokens/behavior/typeahead.js +124 -0
- package/01-tokens/focus-ring-demo.html +133 -0
- package/01-tokens/focus-ring.css +102 -0
- package/01-tokens/page.css +244 -0
- package/01-tokens/polyfill.js +116 -0
- package/01-tokens/responsive-demo.html +171 -0
- package/01-tokens/scrollbar-demo.html +194 -0
- package/01-tokens/scrollbar.css +179 -0
- package/01-tokens/theme-toggle.js +103 -0
- package/01-tokens/tokens.css +972 -0
- package/01-tokens/typography.css +156 -0
- package/02-primitives/badge/README.md +71 -0
- package/02-primitives/badge/badge.css +141 -0
- package/02-primitives/badge/demo.html +101 -0
- package/02-primitives/button/README.md +99 -0
- package/02-primitives/button/button.css +353 -0
- package/02-primitives/button/demo.html +200 -0
- package/02-primitives/card/README.md +145 -0
- package/02-primitives/card/card.css +246 -0
- package/02-primitives/card/demo.html +170 -0
- package/02-primitives/choice/README.md +161 -0
- package/02-primitives/choice/choice.css +395 -0
- package/02-primitives/choice/demo.html +277 -0
- package/02-primitives/combobox/README.md +79 -0
- package/02-primitives/combobox/combobox.css +223 -0
- package/02-primitives/combobox/combobox.js +485 -0
- package/02-primitives/combobox/demo.html +109 -0
- package/02-primitives/date-range/README.md +100 -0
- package/02-primitives/date-range/date-range.css +206 -0
- package/02-primitives/date-range/date-range.js +301 -0
- package/02-primitives/date-range/demo.html +104 -0
- package/02-primitives/input/README.md +170 -0
- package/02-primitives/input/demo.html +246 -0
- package/02-primitives/input/input.css +239 -0
- package/02-primitives/popover/README.md +80 -0
- package/02-primitives/popover/demo.html +150 -0
- package/02-primitives/popover/popover.css +187 -0
- package/02-primitives/popover/popover.js +331 -0
- package/02-primitives/progress/README.md +82 -0
- package/02-primitives/progress/demo.html +154 -0
- package/02-primitives/progress/progress.css +172 -0
- package/02-primitives/select/README.md +95 -0
- package/02-primitives/select/demo.html +121 -0
- package/02-primitives/select/select.css +151 -0
- package/02-primitives/select/select.js +549 -0
- package/02-primitives/separator/README.md +63 -0
- package/02-primitives/separator/demo.html +84 -0
- package/02-primitives/separator/separator.css +73 -0
- package/02-primitives/skeleton/README.md +69 -0
- package/02-primitives/skeleton/demo.html +110 -0
- package/02-primitives/skeleton/skeleton.css +96 -0
- package/02-primitives/switch/README.md +65 -0
- package/02-primitives/switch/demo.html +115 -0
- package/02-primitives/switch/switch.css +370 -0
- package/03-patterns/accordion/README.md +121 -0
- package/03-patterns/accordion/accordion.css +453 -0
- package/03-patterns/accordion/accordion.js +369 -0
- package/03-patterns/accordion/demo.html +223 -0
- package/03-patterns/content/README.md +103 -0
- package/03-patterns/content/content.css +286 -0
- package/03-patterns/content/demo.html +88 -0
- package/03-patterns/drawer/README.md +79 -0
- package/03-patterns/drawer/demo.html +101 -0
- package/03-patterns/drawer/drawer.css +129 -0
- package/03-patterns/drawer/drawer.js +258 -0
- package/03-patterns/dropdown/README.md +78 -0
- package/03-patterns/dropdown/demo.html +101 -0
- package/03-patterns/dropdown/dropdown.css +135 -0
- package/03-patterns/dropdown/dropdown.js +451 -0
- package/03-patterns/form-validation/README.md +147 -0
- package/03-patterns/form-validation/demo.html +280 -0
- package/03-patterns/form-validation/form-validation.css +263 -0
- package/03-patterns/list/README.md +150 -0
- package/03-patterns/list/demo.html +225 -0
- package/03-patterns/list/flip.js +201 -0
- package/03-patterns/list/list.css +162 -0
- package/03-patterns/nav/README.md +225 -0
- package/03-patterns/nav/demo.html +131 -0
- package/03-patterns/nav/nav.css +314 -0
- package/03-patterns/nav/nav.js +430 -0
- package/03-patterns/nav/toc.js +111 -0
- package/03-patterns/overlay/README.md +196 -0
- package/03-patterns/overlay/demo.html +241 -0
- package/03-patterns/overlay/overlay.css +319 -0
- package/03-patterns/overlay/overlay.js +563 -0
- package/03-patterns/pagination/README.md +114 -0
- package/03-patterns/pagination/demo.html +165 -0
- package/03-patterns/pagination/pagination.css +195 -0
- package/03-patterns/pagination/pagination.js +395 -0
- package/03-patterns/reveal/README.md +91 -0
- package/03-patterns/reveal/demo.html +106 -0
- package/03-patterns/reveal/reveal.css +93 -0
- package/03-patterns/reveal/reveal.js +172 -0
- package/03-patterns/states/README.md +141 -0
- package/03-patterns/states/demo.html +240 -0
- package/03-patterns/states/states.css +252 -0
- package/03-patterns/tabs/README.md +84 -0
- package/03-patterns/tabs/demo.html +139 -0
- package/03-patterns/tabs/tabs.css +172 -0
- package/03-patterns/tabs/tabs.js +307 -0
- package/03-patterns/tooltip/README.md +96 -0
- package/03-patterns/tooltip/demo.html +78 -0
- package/03-patterns/tooltip/tooltip.css +89 -0
- package/03-patterns/tooltip/tooltip.js +288 -0
- package/03-patterns/tree/README.md +94 -0
- package/03-patterns/tree/demo.html +138 -0
- package/03-patterns/tree/tree.css +113 -0
- package/03-patterns/tree/tree.js +456 -0
- package/04-recipes/analysis-report/README.md +80 -0
- package/04-recipes/analysis-report/analysis-report.css +187 -0
- package/04-recipes/analysis-report/demo.html +274 -0
- package/04-recipes/data-showcase/README.md +45 -0
- package/04-recipes/data-showcase/demo.html +247 -0
- package/04-recipes/detail-page/README.md +93 -0
- package/04-recipes/detail-page/demo.html +55 -0
- package/04-recipes/detail-page/detail-page.css +190 -0
- package/04-recipes/detail-page/full.html +60 -0
- package/04-recipes/detail-page/null.html +60 -0
- package/04-recipes/detail-page/partial.html +60 -0
- package/04-recipes/detail-page/verify-skeleton.py +19 -0
- package/04-recipes/longform/README.md +93 -0
- package/04-recipes/longform/agc-memory.svg +4 -0
- package/04-recipes/longform/content.html +610 -0
- package/04-recipes/longform/content.md +51 -0
- package/04-recipes/longform/longform.html +627 -0
- package/04-recipes/report/README.md +176 -0
- package/04-recipes/report/demo.html +355 -0
- package/04-recipes/report/report.css +364 -0
- package/04-recipes/table/README.md +103 -0
- package/04-recipes/table/demo.html +455 -0
- package/04-recipes/table/table.css +372 -0
- package/04-recipes/tierA-tokens.css +189 -0
- package/09-assets/README.md +74 -0
- package/09-assets/bar/README.md +94 -0
- package/09-assets/bar/bar.css +166 -0
- package/09-assets/bar/bar.js +320 -0
- package/09-assets/bar/demo.html +176 -0
- package/09-assets/echarts-adapter/demo.html +192 -0
- package/09-assets/echarts-adapter/echarts-adapter.js +277 -0
- package/09-assets/model-viewer/README.md +155 -0
- package/09-assets/model-viewer/demo.html +105 -0
- package/09-assets/model-viewer/model-viewer.css +143 -0
- package/09-assets/model-viewer/model-viewer.js +423 -0
- package/09-assets/scientific-plot/README.md +94 -0
- package/09-assets/scientific-plot/demo.html +252 -0
- package/09-assets/scientific-plot/scientific-plot.css +141 -0
- package/09-assets/scientific-plot/scientific-plot.js +610 -0
- package/09-assets/sparkline/README.md +153 -0
- package/09-assets/sparkline/demo.html +163 -0
- package/09-assets/sparkline/sparkline.css +158 -0
- package/09-assets/sparkline/sparkline.js +508 -0
- package/API.md +332 -0
- package/CHANGELOG.md +3116 -0
- package/CODE_OF_CONDUCT.md +64 -0
- package/CONTRIBUTING.md +89 -0
- package/ERRORS.md +1658 -0
- package/LICENSE +21 -0
- package/README.md +491 -2
- package/SECURITY.md +72 -0
- package/START-HERE.md +164 -0
- package/SUPPORT.md +109 -0
- package/adapters/presentation/README.md +113 -0
- package/adapters/presentation/check.js +101 -0
- package/adapters/presentation/deck.schema.json +74 -0
- package/adapters/presentation/to-doc.js +138 -0
- package/ai/START-HERE.md +104 -0
- package/ai/behaviors.json +380 -0
- package/ai/behaviors.schema.json +61 -0
- package/ai/capabilities.json +56 -0
- package/ai/cli.js +387 -0
- package/ai/components.json +1944 -0
- package/ai/components.meta.json +168 -0
- package/ai/components.schema.json +220 -0
- package/ai/contract.schema.json +147 -0
- package/ai/deprecations.json +55 -0
- package/ai/deprecations.schema.json +85 -0
- package/ai/motion-policy.json +22 -0
- package/ai/patch.js +323 -0
- package/ai/patch.schema.json +63 -0
- package/ai/profiles.json +92 -0
- package/ai/tokens.audience.json +55 -0
- package/ai/tokens.audience.schema.json +73 -0
- package/ai/tokens.json +762 -0
- package/ai/tokens.schema.json +58 -0
- package/ai/tokens.tree.json +752 -0
- package/ai/tokens.tree.schema.json +56 -0
- package/ai/validate.js +383 -0
- package/dist/01-tokens/behavior/dismissable.js +60 -0
- package/dist/01-tokens/behavior/emit.js +21 -0
- package/dist/01-tokens/behavior/focus-return.js +31 -0
- package/dist/01-tokens/behavior/roving.js +77 -0
- package/dist/01-tokens/behavior/typeahead.js +61 -0
- package/dist/01-tokens/focus-ring.css +1 -0
- package/dist/01-tokens/page.css +1 -0
- package/dist/01-tokens/polyfill.js +78 -0
- package/dist/01-tokens/scrollbar.css +1 -0
- package/dist/01-tokens/theme-toggle.js +61 -0
- package/dist/01-tokens/tokens.css +1 -0
- package/dist/01-tokens/typography.css +1 -0
- package/dist/02-primitives/badge/badge.css +1 -0
- package/dist/02-primitives/button/button.css +1 -0
- package/dist/02-primitives/card/card.css +1 -0
- package/dist/02-primitives/choice/choice.css +1 -0
- package/dist/02-primitives/combobox/combobox.css +1 -0
- package/dist/02-primitives/combobox/combobox.js +439 -0
- package/dist/02-primitives/date-range/date-range.css +1 -0
- package/dist/02-primitives/date-range/date-range.js +267 -0
- package/dist/02-primitives/input/input.css +1 -0
- package/dist/02-primitives/popover/popover.css +1 -0
- package/dist/02-primitives/popover/popover.js +257 -0
- package/dist/02-primitives/progress/progress.css +1 -0
- package/dist/02-primitives/select/select.css +1 -0
- package/dist/02-primitives/select/select.js +449 -0
- package/dist/02-primitives/separator/separator.css +1 -0
- package/dist/02-primitives/skeleton/skeleton.css +1 -0
- package/dist/02-primitives/switch/switch.css +1 -0
- package/dist/03-patterns/accordion/accordion.css +1 -0
- package/dist/03-patterns/accordion/accordion.js +219 -0
- package/dist/03-patterns/content/content.css +1 -0
- package/dist/03-patterns/drawer/drawer.css +1 -0
- package/dist/03-patterns/drawer/drawer.js +229 -0
- package/dist/03-patterns/dropdown/dropdown.css +1 -0
- package/dist/03-patterns/dropdown/dropdown.js +365 -0
- package/dist/03-patterns/form-validation/form-validation.css +1 -0
- package/dist/03-patterns/list/flip.js +117 -0
- package/dist/03-patterns/list/list.css +1 -0
- package/dist/03-patterns/nav/nav.css +1 -0
- package/dist/03-patterns/nav/nav.js +322 -0
- package/dist/03-patterns/nav/toc.js +76 -0
- package/dist/03-patterns/overlay/overlay.css +1 -0
- package/dist/03-patterns/overlay/overlay.js +398 -0
- package/dist/03-patterns/pagination/pagination.css +1 -0
- package/dist/03-patterns/pagination/pagination.js +273 -0
- package/dist/03-patterns/reveal/reveal.css +1 -0
- package/dist/03-patterns/reveal/reveal.js +138 -0
- package/dist/03-patterns/states/states.css +1 -0
- package/dist/03-patterns/tabs/tabs.css +1 -0
- package/dist/03-patterns/tabs/tabs.js +242 -0
- package/dist/03-patterns/tooltip/tooltip.css +1 -0
- package/dist/03-patterns/tooltip/tooltip.js +213 -0
- package/dist/03-patterns/tree/tree.css +1 -0
- package/dist/03-patterns/tree/tree.js +383 -0
- package/dist/04-recipes/analysis-report/analysis-report.css +1 -0
- package/dist/04-recipes/detail-page/detail-page.css +1 -0
- package/dist/04-recipes/report/report.css +1 -0
- package/dist/04-recipes/table/table.css +1 -0
- package/dist/04-recipes/tierA-tokens.css +1 -0
- package/dist/09-assets/bar/bar.css +1 -0
- package/dist/09-assets/bar/bar.js +222 -0
- package/dist/09-assets/echarts-adapter/echarts-adapter.js +185 -0
- package/dist/09-assets/model-viewer/model-viewer.css +1 -0
- package/dist/09-assets/model-viewer/model-viewer.js +320 -0
- package/dist/09-assets/scientific-plot/scientific-plot.css +1 -0
- package/dist/09-assets/scientific-plot/scientific-plot.js +568 -0
- package/dist/09-assets/sparkline/sparkline.css +1 -0
- package/dist/09-assets/sparkline/sparkline.js +265 -0
- package/dist/COSTS.md +56 -0
- package/docs/AI-CONTRACT.md +197 -0
- package/docs/BACKLOG.md +127 -0
- package/docs/BENCHMARK.md +252 -0
- package/docs/GUIDANCE.md +154 -0
- package/docs/INVARIANT.md +372 -0
- package/docs/LAYER-BASELINE.md +158 -0
- package/docs/RELEASING.md +275 -0
- package/docs/RFC-THEME-SCOPE.md +141 -0
- package/docs/ROADMAP.md +182 -0
- package/index.html +82 -0
- package/package.json +107 -4
- package/types/index.d.ts +310 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
frontend-lib / 03-patterns / list / flip.js
|
|
3
|
+
列表增删的 FLIP 动画
|
|
4
|
+
|
|
5
|
+
依赖:无
|
|
6
|
+
适用:精简档 / B / C
|
|
7
|
+
|
|
8
|
+
🔴 为什么自己写,不用 auto-animate:
|
|
9
|
+
|
|
10
|
+
项目: auto-animate 自己写 FLIP
|
|
11
|
+
体积 ~8 KB ~1.5 KB
|
|
12
|
+
协议尽调 必需 零风险
|
|
13
|
+
依赖风险 供应链/更新 无
|
|
14
|
+
能力 增删改 + 颜色 + 尺寸 + 表单值 只做"增删时其余项平滑移动"
|
|
15
|
+
|
|
16
|
+
列表 pattern 真正需要的只有最后一列那一项。
|
|
17
|
+
为不需要的能力引入第三方依赖,不划算。
|
|
18
|
+
|
|
19
|
+
FLIP 原理(四步,约 40 行):
|
|
20
|
+
First 记录变化前的位置
|
|
21
|
+
Last DOM 变化后记录新位置
|
|
22
|
+
Invert 用 transform 反向偏移回去(视觉上没动)
|
|
23
|
+
Play 过渡到 transform: 0(看起来平滑移动)
|
|
24
|
+
|
|
25
|
+
🔴 语法刻意用 ES5(无箭头函数 / const / let / 模板字符串):
|
|
26
|
+
精简档 是 某项目 上的 WebView,版本不确定。
|
|
27
|
+
========================================================================= */
|
|
28
|
+
(function (global) {
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
/* ------------------------------------------------------------ 工具 */
|
|
32
|
+
function forceReflow(el) {
|
|
33
|
+
/* 强制浏览器重算布局并记录当前样式值。
|
|
34
|
+
FLIP 的第三步(Invert)靠它把"反向偏移"这个中间态真正记录下来;
|
|
35
|
+
少了这一步,浏览器会认为 transform "没变"而跳过过渡。
|
|
36
|
+
|
|
37
|
+
读 offsetHeight 会强制同步布局 —— 这正是我们要的"副作用"。
|
|
38
|
+
requestAnimationFrame **不能**替代它(rAF 不保证重排)。 */
|
|
39
|
+
return el.offsetHeight;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function idOf(el) {
|
|
43
|
+
/* 不用 dataset —— 兼容性更差的那个 API */
|
|
44
|
+
return el.getAttribute('data-flip-id');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function prefersReducedMotion() {
|
|
48
|
+
return global.matchMedia
|
|
49
|
+
&& global.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ------------------------------------------------------------ 核心 */
|
|
53
|
+
/**
|
|
54
|
+
* @param container 列表容器(它的**直接子元素**就是可动画项)
|
|
55
|
+
* @param mutate 修改 DOM 的函数
|
|
56
|
+
* @param opts { enter: 淡入/上滑, leave: 淡出, duration: ms }
|
|
57
|
+
*
|
|
58
|
+
* 用法:
|
|
59
|
+
* flip(listEl, function () { listEl.removeChild(row); });
|
|
60
|
+
*/
|
|
61
|
+
function flip(container, mutate, opts) {
|
|
62
|
+
opts = opts || {};
|
|
63
|
+
|
|
64
|
+
var DURATION = opts.duration || 260;
|
|
65
|
+
var reduce = prefersReducedMotion();
|
|
66
|
+
|
|
67
|
+
/* --- First:记录每个项变化前的位置 --- */
|
|
68
|
+
var first = {};
|
|
69
|
+
var kids = container.children;
|
|
70
|
+
var i, el, id, box;
|
|
71
|
+
|
|
72
|
+
for (i = 0; i < kids.length; i++) {
|
|
73
|
+
el = kids[i];
|
|
74
|
+
id = idOf(el);
|
|
75
|
+
if (id) { first[id] = el.getBoundingClientRect(); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* --- 让调用方改 DOM --- */
|
|
79
|
+
mutate();
|
|
80
|
+
|
|
81
|
+
if (reduce) {
|
|
82
|
+
/* 🔴 reduced-motion:**减弱,不是归零**
|
|
83
|
+
|
|
84
|
+
之前这里直接 return,等于"完全跳过" —— 与本库 CSS 层的策略
|
|
85
|
+
(spinner 保留、骨架屏停止 = 减弱)自相矛盾。
|
|
86
|
+
|
|
87
|
+
正确的做法是按"是否构成前庭刺激"来分:
|
|
88
|
+
位移 / 缩放 / 旋转 → 有刺激,去掉
|
|
89
|
+
opacity 淡入淡出 → 无刺激,保留
|
|
90
|
+
|
|
91
|
+
所以 reduced-motion 下:其余项**直接落到新位置**(不做 FLIP),
|
|
92
|
+
新增项**仍然淡入**,只是不位移。
|
|
93
|
+
*/
|
|
94
|
+
for (i = 0; i < container.children.length; i++) {
|
|
95
|
+
el = container.children[i];
|
|
96
|
+
id = idOf(el);
|
|
97
|
+
if (!id || first[id] !== undefined) { continue; }
|
|
98
|
+
el.style.opacity = '0';
|
|
99
|
+
el.style.transition = 'none';
|
|
100
|
+
/* 同样用强制重排而不是 rAF —— 与下面 FLIP 主流程保持一致 */
|
|
101
|
+
forceReflow(el);
|
|
102
|
+
el.style.transition = 'opacity ' + DURATION + 'ms linear';
|
|
103
|
+
el.style.opacity = '1';
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* --- Last + Invert + Play --- */
|
|
109
|
+
kids = container.children;
|
|
110
|
+
for (i = 0; i < kids.length; i++) {
|
|
111
|
+
el = kids[i];
|
|
112
|
+
id = idOf(el);
|
|
113
|
+
if (!id) { continue; }
|
|
114
|
+
|
|
115
|
+
box = el.getBoundingClientRect();
|
|
116
|
+
|
|
117
|
+
if (first[id] === undefined) {
|
|
118
|
+
/* ---- 新增项:**只淡入,不位移** ----
|
|
119
|
+
🔴 修正:原来这里还加了 translateY(6px)。
|
|
120
|
+
点击"添加一条"时看到**文字重合了一瞬** —— 根因是:
|
|
121
|
+
新项一边向上位移一边淡入,而它下方的旧行正往下滑开,
|
|
122
|
+
两者在某一帧**交叉**,视觉上就是两行文字叠在一起。
|
|
123
|
+
|
|
124
|
+
位移是这里唯一制造交叉的要素,去掉它即可。
|
|
125
|
+
旧行照常 FLIP 滑开,新项原地淡入 —— 观感更干净。 */
|
|
126
|
+
el.style.opacity = '0';
|
|
127
|
+
el.style.transition = 'none';
|
|
128
|
+
forceReflow(el);
|
|
129
|
+
el.style.transition = 'opacity ' + DURATION + 'ms linear';
|
|
130
|
+
el.style.opacity = '1';
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ---- 已有项:算出位移,反向偏移再过渡回 0 ---- */
|
|
135
|
+
var dx = first[id].left - box.left;
|
|
136
|
+
var dy = first[id].top - box.top;
|
|
137
|
+
|
|
138
|
+
if (dx === 0 && dy === 0) { continue; } /* 没动就别白跑一趟动画 */
|
|
139
|
+
|
|
140
|
+
el.style.transition = 'none';
|
|
141
|
+
el.style.transform = 'translate(' + dx + 'px, ' + dy + 'px)';
|
|
142
|
+
|
|
143
|
+
/* 🔴🔴 修正一个**让动画完全不动**的经典错误(实测反馈"完全不动"):
|
|
144
|
+
|
|
145
|
+
原写法是「下一帧把 transform 设成**同一个值**」——
|
|
146
|
+
el.style.transform = translate(dx,dy);
|
|
147
|
+
rAF(→ el.style.transform = translate(dx,dy)); ← 值没变!
|
|
148
|
+
浏览器看到 transform 没变化,**不播放过渡**,于是元素直接跳到新位置。
|
|
149
|
+
|
|
150
|
+
而且 rAF **不保证**触发布局重算,所以第一步的"反向偏移"可能根本没被记录。
|
|
151
|
+
|
|
152
|
+
正确做法(FLIP 标准流程):
|
|
153
|
+
1. transition:none + 设反向偏移
|
|
154
|
+
2. **强制重排**(显式读一次布局,让浏览器记录当前值)
|
|
155
|
+
3. 设 transition
|
|
156
|
+
4. **把 transform 设回 ''(即 0)** —— 不是设成同样的偏移值
|
|
157
|
+
*/
|
|
158
|
+
forceReflow(el);
|
|
159
|
+
el.style.transition = 'transform ' + DURATION + 'ms ease-out';
|
|
160
|
+
el.style.transform = ''; /* 回到原位 = 视觉上"平滑滑过去" */
|
|
161
|
+
|
|
162
|
+
/* 过渡结束后清掉内联样式 —— 否则会污染 hover / 后续布局 */
|
|
163
|
+
global.setTimeout(function (node) {
|
|
164
|
+
return function () { node.style.transition = ''; };
|
|
165
|
+
}(el), DURATION + 20);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ------------------------------------------------------------ 退场动画
|
|
170
|
+
删除项不能直接 remove —— 那样其余项会"瞬间跳过去",没有补间。
|
|
171
|
+
正确:先淡出并让它留在原位,等动画结束再真正移除。 */
|
|
172
|
+
function removeWithAnimation(container, el, done) {
|
|
173
|
+
var reduce = prefersReducedMotion();
|
|
174
|
+
var DURATION = 260;
|
|
175
|
+
|
|
176
|
+
if (reduce) {
|
|
177
|
+
/* 减弱,不是归零:**淡出保留**(opacity 无前庭刺激),
|
|
178
|
+
只去掉 scale 缩放。 */
|
|
179
|
+
el.style.transition = 'opacity ' + DURATION + 'ms linear';
|
|
180
|
+
el.style.opacity = '0';
|
|
181
|
+
global.setTimeout(function () {
|
|
182
|
+
if (el.parentNode) { el.parentNode.removeChild(el); }
|
|
183
|
+
if (done) { done(); }
|
|
184
|
+
}, DURATION);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
el.style.transition = 'opacity ' + DURATION + 'ms ease-in';
|
|
189
|
+
el.style.opacity = '0';
|
|
190
|
+
/* 轻微缩一下,给出"被移除"的反馈;不缩会像元素凭空消失 */
|
|
191
|
+
el.style.transform = 'scale(0.97)';
|
|
192
|
+
|
|
193
|
+
global.setTimeout(function () {
|
|
194
|
+
if (el.parentNode) { el.parentNode.removeChild(el); }
|
|
195
|
+
if (done) { done(); }
|
|
196
|
+
}, DURATION);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
global.FLIP = { flip: flip, remove: removeWithAnimation };
|
|
200
|
+
|
|
201
|
+
})(window);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
frontend-lib / 03-patterns / list / list.css
|
|
3
|
+
列表:行布局 + 增删动画的样式支撑
|
|
4
|
+
|
|
5
|
+
依赖:01-tokens/tokens.css + 02-primitives/button/button.css
|
|
6
|
+
版本:A ✅ / B ✅ / C ✅
|
|
7
|
+
|
|
8
|
+
配套:flip.js(增删动画,零依赖)
|
|
9
|
+
规范:不堆卡片、每屏一个焦点、用真实数据
|
|
10
|
+
========================================================================= */
|
|
11
|
+
|
|
12
|
+
.list {
|
|
13
|
+
list-style: none;
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
border-top: 1px solid var(--border-decor);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.list__item {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: var(--sp-3);
|
|
23
|
+
padding: var(--sp-3) var(--sp-4) var(--sp-3) calc(var(--sp-4) - 2px);
|
|
24
|
+
|
|
25
|
+
/* 🔴 hover 不跳动的关键:左侧线用 transparent **预留位**,
|
|
26
|
+
hover 时只改颜色。
|
|
27
|
+
如果用 border-inline-start: 2px solid transparent 然后改成 3px,或者
|
|
28
|
+
hover 时才加 border —— 内容会横移 1~2px,这就是"列表在抖"。 */
|
|
29
|
+
border-inline-start: 2px solid transparent;
|
|
30
|
+
border-bottom: 1px solid var(--border-decor);
|
|
31
|
+
|
|
32
|
+
transition: background-color var(--dur-fast) var(--ease-out),
|
|
33
|
+
border-inline-start-color var(--dur-fast) var(--ease-out);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@media (hover: hover) and (pointer: fine) {
|
|
37
|
+
.list__item:hover {
|
|
38
|
+
background: var(--surface-sunken);
|
|
39
|
+
border-inline-start-color: var(--border-decor-str);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/* 🔴 行内操作可聚焦时,整行给反馈。
|
|
43
|
+
只在 :focus-within(焦点进入行内)时给,不给整行加 tabindex ——
|
|
44
|
+
那样 Tab 会为每一行多停一次,键盘用户要按几十次才能穿过列表。 */
|
|
45
|
+
.list__item:focus-within {
|
|
46
|
+
background: var(--surface-sunken);
|
|
47
|
+
border-inline-start-color: var(--accent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* 键盘 Tab 时行内按钮的焦点环(不加会看不见) */
|
|
51
|
+
.list__actions .btn:focus-visible {
|
|
52
|
+
outline: 2px solid var(--accent);
|
|
53
|
+
outline-offset: 1px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ---------------------------------------------------------------- 主信息 */
|
|
57
|
+
.list__main { flex: 1; min-width: 0; } /* min-width:0 防长内容撑破 */
|
|
58
|
+
|
|
59
|
+
.list__title {
|
|
60
|
+
display: block;
|
|
61
|
+
font-size: var(--fs-sm);
|
|
62
|
+
color: var(--text-primary);
|
|
63
|
+
/* 长公司名截断而不是换行 —— 换行会让行高跳变,破坏列表节奏 */
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
text-overflow: ellipsis;
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.list__meta {
|
|
70
|
+
display: block;
|
|
71
|
+
font-size: var(--fs-xs);
|
|
72
|
+
color: var(--text-tertiary);
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
text-overflow: ellipsis;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ---------------------------------------------------------------- 数值列
|
|
79
|
+
🔴 涨跌色遵循中文用户习惯:**红涨绿跌**,与欧美相反。
|
|
80
|
+
这里不做"国际统一",因为使用者是中国 A 股用户。 */
|
|
81
|
+
.list__value {
|
|
82
|
+
font-size: var(--fs-sm);
|
|
83
|
+
white-space: nowrap;
|
|
84
|
+
color: var(--text-secondary);
|
|
85
|
+
}
|
|
86
|
+
.list__value--up { color: var(--danger); } /* 涨 = 红 */
|
|
87
|
+
.list__value--down { color: var(--success); } /* 跌 = 绿 */
|
|
88
|
+
|
|
89
|
+
/* ---------------------------------------------------------------- 行内操作
|
|
90
|
+
默认隐藏:键盘用 focus-within 显示,鼠标用 hover 显示。
|
|
91
|
+
🔴 但**不要用 display:none** —— 那会让操作从 tab 顺序消失。
|
|
92
|
+
正确做法是保持可聚焦(opacity / visibility 都要配合)。 */
|
|
93
|
+
.list__actions {
|
|
94
|
+
display: flex;
|
|
95
|
+
gap: var(--sp-2);
|
|
96
|
+
flex: none;
|
|
97
|
+
opacity: 0;
|
|
98
|
+
transition: opacity var(--dur-fast) var(--ease-out);
|
|
99
|
+
}
|
|
100
|
+
@media (hover: hover) and (pointer: fine) {
|
|
101
|
+
.list__item:hover .list__actions,
|
|
102
|
+
.list__item:focus-within .list__actions { opacity: 1; }
|
|
103
|
+
}
|
|
104
|
+
/* ============================================================================
|
|
105
|
+
窄屏:次信息换行,数值列右对齐
|
|
106
|
+
========================================================================= */
|
|
107
|
+
@media (max-width: 34rem) {
|
|
108
|
+
.list__item {
|
|
109
|
+
flex-wrap: wrap;
|
|
110
|
+
row-gap: var(--sp-1);
|
|
111
|
+
}
|
|
112
|
+
.list__actions { width: 100%; opacity: 1; } /* 触屏没有 hover,必须常显 */
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ============================================================================
|
|
116
|
+
排序时的一行提示(可选)
|
|
117
|
+
========================================================================= */
|
|
118
|
+
.list__note {
|
|
119
|
+
font-size: var(--fs-xs);
|
|
120
|
+
color: var(--text-tertiary);
|
|
121
|
+
padding: var(--sp-2) var(--sp-4);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ============================================================================
|
|
125
|
+
reduced-motion
|
|
126
|
+
=========================================================================
|
|
127
|
+
位移/颜色过渡全部关闭。
|
|
128
|
+
🔴 FLIP 的进出场动画在 JS 里已整体跳过(prefers-reduced-motion 判定),
|
|
129
|
+
这里关掉的是 hover 等常规微交互 —— 两者是不同层次,别漏。
|
|
130
|
+
========================================================================= */
|
|
131
|
+
@media (prefers-reduced-motion: reduce) {
|
|
132
|
+
.list__item,
|
|
133
|
+
.list__actions { transition: none; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ============================================================================
|
|
137
|
+
反模式
|
|
138
|
+
=========================================================================
|
|
139
|
+
1. hover 时加 border / 改 padding
|
|
140
|
+
内容会横移 1~2px,列表"在抖"。正确做法:border 用 transparent 预留位,
|
|
141
|
+
hover 只改颜色。
|
|
142
|
+
|
|
143
|
+
2. 整行加 tabindex="0"
|
|
144
|
+
Tab 会为每一行多停一次。键盘用户穿过 20 行要按 20 次 Tab。
|
|
145
|
+
行内操作本身可聚焦就够了,用 :focus-within 给整行反馈。
|
|
146
|
+
|
|
147
|
+
3. 行内操作用 display:none / visibility:hidden
|
|
148
|
+
从 tab 顺序消失,键盘用户永远到不了"删除"按钮。
|
|
149
|
+
用 opacity + hover/focus-within 控制可见性。
|
|
150
|
+
|
|
151
|
+
4. 长文本直接换行
|
|
152
|
+
行高跳变,列表节奏被打乱。用 ellipsis 截断,完整内容走 title 属性。
|
|
153
|
+
|
|
154
|
+
5. 涨跌色按欧美习惯(绿涨红跌)
|
|
155
|
+
🔴 中国的 A 股用户习惯是**红涨绿跌**。做反了会被当成出错。
|
|
156
|
+
|
|
157
|
+
6. 窄屏仍然隐藏行内操作
|
|
158
|
+
触屏没有 hover,操作会彻底消失。窄屏必须常显。
|
|
159
|
+
|
|
160
|
+
7. 引入 auto-animate 这类库只为了做"增删时其余项平滑移动"
|
|
161
|
+
8 KB 换 40 行能做的事,还要承担协议尽调与供应链风险。见 flip.js 头部。
|
|
162
|
+
========================================================================= */
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# nav —— 页头 / 目录 / 移动端抽屉
|
|
2
|
+
|
|
3
|
+
| 字段 | 内容 |
|
|
4
|
+
|---|---|
|
|
5
|
+
| **来源** | 原创。`toc.js` 是从 `04-recipes/longform` 内联脚本**提取并加固**的;`nav.js` 是从 demo/示例页**手抄了两遍**的抽屉逻辑抽出来的 |
|
|
6
|
+
| **实测** | demo 三件事都能真机验证(页头/目录/抽屉);抽屉的 22 条行为由 `05-audit/nav-drawer.js` 在浏览器里跑 |
|
|
7
|
+
| **适用** | 精简档 / B / C,`toc.js` 与 `nav.js` 均为 ES5 |
|
|
8
|
+
| **依赖** | `01-tokens/tokens.css` + `02-primitives/button/button.css` + `01-tokens/page.css`(页面骨架:`.wrap` / `.lede` / `.note`) + `01-tokens/typography.css` |
|
|
9
|
+
| **规范** | 见本文件「状态切换」一节|
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 一个目录,两个全局
|
|
14
|
+
|
|
15
|
+
`nav/` 里有**两个独立脚本、两个全局**:
|
|
16
|
+
|
|
17
|
+
| 文件 | 全局 | 形态 | 自动初始化 |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| `nav.js` | `Nav` | attach(`Nav.attach(root)`) | **是**,扫 `[data-nav]` |
|
|
20
|
+
| `toc.js` | `Toc` | direct(`Toc.init(opts)`) | 否,必须自己调 |
|
|
21
|
+
|
|
22
|
+
它们**共用同一份 HTML**(页头 + 目录 + 抽屉)但**生命周期无关**:
|
|
23
|
+
抽屉常常用不上目录,目录也可能出现在没有抽屉的长文页里。所以不合并成一个脚本。
|
|
24
|
+
|
|
25
|
+
⚠️ AI 契约(`ai/components.json`)里 `nav` 这个组件记的是 `global: "Nav"` +
|
|
26
|
+
`globalAlso: [{Toc}]` —— 一个组件目录可以导出第二个全局。
|
|
27
|
+
`05-audit/autoinit-gate.py` 会按这张清单核对 `API.md` 的表,
|
|
28
|
+
少记一个就会报「表里出现了库里没有的全局」。
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 抽屉行为层:`nav.js`(可选,但没有它抽屉就是死的)
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<header class="nav" data-nav>
|
|
36
|
+
<button class="nav__toggle" aria-expanded="false" aria-controls="nav-list">…</button>
|
|
37
|
+
<ul class="nav__list" id="nav-list">…</ul>
|
|
38
|
+
<div class="nav__scrim"></div> <!-- 可省,缺了 JS 自己建 -->
|
|
39
|
+
</header>
|
|
40
|
+
<script src="nav.js"></script> <!-- 有 data-nav 就自动初始化 -->
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`nav.css` 只定义了**状态长什么样**(`[data-state="open"]` / `aria-expanded`),
|
|
44
|
+
从没规定**谁来切**。所以在这之前,每个使用者都要抄 60 行 —— 抄出来的版本
|
|
45
|
+
各有各的漏。现在这一层在库里,并且被门禁按 22 条判据盯着。
|
|
46
|
+
|
|
47
|
+
### 🔴 抄出来的版本里那个 bug:`aria-hidden` 打在了祖先上
|
|
48
|
+
|
|
49
|
+
旧 demo 的写法是「把 body 的每个子元素都 `aria-hidden`,跳过 list/scrim/toggle」。
|
|
50
|
+
问题:`<header class="nav">` 是 list 的**祖先**,它不在跳过名单里 ⇒
|
|
51
|
+
整块 `<header>` 被 `aria-hidden="true"` 包住,**抽屉自己也跟着对读屏消失**。
|
|
52
|
+
|
|
53
|
+
而它"看起来完全正常":截图正常、像素比对正常、键盘 Tab 也正常。
|
|
54
|
+
**只有用读屏打开菜单才会发现里面一个字都读不出来。**
|
|
55
|
+
|
|
56
|
+
`nav.js` 的判据是「跳过**包含**导航自身的节点」,而不是「跳过这三个元素」。
|
|
57
|
+
|
|
58
|
+
### 🔴 为什么复用 `drawer.js` 会错
|
|
59
|
+
|
|
60
|
+
`03-patterns/drawer/drawer.js` 的 `Drawer.open()` **自己建 DOM**(new aside、
|
|
61
|
+
塞进 body、关了就删)。而导航列表在文档里本来就有一份,桌面端还要用它横排。
|
|
62
|
+
**生命周期相反**(即用即毁 vs 常驻),所以不共用实现,只共用**核**:
|
|
63
|
+
`emit` / `focus-return` / `dismissable` 三块与 `drawer.js` 逐行同源(`gen-behavior.py` 保证)。
|
|
64
|
+
|
|
65
|
+
### 🔴 断点不写数字
|
|
66
|
+
|
|
67
|
+
"窗口变宽就关掉"的判据是**汉堡按钮还在不在**(`offsetParent !== null`),
|
|
68
|
+
不是 `window.innerWidth > 768`。CSS 里改一个断点值,写死数字的 JS 会**静默错位**。
|
|
69
|
+
|
|
70
|
+
### 事件
|
|
71
|
+
|
|
72
|
+
`fl-nav-open` / `fl-nav-close`(`detail = { root }`)。自动初始化不接回调
|
|
73
|
+
—— 要收通知请听事件(与 `tabs.js` 同一条纪律)。
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 目录高亮:`toc.js`
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
Toc.init({ links: '#toc-list .toc__link', sections: 'main h2[id]' });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
返回一个取消函数(解绑事件)。
|
|
84
|
+
|
|
85
|
+
### 🔴 为什么不用 `IntersectionObserver`(实测到 bug)
|
|
86
|
+
|
|
87
|
+
第一版用的是 `IntersectionObserver` + `rootMargin: '-10% 0px -70% 0px'`,
|
|
88
|
+
靠 `seen[id] = isIntersecting` 增量维护当前节。
|
|
89
|
+
|
|
90
|
+
**实测反馈"导航不跟随文章移动"。** 根因:
|
|
91
|
+
|
|
92
|
+
> **滚到两个 section 之间时,观察区里一个 section 都没有**
|
|
93
|
+
> ⇒ `seen` 全为 false ⇒ 不调 `mark()` ⇒ 高亮停在上一个不动。
|
|
94
|
+
|
|
95
|
+
**这不是边界情况,是常态**——用户大部分时间就滚在两节之间。
|
|
96
|
+
|
|
97
|
+
现在统一用计算:`取最后一个 offsetTop <= 滚动位置 + 100px 的 section`。
|
|
98
|
+
**任何滚动位置都有确定答案**,不会出现"都不在观察区"的状态。
|
|
99
|
+
|
|
100
|
+
**为什么不用 IO 省 CPU**:两者成本其实一样——`scroll` 监听也只在**滚动时**执行
|
|
101
|
+
(已用 `rAF` 节流),不滚动时都不跑。为了一个"看起来更优"的方案换正确性风险,不划算。
|
|
102
|
+
|
|
103
|
+
### ⚠️ `offsetTop` 的前提
|
|
104
|
+
|
|
105
|
+
它相对于 **`offsetParent`**。若把目录放在一个 `position: relative` 的容器里,
|
|
106
|
+
`offsetTop` 会是相对那个容器的值,与 `pageYOffset` 不可比 ⇒ **高亮会错位**。
|
|
107
|
+
|
|
108
|
+
**目录所在的祖先链上不要出现 `position: relative/absolute`。**
|
|
109
|
+
|
|
110
|
+
### 前提:目录项与标题 id 一一对应
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<h2 id="mem">…</h2> ← 标题必须有 id
|
|
114
|
+
<a class="toc__link" href="#mem">…</a> ← 目录指向它
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
少一个,**那一项永远不会被高亮**。`refs.py` 会静态抓这个错。
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 目录的 sticky 必须在库里,不在 demo 里
|
|
124
|
+
|
|
125
|
+
🔴 第一版这条只写在 demo 的内联 `<style>` 里,**任何引了 `nav.css` 的页面都没有 sticky**
|
|
126
|
+
——目录随正文滚出视口,"跟着走"这个最基本的效果没有。
|
|
127
|
+
|
|
128
|
+
**布局规则写在 demo 里 = 分层崩了。** 现在 `nav.css` 里:
|
|
129
|
+
|
|
130
|
+
```css
|
|
131
|
+
@media (min-width: 60rem) {
|
|
132
|
+
.toc { position: sticky; top: 76px; align-self: start; }
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- `top` 要**大于页头高度**(56px + 20px 留白),否则会停在页头下面被遮住
|
|
137
|
+
- `align-self: start` **必须写** —— grid 子项默认 `stretch`,元素被拉高后 sticky 无处可动
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 抽屉:五条判据(都来自 08 规范)
|
|
142
|
+
|
|
143
|
+
### 1. 只动 `transform`,不动 `width/left/top`
|
|
144
|
+
|
|
145
|
+
```css
|
|
146
|
+
.nav__list { transform: translateX(100%); transition: transform 180ms; }
|
|
147
|
+
.nav__list[data-state="open"] { transform: translateX(0); }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
动 `width` 会引起重排,页面抖一下。
|
|
151
|
+
|
|
152
|
+
### 2. 打开时背景必须锁
|
|
153
|
+
|
|
154
|
+
视觉上是覆盖层,逻辑上也要是模态——**两边必须一致**。
|
|
155
|
+
否则用户能 Tab 到抽屉外面看不见的地方。
|
|
156
|
+
|
|
157
|
+
### 3. 焦点必须移进抽屉,并且**关闭后回到汉堡按钮**
|
|
158
|
+
|
|
159
|
+
丢了焦点,键盘用户就不知道自己在哪了。
|
|
160
|
+
|
|
161
|
+
### 4. Esc 必须能关
|
|
162
|
+
|
|
163
|
+
这是唯一"所有人都知道"的操作,不能只靠点遮罩。
|
|
164
|
+
|
|
165
|
+
### 5. 窗口变宽时自动关掉
|
|
166
|
+
|
|
167
|
+
否则抽屉会留在打开状态,而桌面布局下 `nav__list` 是横排的——
|
|
168
|
+
用户会看到一个"开着但不对"的菜单。
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 三个无障碍细节
|
|
173
|
+
|
|
174
|
+
| 做法 | 原因 |
|
|
175
|
+
|---|---|
|
|
176
|
+
| 跳过链接用 `transform` 移出视口,**不用 `display:none`** | `display:none` 会把它移出 tab 顺序,键盘用户 Tab 不到 |
|
|
177
|
+
| 当前页用 `aria-current="page"` | 屏幕阅读器会播报"当前页面",自定义 class 它不知道 |
|
|
178
|
+
| 目录高亮用 `aria-current="true"` | 表示"在当前范围内的当前位置",目录高亮通行做法 |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 汉堡按钮的三条线怎么变 X
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
默认: ───── 三条线
|
|
186
|
+
i::before / i::after 上下偏移 ±5.5px
|
|
187
|
+
|
|
188
|
+
展开: i 自身 rotate(45deg) → 变成 /
|
|
189
|
+
i::before translateY + rotate(-90deg) → 变成 \
|
|
190
|
+
i::after opacity 0 → 消失
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**为什么偏移量是 5.5px**:线宽 1.5px,中心间距 11px,所以偏移 5.5px 让它们重合到中心。
|
|
194
|
+
这个数是算出来的,不是估的。
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 反模式
|
|
199
|
+
|
|
200
|
+
- ❌ **抽屉展开时动 `width` / `left` / `top`** —— 引起重排,页面抖
|
|
201
|
+
- ❌ **抽屉打开后背景仍可 Tab / 可点** —— 视觉是覆盖层,逻辑不是模态
|
|
202
|
+
- ❌ **汉堡按钮小于 44px** —— 触摸目标硬下限
|
|
203
|
+
- ❌ **当前页指示用自定义 class 而非 `aria-current`** —— 屏幕阅读器不知道
|
|
204
|
+
- ❌ **目录项允许换行** —— 行高跳变,目录看着散
|
|
205
|
+
- ❌ **没有 IntersectionObserver 就不做高亮** —— 目录变成死链接
|
|
206
|
+
- ❌ **跳过链接用 `display:none`** —— 移出 tab 顺序,键盘到不了
|
|
207
|
+
- ❌ **页头 sticky 但没背景色** —— 内容从下面透出来像重影
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 门禁
|
|
212
|
+
|
|
213
|
+
| 项 | 能否机械检查 |
|
|
214
|
+
|---|---|
|
|
215
|
+
| CSS 引用完整性 | ✅ `refs.py` |
|
|
216
|
+
| 状态切换的叠放/占位/溢出 | ✅ `switch.py` |
|
|
217
|
+
| 焦点管理、Esc、背景锁、点遮罩、变宽自关 | ✅ `05-audit/nav-drawer.js`(浏览器 + 键盘,22 条) |
|
|
218
|
+
| 读屏里到底听到了什么 | ❌ **必须真机开读屏** |
|
|
219
|
+
| 视觉上别扭不别扭 | ❌ **必须真机** |
|
|
220
|
+
|
|
221
|
+
🟡 0.11.2 之前这里写的是「焦点、Esc、背景锁**静态检查一个都查不出来**,必须真机」。
|
|
222
|
+
那句话当时是对的,但它描述的是一种**可以修的**状态 —— 现在这三件事由
|
|
223
|
+
`nav-drawer.js` 在真浏览器里跑(含连续 8 次 Tab 的焦点陷阱、Esc 后焦点归位、
|
|
224
|
+
`aria-hidden` 是否误伤祖先)。**仍然查不出来的是"读屏实际播报什么"**:
|
|
225
|
+
无头浏览器没有语音输出,`accessibility.snapshot()` 也只能读到树,读不到播报。
|