@icen.ai/ui 0.8.1 → 0.10.0

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 (335) hide show
  1. package/AGENTS.md +176 -75
  2. package/README.md +66 -88
  3. package/dist/base.css +208 -179
  4. package/dist/behaviors/accordion.mjs +8 -8
  5. package/dist/behaviors/ai-branch.d.mts +19 -0
  6. package/dist/behaviors/ai-branch.mjs +1133 -0
  7. package/dist/behaviors/ai-chat.d.mts +12 -10
  8. package/dist/behaviors/ai-chat.mjs +1125 -155
  9. package/dist/behaviors/ai-composer.d.mts +44 -12
  10. package/dist/behaviors/ai-composer.mjs +2084 -523
  11. package/dist/behaviors/ai-core.d.mts +1 -1
  12. package/dist/behaviors/ai-core.mjs +1016 -107
  13. package/dist/behaviors/ai-diff.mjs +1019 -127
  14. package/dist/behaviors/ai-feedback.d.mts +26 -0
  15. package/dist/behaviors/ai-feedback.mjs +1212 -0
  16. package/dist/behaviors/ai-panel.d.mts +21 -7
  17. package/dist/behaviors/ai-panel.mjs +1308 -264
  18. package/dist/behaviors/ai-provider.mjs +1003 -101
  19. package/dist/behaviors/ai-threads.d.mts +38 -0
  20. package/dist/behaviors/ai-threads.mjs +1233 -0
  21. package/dist/behaviors/ai-tool.d.mts +2 -2
  22. package/dist/behaviors/ai-tool.mjs +2042 -396
  23. package/dist/behaviors/ai-tools.d.mts +89 -16
  24. package/dist/behaviors/ai-tools.mjs +4453 -372
  25. package/dist/behaviors/autocomplete.d.mts +53 -0
  26. package/dist/behaviors/autocomplete.mjs +1600 -0
  27. package/dist/behaviors/back-top.d.mts +1 -1
  28. package/dist/behaviors/back-top.mjs +951 -14
  29. package/dist/behaviors/carousel.d.mts +1 -1
  30. package/dist/behaviors/carousel.mjs +906 -11
  31. package/dist/behaviors/cascader.d.mts +48 -0
  32. package/dist/behaviors/cascader.mjs +1853 -0
  33. package/dist/behaviors/charts.d.mts +174 -8
  34. package/dist/behaviors/charts.mjs +2632 -192
  35. package/dist/behaviors/code-block.d.mts +36 -0
  36. package/dist/behaviors/code-block.mjs +1611 -0
  37. package/dist/behaviors/command-palette.d.mts +9 -6
  38. package/dist/behaviors/command-palette.mjs +161 -59
  39. package/dist/behaviors/context-menu.d.mts +9 -8
  40. package/dist/behaviors/context-menu.mjs +1112 -44
  41. package/dist/behaviors/controls.d.mts +13 -13
  42. package/dist/behaviors/controls.mjs +989 -130
  43. package/dist/behaviors/copy.d.mts +21 -2
  44. package/dist/behaviors/copy.mjs +1206 -42
  45. package/dist/behaviors/datatable.d.mts +53 -3
  46. package/dist/behaviors/datatable.mjs +1963 -207
  47. package/dist/behaviors/date-picker.d.mts +0 -1
  48. package/dist/behaviors/date-picker.mjs +1340 -293
  49. package/dist/behaviors/design.d.mts +54 -0
  50. package/dist/behaviors/design.mjs +109 -0
  51. package/dist/behaviors/dropdown.d.mts +5 -6
  52. package/dist/behaviors/dropdown.mjs +262 -110
  53. package/dist/behaviors/events.d.mts +313 -3
  54. package/dist/behaviors/events.mjs +47 -2
  55. package/dist/behaviors/feedback.d.mts +30 -0
  56. package/dist/behaviors/feedback.mjs +296 -0
  57. package/dist/behaviors/focus.d.mts +8 -0
  58. package/dist/behaviors/focus.mjs +24 -0
  59. package/dist/behaviors/form-validate.d.mts +102 -0
  60. package/dist/behaviors/form-validate.mjs +6091 -0
  61. package/dist/behaviors/i18n.d.mts +789 -0
  62. package/dist/behaviors/i18n.mjs +1711 -0
  63. package/dist/behaviors/icons.d.mts +93 -0
  64. package/dist/behaviors/icons.mjs +113 -0
  65. package/dist/behaviors/input.d.mts +9 -1
  66. package/dist/behaviors/input.mjs +1056 -60
  67. package/dist/behaviors/kb-access.d.mts +34 -0
  68. package/dist/behaviors/kb-access.mjs +2459 -0
  69. package/dist/behaviors/kb-acl.d.mts +50 -0
  70. package/dist/behaviors/kb-acl.mjs +1345 -0
  71. package/dist/behaviors/kb-answer.d.mts +38 -0
  72. package/dist/behaviors/kb-answer.mjs +3707 -0
  73. package/dist/behaviors/kb-audit.d.mts +23 -0
  74. package/dist/behaviors/kb-audit.mjs +1297 -0
  75. package/dist/behaviors/kb-canvas.d.mts +44 -0
  76. package/dist/behaviors/kb-canvas.mjs +1512 -0
  77. package/dist/behaviors/kb-chain.d.mts +15 -0
  78. package/dist/behaviors/kb-chain.mjs +1192 -0
  79. package/dist/behaviors/kb-checkpoint.d.mts +16 -0
  80. package/dist/behaviors/kb-checkpoint.mjs +1492 -0
  81. package/dist/behaviors/kb-chunks.d.mts +31 -0
  82. package/dist/behaviors/kb-chunks.mjs +1414 -0
  83. package/dist/behaviors/kb-citation.d.mts +38 -0
  84. package/dist/behaviors/kb-citation.mjs +1699 -0
  85. package/dist/behaviors/kb-clarify.d.mts +20 -0
  86. package/dist/behaviors/kb-clarify.mjs +1176 -0
  87. package/dist/behaviors/kb-conflict.d.mts +22 -0
  88. package/dist/behaviors/kb-conflict.mjs +1206 -0
  89. package/dist/behaviors/kb-connector.d.mts +17 -0
  90. package/dist/behaviors/kb-connector.mjs +1303 -0
  91. package/dist/behaviors/kb-core.d.mts +534 -0
  92. package/dist/behaviors/kb-core.mjs +1805 -0
  93. package/dist/behaviors/kb-eval.d.mts +22 -0
  94. package/dist/behaviors/kb-eval.mjs +2235 -0
  95. package/dist/behaviors/kb-explain.d.mts +11 -0
  96. package/dist/behaviors/kb-explain.mjs +1165 -0
  97. package/dist/behaviors/kb-filter.d.mts +31 -0
  98. package/dist/behaviors/kb-filter.mjs +2417 -0
  99. package/dist/behaviors/kb-gap.d.mts +27 -0
  100. package/dist/behaviors/kb-gap.mjs +1195 -0
  101. package/dist/behaviors/kb-hittest.d.mts +34 -0
  102. package/dist/behaviors/kb-hittest.mjs +1352 -0
  103. package/dist/behaviors/kb-metadata.d.mts +29 -0
  104. package/dist/behaviors/kb-metadata.mjs +1248 -0
  105. package/dist/behaviors/kb-passage.d.mts +41 -0
  106. package/dist/behaviors/kb-passage.mjs +1469 -0
  107. package/dist/behaviors/kb-pipeline.d.mts +18 -0
  108. package/dist/behaviors/kb-pipeline.mjs +1301 -0
  109. package/dist/behaviors/kb-qa.d.mts +26 -0
  110. package/dist/behaviors/kb-qa.mjs +1260 -0
  111. package/dist/behaviors/kb-rerank.d.mts +20 -0
  112. package/dist/behaviors/kb-rerank.mjs +1248 -0
  113. package/dist/behaviors/kb-retrieval.d.mts +49 -0
  114. package/dist/behaviors/kb-retrieval.mjs +2585 -0
  115. package/dist/behaviors/kb-review.d.mts +44 -0
  116. package/dist/behaviors/kb-review.mjs +2282 -0
  117. package/dist/behaviors/kb-sandbox.d.mts +33 -0
  118. package/dist/behaviors/kb-sandbox.mjs +1247 -0
  119. package/dist/behaviors/kb-segment.d.mts +32 -0
  120. package/dist/behaviors/kb-segment.mjs +1431 -0
  121. package/dist/behaviors/kb-select.d.mts +24 -0
  122. package/dist/behaviors/kb-select.mjs +1989 -0
  123. package/dist/behaviors/kb-sources.d.mts +27 -0
  124. package/dist/behaviors/kb-sources.mjs +1287 -0
  125. package/dist/behaviors/kb-sql.d.mts +43 -0
  126. package/dist/behaviors/kb-sql.mjs +1865 -0
  127. package/dist/behaviors/kb-trace.d.mts +16 -0
  128. package/dist/behaviors/kb-trace.mjs +1328 -0
  129. package/dist/behaviors/kb-visibility.d.mts +44 -0
  130. package/dist/behaviors/kb-visibility.mjs +1331 -0
  131. package/dist/behaviors/kb-who-can.d.mts +32 -0
  132. package/dist/behaviors/kb-who-can.mjs +2216 -0
  133. package/dist/behaviors/mentions.d.mts +93 -0
  134. package/dist/behaviors/mentions.mjs +2152 -0
  135. package/dist/behaviors/modal.d.mts +4 -3
  136. package/dist/behaviors/modal.mjs +1055 -33
  137. package/dist/behaviors/nav.d.mts +1 -1
  138. package/dist/behaviors/nav.mjs +6 -6
  139. package/dist/behaviors/notification.d.mts +16 -8
  140. package/dist/behaviors/notification.mjs +1615 -265
  141. package/dist/behaviors/panel.d.mts +4 -0
  142. package/dist/behaviors/panel.mjs +994 -0
  143. package/dist/behaviors/popover.d.mts +28 -5
  144. package/dist/behaviors/popover.mjs +225 -24
  145. package/dist/behaviors/select.d.mts +5 -2
  146. package/dist/behaviors/select.mjs +1410 -117
  147. package/dist/behaviors/sidebar.d.mts +1 -1
  148. package/dist/behaviors/sidebar.mjs +13 -13
  149. package/dist/behaviors/slider.d.mts +1 -1
  150. package/dist/behaviors/slider.mjs +9 -9
  151. package/dist/behaviors/split-pane.d.mts +1 -1
  152. package/dist/behaviors/split-pane.mjs +7 -7
  153. package/dist/behaviors/suggestions.d.mts +27 -0
  154. package/dist/behaviors/suggestions.mjs +1089 -0
  155. package/dist/behaviors/svg-parse.d.mts +14 -0
  156. package/dist/behaviors/svg-parse.mjs +37 -0
  157. package/dist/behaviors/tabs.d.mts +2 -2
  158. package/dist/behaviors/tabs.mjs +25 -25
  159. package/dist/behaviors/tag-input.d.mts +23 -5
  160. package/dist/behaviors/tag-input.mjs +1359 -104
  161. package/dist/behaviors/theme-switcher.d.mts +24 -0
  162. package/dist/behaviors/theme-switcher.mjs +1981 -0
  163. package/dist/behaviors/theme.d.mts +15 -8
  164. package/dist/behaviors/theme.mjs +390 -28
  165. package/dist/behaviors/toast.d.mts +24 -25
  166. package/dist/behaviors/toast.mjs +1266 -88
  167. package/dist/behaviors/tooltip.d.mts +4 -0
  168. package/dist/behaviors/tooltip.mjs +378 -0
  169. package/dist/behaviors/tree-select.d.mts +69 -0
  170. package/dist/behaviors/tree-select.mjs +1907 -0
  171. package/dist/behaviors/tree.d.mts +1 -1
  172. package/dist/behaviors/tree.mjs +15 -15
  173. package/dist/behaviors/upload.d.mts +3 -2
  174. package/dist/behaviors/upload.mjs +1076 -66
  175. package/dist/cli.mjs +5 -6
  176. package/dist/components/ai-branch.mjs +3 -0
  177. package/dist/components/ai-chat.css +846 -458
  178. package/dist/components/ai-diff.css +153 -132
  179. package/dist/components/ai-feedback.mjs +3 -0
  180. package/dist/components/ai-panel.css +495 -270
  181. package/dist/components/ai-threads.mjs +3 -0
  182. package/dist/components/ai-tool.css +393 -238
  183. package/dist/components/autocomplete.css +61 -0
  184. package/dist/components/autocomplete.mjs +3 -0
  185. package/dist/components/back-top.css +34 -31
  186. package/dist/components/badge.css +44 -41
  187. package/dist/components/breadcrumb.css +67 -46
  188. package/dist/components/btn.css +80 -74
  189. package/dist/components/card.css +117 -106
  190. package/dist/components/cascader.css +311 -0
  191. package/dist/components/cascader.mjs +3 -0
  192. package/dist/components/chart-area.css +8 -2
  193. package/dist/components/chart-bar.css +95 -33
  194. package/dist/components/chart-gauge.css +14 -11
  195. package/dist/components/chart-graph.css +60 -0
  196. package/dist/components/chart-graph.mjs +4 -0
  197. package/dist/components/chart-heatmap.css +96 -90
  198. package/dist/components/chart-line.css +8 -3
  199. package/dist/components/chart-map.mjs +3 -0
  200. package/dist/components/chart-pie.css +9 -6
  201. package/dist/components/chart-radar.css +8 -5
  202. package/dist/components/chart-scatter.css +9 -6
  203. package/dist/components/chart-scatter.mjs +1 -1
  204. package/dist/components/chart-sparkline.css +5 -2
  205. package/dist/components/chart-stack.css +14 -11
  206. package/dist/components/charts.css +190 -67
  207. package/dist/components/charts.mjs +1 -0
  208. package/dist/components/code-block.css +418 -0
  209. package/dist/components/code-block.mjs +4 -0
  210. package/dist/components/command-palette.css +138 -117
  211. package/dist/components/content.css +257 -251
  212. package/dist/components/copy.css +72 -44
  213. package/dist/components/datatable.css +423 -181
  214. package/dist/components/date-picker.css +135 -104
  215. package/dist/components/empty.css +68 -65
  216. package/dist/components/feedback.css +201 -183
  217. package/dist/components/form-validate.css +120 -0
  218. package/dist/components/form-validate.mjs +3 -0
  219. package/dist/components/form.css +150 -127
  220. package/dist/components/icon.css +102 -0
  221. package/dist/components/icon.mjs +3 -0
  222. package/dist/components/input.css +159 -104
  223. package/dist/components/kb-access.mjs +4 -0
  224. package/dist/components/kb-acl.mjs +4 -0
  225. package/dist/components/kb-agent.css +496 -0
  226. package/dist/components/kb-answer.mjs +4 -0
  227. package/dist/components/kb-audit.mjs +4 -0
  228. package/dist/components/kb-canvas.mjs +4 -0
  229. package/dist/components/kb-chain.mjs +4 -0
  230. package/dist/components/kb-checkpoint.mjs +4 -0
  231. package/dist/components/kb-chunks.mjs +4 -0
  232. package/dist/components/kb-citation.mjs +4 -0
  233. package/dist/components/kb-clarify.mjs +4 -0
  234. package/dist/components/kb-conflict.mjs +4 -0
  235. package/dist/components/kb-connector.mjs +4 -0
  236. package/dist/components/kb-data.css +653 -0
  237. package/dist/components/kb-eval.mjs +4 -0
  238. package/dist/components/kb-explain.mjs +4 -0
  239. package/dist/components/kb-filter.mjs +4 -0
  240. package/dist/components/kb-gap.mjs +4 -0
  241. package/dist/components/kb-ground.css +578 -0
  242. package/dist/components/kb-hittest.mjs +4 -0
  243. package/dist/components/kb-ingest.css +882 -0
  244. package/dist/components/kb-metadata.mjs +4 -0
  245. package/dist/components/kb-ops.css +554 -0
  246. package/dist/components/kb-passage.mjs +4 -0
  247. package/dist/components/kb-perm.css +433 -0
  248. package/dist/components/kb-pipeline.mjs +4 -0
  249. package/dist/components/kb-qa.mjs +4 -0
  250. package/dist/components/kb-rerank.mjs +4 -0
  251. package/dist/components/kb-retrieval.mjs +4 -0
  252. package/dist/components/kb-review.mjs +4 -0
  253. package/dist/components/kb-sandbox.mjs +4 -0
  254. package/dist/components/kb-search.css +897 -0
  255. package/dist/components/kb-segment.mjs +4 -0
  256. package/dist/components/kb-sources.mjs +4 -0
  257. package/dist/components/kb-sql.mjs +4 -0
  258. package/dist/components/kb-trace.mjs +4 -0
  259. package/dist/components/kb-visibility.mjs +4 -0
  260. package/dist/components/kb-who-can.mjs +4 -0
  261. package/dist/components/kb.css +263 -0
  262. package/dist/components/kb.mjs +9 -0
  263. package/dist/components/layout.css +85 -82
  264. package/dist/components/material-chinese.css +73 -0
  265. package/dist/components/material-soviet.css +76 -0
  266. package/dist/components/material.css +275 -0
  267. package/dist/components/media.css +220 -166
  268. package/dist/components/mentions.css +189 -0
  269. package/dist/components/mentions.mjs +3 -0
  270. package/dist/components/menu.css +113 -105
  271. package/dist/components/modal.css +107 -112
  272. package/dist/components/nav.css +118 -115
  273. package/dist/components/notification.css +238 -174
  274. package/dist/components/pagination.css +56 -53
  275. package/dist/components/panel.css +66 -63
  276. package/dist/components/panel.mjs +1 -0
  277. package/dist/components/pill.css +81 -71
  278. package/dist/components/popover.css +67 -53
  279. package/dist/components/scroll-area.css +113 -110
  280. package/dist/components/segmented.css +146 -143
  281. package/dist/components/select.css +206 -186
  282. package/dist/components/sidebar.css +137 -127
  283. package/dist/components/slider.css +117 -114
  284. package/dist/components/split-pane.css +112 -109
  285. package/dist/components/stat.css +112 -106
  286. package/dist/components/steps.css +109 -103
  287. package/dist/components/switch.css +138 -134
  288. package/dist/components/table.css +131 -107
  289. package/dist/components/tabs.css +114 -108
  290. package/dist/components/tag-input.css +48 -133
  291. package/dist/components/tag.css +81 -71
  292. package/dist/components/theme-switcher.css +105 -0
  293. package/dist/components/theme-switcher.mjs +3 -0
  294. package/dist/components/toast.css +100 -86
  295. package/dist/components/toolbar.css +45 -42
  296. package/dist/components/tooltip.css +102 -43
  297. package/dist/components/tooltip.mjs +1 -0
  298. package/dist/components/tree-select.css +416 -0
  299. package/dist/components/tree-select.mjs +3 -0
  300. package/dist/components/upload.css +135 -92
  301. package/dist/fonts/LICENSES/ark-pixel/OFL.txt +96 -0
  302. package/dist/fonts/LICENSES/cubic-11/OFL.txt +108 -0
  303. package/dist/fonts/LICENSES/galmuri/LICENSE.txt +93 -0
  304. package/dist/fonts/OFL.txt +96 -0
  305. package/dist/fonts/README.md +14 -0
  306. package/dist/fonts/fusion-pixel-12px-zh-hans.woff2 +0 -0
  307. package/dist/index.d.mts +64 -15
  308. package/dist/index.mjs +28049 -8595
  309. package/dist/react.d.mts +91 -0
  310. package/dist/react.mjs +8208 -0
  311. package/dist/registry.json +454 -15
  312. package/dist/tokens/chinese.css +44 -0
  313. package/dist/tokens/colors.css +365 -1222
  314. package/dist/tokens/design.css +109 -0
  315. package/dist/tokens/dtcg.json +11198 -0
  316. package/dist/tokens/figma.variables.json +7017 -0
  317. package/dist/tokens/icon-masks.css +78 -0
  318. package/dist/tokens/pixel.css +50 -0
  319. package/dist/tokens/soviet.css +44 -0
  320. package/dist/tokens/style-profiles.css +36 -547
  321. package/dist/tokens/themes.json +308 -0
  322. package/dist/tokens/tokens-studio.json +6728 -0
  323. package/dist/tokens/typography.css +21 -18
  324. package/dist/tokens.css +1555 -1801
  325. package/dist/ui.css +16147 -8597
  326. package/dist/vue.d.mts +344 -0
  327. package/dist/vue.mjs +7781 -0
  328. package/docs/spec/ai-input-experience.md +75 -0
  329. package/docs/spec/ai-native.md +238 -147
  330. package/docs/spec/design-philosophy.md +189 -0
  331. package/docs/spec/design-primitives.md +196 -0
  332. package/docs/spec/kb-family.md +475 -0
  333. package/package.json +42 -7
  334. package/dist/retro-effects.css +0 -255
  335. package/dist/tokens/retro-effects.css +0 -255
