agentcss 0.0.0-stage → 0.11.3

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.
Files changed (274) hide show
  1. package/01-tokens/README.md +236 -0
  2. package/01-tokens/behavior/dismissable.js +132 -0
  3. package/01-tokens/behavior/emit.js +76 -0
  4. package/01-tokens/behavior/focus-return.js +81 -0
  5. package/01-tokens/behavior/roving.js +152 -0
  6. package/01-tokens/behavior/typeahead.js +124 -0
  7. package/01-tokens/focus-ring-demo.html +133 -0
  8. package/01-tokens/focus-ring.css +102 -0
  9. package/01-tokens/page.css +244 -0
  10. package/01-tokens/polyfill.js +116 -0
  11. package/01-tokens/responsive-demo.html +171 -0
  12. package/01-tokens/scrollbar-demo.html +194 -0
  13. package/01-tokens/scrollbar.css +179 -0
  14. package/01-tokens/theme-toggle.js +103 -0
  15. package/01-tokens/tokens.css +972 -0
  16. package/01-tokens/typography.css +156 -0
  17. package/02-primitives/badge/README.md +71 -0
  18. package/02-primitives/badge/badge.css +141 -0
  19. package/02-primitives/badge/demo.html +101 -0
  20. package/02-primitives/button/README.md +99 -0
  21. package/02-primitives/button/button.css +353 -0
  22. package/02-primitives/button/demo.html +200 -0
  23. package/02-primitives/card/README.md +145 -0
  24. package/02-primitives/card/card.css +246 -0
  25. package/02-primitives/card/demo.html +170 -0
  26. package/02-primitives/choice/README.md +161 -0
  27. package/02-primitives/choice/choice.css +395 -0
  28. package/02-primitives/choice/demo.html +277 -0
  29. package/02-primitives/combobox/README.md +79 -0
  30. package/02-primitives/combobox/combobox.css +223 -0
  31. package/02-primitives/combobox/combobox.js +485 -0
  32. package/02-primitives/combobox/demo.html +109 -0
  33. package/02-primitives/date-range/README.md +100 -0
  34. package/02-primitives/date-range/date-range.css +206 -0
  35. package/02-primitives/date-range/date-range.js +301 -0
  36. package/02-primitives/date-range/demo.html +104 -0
  37. package/02-primitives/input/README.md +170 -0
  38. package/02-primitives/input/demo.html +246 -0
  39. package/02-primitives/input/input.css +239 -0
  40. package/02-primitives/popover/README.md +80 -0
  41. package/02-primitives/popover/demo.html +150 -0
  42. package/02-primitives/popover/popover.css +187 -0
  43. package/02-primitives/popover/popover.js +331 -0
  44. package/02-primitives/progress/README.md +82 -0
  45. package/02-primitives/progress/demo.html +154 -0
  46. package/02-primitives/progress/progress.css +172 -0
  47. package/02-primitives/select/README.md +95 -0
  48. package/02-primitives/select/demo.html +121 -0
  49. package/02-primitives/select/select.css +151 -0
  50. package/02-primitives/select/select.js +549 -0
  51. package/02-primitives/separator/README.md +63 -0
  52. package/02-primitives/separator/demo.html +84 -0
  53. package/02-primitives/separator/separator.css +73 -0
  54. package/02-primitives/skeleton/README.md +69 -0
  55. package/02-primitives/skeleton/demo.html +110 -0
  56. package/02-primitives/skeleton/skeleton.css +96 -0
  57. package/02-primitives/switch/README.md +65 -0
  58. package/02-primitives/switch/demo.html +115 -0
  59. package/02-primitives/switch/switch.css +370 -0
  60. package/03-patterns/accordion/README.md +121 -0
  61. package/03-patterns/accordion/accordion.css +453 -0
  62. package/03-patterns/accordion/accordion.js +369 -0
  63. package/03-patterns/accordion/demo.html +223 -0
  64. package/03-patterns/content/README.md +103 -0
  65. package/03-patterns/content/content.css +286 -0
  66. package/03-patterns/content/demo.html +88 -0
  67. package/03-patterns/drawer/README.md +79 -0
  68. package/03-patterns/drawer/demo.html +101 -0
  69. package/03-patterns/drawer/drawer.css +129 -0
  70. package/03-patterns/drawer/drawer.js +258 -0
  71. package/03-patterns/dropdown/README.md +78 -0
  72. package/03-patterns/dropdown/demo.html +101 -0
  73. package/03-patterns/dropdown/dropdown.css +135 -0
  74. package/03-patterns/dropdown/dropdown.js +451 -0
  75. package/03-patterns/form-validation/README.md +147 -0
  76. package/03-patterns/form-validation/demo.html +280 -0
  77. package/03-patterns/form-validation/form-validation.css +263 -0
  78. package/03-patterns/list/README.md +150 -0
  79. package/03-patterns/list/demo.html +225 -0
  80. package/03-patterns/list/flip.js +201 -0
  81. package/03-patterns/list/list.css +162 -0
  82. package/03-patterns/nav/README.md +225 -0
  83. package/03-patterns/nav/demo.html +131 -0
  84. package/03-patterns/nav/nav.css +314 -0
  85. package/03-patterns/nav/nav.js +430 -0
  86. package/03-patterns/nav/toc.js +111 -0
  87. package/03-patterns/overlay/README.md +196 -0
  88. package/03-patterns/overlay/demo.html +241 -0
  89. package/03-patterns/overlay/overlay.css +319 -0
  90. package/03-patterns/overlay/overlay.js +563 -0
  91. package/03-patterns/pagination/README.md +114 -0
  92. package/03-patterns/pagination/demo.html +165 -0
  93. package/03-patterns/pagination/pagination.css +195 -0
  94. package/03-patterns/pagination/pagination.js +395 -0
  95. package/03-patterns/reveal/README.md +91 -0
  96. package/03-patterns/reveal/demo.html +106 -0
  97. package/03-patterns/reveal/reveal.css +93 -0
  98. package/03-patterns/reveal/reveal.js +172 -0
  99. package/03-patterns/states/README.md +141 -0
  100. package/03-patterns/states/demo.html +240 -0
  101. package/03-patterns/states/states.css +252 -0
  102. package/03-patterns/tabs/README.md +84 -0
  103. package/03-patterns/tabs/demo.html +139 -0
  104. package/03-patterns/tabs/tabs.css +172 -0
  105. package/03-patterns/tabs/tabs.js +307 -0
  106. package/03-patterns/tooltip/README.md +96 -0
  107. package/03-patterns/tooltip/demo.html +78 -0
  108. package/03-patterns/tooltip/tooltip.css +89 -0
  109. package/03-patterns/tooltip/tooltip.js +288 -0
  110. package/03-patterns/tree/README.md +94 -0
  111. package/03-patterns/tree/demo.html +138 -0
  112. package/03-patterns/tree/tree.css +113 -0
  113. package/03-patterns/tree/tree.js +456 -0
  114. package/04-recipes/analysis-report/README.md +80 -0
  115. package/04-recipes/analysis-report/analysis-report.css +187 -0
  116. package/04-recipes/analysis-report/demo.html +274 -0
  117. package/04-recipes/data-showcase/README.md +45 -0
  118. package/04-recipes/data-showcase/demo.html +247 -0
  119. package/04-recipes/detail-page/README.md +93 -0
  120. package/04-recipes/detail-page/demo.html +55 -0
  121. package/04-recipes/detail-page/detail-page.css +190 -0
  122. package/04-recipes/detail-page/full.html +60 -0
  123. package/04-recipes/detail-page/null.html +60 -0
  124. package/04-recipes/detail-page/partial.html +60 -0
  125. package/04-recipes/detail-page/verify-skeleton.py +19 -0
  126. package/04-recipes/longform/README.md +93 -0
  127. package/04-recipes/longform/agc-memory.svg +4 -0
  128. package/04-recipes/longform/content.html +610 -0
  129. package/04-recipes/longform/content.md +51 -0
  130. package/04-recipes/longform/longform.html +627 -0
  131. package/04-recipes/report/README.md +176 -0
  132. package/04-recipes/report/demo.html +355 -0
  133. package/04-recipes/report/report.css +364 -0
  134. package/04-recipes/table/README.md +103 -0
  135. package/04-recipes/table/demo.html +455 -0
  136. package/04-recipes/table/table.css +372 -0
  137. package/04-recipes/tierA-tokens.css +189 -0
  138. package/09-assets/README.md +74 -0
  139. package/09-assets/bar/README.md +94 -0
  140. package/09-assets/bar/bar.css +166 -0
  141. package/09-assets/bar/bar.js +320 -0
  142. package/09-assets/bar/demo.html +176 -0
  143. package/09-assets/echarts-adapter/demo.html +192 -0
  144. package/09-assets/echarts-adapter/echarts-adapter.js +277 -0
  145. package/09-assets/model-viewer/README.md +155 -0
  146. package/09-assets/model-viewer/demo.html +105 -0
  147. package/09-assets/model-viewer/model-viewer.css +143 -0
  148. package/09-assets/model-viewer/model-viewer.js +423 -0
  149. package/09-assets/scientific-plot/README.md +94 -0
  150. package/09-assets/scientific-plot/demo.html +252 -0
  151. package/09-assets/scientific-plot/scientific-plot.css +141 -0
  152. package/09-assets/scientific-plot/scientific-plot.js +610 -0
  153. package/09-assets/sparkline/README.md +153 -0
  154. package/09-assets/sparkline/demo.html +163 -0
  155. package/09-assets/sparkline/sparkline.css +158 -0
  156. package/09-assets/sparkline/sparkline.js +508 -0
  157. package/API.md +332 -0
  158. package/CHANGELOG.md +3138 -0
  159. package/CODE_OF_CONDUCT.md +64 -0
  160. package/CONTRIBUTING.md +89 -0
  161. package/ERRORS.md +1658 -0
  162. package/LICENSE +21 -0
  163. package/README.md +497 -2
  164. package/SECURITY.md +72 -0
  165. package/START-HERE.md +164 -0
  166. package/SUPPORT.md +109 -0
  167. package/adapters/presentation/README.md +113 -0
  168. package/adapters/presentation/check.js +101 -0
  169. package/adapters/presentation/deck.schema.json +74 -0
  170. package/adapters/presentation/to-doc.js +138 -0
  171. package/ai/START-HERE.md +104 -0
  172. package/ai/behaviors.json +380 -0
  173. package/ai/behaviors.schema.json +61 -0
  174. package/ai/capabilities.json +56 -0
  175. package/ai/cli.js +387 -0
  176. package/ai/components.json +1944 -0
  177. package/ai/components.meta.json +168 -0
  178. package/ai/components.schema.json +220 -0
  179. package/ai/contract.schema.json +147 -0
  180. package/ai/deprecations.json +55 -0
  181. package/ai/deprecations.schema.json +85 -0
  182. package/ai/motion-policy.json +22 -0
  183. package/ai/patch.js +323 -0
  184. package/ai/patch.schema.json +63 -0
  185. package/ai/profiles.json +92 -0
  186. package/ai/tokens.audience.json +55 -0
  187. package/ai/tokens.audience.schema.json +73 -0
  188. package/ai/tokens.json +762 -0
  189. package/ai/tokens.schema.json +58 -0
  190. package/ai/tokens.tree.json +752 -0
  191. package/ai/tokens.tree.schema.json +56 -0
  192. package/ai/validate.js +383 -0
  193. package/dist/01-tokens/behavior/dismissable.js +60 -0
  194. package/dist/01-tokens/behavior/emit.js +21 -0
  195. package/dist/01-tokens/behavior/focus-return.js +31 -0
  196. package/dist/01-tokens/behavior/roving.js +77 -0
  197. package/dist/01-tokens/behavior/typeahead.js +61 -0
  198. package/dist/01-tokens/focus-ring.css +1 -0
  199. package/dist/01-tokens/page.css +1 -0
  200. package/dist/01-tokens/polyfill.js +78 -0
  201. package/dist/01-tokens/scrollbar.css +1 -0
  202. package/dist/01-tokens/theme-toggle.js +61 -0
  203. package/dist/01-tokens/tokens.css +1 -0
  204. package/dist/01-tokens/typography.css +1 -0
  205. package/dist/02-primitives/badge/badge.css +1 -0
  206. package/dist/02-primitives/button/button.css +1 -0
  207. package/dist/02-primitives/card/card.css +1 -0
  208. package/dist/02-primitives/choice/choice.css +1 -0
  209. package/dist/02-primitives/combobox/combobox.css +1 -0
  210. package/dist/02-primitives/combobox/combobox.js +439 -0
  211. package/dist/02-primitives/date-range/date-range.css +1 -0
  212. package/dist/02-primitives/date-range/date-range.js +267 -0
  213. package/dist/02-primitives/input/input.css +1 -0
  214. package/dist/02-primitives/popover/popover.css +1 -0
  215. package/dist/02-primitives/popover/popover.js +257 -0
  216. package/dist/02-primitives/progress/progress.css +1 -0
  217. package/dist/02-primitives/select/select.css +1 -0
  218. package/dist/02-primitives/select/select.js +449 -0
  219. package/dist/02-primitives/separator/separator.css +1 -0
  220. package/dist/02-primitives/skeleton/skeleton.css +1 -0
  221. package/dist/02-primitives/switch/switch.css +1 -0
  222. package/dist/03-patterns/accordion/accordion.css +1 -0
  223. package/dist/03-patterns/accordion/accordion.js +219 -0
  224. package/dist/03-patterns/content/content.css +1 -0
  225. package/dist/03-patterns/drawer/drawer.css +1 -0
  226. package/dist/03-patterns/drawer/drawer.js +229 -0
  227. package/dist/03-patterns/dropdown/dropdown.css +1 -0
  228. package/dist/03-patterns/dropdown/dropdown.js +365 -0
  229. package/dist/03-patterns/form-validation/form-validation.css +1 -0
  230. package/dist/03-patterns/list/flip.js +117 -0
  231. package/dist/03-patterns/list/list.css +1 -0
  232. package/dist/03-patterns/nav/nav.css +1 -0
  233. package/dist/03-patterns/nav/nav.js +322 -0
  234. package/dist/03-patterns/nav/toc.js +76 -0
  235. package/dist/03-patterns/overlay/overlay.css +1 -0
  236. package/dist/03-patterns/overlay/overlay.js +398 -0
  237. package/dist/03-patterns/pagination/pagination.css +1 -0
  238. package/dist/03-patterns/pagination/pagination.js +273 -0
  239. package/dist/03-patterns/reveal/reveal.css +1 -0
  240. package/dist/03-patterns/reveal/reveal.js +138 -0
  241. package/dist/03-patterns/states/states.css +1 -0
  242. package/dist/03-patterns/tabs/tabs.css +1 -0
  243. package/dist/03-patterns/tabs/tabs.js +242 -0
  244. package/dist/03-patterns/tooltip/tooltip.css +1 -0
  245. package/dist/03-patterns/tooltip/tooltip.js +213 -0
  246. package/dist/03-patterns/tree/tree.css +1 -0
  247. package/dist/03-patterns/tree/tree.js +383 -0
  248. package/dist/04-recipes/analysis-report/analysis-report.css +1 -0
  249. package/dist/04-recipes/detail-page/detail-page.css +1 -0
  250. package/dist/04-recipes/report/report.css +1 -0
  251. package/dist/04-recipes/table/table.css +1 -0
  252. package/dist/04-recipes/tierA-tokens.css +1 -0
  253. package/dist/09-assets/bar/bar.css +1 -0
  254. package/dist/09-assets/bar/bar.js +222 -0
  255. package/dist/09-assets/echarts-adapter/echarts-adapter.js +185 -0
  256. package/dist/09-assets/model-viewer/model-viewer.css +1 -0
  257. package/dist/09-assets/model-viewer/model-viewer.js +320 -0
  258. package/dist/09-assets/scientific-plot/scientific-plot.css +1 -0
  259. package/dist/09-assets/scientific-plot/scientific-plot.js +568 -0
  260. package/dist/09-assets/sparkline/sparkline.css +1 -0
  261. package/dist/09-assets/sparkline/sparkline.js +265 -0
  262. package/dist/COSTS.md +56 -0
  263. package/docs/AI-CONTRACT.md +197 -0
  264. package/docs/BACKLOG.md +127 -0
  265. package/docs/BENCHMARK.md +252 -0
  266. package/docs/GUIDANCE.md +154 -0
  267. package/docs/INVARIANT.md +372 -0
  268. package/docs/LAYER-BASELINE.md +158 -0
  269. package/docs/RELEASING.md +311 -0
  270. package/docs/RFC-THEME-SCOPE.md +141 -0
  271. package/docs/ROADMAP.md +182 -0
  272. package/index.html +82 -0
  273. package/package.json +107 -4
  274. package/types/index.d.ts +310 -0
