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,135 @@
1
+ /* ============================================================================
2
+ dropdown.css — 下拉菜单(B3 /
3
+ ============================================================================
4
+ 依据:APG(ARIA Authoring Practices)Menu Button 模式
5
+
6
+ ⭐ 四条键盘契约(APG 明确要求,缺一条就不算合规):
7
+ ① **↑↓** 在项之间移动
8
+ ② **Esc** 关闭并把焦点**还给按钮**
9
+ ③ **Tab** 关闭菜单(焦点移到下一个元素)
10
+ ④ **Enter / Space** 激活当前项
11
+
12
+ ⚠️ 另有两条容易漏的:
13
+ · 关闭后**焦点必须归位**(不归位 ⇒ 键盘用户迷路)
14
+ · 菜单项的**命中区 ≥ 44px**(密集列表也要够大)
15
+ ============================================================================ */
16
+
17
+ .dd {
18
+ position: relative;
19
+ display: inline-block;
20
+ }
21
+
22
+ /* ---------- 触发按钮 ---------- */
23
+ .dd__btn {
24
+ display: inline-flex;
25
+ align-items: center;
26
+ gap: var(--sp-2);
27
+ min-height: 44px; /* WCAG 2.5.5 AAA */
28
+ padding: 0 var(--sp-4);
29
+ background: var(--surface);
30
+ color: var(--text-primary);
31
+ border: 1px solid var(--border-control);
32
+ border-radius: var(--r-sm);
33
+ font: inherit;
34
+ font-size: var(--fs-sm);
35
+ cursor: pointer;
36
+ }
37
+
38
+ .dd__btn:focus-visible {
39
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
40
+ outline-offset: var(--focus-ring-offset);
41
+ }
42
+
43
+ .dd__caret {
44
+ width: 0;
45
+ height: 0;
46
+ border: 4px solid transparent;
47
+ border-top-color: currentColor;
48
+ /* 展开时翻转(形状线索,不只靠阴影)*/
49
+ transition: transform 140ms var(--ease-out, ease);
50
+ }
51
+
52
+ .dd__btn[aria-expanded="true"] .dd__caret {
53
+ transform: rotate(180deg);
54
+ }
55
+
56
+ /* ---------- 菜单 ---------- */
57
+ .dd__menu {
58
+ position: absolute;
59
+ top: calc(100% + var(--sp-1));
60
+ inset-inline-start: 0;
61
+ z-index: var(--z-raised);
62
+ min-width: 180px;
63
+ margin: 0;
64
+ padding: var(--sp-1);
65
+ list-style: none;
66
+ background: var(--surface-raised);
67
+ border: 1px solid var(--border-decor);
68
+ border-radius: var(--r-sm);
69
+ box-shadow: var(--shadow-2, 0 6px 18px rgba(0, 0, 0, .14));
70
+ }
71
+
72
+ /* 🔴 关闭态用 hidden 属性 ⇒ 移出无障碍树 + 不可聚焦 */
73
+ .dd__menu[hidden] { display: none; }
74
+
75
+ .dd__item {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: var(--sp-2);
79
+ /* 🔴 修:原来写 `min-height: 44px`,但 <li> 是 `display:list-item`
80
+ ⇒ 在 flex 容器里**未被拉伸**,实测只有 24px。
81
+ ⇒ 改用 `height` 明确给足(或保持 flex 但显式 height)。
82
+ ⚠️ 这类问题在桌面小屏看不出来,只有量了才发现。 */
83
+ height: 44px; /* 明确高度,不用 min-height */
84
+ box-sizing: border-box;
85
+ padding: 0 var(--sp-3);
86
+ border-radius: var(--r-xs, 3px);
87
+ color: var(--text-primary);
88
+ font-size: var(--fs-sm);
89
+ cursor: pointer;
90
+ user-select: none;
91
+ }
92
+
93
+ /* ⭐ 焦点与悬停**用同一个视觉**(键盘与鼠标看到的是同一个高亮)*/
94
+ .dd__item:focus,
95
+ .dd__item[data-state="active"] {
96
+ outline: none; /* 自己画,不用默认 outline */
97
+ background: var(--accent-soft);
98
+ color: var(--text-primary);
99
+ }
100
+
101
+ @media (hover: hover) and (pointer: fine) {
102
+ .dd__item:hover { background: var(--surface-sunken); }
103
+ }
104
+
105
+ .dd__item[aria-disabled="true"] {
106
+ color: var(--text-tertiary);
107
+ cursor: not-allowed;
108
+ background: var(--surface-sunken); /* 不用 opacity(对比度不可控)*/
109
+ }
110
+
111
+ .dd__sep {
112
+ height: 1px;
113
+ margin: var(--sp-1) 0;
114
+ background: var(--border-decor);
115
+ /* 纯装饰:读屏不需要知道有分隔线 */
116
+ list-style: none;
117
+ }
118
+
119
+ .dd__sep[role="separator"] { display: block; }
120
+
121
+ /* ---------- 菜单项里的勾(选中性,不只靠颜色)---------- */
122
+ .dd__check {
123
+ width: 1em;
124
+ font-weight: 700;
125
+ color: var(--accent);
126
+ }
127
+
128
+ @media (prefers-reduced-motion: reduce) {
129
+ .dd__caret { transition: none; }
130
+ }
131
+
132
+ /* ---------- 窄屏:菜单贴右,避免出屏 ---------- */
133
+ @media (max-width: 30rem) {
134
+ .dd--end .dd__menu { inset-inline-start: auto; inset-inline-end: 0; }
135
+ }
@@ -0,0 +1,451 @@
1
+ /**
2
+ * dropdown.js — 下拉菜单(B3 /
3
+ *
4
+ * 依据:APG Menu Button 模式
5
+ * ① ↑↓ 移动 ② Esc 关闭并**归位焦点**
6
+ * ③ Tab 关闭 ④ Enter/Space 激活
7
+ * ⑤ Home/End 跳首末
8
+ *
9
+ * 语义:role="menu" + role="menuitem"(APG 允许用 ul/li + role)
10
+ * ⚠️ 只用 role=menu 时,读屏会进入"应用模式"(方向键导航)——
11
+ * 这正是菜单想要的,但**必须**保证键盘契约完整,否则用户会被困住。
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: roving ==== */
43
+ var flRoving = function (opts) {
44
+ var opt = opts || {};
45
+ var container = opt.container;
46
+ var all = opt.items || function () { return []; };
47
+ var movable = opt.movable || all;
48
+ var nodeOf = opt.nodeOf || function (x) { return x; };
49
+ var axis = opt.axis || 'x';
50
+ var loop = opt.loop !== false;
51
+ var homeEnd = opt.homeEnd !== false;
52
+ /* ④ tabindex: false ⇒ 只要步进,不要 roving tabindex(理由见文件头)*/
53
+ var roveTab = opt.tabindex !== false;
54
+ var onMove = opt.onMove || function () {};
55
+
56
+ /* ① tabindex 落在**全集**上:禁用项也必须是 -1,否则 Tab 会停上去 */
57
+ function rove(target) {
58
+ if (!roveTab) return target;
59
+ var l = all() || [];
60
+ for (var i = 0; i < l.length; i++) {
61
+ var n = nodeOf(l[i]);
62
+ if (!n || !n.setAttribute) continue;
63
+ n.setAttribute('tabindex', n === target ? '0' : '-1');
64
+ }
65
+ return target;
66
+ }
67
+
68
+ /* ② 取模循环:对负数、对 |d| > n 都对 */
69
+ function step(i, d) {
70
+ var n = (movable() || []).length;
71
+ if (!n) return -1;
72
+ if (loop) return ((i + d) % n + n) % n;
73
+ var t = i + d;
74
+ return (t < 0 || t >= n) ? -1 : t;
75
+ }
76
+
77
+ function indexOfNode(node) {
78
+ var l = movable() || [];
79
+ for (var i = 0; i < l.length; i++) {
80
+ if (nodeOf(l[i]) === node) return i;
81
+ }
82
+ return -1;
83
+ }
84
+
85
+ function onKey(e) {
86
+ var l = movable() || [];
87
+ if (!l.length) return;
88
+ var i = indexOfNode(e.target);
89
+ if (i < 0) return;
90
+ var k = e.key;
91
+ var next = -1;
92
+ var reason = 'step';
93
+
94
+ if (axis === 'y') {
95
+ if (k === 'ArrowDown') next = step(i, 1);
96
+ else if (k === 'ArrowUp') next = step(i, -1);
97
+ } else {
98
+ if (k === 'ArrowRight') next = step(i, 1);
99
+ else if (k === 'ArrowLeft') next = step(i, -1);
100
+ }
101
+ if (homeEnd && k === 'Home') { next = 0; reason = 'home'; }
102
+ else if (homeEnd && k === 'End') { next = l.length - 1; reason = 'end'; }
103
+
104
+ if (next < 0 || next >= l.length) return;
105
+ /* ③ 只拦事件,不替组件决定移动后做什么 */
106
+ if (e.preventDefault) e.preventDefault();
107
+ if (e.stopPropagation) e.stopPropagation();
108
+ onMove(next, l[next], reason);
109
+ }
110
+
111
+ if (container) container.addEventListener('keydown', onKey);
112
+
113
+ /* ⚠️ 只暴露有人用的东西(理由见文件头)*/
114
+ return {
115
+ rove: rove,
116
+ step: step,
117
+ indexOf: indexOfNode,
118
+ destroy: function () {
119
+ if (container) container.removeEventListener('keydown', onKey);
120
+ },
121
+ };
122
+ };
123
+ /* ==== BEHAVIOR INJECT END: roving ==== */
124
+
125
+ /* ==== BEHAVIOR INJECT BEGIN: focus-return ==== */
126
+ var flFocusReturn = function (opts) {
127
+ var opt = opts || {};
128
+ var saved = null;
129
+
130
+ return {
131
+ save: function () {
132
+ var a = document.activeElement;
133
+ saved = (a && a !== document.body && a.focus) ? a : null;
134
+ return saved;
135
+ },
136
+
137
+ restore: function () {
138
+ var target = saved;
139
+ /* ⚠️ 见文档 ②:不判 contains 的话,元素被移除后 focus() 不报错
140
+ 但焦点不动 ⇒ 用户以为还回去了,其实掉在 body 上。 */
141
+ if (!target || !document.contains(target)) target = opt.fallback || null;
142
+ if (!target) { saved = null; return false; }
143
+ try {
144
+ target.focus();
145
+ } catch (e) {
146
+ saved = null;
147
+ return false;
148
+ }
149
+ var ok = document.activeElement === target;
150
+ saved = null; /* 见文档 ③ */
151
+ return ok;
152
+ },
153
+
154
+ target: function () { return saved; },
155
+ clear: function () { saved = null; },
156
+ };
157
+ };
158
+ /* ==== BEHAVIOR INJECT END: focus-return ==== */
159
+ /* ==== BEHAVIOR INJECT BEGIN: dismissable ==== */
160
+ var flDismissable = function (opts) {
161
+ var opt = opts || {};
162
+ var escOn = opt.escOn || [];
163
+ var inside = opt.inside || [];
164
+ var when = opt.when || function () { return true; };
165
+ var onDismiss = opt.onDismiss || function () {};
166
+
167
+ var shared = window.__flDismiss || (window.__flDismiss = { stack: [], bound: false });
168
+
169
+ function isInside(node) {
170
+ if (!node) return false;
171
+ for (var i = 0; i < inside.length; i++) {
172
+ var el = inside[i];
173
+ if (!el || !el.contains) continue;
174
+ if (el === node || el.contains(node)) return true;
175
+ }
176
+ return false;
177
+ }
178
+
179
+ function onKey(e) {
180
+ if (!when()) return;
181
+ var k = e.key;
182
+ if (k !== 'Escape' && k !== 'Esc' && e.keyCode !== 27) return;
183
+ /* 🔴 为什么 onDismiss 在前、preventDefault/stopPropagation 在后
184
+ (0.8.8 改):**分级 Esc** 需要核让组件有机会说"这次我没关"。
185
+ combobox 的 Esc 是三级的:
186
+ ① 列表开着 → 关列表
187
+ ② 列表关了、输入有字 → 清空输入
188
+ ③ 都没了 → 什么都不做(此时**不该**阻止默认行为,
189
+ 让 Esc 能继续传给外层浮层)
190
+ 而原实现是「无条件 preventDefault + stopPropagation,再调 onDismiss」
191
+ ⇒ 组件根本无法表达"这次我没关"。
192
+ ⇒ 改成:先问onDismiss,它返回 false 就**不干预**默认行为。
193
+ ⚠️ 顺序安全性:stopPropagation 只需在**本次监听器执行期间**被调用即可
194
+ 阻止冒泡到外层,放在 onDismiss 之后仍然有效。
195
+ */
196
+ var closed = onDismiss(e, 'esc');
197
+ if (closed === false) return;
198
+ if (e.preventDefault) e.preventDefault();
199
+ if (e.stopPropagation) e.stopPropagation();
200
+ }
201
+
202
+ function onDocClick(e) {
203
+ var top = null;
204
+ for (var i = shared.stack.length - 1; i >= 0; i--) {
205
+ if (shared.stack[i].when()) { top = shared.stack[i]; break; }
206
+ }
207
+ if (!top || top.isInside(e.target)) return;
208
+ top.onDismiss(e, 'outside');
209
+ }
210
+
211
+ var rec = { when: when, isInside: isInside, onDismiss: onDismiss };
212
+
213
+ for (var j = 0; j < escOn.length; j++) {
214
+ if (escOn[j]) escOn[j].addEventListener('keydown', onKey);
215
+ }
216
+ shared.stack.push(rec);
217
+ if (!shared.bound) {
218
+ shared.bound = true;
219
+ document.addEventListener('click', onDocClick, true);
220
+ }
221
+
222
+ return {
223
+ destroy: function () {
224
+ for (var j = 0; j < escOn.length; j++) {
225
+ if (escOn[j]) escOn[j].removeEventListener('keydown', onKey);
226
+ }
227
+ var k = shared.stack.indexOf(rec);
228
+ if (k >= 0) shared.stack.splice(k, 1);
229
+ },
230
+ };
231
+ };
232
+ /* ==== BEHAVIOR INJECT END: dismissable ==== */
233
+
234
+ /* ---- 旧环境兼容:Element.closest 在老 WebView 上不存在 ----
235
+ 直接调用会在真机上抛 `is not a function`(本地完全正常,
236
+ 只在老环境炸 —— 属静默失效)。这里做模块内兜底,
237
+ 不污染全局,也不需要使用者额外引入 polyfill。 */
238
+ function closest(el, sel) {
239
+ if (!el) return null;
240
+ if (el.closest) return el.closest(sel);
241
+ while (el && el.nodeType === 1) {
242
+ if (elMatches(el, sel)) return el;
243
+ el = el.parentElement;
244
+ }
245
+ return null;
246
+ }
247
+ function elMatches(el, sel) {
248
+ var m = sel.match(/^([a-zA-Z][\w-]*)/);
249
+ if (m && el.tagName.toLowerCase() !== m[1].toLowerCase()) return false;
250
+ var cls = sel.match(/\.([\w-]+)/g);
251
+ if (cls) {
252
+ for (var i = 0; i < cls.length; i++) {
253
+ var c = cls[i].slice(1);
254
+ if ((' ' + (el.className || '') + ' ').indexOf(' ' + c + ' ') < 0) return false;
255
+ }
256
+ }
257
+ var id = sel.match(/#([\w-]+)/);
258
+ if (id && el.id !== id[1]) return false;
259
+ var at = sel.match(/\[([\w-]+)(?:=["']?([^\]"']*)["']?)?\]/);
260
+ if (at) {
261
+ var v = el.getAttribute(at[1]);
262
+ if (v === null) return false;
263
+ if (at[2] !== undefined && v !== at[2]) return false;
264
+ }
265
+ return true;
266
+ }
267
+
268
+ var FOCUSABLE = 'li[role="menuitem"]:not([aria-disabled="true"])';
269
+
270
+ function itemsOf(menu) {
271
+ return Array.prototype.slice.call(menu.querySelectorAll(FOCUSABLE));
272
+ }
273
+
274
+ function create(root, opt) {
275
+ opt = opt || {};
276
+ var btn = root.querySelector('[data-dd-btn]') || root.querySelector('button');
277
+ var menu = root.querySelector('[data-dd-menu]');
278
+ if (!btn || !menu) throw new Error('dropdown: 缺少 [data-dd-btn] 或 [data-dd-menu]');
279
+
280
+ /* 焦点归位核:open() 时 save()、close() 时 restore()。
281
+ 原先是 `btn.focus()` + `document.contains(btn)` 手写判存活,
282
+ 核内部做的就是这件事(元素被移除 ⇒ 退到 fallback)。 */
283
+ var fr = flFocusReturn({ fallback: btn });
284
+ var active = -1;
285
+
286
+ /* APG:菜单要么全 aria-hidden,要么全不设 —— 不能半吊子 */
287
+ menu.setAttribute('role', 'menu');
288
+ if (!menu.getAttribute('aria-label') && !menu.getAttribute('aria-labelledby')) {
289
+ menu.setAttribute('aria-label', opt.label || '菜单');
290
+ }
291
+ Array.prototype.forEach.call(menu.querySelectorAll('li'), function (li) {
292
+ /* 🔴 修一个**静默失效**(实测发现):
293
+ demo 的 <li> 上**根本没有 dd__item 类** ⇒ CSS 里的
294
+ `height: 44px` / 内边距 / hover **全部没生效**
295
+ ⇒ 实测每项只有 24px(浏览器默认行高)。
296
+ ⚠️ 这类问题在页面上"看着还行",只有量尺寸才发现。
297
+ ⇒ 正解:JS 在初始化时**补上语义类**,不依赖作者手写。
298
+ (组件级 class 由组件自己管,比要求每个使用者记得写更可靠。) */
299
+ if (!li.hasAttribute('data-dd-sep') &&
300
+ String(li.className).indexOf('dd__item') < 0) {
301
+ li.className = li.className ? (li.className + ' dd__item') : 'dd__item';
302
+ }
303
+ if (li.hasAttribute('data-dd-sep')) {
304
+ li.setAttribute('role', 'separator');
305
+ li.setAttribute('aria-hidden', 'true');
306
+ } else if (!li.hasAttribute('role')) {
307
+ li.setAttribute('role', 'menuitem');
308
+ /* tabindex="-1":菜单项**不参与 Tab 序列**
309
+ (APG:Tab 用来**关闭菜单**,方向键才在项间移动)*/
310
+ li.setAttribute('tabindex', '-1');
311
+ }
312
+ });
313
+
314
+ function open() {
315
+ /* 打开这一刻先记焦点,close() 才能还回去。 */
316
+ fr.save();
317
+ menu.hidden = false;
318
+ btn.setAttribute('aria-expanded', 'true');
319
+ active = -1;
320
+ flEmit(root, 'fl-open', null);
321
+ if (opt.onOpen) opt.onOpen();
322
+ }
323
+
324
+ function close(restoreFocus) {
325
+ /* ⭐ APG:Esc 关闭时**焦点必须还给按钮**,否则键盘用户迷路 */
326
+ menu.hidden = true;
327
+ btn.setAttribute('aria-expanded', 'false');
328
+ active = -1;
329
+ if (restoreFocus !== false) fr.restore();
330
+ flEmit(root, 'fl-close', null);
331
+ if (opt.onClose) opt.onClose();
332
+ }
333
+
334
+ function focusAt(i) {
335
+ var list = itemsOf(menu);
336
+ if (!list.length) return;
337
+ if (i < 0) i = list.length - 1;
338
+ if (i >= list.length) i = 0;
339
+ list.forEach(function (x) { x.removeAttribute('data-state'); });
340
+ list[i].setAttribute('data-state', 'active');
341
+ list[i].focus();
342
+ active = i;
343
+ }
344
+
345
+ function current() {
346
+ var list = itemsOf(menu);
347
+ return active >= 0 ? list[active] : document.activeElement;
348
+ }
349
+
350
+ /* ↑↓ 与 Home / End 走 roving 核(01-tokens/behavior/roving.js)。
351
+ ⭐ tabindex: false —— 本组件按 APG **不用** roving tabindex:
352
+ 菜单项永远是 tabindex="-1",焦点由程序移动,Tab 的作用是**关闭菜单**。
353
+ 若让核把某一项改成 "0",菜单里就会多出一个 Tab 停靠点,
354
+ 「Tab 用来关闭菜单」这条契约当场失效(dropdown-check 第 41 行在守)。
355
+ ⇒ 要的是同一套**步进 / 绕回 / Home·End**,不是同一套 tabindex。 */
356
+ var roving = flRoving({
357
+ container: root,
358
+ items: function () { return itemsOf(menu); },
359
+ axis: 'y',
360
+ tabindex: false,
361
+ onMove: function (i) { focusAt(i); },
362
+ });
363
+
364
+ /* ---------- 按钮 ---------- */
365
+ /* 🔴 修:原来先 `setAttribute('aria-controls', menu.id || '')`
366
+ **再**给 menu.id ⇒ aria-controls 永远拿到空串。
367
+ ⇒ 顺序必须反过来:**先确保有 id,再引用**。 */
368
+ if (!menu.id) menu.id = 'dd-menu-' + (new Date()).getTime();
369
+ btn.setAttribute('aria-haspopup', 'true');
370
+ btn.setAttribute('aria-expanded', 'false');
371
+ btn.setAttribute('aria-controls', menu.id);
372
+
373
+ btn.addEventListener('click', function (e) {
374
+ e.stopPropagation();
375
+ if (menu.hidden) open(); else close();
376
+ });
377
+
378
+ /* ---------- 菜单项 ---------- */
379
+ menu.addEventListener('click', function (e) {
380
+ var li = e.target.closest ? closest(e.target, '[role="menuitem"]') : null;
381
+ if (!li) return;
382
+ e.stopPropagation();
383
+ if (li.getAttribute('aria-disabled') === 'true') return;
384
+ close(); /* 激活后关闭 + 归位 */
385
+ flEmit(root, 'fl-select',
386
+ { value: li.getAttribute('data-value') || li.textContent.trim(), item: li });
387
+ if (opt.onSelect) opt.onSelect(li.getAttribute('data-value') || li.textContent.trim(), li);
388
+ });
389
+
390
+ /* ---------- 键盘(APG Menu Button 全部契约)---------- */
391
+ root.addEventListener('keydown', function (e) {
392
+ var k = e.key;
393
+
394
+ /* 按钮上:↓ / Enter / Space 打开并聚焦首项 */
395
+ if (e.target === btn) {
396
+ if (k === 'ArrowDown' || k === 'Enter' || k === ' ' || k === 'Spacebar') {
397
+ e.preventDefault();
398
+ open();
399
+ focusAt(0);
400
+ }
401
+ return;
402
+ }
403
+
404
+ /* 🔴 修一个**静默失效的真 bug**:
405
+ 原来写的是 `e.target.closest !== menu`
406
+ ⇒ `e.target.closest` 是**函数本身**(没加括号!),
407
+ 它永远不等于 menu ⇒ 这个 if **永远为真** ⇒ 每次都提前 return
408
+ ⇒ **整个键盘导航(↑↓ Esc Home End)全部失效**,而且不报任何错。
409
+ ⚠️ 这类 bug 最危险:功能看起来"有实现",实际一行都没跑到。
410
+ ⇒ 正解:`closest(e.target, '[data-dd-menu]')`(**加括号调用**)。 */
411
+ if (closest(e.target, '[data-dd-menu]') !== menu) return;
412
+
413
+ /* ① ↑↓ 移动(循环)· ⑤ Home / End 跳首末
414
+ ⇒ 已移到 roving 核(见上面的 flRoving),这里不再重复实现。
415
+ ⚠️ 不要在这里再写一遍:核与组件挂在**同一个 root** 上,
416
+ 两个监听器都会跑 ⇒ 一次按键走两步。 */
417
+
418
+ /* ② Esc 关闭 + 归位 —— 已移到 dismissable 核(01-tokens/behavior/dismissable.js)。
419
+ 留在原地会**关两次**(同一元素上的两个监听器,stopPropagation 挡不住
420
+ 同级的另一个监听器,那是 stopImmediatePropagation 的活)⇒ onClose 被调两遍。 */
421
+
422
+ /* ③ Tab 关闭(不拦截,让焦点自然走到下一个)*/
423
+ if (k === 'Tab') { close(false); return; }
424
+
425
+ /* ④ Enter / Space 激活 */
426
+ if (k === 'Enter' || k === ' ' || k === 'Spacebar') {
427
+ e.preventDefault();
428
+ var li = current();
429
+ if (li) li.click();
430
+ }
431
+ });
432
+
433
+ /* Esc / 点击外部关闭:走 dismissable 核(01-tokens/behavior/dismissable.js)。
434
+ ⚠️ 外部点击必须归位焦点吗?不 —— APG 只在 **Esc** 时要求归还,
435
+ 点外面是用户主动去别处,抢回焦点反而打断他。⇒ onDismiss 按 reason 分。 */
436
+ var dismiss = flDismissable({
437
+ escOn: [root],
438
+ inside: [root],
439
+ when: function () { return !menu.hidden; },
440
+ onDismiss: function (e, reason) { close(reason === 'esc'); },
441
+ });
442
+
443
+ return {
444
+ open: open,
445
+ close: close,
446
+ destroy: function () { roving.destroy(); dismiss.destroy(); menu.hidden = true; },
447
+ };
448
+ }
449
+
450
+ global.Dropdown = { create: create };
451
+ })(window);