@@ -0,0 +1,54 @@
1
+ /** Complete visual recipes share CSS, while each brand only supplies seeds.
2
+ * CSS is compiled without a DOM for SSR/static builds. Import tokens.css first.
3
+ * Mount a compiled recipe once, then put data-icen-preset on documentElement.
4
+ * Appearance belongs on html so body-portalled menus inherit the same tokens.
5
+ */
6
+ interface DesignPalette {
7
+ canvas: string;
8
+ surface: string;
9
+ ink: string;
10
+ muted: string;
11
+ accent: string;
12
+ onAccent: string;
13
+ success?: string;
14
+ warning?: string;
15
+ error?: string;
16
+ info?: string;
17
+ }
18
+ interface DesignDefinition {
19
+ /** Lowercase CSS identifier; modern is the built-in 现代 recipe. */
20
+ name: string;
21
+ light: DesignPalette;
22
+ dark: DesignPalette;
23
+ /** CSS px; the shared recipe derives the six geometric roles. */
24
+ radius?: number;
25
+ space?: number;
26
+ controlHeight?: number;
27
+ fontSize?: number;
28
+ font?: string;
29
+ glassOpacity?: number;
30
+ glassBlur?: number;
31
+ motion?: number;
32
+ }
33
+ interface CompiledDesign {
34
+ readonly name: string;
35
+ readonly cssText: string;
36
+ }
37
+ /** WCAG sRGB contrast; six-digit opaque hex only, to keep compilation auditable. */
38
+ declare function designContrast(a: string, b: string): number;
39
+ /** Compile an independent brand with light AND dark palettes; never injects CSS.
40
+ * Validation covers body/muted/accent text on both surfaces and button labels.
41
+ * Translucent materials still need visual checks over representative content.
42
+ */
43
+ declare function createDesign(definition: DesignDefinition): CompiledDesign;
44
+ /** Attach trusted, compiled author CSS; safe on the server. CSP sites can write
45
+ * cssText into a static stylesheet instead, or provide the page's nonce. */
46
+ declare function mountDesign(design: CompiledDesign, nonce?: string): () => void;
47
+ /** Finish palette changes without crossfading text through unreadable midtones.
48
+ * Call before changing design/theme attributes; hover feedback resumes next frame.
49
+ */
50
+ declare function prepareDesignChange(): void;
51
+ /** Set a compiled brand theme. null restores the 现代 theme; primitives remain independent. */
52
+ declare function setDesign(name: string | null): void;
53
+
54
+ export { type CompiledDesign, type DesignDefinition, type DesignPalette, createDesign, designContrast, mountDesign, prepareDesignChange, setDesign };
@@ -0,0 +1,109 @@
1
+ // src/behaviors/design.ts
2
+ var NAME = /^[a-z][a-z0-9-]{1,30}$/;
3
+ var HEX = /^#[\da-f]{6}$/i;
4
+ var paletteKeys = ["canvas", "surface", "ink", "muted", "accent", "onAccent", "success", "warning", "error", "info"];
5
+ var seedName = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
6
+ function luminance(hex) {
7
+ const rgb = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
8
+ return rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722;
9
+ }
10
+ function designContrast(a, b) {
11
+ if (!HEX.test(a) || !HEX.test(b)) throw new Error("Design colors must use #rrggbb.");
12
+ const x = luminance(a), y = luminance(b);
13
+ return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
14
+ }
15
+ function compilePalette(palette, mode) {
16
+ const required = ["canvas", "surface", "ink", "muted", "accent", "onAccent"];
17
+ for (const key of required) {
18
+ if (typeof palette[key] !== "string" || !HEX.test(palette[key])) throw new Error(`${mode}.${key} must use #rrggbb.`);
19
+ }
20
+ for (const key of paletteKeys) {
21
+ const color = palette[key];
22
+ if (color !== void 0 && !HEX.test(color)) throw new Error(`${mode}.${key} must use #rrggbb.`);
23
+ }
24
+ for (const bg of ["canvas", "surface"]) {
25
+ for (const fg of ["ink", "muted", "accent"]) {
26
+ if (designContrast(palette[bg], palette[fg]) < 4.5) throw new Error(`${mode}.${fg} on ${bg} must reach 4.5:1 contrast.`);
27
+ }
28
+ }
29
+ if (designContrast(palette.accent, palette.onAccent) < 4.5) throw new Error(`${mode}.onAccent must reach 4.5:1 contrast.`);
30
+ return paletteKeys.filter((key) => palette[key] !== void 0).map((key) => ` --icen-design-${seedName(key)}: ${palette[key]};`).join("\n") + `
31
+ --icen-design-accent-strong: ${palette.accent};
32
+ --icen-design-accent-text: ${palette.accent};
33
+ --icen-design-faint: ${palette.muted};
34
+ --icen-design-raised: color-mix(in srgb, ${palette.surface} 96%, ${palette.ink});`;
35
+ }
36
+ function createDesign(definition) {
37
+ if (!NAME.test(definition.name)) throw new Error("Invalid design name.");
38
+ const knobs = [];
39
+ const ranges = {
40
+ radius: [0, 32, "px"],
41
+ space: [2, 8, "px"],
42
+ controlHeight: [32, 56, "px"],
43
+ fontSize: [12, 24, "px"],
44
+ glassOpacity: [70, 100, "%"],
45
+ glassBlur: [0, 40, "px"],
46
+ motion: [0, 1, ""]
47
+ };
48
+ for (const key of Object.keys(ranges)) {
49
+ const value = definition[key];
50
+ if (value === void 0) continue;
51
+ const [min, max, unit] = ranges[key];
52
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new Error(`${key} must be between ${min} and ${max}.`);
53
+ knobs.push(` --icen-design-${seedName(key)}: ${value}${unit};`);
54
+ }
55
+ if (definition.font !== void 0) {
56
+ if (!definition.font.trim() || /[;{}<>\\\n\r]|\/\*/.test(definition.font)) throw new Error("Invalid font stack.");
57
+ knobs.push(` --icen-design-font: ${definition.font};`);
58
+ }
59
+ const selector = `:root[data-icen-preset="${definition.name}"]`;
60
+ const cssText = `${selector} {
61
+ ${compilePalette(definition.light, "light")}
62
+ ${knobs.join("\n")}
63
+ }
64
+ ${selector}[data-icen-theme="dark"] {
65
+ ${compilePalette(definition.dark, "dark")}
66
+ ${knobs.join("\n")}
67
+ }
68
+ `;
69
+ return Object.freeze({ name: definition.name, cssText });
70
+ }
71
+ function mountDesign(design, nonce) {
72
+ if (typeof document === "undefined") return () => {
73
+ };
74
+ const style = document.createElement("style");
75
+ if (nonce) style.nonce = nonce;
76
+ style.textContent = design.cssText;
77
+ document.head.append(style);
78
+ return () => style.remove();
79
+ }
80
+ var appearanceFrame;
81
+ function prepareDesignChange() {
82
+ if (typeof document === "undefined" || typeof window.requestAnimationFrame !== "function") return;
83
+ const root = document.documentElement;
84
+ root.setAttribute("data-icen-design-changing", "");
85
+ void root.offsetWidth;
86
+ if (appearanceFrame !== void 0) window.cancelAnimationFrame(appearanceFrame);
87
+ appearanceFrame = window.requestAnimationFrame(() => {
88
+ appearanceFrame = window.requestAnimationFrame(() => {
89
+ void root.offsetWidth;
90
+ root.removeAttribute("data-icen-design-changing");
91
+ appearanceFrame = void 0;
92
+ });
93
+ });
94
+ }
95
+ function setDesign(name) {
96
+ if (name !== null && !NAME.test(name)) throw new Error("Invalid design name.");
97
+ if (typeof document === "undefined") return;
98
+ prepareDesignChange();
99
+ document.documentElement.setAttribute("data-icen-preset", name ?? "modern");
100
+ if (!document.documentElement.hasAttribute("data-icen-style")) document.documentElement.setAttribute("data-icen-style", "modern");
101
+ document.documentElement.removeAttribute("data-icen-design");
102
+ }
103
+ export {
104
+ createDesign,
105
+ designContrast,
106
+ mountDesign,
107
+ prepareDesignChange,
108
+ setDesign
109
+ };
@@ -1,8 +1,7 @@
1
- /**
2
- * 绑定全部 [data-dropdown-trigger](document 级委托,重复调用安全)。
3
- * 返回销毁函数:摘除 document 委托并复位幂等标记(销毁后可重新 init);
4
- * 销毁时若有面板开着会顺带收起。SSR 下返回 no-op。
5
- */
1
+ /** Open an authored menu and focus its first/last available action. */
2
+ declare function openDropdown(trigger: HTMLElement, edge?: 'first' | 'last'): void;
3
+ /** Closing is interruptible: reopening/destroying first restores template nodes. */
4
+ declare function closeDropdown(restoreFocus?: boolean): void;
6
5
  declare function initDropdown(root?: ParentNode): () => void;
