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.
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 +3116 -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 +491 -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 +275 -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,129 @@
1
+ /* ============================================================================
2
+ drawer.css — 抽屉(D2 /
3
+ ============================================================================
4
+ 依据:APG Dialog 模式(Drawer 是从侧边滑入的 Dialog)
5
+
6
+ ⭐ 与 dialog 的差别只有"从哪进来",**三条硬要求完全一样**:
7
+ ① **焦点陷阱**:Tab 不能跑到抽屉外
8
+ ② **Esc 关闭**
9
+ ③ ⭐ **关闭后焦点归位**(回到触发它的按钮)
10
+ —— 这条最容易漏,且**影响最大**:
11
+ 键盘用户被"扔"回页面顶部,得重新 Tab 一遍。
12
+
13
+ ⚠️ 抽屉是**模态**(modal):背景必须 `inert` 或 `aria-hidden`,
14
+ 否则读屏会同时读到抽屉与背景("双重朗读")。
15
+ ============================================================================ */
16
+
17
+ .drawer-backdrop {
18
+ position: fixed;
19
+ inset: 0;
20
+ z-index: var(--z-modal);
21
+ /*🔴 改:原来是写死的 rgba(0,0,0,.42)——
22
+ 暗色模式下遮罩明显偏黑(暗色下 --scrim 是 0.62 的黑,
23
+ 而写死的 .42 在深底上几乎看不出来)。
24
+ overlay.css 与 nav.css 都用 var(--scrim),只有这里漏了 ⇒
25
+ 同一套库里三个遮罩表现不一致。
26
+ 统一走令牌后,暗色跟随自动生效。 */
27
+ background: var(--scrim);
28
+ display: flex;
29
+ /* 抽屉从右侧滑入(可改成 left / top / bottom)*/
30
+ justify-content: flex-end;
31
+ }
32
+
33
+ .drawer-backdrop[hidden] { display: none; }
34
+
35
+ .drawer {
36
+ width: min(420px, 92vw);
37
+ height: 100%;
38
+ display: flex;
39
+ flex-direction: column;
40
+ background: var(--surface);
41
+ border-inline-start: 1px solid var(--border-decor);
42
+ box-shadow: var(--shadow-2);
43
+ /* 焦点在抽屉里时,浏览器不会自动滚进视野 ⇒ 手动处理 */
44
+ overflow: hidden;
45
+ }
46
+
47
+ .drawer--left { border-inline-start: 0; border-inline-end: 1px solid var(--border-decor); }
48
+ .drawer--top { width: 100%; height: min(60vh, 420px); border-inline-start: 0;
49
+ border-bottom: 1px solid var(--border-decor); }
50
+ .drawer--bottom { width: 100%; height: min(60vh, 420px); border-inline-start: 0;
51
+ border-top: 1px solid var(--border-decor); }
52
+
53
+ /* ---------- 头 ---------- */
54
+ .drawer__head {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: var(--sp-3);
58
+ padding: var(--sp-4);
59
+ border-bottom: 1px solid var(--border-decor);
60
+ flex: 0 0 auto;
61
+ }
62
+
63
+ .drawer__title {
64
+ flex: 1 1 auto;
65
+ margin: 0;
66
+ font-size: var(--fs-lg);
67
+ font-weight: 600;
68
+ }
69
+
70
+ .drawer__close {
71
+ flex: 0 0 auto;
72
+ width: 44px; /* 🔴 命中区(关闭按钮是独立目标,用 44)*/
73
+ height: 44px;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ background: none;
78
+ border: 0;
79
+ border-radius: var(--r-sm);
80
+ color: var(--text-secondary);
81
+ font-size: 20px;
82
+ line-height: 1;
83
+ cursor: pointer;
84
+ }
85
+
86
+ .drawer__close:focus-visible {
87
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
88
+ outline-offset: var(--focus-ring-offset);
89
+ }
90
+
91
+ @media (hover: hover) and (pointer: fine) {
92
+ .drawer__close:hover { background: var(--surface-sunken); }
93
+ }
94
+
95
+ /* ---------- 体 / 脚 ---------- */
96
+ .drawer__body {
97
+ flex: 1 1 auto;
98
+ padding: var(--sp-4);
99
+ overflow-y: auto;
100
+ -webkit-overflow-scrolling: touch;
101
+ }
102
+
103
+ .drawer__foot {
104
+ flex: 0 0 auto;
105
+ display: flex;
106
+ gap: var(--sp-2);
107
+ justify-content: flex-end;
108
+ padding: var(--sp-4);
109
+ border-top: 1px solid var(--border-decor);
110
+ }
111
+
112
+ /* ---------- 进出场动画 ----------
113
+ ⚠️ 用 transform 而不是 left/right(合成层动画,不触发重排)。
114
+ ⚠️ 动画只做"位移 + 少量透明度",**不做大幅缩放** —— 那看起来廉价。 */
115
+ @media (prefers-reduced-motion: no-preference) {
116
+ /* --ease-drawer:滑入面板专用曲线(不是通用的 --ease-out) */
117
+ .drawer--right { animation: drawer-in-right 220ms var(--ease-drawer, ease); }
118
+ .drawer--left { animation: drawer-in-left 220ms var(--ease-drawer, ease); }
119
+ .drawer--top { animation: drawer-in-top 220ms var(--ease-drawer, ease); }
120
+ .drawer--bottom { animation: drawer-in-bottom 220ms var(--ease-drawer, ease); }
121
+
122
+ @keyframes drawer-in-right { from { transform: translateX(16px); opacity: .6; } }
123
+ @keyframes drawer-in-left { from { transform: translateX(-16px); opacity: .6; } }
124
+ @keyframes drawer-in-top { from { transform: translateY(-16px); opacity: .6; } }
125
+ @keyframes drawer-in-bottom { from { transform: translateY(16px); opacity: .6; } }
126
+ }
127
+
128
+ /* 退场(JS 设这个属性,等动画结束再移除节点)*/
129
+ .drawer[data-state="leaving"] { opacity: 0; transition: opacity 160ms var(--ease-out, ease); }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * drawer.js — 抽屉(D2 /
3
+ *
4
+ * 依据:APG Dialog 模式
5
+ * ① **焦点陷阱**:Tab 在抽屉内循环
6
+ * ② **Esc 关闭**
7
+ * ③ ⭐ **关闭后焦点归位**(回到触发它的元素)
8
+ * ④ 背景 inert(避免读屏"双重朗读")
9
+ * ⑤ 锁滚动(背景不该跟着滚)
10
+ *
11
+ * ⚠️ 与 dialog 的差别只是"从哪进来",契约完全一样。
12
+ *
13
+ * 无依赖 · ES5
14
+ */
15
+ (function (global) {
16
+ 'use strict';
17
+
18
+ /* ==== BEHAVIOR INJECT BEGIN: emit ==== */
19
+ var flEmit = function (el, name, detail) {
20
+ if (!el || !el.dispatchEvent) return null;
21
+ var type = name.indexOf('fl-') === 0 ? name : 'fl-' + name;
22
+ var ev = null;
23
+ /* 老 WebView 没有 CustomEvent 构造函数 ⇒ 兜底走 createEvent(理由见文件头)*/
24
+ if (typeof window.CustomEvent === 'function') {
25
+ try {
26
+ ev = new window.CustomEvent(type, {
27
+ detail: detail || null, bubbles: true, cancelable: false
28
+ });
29
+ } catch (e) { ev = null; }
30
+ }
31
+ if (!ev) {
32
+ try {
33
+ ev = document.createEvent('CustomEvent');
34
+ ev.initCustomEvent(type, true, false, detail || null);
35
+ } catch (e2) { return null; }
36
+ }
37
+ el.dispatchEvent(ev);
38
+ return ev;
39
+ };
40
+ /* ==== BEHAVIOR INJECT END: emit ==== */
41
+
42
+ /* ==== BEHAVIOR INJECT BEGIN: focus-return ==== */
43
+ var flFocusReturn = function (opts) {
44
+ var opt = opts || {};
45
+ var saved = null;
46
+
47
+ return {
48
+ save: function () {
49
+ var a = document.activeElement;
50
+ saved = (a && a !== document.body && a.focus) ? a : null;
51
+ return saved;
52
+ },
53
+
54
+ restore: function () {
55
+ var target = saved;
56
+ /* ⚠️ 见文档 ②:不判 contains 的话,元素被移除后 focus() 不报错
57
+ 但焦点不动 ⇒ 用户以为还回去了,其实掉在 body 上。 */
58
+ if (!target || !document.contains(target)) target = opt.fallback || null;
59
+ if (!target) { saved = null; return false; }
60
+ try {
61
+ target.focus();
62
+ } catch (e) {
63
+ saved = null;
64
+ return false;
65
+ }
66
+ var ok = document.activeElement === target;
67
+ saved = null; /* 见文档 ③ */
68
+ return ok;
69
+ },
70
+
71
+ target: function () { return saved; },
72
+ clear: function () { saved = null; },
73
+ };
74
+ };
75
+ /* ==== BEHAVIOR INJECT END: focus-return ==== */
76
+
77
+ var FOCUSABLE = [
78
+ 'a[href]',
79
+ 'button:not([disabled])',
80
+ 'input:not([disabled]):not([type="hidden"])',
81
+ 'select:not([disabled])',
82
+ 'textarea:not([disabled])',
83
+ '[tabindex]:not([tabindex="-1"])',
84
+ ].join(',');
85
+
86
+ var lockCount = 0, savedScroll = 0, inerts = [];
87
+
88
+ function lockScroll() {
89
+ if (lockCount === 0) {
90
+ savedScroll = window.pageYOffset || 0;
91
+ document.body.style.overflow = 'hidden';
92
+ }
93
+ lockCount++;
94
+ }
95
+ function unlockScroll() {
96
+ lockCount = Math.max(0, lockCount - 1);
97
+ if (lockCount === 0) {
98
+ document.body.style.overflow = '';
99
+ window.scrollTo(0, savedScroll);
100
+ }
101
+ }
102
+
103
+ function setBackgroundInert(on) {
104
+ if (on) {
105
+ if (inerts.length) return;
106
+ /* ⚠️ 只把 body 的**直接子元素**(除抽屉容器)设 inert。
107
+ 抽屉本身也在 body 下 ⇒ 必须排除,否则刚打开就 inert 了。 */
108
+ Array.prototype.forEach.call(document.body.children, function (n) {
109
+ if (n.hasAttribute && n.hasAttribute('data-drawer-host')) return;
110
+ if (n.tagName === 'SCRIPT' || n.tagName === 'STYLE') return;
111
+ n.inert = true;
112
+ n.setAttribute('aria-hidden', 'true');
113
+ inerts.push(n);
114
+ });
115
+ } else {
116
+ inerts.forEach(function (n) {
117
+ n.inert = false;
118
+ n.removeAttribute('aria-hidden');
119
+ });
120
+ inerts = [];
121
+ }
122
+ }
123
+
124
+ function open(opt) {
125
+ opt = opt || {};
126
+ var place = opt.place || 'right';
127
+
128
+ /* ⭐ 记住"打开那一刻的焦点"—— 关闭时要还回去。
129
+ (必须在插入 DOM **之前**记录,否则会记到新节点上。)
130
+ 走 focus-return 核(01-tokens/behavior/focus-return.js):
131
+ 抽屉没有 fallback —— 打开它的元素若被删了,就维持现状不动,
132
+ 不假装"还回去了"。 */
133
+ var fr = flFocusReturn({});
134
+ fr.save();
135
+
136
+ var host = document.createElement('div');
137
+ host.className = 'drawer-backdrop';
138
+ host.setAttribute('data-drawer-host', '');
139
+ if (opt.label) host.setAttribute('aria-label', opt.label);
140
+
141
+ var d = document.createElement('aside');
142
+ d.className = 'drawer drawer--' + place;
143
+ d.setAttribute('role', 'dialog');
144
+ d.setAttribute('aria-modal', 'true');
145
+ if (!d.id) d.id = 'drawer-' + (new Date()).getTime();
146
+ if (opt.title) d.setAttribute('aria-labelledby', d.id + '-title');
147
+ else d.setAttribute('aria-label', opt.label || '抽屉');
148
+ d.tabIndex = -1; /* 焦点陷阱的兜底落点 */
149
+
150
+ var head = document.createElement('div');
151
+ head.className = 'drawer__head';
152
+ var h = document.createElement('h2');
153
+ h.className = 'drawer__title';
154
+ h.id = d.id + '-title';
155
+ h.textContent = opt.title || '';
156
+ var closeBtn = document.createElement('button'); /* 🔴 不能叫 close:
157
+ 下方有 `function close()`,函数声明会**提升并覆盖**同名变量,
158
+ 导致上面所有 close.addEventListener 绑在函数上(静默失败)。 */
159
+ closeBtn.className = 'drawer__close';
160
+ closeBtn.type = 'button';
161
+ closeBtn.setAttribute('aria-label', '关闭');
162
+ closeBtn.textContent = '\u00D7';
163
+ head.appendChild(h);
164
+ head.appendChild(closeBtn);
165
+
166
+ var body = document.createElement('div');
167
+ body.className = 'drawer__body';
168
+ if (typeof opt.content === 'string') body.innerHTML = opt.content;
169
+ else if (opt.content) body.appendChild(opt.content);
170
+
171
+ d.appendChild(head);
172
+ d.appendChild(body);
173
+
174
+ if (opt.actions && opt.actions.length) {
175
+ var foot = document.createElement('div');
176
+ foot.className = 'drawer__foot';
177
+ opt.actions.forEach(function (a) {
178
+ var b = document.createElement('button');
179
+ b.type = 'button';
180
+ b.className = 'btn' + (a.variant ? ' btn--' + a.variant : '');
181
+ b.textContent = a.label;
182
+ b.addEventListener('click', function () {
183
+ if (a.onClick) a.onClick(close);
184
+ if (a.keepOpen !== true) close();
185
+ });
186
+ foot.appendChild(b);
187
+ });
188
+ d.appendChild(foot);
189
+ }
190
+
191
+ host.appendChild(d);
192
+ document.body.appendChild(host);
193
+
194
+ setBackgroundInert(true);
195
+ lockScroll();
196
+
197
+ /* 初始焦点:优先指定项,其次第一个可聚焦元素,最后容器本身 */
198
+ var f = d.querySelectorAll(FOCUSABLE);
199
+ if (opt.initialFocus) {
200
+ var t = d.querySelector(opt.initialFocus);
201
+ if (t) t.focus();
202
+ else (f[0] || d).focus();
203
+ } else {
204
+ (f[0] || d).focus();
205
+ }
206
+
207
+ function onKey(e) {
208
+ var k = e.key;
209
+ /* ② Esc 关闭 */
210
+ if (k === 'Escape' || k === 'Esc') { e.preventDefault(); close(); return; }
211
+ if (k !== 'Tab') return;
212
+ /* ① 焦点陷阱:Tab / Shift+Tab 在抽屉内循环 */
213
+ var list = [].slice.call(d.querySelectorAll(FOCUSABLE))
214
+ .filter(function (x) { return x.offsetParent !== null; });
215
+ if (!list.length) { e.preventDefault(); d.focus(); return; }
216
+ var first = list[0], last = list[list.length - 1];
217
+ if (e.shiftKey && (document.activeElement === first || document.activeElement === d)) {
218
+ e.preventDefault(); last.focus();
219
+ } else if (!e.shiftKey && document.activeElement === last) {
220
+ e.preventDefault(); first.focus();
221
+ }
222
+ }
223
+ function onBackdropClick(e) { if (e.target === host) close(); }
224
+
225
+ d.addEventListener('keydown', onKey);
226
+ host.addEventListener('click', onBackdropClick);
227
+ closeBtn.addEventListener('click', function () { close(); });
228
+
229
+ var closed = false;
230
+ function close() {
231
+ if (closed) return;
232
+ closed = true;
233
+ d.setAttribute('data-state', 'leaving');
234
+ d.removeEventListener('keydown', onKey);
235
+ host.removeEventListener('click', onBackdropClick);
236
+ setBackgroundInert(false);
237
+ unlockScroll();
238
+ /* 动画结束再移除;reduced-motion 下没有 transition ⇒ 兜底定时器 */
239
+ var done = false;
240
+ function remove() {
241
+ if (done) return; done = true;
242
+ if (host.parentNode) host.parentNode.removeChild(host);
243
+ }
244
+ d.addEventListener('transitionend', remove, { once: true });
245
+ setTimeout(remove, 320);
246
+ /* ⭐ ③ 焦点归位:回到打开抽屉的那个元素(核里已处理
247
+ "元素被移除 / 不可聚焦"两种降级,都不抛错)。 */
248
+ fr.restore();
249
+ /* 抽屉用完即毁 ⇒ 必须在 host 被移出 DOM **之前**派发,否则没人收得到 */
250
+ flEmit(d, 'fl-close', null);
251
+ if (opt.onClose) opt.onClose();
252
+ }
253
+
254
+ return { el: d, close: close };
255
+ }
256
+
257
+ global.Drawer = { open: open };
258
+ })(window);
@@ -0,0 +1,78 @@
1
+ # dropdown · 下拉菜单
2
+
3
+ 依据 **APG(ARIA Authoring Practices)Menu Button** 模式。
4
+
5
+ ## 🔴 APG 的四条键盘契约(缺一条就不算合规)
6
+
7
+ | 键 | 行为 |
8
+ |---|---|
9
+ | `↑` `↓` | 在**项**之间移动 |
10
+ | `Esc` | 关闭,并把焦点**还给按钮** |
11
+ | `Tab` | 关闭菜单(焦点移到下一个元素)|
12
+ | `Enter` / `Space` | 激活当前项 |
13
+
14
+ ## ⚠️ 另有两条容易漏的
15
+
16
+ - **关闭后焦点必须归位** —— 不归位,键盘用户在页面里迷路
17
+ - **菜单项命中区 ≥ 44px** —— 密集列表也必须够大(WCAG 2.5.5AAA)
18
+
19
+ ## 结构
20
+
21
+ ⚠️ **类名前缀是 `dd__`,不是 `dropdown__`**;标记是 `data-dd-*`。
22
+
23
+ ```html
24
+ <div class="dd">
25
+ <button class="dd__btn" data-dd-btn aria-haspopup="menu" aria-expanded="false">
26
+ <span>操作</span><span class="dd__caret" aria-hidden="true"></span>
27
+ </button>
28
+ <div class="dd__menu" data-dd-menu role="menu">
29
+ <button class="dd__item" role="menuitem" data-value="edit">编辑</button>
30
+ <hr class="dd__sep" data-dd-sep role="separator">
31
+ <button class="dd__item" role="menuitem" data-value="del">删除</button>
32
+ </div>
33
+ </div>
34
+ ```
35
+
36
+ | 部件 | 说明 |
37
+ |---|---|
38
+ | `.dd` | 根 |
39
+ | `.dd__btn` / `[data-dd-btn]` | 触发按钮 |
40
+ | `.dd__menu` / `[data-dd-menu]` | 菜单本体,**必需** |
41
+ | `.dd__item` | 菜单项,带 `data-value` |
42
+ | `.dd__sep` / `[data-dd-sep]` | 分隔线 |
43
+ | `.dd__check` | 多选勾选位 |
44
+ | `.dd--end` | 菜单右对齐 |
45
+
46
+ ## 启用
47
+
48
+ ```js
49
+ const dd = Dropdown.create(元素);
50
+ ```
51
+
52
+ - `[data-dd-btn]` 找不到时**退回第一个 `<button>`**(省事的写法也能跑)
53
+ - `[data-dd-menu]` 找不到 → **直接抛错**
54
+
55
+ ## 🔴 `role="menu"` 要么全设要么全不设
56
+
57
+ ```js
58
+ // APG:菜单要么全 aria-hidden,要么全不设 —— 不能半吊子
59
+ menu.setAttribute('role', 'menu');
60
+ ```
61
+
62
+ `aria-hidden` 打了一半在个别项上,读屏会读到一半就断掉。
63
+
64
+ ## 状态写在 DOM 属性上
65
+
66
+ `data-state` / `data-value`,**不用 class 切换** —— 这是全库 INVARIANT I-8
67
+ (状态必须活在 DOM 属性上),也是门禁会查的东西。
68
+
69
+ ## 成熟度:beta(两条行为债,如实列出)
70
+
71
+ | 债 | 现在的后果 |
72
+ |---|---|
73
+ | `focus-return:manual` | 关闭后归位是手写的,不是统一机制 |
74
+ | `typeahead:gap` | **没有类型搜索** —— APG 的 Menu Button 不硬性要求,但列表长时体验缺失 |
75
+
76
+ ## 门禁
77
+
78
+ `05-audit/dropdown-check.js`
@@ -0,0 +1,101 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <link rel="icon" href="data:,">
7
+ <title>dropdown · 下拉菜单 · frontend-lib</title>
8
+ <link rel="stylesheet" href="../../01-tokens/tokens.css">
9
+ <link rel="stylesheet" href="../../01-tokens/typography.css">
10
+ <link rel="stylesheet" href="../../01-tokens/focus-ring.css">
11
+ <link rel="stylesheet" href="../../01-tokens/page.css">
12
+ <link rel="stylesheet" href="../../02-primitives/button/button.css">
13
+ <link rel="stylesheet" href="dropdown.css">
14
+ <style>
15
+ .pad { padding: var(--sp-4) 0 var(--sp-9); }
16
+ .log {
17
+ margin-top: var(--sp-4); padding: var(--sp-3);
18
+ background: var(--surface-sunken); border-radius: var(--r-sm);
19
+ font-family: var(--font-mono, monospace); font-size: var(--fs-xs);
20
+ color: var(--text-secondary); min-height: 2.6em;
21
+ }
22
+ </style>
23
+ </head>
24
+ <body class="page page">
25
+ <main class="wrap">
26
+
27
+ <h1>dropdown · 下拉菜单</h1>
28
+ <p class="note">
29
+ 依据 <strong>APG Menu Button</strong>:<code>↑↓</code> 移动 ·
30
+ <code>Esc</code> 关闭并<strong>把焦点还给按钮</strong> ·
31
+ <code>Tab</code> 关闭 · <code>Enter</code>/<code>Space</code> 激活 ·
32
+ <code>Home</code>/<code>End</code> 跳首末。
33
+ <br>⚠️ 菜单项 <code>tabindex="-1"</code>:<strong>不参与 Tab 序列</strong>,
34
+ 因为 Tab 的语义是「关闭菜单」。
35
+ </p>
36
+
37
+ <h2>1 · 基本用法(试试键盘)</h2>
38
+ <div class="pad">
39
+ <div class="dd" id="d1">
40
+ <button class="dd__btn" data-dd-btn>操作 <span class="dd__caret"></span></button>
41
+ <ul class="dd__menu" data-dd-menu hidden>
42
+ <li data-value="edit">编辑</li>
43
+ <li data-value="copy">复制</li>
44
+ <li data-value="paste">粘贴</li>
45
+ <li data-dd-sep></li>
46
+ <li data-value="del" aria-disabled="true">删除(不可用)</li>
47
+ </ul>
48
+ </div>
49
+ <div class="log" id="log">(选中项会显示在这里)</div>
50
+ </div>
51
+
52
+ <h2>2 · 带勾选(选中性不只靠颜色)</h2>
53
+ <div class="pad">
54
+ <div class="dd" id="d2">
55
+ <button class="dd__btn" data-dd-btn>排序方式 <span class="dd__caret"></span></button>
56
+ <ul class="dd__menu" data-dd-menu hidden>
57
+ <li data-value="date"><span class="dd__check" data-check>✓</span>按日期</li>
58
+ <li data-value="name"><span class="dd__check" data-check></span>按名称</li>
59
+ <li data-value="size"><span class="dd__check" data-check></span>按大小</li>
60
+ </ul>
61
+ </div>
62
+ </div>
63
+
64
+ <h2>3 · 靠右对齐(窄屏不出屏)</h2>
65
+ <div class="pad">
66
+ <div class="dd dd--end" id="d3">
67
+ <button class="dd__btn" data-dd-btn>账号 <span class="dd__caret"></span></button>
68
+ <ul class="dd__menu" data-dd-menu hidden>
69
+ <li data-value="profile">个人资料</li>
70
+ <li data-value="billing">账单</li>
71
+ <li data-value="signout">退出登录</li>
72
+ </ul>
73
+ </div>
74
+ </div>
75
+
76
+ </main>
77
+
78
+ <script src="dropdown.js"></script>
79
+ <script>
80
+ (function () {
81
+ var log = document.getElementById('log');
82
+ function onSelect(v) { log.textContent = '选中:' + v; }
83
+
84
+ Dropdown.create(document.getElementById('d1'), { onSelect: onSelect, label: '操作' });
85
+ Dropdown.create(document.getElementById('d2'), { onSelect: onSelect, label: '排序方式' });
86
+ Dropdown.create(document.getElementById('d3'), { onSelect: onSelect, label: '账号' });
87
+
88
+ /* 选中性:点中某项后把 ✓ 挪过去(不是只改颜色)*/
89
+ document.getElementById('d2').__dd = true;
90
+ var d2 = document.getElementById('d2');
91
+ d2.addEventListener('click', function (e) {
92
+ var li = e.target.closest('li[data-value]');
93
+ if (!li) return;
94
+ d2.querySelectorAll('[data-check]').forEach(function (s) { s.textContent = ''; });
95
+ var c = li.querySelector('[data-check]');
96
+ if (c) c.textContent = '✓';
97
+ });
98
+ })();
99
+ </script>
100
+ </body>
101
+ </html>