@icen.ai/ui 0.9.0 → 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 (281) hide show
  1. package/AGENTS.md +138 -80
  2. package/README.md +66 -91
  3. package/dist/base.css +208 -179
  4. package/dist/behaviors/accordion.mjs +8 -8
  5. package/dist/behaviors/ai-branch.d.mts +1 -1
  6. package/dist/behaviors/ai-branch.mjs +1010 -94
  7. package/dist/behaviors/ai-chat.d.mts +7 -7
  8. package/dist/behaviors/ai-chat.mjs +1071 -161
  9. package/dist/behaviors/ai-composer.d.mts +44 -12
  10. package/dist/behaviors/ai-composer.mjs +1997 -524
  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 +1 -1
  15. package/dist/behaviors/ai-feedback.mjs +1035 -126
  16. package/dist/behaviors/ai-panel.d.mts +8 -8
  17. package/dist/behaviors/ai-panel.mjs +1284 -266
  18. package/dist/behaviors/ai-provider.mjs +1003 -101
  19. package/dist/behaviors/ai-threads.d.mts +1 -1
  20. package/dist/behaviors/ai-threads.mjs +1068 -154
  21. package/dist/behaviors/ai-tool.d.mts +2 -2
  22. package/dist/behaviors/ai-tool.mjs +2037 -396
  23. package/dist/behaviors/ai-tools.d.mts +89 -16
  24. package/dist/behaviors/ai-tools.mjs +4448 -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 +72 -8
  34. package/dist/behaviors/charts.mjs +1901 -256
  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 +120 -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 +1 -1
  68. package/dist/behaviors/kb-access.mjs +2129 -239
  69. package/dist/behaviors/kb-acl.d.mts +6 -6
  70. package/dist/behaviors/kb-acl.mjs +1119 -179
  71. package/dist/behaviors/kb-answer.mjs +2070 -418
  72. package/dist/behaviors/kb-audit.d.mts +1 -1
  73. package/dist/behaviors/kb-audit.mjs +1076 -154
  74. package/dist/behaviors/kb-canvas.mjs +1100 -177
  75. package/dist/behaviors/kb-chain.mjs +1022 -100
  76. package/dist/behaviors/kb-checkpoint.d.mts +1 -1
  77. package/dist/behaviors/kb-checkpoint.mjs +1319 -169
  78. package/dist/behaviors/kb-chunks.mjs +1152 -213
  79. package/dist/behaviors/kb-citation.d.mts +1 -1
  80. package/dist/behaviors/kb-citation.mjs +1201 -179
  81. package/dist/behaviors/kb-clarify.mjs +1058 -130
  82. package/dist/behaviors/kb-conflict.mjs +1041 -119
  83. package/dist/behaviors/kb-connector.mjs +1104 -176
  84. package/dist/behaviors/kb-core.d.mts +6 -1
  85. package/dist/behaviors/kb-core.mjs +1077 -162
  86. package/dist/behaviors/kb-eval.mjs +1971 -144
  87. package/dist/behaviors/kb-explain.d.mts +1 -1
  88. package/dist/behaviors/kb-explain.mjs +1024 -102
  89. package/dist/behaviors/kb-filter.mjs +2008 -178
  90. package/dist/behaviors/kb-gap.d.mts +1 -1
  91. package/dist/behaviors/kb-gap.mjs +1043 -121
  92. package/dist/behaviors/kb-hittest.d.mts +1 -1
  93. package/dist/behaviors/kb-hittest.mjs +1093 -150
  94. package/dist/behaviors/kb-metadata.mjs +1052 -130
  95. package/dist/behaviors/kb-passage.d.mts +2 -2
  96. package/dist/behaviors/kb-passage.mjs +1101 -161
  97. package/dist/behaviors/kb-pipeline.d.mts +2 -2
  98. package/dist/behaviors/kb-pipeline.mjs +1082 -162
  99. package/dist/behaviors/kb-qa.mjs +1088 -160
  100. package/dist/behaviors/kb-rerank.mjs +1047 -114
  101. package/dist/behaviors/kb-retrieval.mjs +2063 -224
  102. package/dist/behaviors/kb-review.mjs +1990 -159
  103. package/dist/behaviors/kb-sandbox.d.mts +1 -1
  104. package/dist/behaviors/kb-sandbox.mjs +1026 -104
  105. package/dist/behaviors/kb-segment.mjs +1111 -179
  106. package/dist/behaviors/kb-select.d.mts +24 -0
  107. package/dist/behaviors/kb-select.mjs +1989 -0
  108. package/dist/behaviors/kb-sources.d.mts +1 -1
  109. package/dist/behaviors/kb-sources.mjs +1084 -170
  110. package/dist/behaviors/kb-sql.d.mts +1 -1
  111. package/dist/behaviors/kb-sql.mjs +1143 -226
  112. package/dist/behaviors/kb-trace.d.mts +1 -1
  113. package/dist/behaviors/kb-trace.mjs +1042 -128
  114. package/dist/behaviors/kb-visibility.mjs +1086 -158
  115. package/dist/behaviors/kb-who-can.mjs +1974 -137
  116. package/dist/behaviors/mentions.d.mts +93 -0
  117. package/dist/behaviors/mentions.mjs +2152 -0
  118. package/dist/behaviors/modal.d.mts +4 -3
  119. package/dist/behaviors/modal.mjs +1055 -33
  120. package/dist/behaviors/nav.d.mts +1 -1
  121. package/dist/behaviors/nav.mjs +6 -6
  122. package/dist/behaviors/notification.d.mts +16 -8
  123. package/dist/behaviors/notification.mjs +1615 -265
  124. package/dist/behaviors/panel.d.mts +4 -0
  125. package/dist/behaviors/panel.mjs +994 -0
  126. package/dist/behaviors/popover.d.mts +28 -5
  127. package/dist/behaviors/popover.mjs +225 -24
  128. package/dist/behaviors/select.d.mts +5 -2
  129. package/dist/behaviors/select.mjs +1410 -117
  130. package/dist/behaviors/sidebar.d.mts +1 -1
  131. package/dist/behaviors/sidebar.mjs +13 -13
  132. package/dist/behaviors/slider.d.mts +1 -1
  133. package/dist/behaviors/slider.mjs +9 -9
  134. package/dist/behaviors/split-pane.d.mts +1 -1
  135. package/dist/behaviors/split-pane.mjs +7 -7
  136. package/dist/behaviors/suggestions.d.mts +27 -0
  137. package/dist/behaviors/suggestions.mjs +1089 -0
  138. package/dist/behaviors/svg-parse.d.mts +14 -0
  139. package/dist/behaviors/svg-parse.mjs +37 -0
  140. package/dist/behaviors/tabs.d.mts +2 -2
  141. package/dist/behaviors/tabs.mjs +25 -25
  142. package/dist/behaviors/tag-input.d.mts +23 -5
  143. package/dist/behaviors/tag-input.mjs +1359 -104
  144. package/dist/behaviors/theme-switcher.d.mts +24 -0
  145. package/dist/behaviors/theme-switcher.mjs +1981 -0
  146. package/dist/behaviors/theme.d.mts +15 -8
  147. package/dist/behaviors/theme.mjs +390 -28
  148. package/dist/behaviors/toast.d.mts +24 -25
  149. package/dist/behaviors/toast.mjs +1266 -88
  150. package/dist/behaviors/tooltip.d.mts +4 -0
  151. package/dist/behaviors/tooltip.mjs +378 -0
  152. package/dist/behaviors/tree-select.d.mts +69 -0
  153. package/dist/behaviors/tree-select.mjs +1907 -0
  154. package/dist/behaviors/tree.d.mts +1 -1
  155. package/dist/behaviors/tree.mjs +15 -15
  156. package/dist/behaviors/upload.d.mts +3 -2
  157. package/dist/behaviors/upload.mjs +1076 -66
  158. package/dist/cli.mjs +5 -6
  159. package/dist/components/ai-chat.css +666 -573
  160. package/dist/components/ai-diff.css +153 -132
  161. package/dist/components/ai-panel.css +351 -343
  162. package/dist/components/ai-tool.css +393 -238
  163. package/dist/components/autocomplete.css +61 -0
  164. package/dist/components/autocomplete.mjs +3 -0
  165. package/dist/components/back-top.css +34 -31
  166. package/dist/components/badge.css +44 -41
  167. package/dist/components/breadcrumb.css +67 -46
  168. package/dist/components/btn.css +80 -74
  169. package/dist/components/card.css +117 -106
  170. package/dist/components/cascader.css +311 -0
  171. package/dist/components/cascader.mjs +3 -0
  172. package/dist/components/chart-area.css +8 -2
  173. package/dist/components/chart-bar.css +95 -33
  174. package/dist/components/chart-gauge.css +14 -11
  175. package/dist/components/chart-graph.css +20 -17
  176. package/dist/components/chart-heatmap.css +96 -90
  177. package/dist/components/chart-line.css +8 -3
  178. package/dist/components/chart-pie.css +9 -6
  179. package/dist/components/chart-radar.css +8 -5
  180. package/dist/components/chart-scatter.css +9 -6
  181. package/dist/components/chart-sparkline.css +5 -2
  182. package/dist/components/chart-stack.css +14 -11
  183. package/dist/components/charts.css +164 -73
  184. package/dist/components/code-block.css +418 -0
  185. package/dist/components/code-block.mjs +4 -0
  186. package/dist/components/command-palette.css +138 -117
  187. package/dist/components/content.css +257 -251
  188. package/dist/components/copy.css +72 -44
  189. package/dist/components/datatable.css +423 -181
  190. package/dist/components/date-picker.css +135 -104
  191. package/dist/components/empty.css +68 -65
  192. package/dist/components/feedback.css +201 -183
  193. package/dist/components/form-validate.css +120 -0
  194. package/dist/components/form-validate.mjs +3 -0
  195. package/dist/components/form.css +150 -127
  196. package/dist/components/icon.css +102 -0
  197. package/dist/components/icon.mjs +3 -0
  198. package/dist/components/input.css +159 -104
  199. package/dist/components/kb-agent.css +188 -173
  200. package/dist/components/kb-data.css +257 -253
  201. package/dist/components/kb-ground.css +197 -185
  202. package/dist/components/kb-ingest.css +334 -350
  203. package/dist/components/kb-ops.css +265 -259
  204. package/dist/components/kb-perm.css +260 -271
  205. package/dist/components/kb-search.css +317 -360
  206. package/dist/components/kb.css +120 -109
  207. package/dist/components/layout.css +85 -82
  208. package/dist/components/material-chinese.css +73 -0
  209. package/dist/components/material-soviet.css +76 -0
  210. package/dist/components/material.css +275 -0
  211. package/dist/components/media.css +220 -166
  212. package/dist/components/mentions.css +189 -0
  213. package/dist/components/mentions.mjs +3 -0
  214. package/dist/components/menu.css +113 -105
  215. package/dist/components/modal.css +107 -112
  216. package/dist/components/nav.css +118 -115
  217. package/dist/components/notification.css +238 -174
  218. package/dist/components/pagination.css +56 -53
  219. package/dist/components/panel.css +66 -63
  220. package/dist/components/panel.mjs +1 -0
  221. package/dist/components/pill.css +81 -71
  222. package/dist/components/popover.css +67 -53
  223. package/dist/components/scroll-area.css +113 -110
  224. package/dist/components/segmented.css +146 -143
  225. package/dist/components/select.css +206 -186
  226. package/dist/components/sidebar.css +137 -127
  227. package/dist/components/slider.css +117 -114
  228. package/dist/components/split-pane.css +112 -109
  229. package/dist/components/stat.css +112 -106
  230. package/dist/components/steps.css +109 -103
  231. package/dist/components/switch.css +138 -134
  232. package/dist/components/table.css +131 -107
  233. package/dist/components/tabs.css +114 -108
  234. package/dist/components/tag-input.css +48 -133
  235. package/dist/components/tag.css +81 -71
  236. package/dist/components/theme-switcher.css +105 -0
  237. package/dist/components/theme-switcher.mjs +3 -0
  238. package/dist/components/toast.css +100 -86
  239. package/dist/components/toolbar.css +45 -42
  240. package/dist/components/tooltip.css +102 -43
  241. package/dist/components/tooltip.mjs +1 -0
  242. package/dist/components/tree-select.css +416 -0
  243. package/dist/components/tree-select.mjs +3 -0
  244. package/dist/components/upload.css +135 -92
  245. package/dist/fonts/LICENSES/ark-pixel/OFL.txt +96 -0
  246. package/dist/fonts/LICENSES/cubic-11/OFL.txt +108 -0
  247. package/dist/fonts/LICENSES/galmuri/LICENSE.txt +93 -0
  248. package/dist/fonts/OFL.txt +96 -0
  249. package/dist/fonts/README.md +14 -0
  250. package/dist/fonts/fusion-pixel-12px-zh-hans.woff2 +0 -0
  251. package/dist/index.d.mts +28 -14
  252. package/dist/index.mjs +20705 -9601
  253. package/dist/react.d.mts +91 -0
  254. package/dist/react.mjs +8208 -0
  255. package/dist/registry.json +130 -13
  256. package/dist/tokens/chinese.css +44 -0
  257. package/dist/tokens/colors.css +365 -1222
  258. package/dist/tokens/design.css +109 -0
  259. package/dist/tokens/dtcg.json +11198 -0
  260. package/dist/tokens/figma.variables.json +7017 -0
  261. package/dist/tokens/icon-masks.css +78 -0
  262. package/dist/tokens/pixel.css +50 -0
  263. package/dist/tokens/soviet.css +44 -0
  264. package/dist/tokens/style-profiles.css +36 -547
  265. package/dist/tokens/themes.json +308 -0
  266. package/dist/tokens/tokens-studio.json +6728 -0
  267. package/dist/tokens/typography.css +21 -18
  268. package/dist/tokens.css +1555 -1801
  269. package/dist/ui.css +11975 -9831
  270. package/dist/vue.d.mts +344 -0
  271. package/dist/vue.mjs +7781 -0
  272. package/docs/spec/ai-input-experience.md +75 -0
  273. package/docs/spec/ai-native.md +238 -147
  274. package/docs/spec/design-philosophy.md +189 -0
  275. package/docs/spec/design-primitives.md +196 -0
  276. package/docs/spec/kb-family.md +147 -115
  277. package/package.json +43 -8
  278. package/dist/retro-effects.css +0 -255
  279. package/dist/tokens/retro-effects.css +0 -255
  280. package/docs/research/2026-10-03-ai-kb-components.md +0 -392
  281. package/docs/research/2026-10-03-kb-permissions.md +0 -992
@@ -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
  };