7
6
 
8
- export { initDropdown };
7
+ export { closeDropdown, initDropdown, openDropdown };
@@ -1,10 +1,32 @@
1
1
  // src/behaviors/popover.ts
2
+ function revealPopoverItem(item, scrollport, opts = {}) {
3
+ if (typeof document === "undefined") return;
4
+ if (opts.focus) item.focus({ preventScroll: true });
5
+ const rect = item.getBoundingClientRect(), bounds = scrollport.getBoundingClientRect();
6
+ const style = getComputedStyle(scrollport);
7
+ const inset = (value, extent) => {
8
+ const amount = Number.parseFloat(value) || 0;
9
+ return value.endsWith("%") ? amount * extent / 100 : amount;
10
+ };
11
+ if (opts.axis !== "inline") {
12
+ const top = bounds.top + scrollport.clientTop + inset(style.scrollPaddingTop, scrollport.clientHeight);
13
+ const bottom = bounds.top + scrollport.clientTop + scrollport.clientHeight - inset(style.scrollPaddingBottom, scrollport.clientHeight);
14
+ if (rect.top < top) scrollport.scrollTop -= top - rect.top;
15
+ else if (rect.bottom > bottom) scrollport.scrollTop += rect.bottom - bottom;
16
+ }
17
+ if (opts.axis === "inline" || opts.axis === "both") {
18
+ const left = bounds.left + scrollport.clientLeft + inset(style.scrollPaddingLeft, scrollport.clientWidth);
19
+ const right = bounds.left + scrollport.clientLeft + scrollport.clientWidth - inset(style.scrollPaddingRight, scrollport.clientWidth);
20
+ if (rect.left < left) scrollport.scrollLeft -= left - rect.left;
21
+ else if (rect.right > right) scrollport.scrollLeft += rect.right - right;
22
+ }
23
+ }
2
24
  var PANEL_SIZING_ATTRS = {
3
- width: "data-panel-width",
4
- minWidth: "data-panel-min",
5
- maxWidth: "data-panel-max",
6
- minHeight: "data-panel-min-height",
7
- maxHeight: "data-panel-max-height"
25
+ width: "data-icen-panel-width",
26
+ minWidth: "data-icen-panel-min",
27
+ maxWidth: "data-icen-panel-max",
28
+ minHeight: "data-icen-panel-min-height",
29
+ maxHeight: "data-icen-panel-max-height"
8
30
  };