@@ -0,0 +1,430 @@
1
+ /*
2
+ * nav.js — 导航的移动端抽屉行为层(可选增强)
3
+ *
4
+ * 语法刻意用 ES5(无箭头函数 / const / let / 模板字符串 / Element.closest)——
5
+ * 精简档 WebView 的版本不确定,理由与 tabs.js / accordion.js 一致。
6
+ *
7
+ * ==========================================================================
8
+ * 🔴 为什么导航需要行为层(而不是只给 CSS)
9
+ * --------------------------------------------------------------------------
10
+ * `nav.css` 把抽屉的**样子**和状态约定都定义好了:
11
+ * .nav__list[data-state="open"] ⇒ 抽屉推出来
12
+ * .nav__scrim[data-state="open"] ⇒ 遮罩出现
13
+ * .nav__toggle[aria-expanded=true] ⇒ 汉堡变成叉
14
+ * 但它一个字都没说**谁来切这三个状态**。于是在这之前,每个使用者都要
15
+ * 自己抄 60 行(demo 里抄了一份、示例页又抄了一份)——
16
+ * 而抄出来的版本各有各的漏:demo 那份把 aria-hidden 打在了抽屉的**祖先**上,
17
+ * 于是抽屉本身对读屏消失了(详见下方 ④)。
18
+ *
19
+ * ==========================================================================
20
+ * 🔴 为什么不用 drawer.js(同样是抽屉,为什么不复用)
21
+ * --------------------------------------------------------------------------
22
+ * `03-patterns/drawer/drawer.js` 的 `Drawer.open()` **自己建 DOM**:
23
+ * 它 new 一个 aside、塞进 body、用完即毁。
24
+ * 而导航的列表**在文档里本来就有一份**,桌面端还要用它(横排导航),
25
+ * 不能"开一次建一个、关了就删掉"。
26
+ * ⇒ 两者的关闭契约一样(Esc / 焦点归位 / 锁滚动 / 背景 inert),
27
+ * 但生命周期相反(即用即毁 vs 常驻),所以不共用实现,只共用**核**:
28
+ * emit / focus-return / dismissable 三块与 drawer.js 逐行同源。
29
+ *
30
+ * ==========================================================================
31
+ * 契约(每条都能被测出来)
32
+ * --------------------------------------------------------------------------
33
+ * ① 打开:list + scrim 加 data-state="open",toggle 的 aria-expanded=true,
34
+ * aria-label 换成「关闭菜单」,焦点进入抽屉第一项
35
+ * ② 关闭:三个状态全部复位,**焦点必须回到打开它的那个元素**
36
+ * (元素被移除时退回到 toggle —— 不能让焦点掉在 body 上)
37
+ * ③ 关得掉:Esc、点遮罩、点抽屉里的链接、视口变宽到抽屉不适用
38
+ * ④ 背景 inert:抽屉打开时,正文对读屏和键盘都不可达
39
+ * 🔴 只对**不含导航本身**的 body 子元素设 aria-hidden/inert。
40
+ * 给祖先设 aria-hidden 会把抽屉一起隐掉 —— 这是 demo 旧版的真 bug,
41
+ * 而且它"看起来完全正常",只有用读屏才会发现抽屉读不到。
42
+ * ⑤ 焦点陷阱:打开期间 Tab / Shift+Tab 在抽屉内循环
43
+ * (视觉上是覆盖层,键盘上就必须是模态,两边不能不一致)
44
+ * ⑥ 断点无关:不写 48rem 这个数字。判据是**toggle 还在不在**
45
+ * —— 桌面端 `.nav__toggle{display:none}`,看不到就不该有抽屉。
46
+ * 写死数字意味着 CSS 改一个值,JS 就静默错位。
47
+ *
48
+ * ==========================================================================
49
+ * 用法
50
+ * --------------------------------------------------------------------------
51
+ * <header class="nav" data-nav>
52
+ * <button class="nav__toggle" aria-expanded="false" aria-controls="nav-list">…</button>
53
+ * <ul class="nav__list" id="nav-list">…</ul>
54
+ * <div class="nav__scrim"></div> <!-- 可省,缺了会自动建 -->
55
+ * </header>
56
+ *
57
+ * <script src="nav.js"></script> <!-- 有 data-nav 就自动初始化 -->
58
+ *
59
+ * 手动:new Nav(rootEl, { labels: { open: '打开菜单', close: '关闭菜单' } })
60
+ * 事件:fl-nav-open / fl-nav-close(detail = { root: <nav> })
61
+ */
62
+ (function () {
63
+ 'use strict';
64
+
65
+ /* ==== BEHAVIOR INJECT BEGIN: emit ==== */
66
+ var flEmit = function (el, name, detail) {
67
+ if (!el || !el.dispatchEvent) return null;
68
+ var type = name.indexOf('fl-') === 0 ? name : 'fl-' + name;
69
+ var ev = null;
70
+ /* 老 WebView 没有 CustomEvent 构造函数 ⇒ 兜底走 createEvent(理由见文件头)*/
71
+ if (typeof window.CustomEvent === 'function') {
72
+ try {
73
+ ev = new window.CustomEvent(type, {
74
+ detail: detail || null, bubbles: true, cancelable: false
75
+ });
76
+ } catch (e) { ev = null; }
77
+ }
78
+ if (!ev) {
79
+ try {
80
+ ev = document.createEvent('CustomEvent');
81
+ ev.initCustomEvent(type, true, false, detail || null);
82
+ } catch (e2) { return null; }
83
+ }
84
+ el.dispatchEvent(ev);
85
+ return ev;
86
+ };
87
+ /* ==== BEHAVIOR INJECT END: emit ==== */
88
+
89
+ /* ==== BEHAVIOR INJECT BEGIN: focus-return ==== */
90
+ var flFocusReturn = function (opts) {
91
+ var opt = opts || {};
92
+ var saved = null;
93
+
94
+ return {
95
+ save: function () {
96
+ var a = document.activeElement;
97
+ saved = (a && a !== document.body && a.focus) ? a : null;
98
+ return saved;
99
+ },
100
+
101
+ restore: function () {
102
+ var target = saved;
103
+ /* ⚠️ 见文档 ②:不判 contains 的话,元素被移除后 focus() 不报错
104
+ 但焦点不动 ⇒ 用户以为还回去了,其实掉在 body 上。 */
105
+ if (!target || !document.contains(target)) target = opt.fallback || null;
106
+ if (!target) { saved = null; return false; }
107
+ try {
108
+ target.focus();
109
+ } catch (e) {
110
+ saved = null;
111
+ return false;
112
+ }
113
+ var ok = document.activeElement === target;
114
+ saved = null; /* 见文档 ③ */
115
+ return ok;
116
+ },
117
+
118
+ target: function () { return saved; },
119
+ clear: function () { saved = null; },
120
+ };
121
+ };
122
+ /* ==== BEHAVIOR INJECT END: focus-return ==== */
123
+
124
+ /* ==== BEHAVIOR INJECT BEGIN: dismissable ==== */
125
+ var flDismissable = function (opts) {
126
+ var opt = opts || {};
127
+ var escOn = opt.escOn || [];
128
+ var inside = opt.inside || [];
129
+ var when = opt.when || function () { return true; };
130
+ var onDismiss = opt.onDismiss || function () {};
131
+
132
+ var shared = window.__flDismiss || (window.__flDismiss = { stack: [], bound: false });
133
+
134
+ function isInside(node) {
135
+ if (!node) return false;
136
+ for (var i = 0; i < inside.length; i++) {
137
+ var el = inside[i];
138
+ if (!el || !el.contains) continue;
139
+ if (el === node || el.contains(node)) return true;
140
+ }
141
+ return false;
142
+ }
143
+
144
+ function onKey(e) {
145
+ if (!when()) return;
146
+ var k = e.key;
147
+ if (k !== 'Escape' && k !== 'Esc' && e.keyCode !== 27) return;
148
+ /* 🔴 为什么 onDismiss 在前、preventDefault/stopPropagation 在后
149
+ (0.8.8 改):**分级 Esc** 需要核让组件有机会说"这次我没关"。
150
+ combobox 的 Esc 是三级的:
151
+ ① 列表开着 → 关列表
152
+ ② 列表关了、输入有字 → 清空输入
153
+ ③ 都没了 → 什么都不做(此时**不该**阻止默认行为,
154
+ 让 Esc 能继续传给外层浮层)
155
+ 而原实现是「无条件 preventDefault + stopPropagation,再调 onDismiss」
156
+ ⇒ 组件根本无法表达"这次我没关"。
157
+ ⇒ 改成:先问onDismiss,它返回 false 就**不干预**默认行为。
158
+ ⚠️ 顺序安全性:stopPropagation 只需在**本次监听器执行期间**被调用即可
159
+ 阻止冒泡到外层,放在 onDismiss 之后仍然有效。
160
+ */
161
+ var closed = onDismiss(e, 'esc');
162
+ if (closed === false) return;
163
+ if (e.preventDefault) e.preventDefault();
164
+ if (e.stopPropagation) e.stopPropagation();
165
+ }
166
+
167
+ function onDocClick(e) {
168
+ var top = null;
169
+ for (var i = shared.stack.length - 1; i >= 0; i--) {
170
+ if (shared.stack[i].when()) { top = shared.stack[i]; break; }
171
+ }
172
+ if (!top || top.isInside(e.target)) return;
173
+ top.onDismiss(e, 'outside');
174
+ }
175
+
176
+ var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
177
+
178
+ for (var j = 0; j < escOn.length; j++) {
179
+ if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
180
+ }
181
+ shared.stack.push(rec);
182
+ if (!shared.bound) {
183
+ shared.bound = true;
184
+ document.addEventListener('click', onDocClick, true);
185
+ }
186
+
187
+ return {
188
+ destroy: function () {
189
+ for (var j = 0; j < escOn.length; j++) {
190
+ if (escOn[j]) escOn[j].removeEventListener('keydown', onKey);
191
+ }
192
+ var k = shared.stack.indexOf(rec);
193
+ if (k >= 0) shared.stack.splice(k, 1);
194
+ },
195
+ };
196
+ };
197
+ /* ==== BEHAVIOR INJECT END: dismissable ==== */
198
+
199
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]),' +
200
+ ' select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
201
+
202
+ /* 元素是否**真的**可见。用 offsetParent 而不是 getComputedStyle:
203
+ offsetParent 是 null 当且仅当元素或其祖先 display:none ——
204
+ 老 WebView 上 getComputedStyle 拿 display 也能用,但每次调用都触发样式计算,
205
+ 焦点陷阱里要循环调用,代价不划算。 */
206
+ function isVisible(el) {
207
+ return !!(el && el.offsetParent !== null);
208
+ }
209
+
210
+ function each(nodes, fn) {
211
+ for (var i = 0; i < nodes.length; i++) fn(nodes[i], i);
212
+ }
213
+
214
+ function Nav(root, opts) {
215
+ var self = this;
216
+ this.root = root;
217
+ this.opts = opts || {};
218
+ this.labels = this.opts.labels || {};
219
+ this.labelOpen = this.labels.open || '打开菜单';
220
+ this.labelClose = this.labels.close || '关闭菜单';
221
+
222
+ this.toggle = root.querySelector('[data-nav-toggle]') || root.querySelector('.nav__toggle');
223
+ this.list = root.querySelector('[data-nav-list]') || root.querySelector('.nav__list');
224
+
225
+ if (!this.toggle || !this.list) {
226
+ /* 不抛异常:一个页面上可能同时有"只有静态导航"和"有抽屉"的实例,
227
+ 为一个缺按钮的实例把整页的 JS 打断,代价远大于收益。
228
+ 但也不能完全沉默 —— 沉默是这类缺陷最难查的原因。 */
229
+ if (window.console && console.warn) {
230
+ console.warn('nav.js: 缺少 .nav__toggle 或 .nav__list,已跳过', root);
231
+ }
232
+ return;
233
+ }
234
+
235
+ this.scrim = root.querySelector('.nav__scrim');
236
+ if (!this.scrim) {
237
+ this.scrim = document.createElement('div');
238
+ this.scrim.className = 'nav__scrim';
239
+ root.appendChild(this.scrim);
240
+ }
241
+ /* 遮罩是纯装饰:它只承载"点这里关闭",语义上没有任何内容。
242
+ 不标 aria-hidden 的话,读屏会读到一个空的 div。 */
243
+ this.scrim.setAttribute('aria-hidden', 'true');
244
+
245
+ /* aria-controls 指向列表:这条关系必须由 JS 补,
246
+ 因为列表 id 是页面自己起的,CSS 无从知道。 */
247
+ if (this.list.id && !this.toggle.getAttribute('aria-controls')) {
248
+ this.toggle.setAttribute('aria-controls', this.list.id);
249
+ }
250
+
251
+ this.fr = flFocusReturn({ fallback: this.toggle });
252
+ this.savedOverflow = null;
253
+ this.inerted = [];
254
+
255
+ this.onToggleClick = function () { self.isOpen() ? self.close() : self.open(); };
256
+ this.onListClick = function (e) {
257
+ var t = e.target;
258
+ /* 点链接:让浏览器先跑完这次点击(导航/锚点),再关。
259
+ 顺序反了的话 close() 的焦点归位会把锚点跳转的焦点抢回来。 */
260
+ if (t && (t.tagName === 'A' || (t.parentNode && t.parentNode.tagName === 'A'))) {
261
+ setTimeout(function () { self.close(); }, 0);
262
+ }
263
+ };
264
+ this.onKeydown = function (e) { self._trap(e); };
265
+ this.onResize = function () {
266
+ /* ⑥ 判据是"toggle 还看得见吗",不是窗口宽度 < 48rem。 */
267
+ if (self.isOpen() && !isVisible(self.toggle)) self.close();
268
+ };
269
+
270
+ this.toggle.addEventListener('click', this.onToggleClick);
271
+ this.list.addEventListener('click', this.onListClick);
272
+ window.addEventListener('resize', this.onResize);
273
+
274
+ /* ③ 关得掉:Esc + 点遮罩 + 点抽屉外面。
275
+ ⚠️ toggle 算"内部"—— 否则点汉堡会先被核判成"点外部"关掉,
276
+ 再被 toggle 的 click 打开,表现是"点了没反应"。
277
+ ⚠️ 遮罩**不能**算内部 —— 遮罩的唯一职责就是"点它关闭",
278
+ 把它写进 inside 会让这句注释变成谎话(实测:点遮罩后仍然开着)。 */
279
+ this.dismiss = flDismissable({
280
+ escOn: [document],
281
+ inside: [this.list, this.toggle],
282
+ when: function () { return self.isOpen(); },
283
+ onDismiss: function () { self.close(); },
284
+ });
285
+
286
+ /* 初始状态对齐:CSS 靠 [data-state] + aria-expanded 两处判断,
287
+ HTML 里只写 aria-expanded 的话,CSS 也是对的;
288
+ 但 aria-expanded 写错(比如模板里抄成 true)就会一进页面就是开着的抽屉。 */
289
+ if (this.toggle.getAttribute('aria-expanded') === 'true') this.close();
290
+ else this.toggle.setAttribute('aria-expanded', 'false');
291
+ }
292
+
293
+ Nav.prototype.isOpen = function () {
294
+ return this.toggle.getAttribute('aria-expanded') === 'true';
295
+ };
296
+
297
+ /* ④ 背景 inert:跳过"包含导航自身"的节点。
298
+ 给祖先设 aria-hidden 会把抽屉一起隐藏掉,读屏用户打开菜单后读不到任何东西。 */
299
+ Nav.prototype._inertBackground = function (on) {
300
+ var self = this;
301
+ if (on) {
302
+ if (this.inerted.length) return;
303
+ each(document.body.children, function (n) {
304
+ if (n === self.root || (n.contains && n.contains(self.root))) return;
305
+ if (n.tagName === 'SCRIPT' || n.tagName === 'STYLE' || n.tagName === 'LINK') return;
306
+ if (!n.setAttribute) return;
307
+ var had = n.getAttribute('aria-hidden');
308
+ n.setAttribute('data-nav-had-hidden', had === null ? '' : had);
309
+ n.setAttribute('aria-hidden', 'true');
310
+ n.inert = true;
311
+ self.inerted.push(n);
312
+ });
313
+ } else {
314
+ each(this.inerted, function (n) {
315
+ var had = n.getAttribute('data-nav-had-hidden');
316
+ if (had) n.setAttribute('aria-hidden', had);
317
+ else n.removeAttribute('aria-hidden');
318
+ n.removeAttribute('data-nav-had-hidden');
319
+ n.inert = false;
320
+ });
321
+ this.inerted = [];
322
+ }
323
+ };
324
+
325
+ Nav.prototype.open = function () {
326
+ if (this.isOpen()) return;
327
+ this.fr.save();
328
+ this.scrim.setAttribute('data-state', 'open');
329
+ this.list.setAttribute('data-state', 'open');
330
+ this.toggle.setAttribute('aria-expanded', 'true');
331
+ this.toggle.setAttribute('aria-label', this.labelClose);
332
+
333
+ /* 锁滚动:记下原来的**内联**值再覆盖,还原时才不会把使用者自己的
334
+ `body{overflow:hidden}`(比如另一个模态开着)清成空。 */
335
+ if (this.savedOverflow === null) {
336
+ this.savedOverflow = document.body.style.overflow || '';
337
+ }
338
+ document.body.style.overflow = 'hidden';
339
+
340
+ this._inertBackground(true);
341
+ document.addEventListener('keydown', this.onKeydown, true);
342
+
343
+ /* ① 焦点进入抽屉第一项 —— 键盘用户不能留在抽屉外面。 */
344
+ var f = this._focusables();
345
+ (f[0] || this.list).focus();
346
+
347
+ flEmit(this.root, 'fl-nav-open', { root: this.root });
348
+ if (this.opts.onOpen) this.opts.onOpen();
349
+ };
350
+
351
+ Nav.prototype.close = function () {
352
+ if (!this.isOpen()) return;
353
+ this.list.removeAttribute('data-state');
354
+ this.scrim.removeAttribute('data-state');
355
+ this.toggle.setAttribute('aria-expanded', 'false');
356
+ this.toggle.setAttribute('aria-label', this.labelOpen);
357
+
358
+ if (this.savedOverflow !== null) {
359
+ document.body.style.overflow = this.savedOverflow;
360
+ this.savedOverflow = null;
361
+ }
362
+ this._inertBackground(false);
363
+ document.removeEventListener('keydown', this.onKeydown, true);
364
+
365
+ /* ② 焦点归位(核里已处理"元素被移除/不可聚焦"两种降级)。 */
366
+ this.fr.restore();
367
+
368
+ flEmit(this.root, 'fl-nav-close', { root: this.root });
369
+ if (this.opts.onClose) this.opts.onClose();
370
+ };
371
+
372
+ Nav.prototype._focusables = function () {
373
+ var out = [];
374
+ each(this.list.querySelectorAll(FOCUSABLE), function (el) {
375
+ /* 只算真的看得见的:抽屉里可能有 display:none 的二级菜单项 */
376
+ if (isVisible(el)) out.push(el);
377
+ });
378
+ return out;
379
+ };
380
+
381
+ /* ⑤ 焦点陷阱:Tab 在抽屉内循环。只有开着才拦 ——
382
+ 关着的时候用户要能正常 Tab 过整页。 */
383
+ Nav.prototype._trap = function (e) {
384
+ if (!this.isOpen()) return;
385
+ var k = e.key;
386
+ if (k !== 'Tab' && e.keyCode !== 9) return;
387
+ var list = this._focusables();
388
+ if (!list.length) { e.preventDefault(); this.list.focus(); return; }
389
+ var first = list[0];
390
+ var last = list[list.length - 1];
391
+ var active = document.activeElement;
392
+ if (e.shiftKey && (active === first || active === this.list)) {
393
+ e.preventDefault();
394
+ last.focus();
395
+ } else if (!e.shiftKey && active === last) {
396
+ e.preventDefault();
397
+ first.focus();
398
+ }
399
+ };
400
+
401
+ Nav.prototype.destroy = function () {
402
+ if (!this.toggle || !this.list) return;
403
+ this.close();
404
+ this.toggle.removeEventListener('click', this.onToggleClick);
405
+ this.list.removeEventListener('click', this.onListClick);
406
+ window.removeEventListener('resize', this.onResize);
407
+ if (this.dismiss) this.dismiss.destroy();
408
+ };
409
+
410
+ /* 列表要能被脚本聚焦(陷阱的空列表兜底会 focus 它) */
411
+ Nav.attach = function (root, opts) {
412
+ root.navInstance = new Nav(root, opts);
413
+ return root.navInstance;
414
+ };
415
+
416
+ window.Nav = Nav;
417
+
418
+ function init() {
419
+ each(document.querySelectorAll('[data-nav]'), function (el) {
420
+ if (!el.navInstance) Nav.attach(el, {});
421
+ });
422
+ }
423
+
424
+ // @autoinit: [data-nav] —— 扫属性自动建实例(详见 README「自动初始化」)
425
+ if (document.readyState === 'loading') {
426
+ document.addEventListener('DOMContentLoaded', init);
427
+ } else {
428
+ init();
429
+ }
430
+ })();
@@ -0,0 +1,111 @@
1
+ /* ============================================================================
2
+ frontend-lib / 03-patterns / nav / toc.js
3
+ 长文目录:滚动时高亮当前节
4
+
5
+ 🔴 这是**从 04-recipes/longform 内联脚本提取并加固**的模块 ——
6
+ 那份是精简档 自包含产物,不能反过来引本文件(会造成循环依赖)。
7
+ 提取的目的:让 recipe 里的实现与库里的实现是同一套逻辑。
8
+
9
+ 依赖:无
10
+ 适用:精简档 / B / C(ES5)
11
+
12
+ API:
13
+ Toc.init({ links: '.toc a', sections: 'h2[id]' }); // 返回一个可调用的取消函数
14
+ ========================================================================= */
15
+ (function (global) {
16
+ 'use strict';
17
+
18
+ function raf(fn) {
19
+ return (global.requestAnimationFrame || function (f) {
20
+ return global.setTimeout(f, 16);
21
+ })(fn);
22
+ }
23
+
24
+ function init(opts) {
25
+ opts = opts || {};
26
+ var linkSel = opts.links || '.toc a';
27
+ var secSel = opts.sections || 'h2[id]';
28
+
29
+ var linkEls = document.querySelectorAll(linkSel);
30
+ var sections = document.querySelectorAll(secSel);
31
+ if (!linkEls.length || !sections.length) { return function () {}; }
32
+
33
+ var map = {};
34
+ for (var i = 0; i < linkEls.length; i++) {
35
+ var href = linkEls[i].getAttribute('href') || '';
36
+ if (href.charAt(0) === '#') { map[href.slice(1)] = linkEls[i]; }
37
+ }
38
+
39
+ function mark(id) {
40
+ for (var k in map) {
41
+ if (Object.prototype.hasOwnProperty.call(map, k)) {
42
+ map[k].removeAttribute('aria-current');
43
+ }
44
+ }
45
+ if (map[id]) { map[id].setAttribute('aria-current', 'true'); }
46
+ }
47
+
48
+ /* ----------------------------------------------------------------
49
+ 判断"当前在第几节" —— **统一用计算,不用 IntersectionObserver**
50
+
51
+ 🔴 修正一个真 bug(实测反馈"导航不跟随文章移动"):
52
+
53
+ 旧实现用 IntersectionObserver + rootMargin:'-10% 0px -70% 0px',
54
+ 靠 `seen[id] = isIntersecting` 增量维护。问题在于:
55
+
56
+ **滚到两个 section 之间时,观察区里一个 section 都没有**
57
+ ⇒ seen 全为 false ⇒ 不调 mark() ⇒ 高亮停在上一个不动。
58
+
59
+ 这不是边界情况,是**常态**——用户大部分时间就滚在两节之间。
60
+
61
+ 改成"取最后一个已滚过的 section":任何滚动位置都有确定答案,
62
+ 不会出现"两个都不在观察区"的状态。
63
+
64
+ 为什么不用 IntersectionObserver 省 CPU:
65
+ 两者成本其实一样 —— scroll 监听也只在**滚动时**执行(已用 rAF 节流),
66
+ 不滚动时都不跑。为了一个"看起来更优"的方案换来正确性风险,不划算。
67
+ ---------------------------------------------------------------- */
68
+ function currentId() {
69
+ var y = global.pageYOffset || document.documentElement.scrollTop || 0;
70
+ /* 偏移 100px:标题滚到视口顶之前就认为进入该节,
71
+ 否则用户看标题时高亮还停在上一个 */
72
+ var line = y + 100;
73
+ var found = sections[0];
74
+ for (var n = 0; n < sections.length; n++) {
75
+ if (sections[n].offsetTop <= line) { found = sections[n]; } else { break; }
76
+ }
77
+ /* 滚到底部时高亮最后一节,否则最后一段永远不亮 */
78
+ var doc = document.documentElement;
79
+ if (global.innerHeight + y >= doc.scrollHeight - 2 && sections.length) {
80
+ found = sections[sections.length - 1];
81
+ }
82
+ return found ? found.id : null;
83
+ }
84
+
85
+ var ticking = false;
86
+ function update() {
87
+ if (ticking) { return; }
88
+ ticking = true;
89
+ raf(function () {
90
+ ticking = false;
91
+ var id = currentId();
92
+ if (id) { mark(id); }
93
+ });
94
+ }
95
+
96
+ global.addEventListener('scroll', update, { passive: true });
97
+ global.addEventListener('resize', update);
98
+ /* 锚点跳转后浏览器不一定会触发 scroll,所以补一帧主动算一次 */
99
+ global.addEventListener('hashchange', update);
100
+ update();
101
+
102
+ return function () {
103
+ global.removeEventListener('scroll', update);
104
+ global.removeEventListener('resize', update);
105
+ global.removeEventListener('hashchange', update);
106
+ };
107
+ }
108
+
109
+ global.Toc = { init: init };
110
+
111
+ })(window);