9
31
  function readPanelSizing(el) {
10
32
  const out = {};
@@ -65,10 +87,10 @@ function computePopoverLayout(anchor, viewport, opts = {}) {
65
87
  const preferredSide = opts.side ?? "top";
66
88
  const align = opts.align ?? "start";
67
89
  if (preferredSide === "left" || preferredSide === "right") {
90
+ const minWidth2 = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
68
91
  const availableLeft = Math.max(0, anchor.left - margin - offset);
69
92
  const availableRight = Math.max(0, viewport.width - anchor.right - margin - offset);
70
- const side2 = (preferredSide === "left" ? availableLeft : availableRight) >= DEFAULT_MIN_WIDTH ? preferredSide : preferredSide === "left" ? "right" : "left";
71
- const minWidth2 = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
93
+ const side2 = (preferredSide === "left" ? availableLeft : availableRight) >= minWidth2 ? preferredSide : preferredSide === "left" ? "right" : "left";
72
94
  const maxWidth2 = Math.min(
73
95
  Math.max(opts.maxWidth ?? DEFAULT_MAX_WIDTH, minWidth2),
74
96
  side2 === "left" ? availableLeft : availableRight
@@ -97,7 +119,7 @@ function computePopoverLayout(anchor, viewport, opts = {}) {
97
119
  const minHeight = Math.min(Math.max(80, opts.minHeight ?? DEFAULT_MIN_HEIGHT), availableHeight);
98
120
  const requestedMaxHeight = Math.max(minHeight, opts.maxHeight ?? DEFAULT_MAX_HEIGHT);
99
121
  const fallbackHeight = Math.min(260, requestedMaxHeight);
100
- const contentHeight = Math.max(opts.contentHeightHint ?? 0, fallbackHeight);
122
+ const contentHeight = opts.contentHeightHint != null && opts.contentHeightHint > 0 ? opts.contentHeightHint : fallbackHeight;
101
123
  const desiredHeight = clamp(contentHeight, minHeight, Math.min(requestedMaxHeight, availableHeight));
102
124
  const side = chooseSide(preferredSide, availableTop, availableBottom, desiredHeight);
103
125
  const sideSpace = side === "top" ? availableTop : availableBottom;
@@ -113,6 +135,14 @@ function computePopoverLayout(anchor, viewport, opts = {}) {
113
135
  const top = clamp(desiredTop, margin, viewport.height - margin - positionedHeight);
114
136
  return { side, left, top, width, maxHeight };
115
137
  }
138
+ var menuCloser = null;
139
+ function claimMenuFocus(close) {
140
+ menuCloser?.();
141
+ menuCloser = close;
142
+ return () => {
143
+ if (menuCloser === close) menuCloser = null;
144
+ };
145
+ }
116
146
 
117
147
  // src/behaviors/events.ts
118
148
  function emitIcen(source, type, detail) {
@@ -126,154 +156,276 @@ function emitMenuSelect(source, item) {
126
156
  emitIcen(item, "icen:menu-select", {
127
157
  source,
128
158
  item,
129
- value: item.dataset.value ?? "",
159
+ value: item.dataset.icenValue ?? "",
130
160
  /* 优先专用 label 子元素(避开快捷键/图标文本混入),无则退整项文本 */
131
- label: (item.querySelector(".menu-item-label, .command-item-label")?.textContent ?? item.textContent ?? "").trim().slice(0, 80)
161
+ label: (item.querySelector(".icen-menu-item-label, .command-item-label")?.textContent ?? item.textContent ?? "").trim().slice(0, 80)
132
162
  });
133
163
  }
134
164
 
135
165
  // src/behaviors/dropdown.ts
136
- var VIEWPORT_MARGIN = 12;
137
- var SIDE_OFFSET = 4;
138
166
  var active = null;
139
- var initialized = false;
140
- function menuItems(panel) {
141
- return Array.from(panel.querySelectorAll(".menu-item:not(.is-disabled):not(:disabled)"));
142
- }
143
- function setHighlight(next) {
167
+ var sequence = 0;
168
+ var scopes = /* @__PURE__ */ new Map();
169
+ var finishClosing = null;
170
+ var releaseMenu = null;
171
+ var classDisabled = /* @__PURE__ */ new WeakSet();
172
+ var unavailable = (item) => item.matches(".is-disabled,:disabled") || Boolean(item.closest('[aria-disabled="true"],[hidden],[inert],[data-icen-disabled]'));
173
+ var items = (menu) => Array.from(menu.querySelectorAll(".icen-menu-item")).filter((item) => !unavailable(item));
174
+ var within = (scope, node) => scope === node || scope instanceof Node && scope.contains(node);
175
+ function setHighlight(index, focus = true) {
144
176
  if (!active) return;
145
- active.items.forEach((el, i) => el.classList.toggle("is-highlighted", i === next));
146
- active.highlight = next;
177
+ const rows = items(active.panel);
178
+ active.highlight = index;
179
+ active.panel.querySelectorAll(".icen-menu-item").forEach((item) => {
180
+ const selected = item === rows[index];
181
+ item.classList.toggle("is-highlighted", selected);
182
+ item.tabIndex = selected ? 0 : -1;
183
+ });
184
+ if (rows[index]) revealPopoverItem(rows[index], active.panel, { focus });
147
185
  }
148
186
  function place() {
149
187
  if (!active) return;
150
188
  const { trigger, panel } = active;
151
- const rect = trigger.getBoundingClientRect();
152
- const layout = computePopoverLayout(
153
- rect,
154
- { width: window.innerWidth, height: window.innerHeight },
155
- { side: "bottom", offset: SIDE_OFFSET, margin: VIEWPORT_MARGIN }
156
- );
189
+ if (!trigger.isConnected) {
190
+ closeDropdown(false);
191
+ return;
192
+ }
193
+ const sizing = readPanelSizing(trigger.closest("[data-icen-dropdown]"));
194
+ applyPanelSizing(panel, null);
195
+ applyPanelSizing(panel, sizing);
196
+ const width = panel.offsetWidth || panel.getBoundingClientRect().width || 180;
197
+ const height = panel.scrollHeight || panel.offsetHeight;
198
+ const cssMax = parseFloat(getComputedStyle(panel).maxHeight);
199
+ const layout = computePopoverLayout(trigger.getBoundingClientRect(), { width: window.innerWidth, height: window.innerHeight }, {
200
+ side: "bottom",
201
+ align: getComputedStyle(trigger).direction === "rtl" ? "end" : "start",
202
+ offset: 4,
203
+ margin: 12,
204
+ minWidth: width,
205
+ maxWidth: width,
206
+ minHeight: 40,
207
+ maxHeight: sizing.maxHeight ?? (Number.isFinite(cssMax) ? cssMax : 420),
208
+ contentHeightHint: height
209
+ });
157
210
  panel.style.left = `${layout.left}px`;
158
211
  panel.style.top = `${layout.top}px`;
159
- panel.dataset.side = layout.side;
212
+ panel.style.width = `${layout.width}px`;
213
+ panel.style.minWidth = `${layout.width}px`;
214
+ panel.style.maxHeight = `${layout.maxHeight}px`;
215
+ panel.dataset.icenSide = layout.side;
160
216
  }
161
- function openDropdown(trigger, highlightFirst = false) {
162
- closeDropdown();
163
- const root = trigger.closest("[data-dropdown]");
164
- const template = root?.querySelector("[data-dropdown-menu]");
217
+ function openDropdown(trigger, edge = "first") {
218
+ if (typeof document === "undefined" || unavailable(trigger)) return;
219
+ closeDropdown(false);
220
+ finishClosing?.();
221
+ const root = trigger.closest("[data-icen-dropdown]");
222
+ const template = root?.querySelector("[data-icen-dropdown-menu]");
165
223
  if (!root || !template) return;
224
+ releaseMenu = claimMenuFocus(() => closeDropdown(false));
225
+ if (trigger instanceof HTMLButtonElement && !trigger.hasAttribute("type")) trigger.type = "button";
226
+ trigger.setAttribute("aria-haspopup", "menu");
166
227
  const panel = document.createElement("div");
167
- panel.className = "menu";
228
+ panel.className = "icen-menu";
229
+ panel.id = `icen-dropdown-${++sequence}`;
168
230
  panel.setAttribute("role", "menu");
231
+ panel.tabIndex = -1;
169
232
  panel.style.visibility = "hidden";
233
+ trigger.id ||= `icen-dropdown-trigger-${sequence}`;
234
+ panel.setAttribute("aria-labelledby", trigger.id);
235
+ trigger.setAttribute("aria-controls", panel.id);
170
236
  applyPanelSizing(panel, readPanelSizing(root));
171
- while (template.firstChild) panel.appendChild(template.firstChild);
172
- panel.querySelectorAll(".menu-item").forEach((el) => el.setAttribute("role", "menuitem"));
173
- document.body.appendChild(panel);
174
- active = { trigger, template, panel, items: menuItems(panel), highlight: -1 };
237
+ while (template.firstChild) panel.append(template.firstChild);
238
+ panel.querySelectorAll(".icen-menu-item").forEach((item) => {
239
+ if (item instanceof HTMLButtonElement && !item.hasAttribute("type")) item.type = "button";
240
+ if (!item.hasAttribute("role")) item.setAttribute("role", item.hasAttribute("aria-checked") || item.classList.contains("icen-menu-item--checked") ? "menuitemcheckbox" : "menuitem");
241
+ if (item.getAttribute("role") === "menuitemcheckbox" && !item.hasAttribute("aria-checked")) item.setAttribute("aria-checked", String(item.classList.contains("icen-menu-item--checked")));
242
+ if (classDisabled.has(item)) {
243
+ item.removeAttribute("aria-disabled");
244
+ classDisabled.delete(item);
245
+ }
246
+ if (item.classList.contains("is-disabled") && !item.hasAttribute("aria-disabled")) {
247
+ item.setAttribute("aria-disabled", "true");
248
+ classDisabled.add(item);
249
+ }
250
+ item.tabIndex = -1;
251
+ item.querySelector(".icen-menu-item-check")?.remove();
252
+ item.querySelector(".icen-menu-item-shortcut")?.setAttribute("aria-hidden", "true");
253
+ });
254
+ panel.querySelectorAll(".icen-menu-separator").forEach((separator) => separator.setAttribute("role", "separator"));
255
+ document.body.append(panel);
256
+ active = { trigger, template, panel, highlight: -1, typed: "", typedAt: 0 };
175
257
  trigger.setAttribute("aria-expanded", "true");
176
258
  place();
177
259
  panel.style.visibility = "";
178
- if (highlightFirst && active.items.length > 0) setHighlight(0);
260
+ const rows = items(panel);
261
+ if (rows.length) setHighlight(edge === "last" ? rows.length - 1 : 0);
262
+ else panel.focus({ preventScroll: true });
263
+ panel.addEventListener("pointermove", onPointerMove);
264
+ panel.addEventListener("focusin", (event) => {
265
+ const item = event.target instanceof Element ? event.target.closest(".icen-menu-item") : null;
266
+ if (active?.panel === panel && item) setHighlight(items(panel).indexOf(item), false);
267
+ });
268
+ panel.addEventListener("click", (event) => {
269
+ const item = event.target instanceof Element ? event.target.closest(".icen-menu-item") : null;
270
+ if (item && unavailable(item)) {
271
+ event.preventDefault();
272
+ event.stopImmediatePropagation();
273
+ }
274
+ }, true);
275
+ document.addEventListener("click", onClick);
179
276
  document.addEventListener("pointerdown", onOutsidePointerDown, true);
180
277
  document.addEventListener("scroll", onScroll, true);
181
- window.addEventListener("keydown", onKeyDown);
278
+ window.addEventListener("resize", place);
279
+ window.addEventListener("keydown", onKeyDown, true);
182
280
  }
183
- function closeDropdown(restoreFocus = false) {
281
+ function closeDropdown(restoreFocus = true) {
184
282
  if (!active) return;
185
283
  const { trigger, template, panel } = active;
186
284
  active = null;
285
+ releaseMenu?.();
286
+ releaseMenu = null;
187
287
  trigger.setAttribute("aria-expanded", "false");
188
- while (panel.firstChild) template.appendChild(panel.firstChild);
189
- panel.remove();
288
+ trigger.removeAttribute("aria-controls");
289
+ panel.removeEventListener("pointermove", onPointerMove);
190
290
  document.removeEventListener("pointerdown", onOutsidePointerDown, true);
191
291
  document.removeEventListener("scroll", onScroll, true);
192
- window.removeEventListener("keydown", onKeyDown);
193
- if (restoreFocus) trigger.focus();
292
+ window.removeEventListener("resize", place);
293
+ window.removeEventListener("keydown", onKeyDown, true);
294
+ if (!scopes.size) document.removeEventListener("click", onClick);
295
+ if (restoreFocus && trigger.isConnected) trigger.focus({ preventScroll: true });
296
+ panel.inert = true;
297
+ panel.setAttribute("aria-hidden", "true");
298
+ panel.dataset.icenState = "closing";
299
+ let done = false;
300
+ let animation;
301
+ const finish = () => {
302
+ if (done) return;
303
+ done = true;
304
+ animation?.cancel();
305
+ while (panel.firstChild) template.append(panel.firstChild);
306
+ panel.remove();
307
+ if (finishClosing === finish) finishClosing = null;
308
+ };
309
+ finishClosing = finish;
310
+ const css = getComputedStyle(panel), duration = parseFloat(css.animationDuration) * (css.animationDuration.endsWith("ms") ? 1 : 1e3);
311
+ if (typeof panel.animate === "function" && trigger.isConnected && duration !== 0 && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
312
+ animation = panel.animate([{ opacity: css.opacity, transform: css.transform === "none" ? "scale(1)" : css.transform }, { opacity: 0, transform: "scale(.985)" }], { duration: Number.isFinite(duration) ? Math.min(90, duration * 0.75) : 90, easing: css.getPropertyValue("--icen-ease-out").trim() || "ease-out", fill: "forwards" });
313
+ void animation.finished.then(finish, finish);
314
+ } else finish();
194
315
  }
195
- function onOutsidePointerDown(e) {
196
- if (!active) return;
197
- const target = e.target;
198
- if (!(target instanceof Node)) return;
199
- if (active.panel.contains(target) || active.trigger.contains(target)) return;
200
- closeDropdown();
316
+ function onOutsidePointerDown(event) {
317
+ if (active && event.target instanceof Node && !active.panel.contains(event.target) && !active.trigger.contains(event.target)) closeDropdown(false);
201
318
  }
202
- function onScroll(e) {
203
- if (!active) return;
204
- const target = e.target;
205
- if (target instanceof Node && active.panel.contains(target)) return;
206
- closeDropdown();
319
+ function onScroll(event) {
320
+ if (active && !(event.target instanceof Node && active.panel.contains(event.target))) closeDropdown(false);
207
321
  }
208
- function onKeyDown(e) {
209
- if (!active) return;
210
- const { items } = active;
211
- if (e.key === "Escape") {
212
- e.preventDefault();
213
- closeDropdown(true);
322
+ function onPointerMove(event) {
323
+ if (!active || event.pointerType === "touch" || !(event.target instanceof Element)) return;
324
+ const item = event.target.closest(".icen-menu-item");
325
+ if (item && active.panel.contains(item)) setHighlight(items(active.panel).indexOf(item), false);
326
+ }
327
+ function onKeyDown(event) {
328
+ if (!active || event.isComposing || event.keyCode === 229) return;
329
+ if (event.key === "Escape") {
330
+ event.preventDefault();
331
+ event.stopPropagation();
332
+ closeDropdown();
214
333
  return;
215
334
  }
216
- if (items.length === 0) return;
217
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
218
- e.preventDefault();
219
- const offset = e.key === "ArrowDown" ? 1 : -1;
220
- const next = active.highlight < 0 ? offset > 0 ? 0 : items.length - 1 : (active.highlight + offset + items.length) % items.length;
335
+ if (event.key === "Tab") {
336
+ closeDropdown();
337
+ return;
338
+ }
339
+ if (event.target instanceof Element && event.target.matches('input,textarea,[contenteditable="true"]')) return;
340
+ const rows = items(active.panel);
341
+ if (!rows.length) return;
342
+ if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
343
+ event.preventDefault();
344
+ const next = event.key === "Home" ? 0 : event.key === "End" ? rows.length - 1 : (active.highlight + (event.key === "ArrowDown" ? 1 : -1) + rows.length) % rows.length;
221
345
  setHighlight(next);
222
346
  return;
223
347
  }
224
- if (e.key === "Enter" && active.highlight >= 0) {
225
- e.preventDefault();
226
- const item = items[active.highlight];
227
- emitMenuSelect(active.panel, item);
228
- closeDropdown(true);
229
- item.click();
348
+ if (event.key === "Enter" || event.key === " ") {
349
+ event.preventDefault();
350
+ rows[active.highlight]?.click();
351
+ return;
352
+ }
353
+ if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return;
354
+ event.preventDefault();
355
+ const now = Date.now();
356
+ active.typed = now - active.typedAt < 600 ? active.typed + event.key : event.key;
357
+ active.typedAt = now;
358
+ let query = active.typed.toLocaleLowerCase();
359
+ if ([...query].every((character) => character === query[0])) query = active.typed = query[0];
360
+ for (let step = 1; step <= rows.length; step++) {
361
+ const index = (Math.max(0, active.highlight) + (query.length === 1 ? step : step - 1)) % rows.length;
362
+ const label = rows[index].querySelector(".icen-menu-item-label")?.textContent ?? rows[index].textContent ?? "";
363
+ if (label.trim().toLocaleLowerCase().startsWith(query)) {
364
+ setHighlight(index);
365
+ break;
366
+ }
367
+ }
368
+ }
369
+ function onClick(event) {
370
+ if (!(event.target instanceof Element)) return;
371
+ const item = event.target.closest(".icen-menu-item");
372
+ if (active && item && active.panel.contains(item)) {
373
+ if (unavailable(item)) {
374
+ event.preventDefault();
375
+ event.stopPropagation();
376
+ return;
377
+ }
378
+ const current = active;
379
+ emitMenuSelect(current.panel, item);
380
+ if (active === current && !item.hasAttribute("data-icen-menu-keep-open")) closeDropdown(current.panel.contains(document.activeElement));
381
+ return;
230
382
  }
383
+ const trigger = event.target.closest("[data-icen-dropdown-trigger]");
384
+ if (!trigger || ![...scopes.keys()].some((scope) => within(scope, trigger))) return;
385
+ event.preventDefault();
386
+ if (active?.trigger === trigger) closeDropdown();
387
+ else openDropdown(trigger);
388
+ }
389
+ function onTriggerKey(event) {
390
+ if (active || event.isComposing || !["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) return;
391
+ const trigger = event.target instanceof Element ? event.target.closest("[data-icen-dropdown-trigger]") : null;
392
+ if (!trigger || ![...scopes.keys()].some((scope) => within(scope, trigger))) return;
393
+ event.preventDefault();
394
+ openDropdown(trigger, event.key === "ArrowUp" ? "last" : "first");
395
+ event.stopPropagation();
231
396
  }
232
397
  function initDropdown(root) {
233
398
  if (typeof document === "undefined") return () => {
234
399
  };
235
- const scope = root ?? document;
236
- scope.querySelectorAll("[data-dropdown-trigger]").forEach((el) => el.setAttribute("aria-expanded", "false"));
237
- if (initialized) return () => {
238
- };
239
- initialized = true;
240
- const onClick = (e) => {
241
- const target = e.target instanceof Element ? e.target : null;
242
- if (!target) return;
243
- if (active && target.closest(".menu-item") && active.panel.contains(target)) {
244
- const item = target.closest(".menu-item");
245
- if (item) {
246
- if (!item.matches(".is-disabled") && !item.matches(":disabled")) {
247
- emitMenuSelect(active.panel, item);
248
- }
249
- closeDropdown();
250
- return;
251
- }
400
+ const scope = root ?? document, existing = scopes.get(scope);
401
+ if (existing) return existing;
402
+ const triggers = [...scope instanceof HTMLElement && scope.matches("[data-icen-dropdown-trigger]") ? [scope] : [], ...scope.querySelectorAll("[data-icen-dropdown-trigger]")];
403
+ triggers.forEach((trigger) => {
404
+ if (active?.trigger !== trigger) trigger.setAttribute("aria-expanded", "false");
405
+ trigger.setAttribute("aria-haspopup", "menu");
406
+ if (trigger instanceof HTMLButtonElement && !trigger.hasAttribute("type")) trigger.type = "button";
407
+ });
408
+ if (!scopes.size) {
409
+ document.addEventListener("click", onClick);
410
+ document.addEventListener("keydown", onTriggerKey);
411
+ }
412
+ let disposed = false;
413
+ const dispose = () => {
414
+ if (disposed) return;
415
+ disposed = true;
416
+ scopes.delete(scope);
417
+ if (active && within(scope, active.trigger)) closeDropdown(false);
418
+ finishClosing?.();
419
+ if (!scopes.size) {
420
+ if (!active) document.removeEventListener("click", onClick);
421
+ document.removeEventListener("keydown", onTriggerKey);
252
422
  }
253
- const trigger = target.closest("[data-dropdown-trigger]");
254
- if (!trigger) return;
255
- if (active && active.trigger === trigger) closeDropdown();
256
- else openDropdown(trigger);
257
- };
258
- document.addEventListener("click", onClick);
259
- const onKeyDown2 = (e) => {
260
- if (e.key !== "ArrowDown") return;
261
- if (active) return;
262
- const target = e.target instanceof Element ? e.target : null;
263
- const trigger = target?.closest("[data-dropdown-trigger]");
264
- if (!trigger) return;
265
- e.preventDefault();
266
- openDropdown(trigger, true);
267
- };
268
- document.addEventListener("keydown", onKeyDown2);
269
- const destroy = () => {
270
- document.removeEventListener("click", onClick);
271
- document.removeEventListener("keydown", onKeyDown2);
272
- closeDropdown();
273
- initialized = false;
274
423
  };
275
- return destroy;
424
+ scopes.set(scope, dispose);
425
+ return dispose;
276
426
  }
277
427
  export {
278
- initDropdown
428
+ closeDropdown,
429
+ initDropdown,
430
+ openDropdown
279
431
  };