@icen.ai/ui 0.10.0 → 0.11.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 (355) hide show
  1. package/AGENTS.md +79 -309
  2. package/LICENSE +21 -0
  3. package/README.md +43 -54
  4. package/dist/base.css +57 -16
  5. package/dist/behaviors/accordion.mjs +1 -1
  6. package/dist/behaviors/ai-branch.mjs +1 -0
  7. package/dist/behaviors/ai-chat.d.mts +2 -2
  8. package/dist/behaviors/ai-chat.mjs +53 -10
  9. package/dist/behaviors/ai-composer.d.mts +5 -5
  10. package/dist/behaviors/ai-composer.mjs +21 -10
  11. package/dist/behaviors/ai-core.mjs +1 -0
  12. package/dist/behaviors/ai-diff.mjs +1 -0
  13. package/dist/behaviors/ai-feedback.mjs +1 -0
  14. package/dist/behaviors/ai-panel.d.mts +2 -2
  15. package/dist/behaviors/ai-panel.mjs +1 -0
  16. package/dist/behaviors/ai-provider.d.mts +1 -1
  17. package/dist/behaviors/ai-provider.mjs +1 -0
  18. package/dist/behaviors/ai-threads.mjs +1 -0
  19. package/dist/behaviors/ai-tool.d.mts +1 -1
  20. package/dist/behaviors/ai-tool.mjs +57 -24
  21. package/dist/behaviors/ai-tools.mjs +937 -28
  22. package/dist/behaviors/autocomplete.d.mts +1 -1
  23. package/dist/behaviors/autocomplete.mjs +1 -0
  24. package/dist/behaviors/back-top.mjs +1 -0
  25. package/dist/behaviors/carousel.mjs +8 -1
  26. package/dist/behaviors/cascader.mjs +14 -0
  27. package/dist/behaviors/charts.mjs +56 -20
  28. package/dist/behaviors/code-block.mjs +9 -1
  29. package/dist/behaviors/command-palette.mjs +3 -3
  30. package/dist/behaviors/context-menu.d.mts +2 -2
  31. package/dist/behaviors/context-menu.mjs +1 -0
  32. package/dist/behaviors/controls.d.mts +8 -8
  33. package/dist/behaviors/controls.mjs +37 -6
  34. package/dist/behaviors/copy.mjs +1 -0
  35. package/dist/behaviors/datatable.d.mts +3 -3
  36. package/dist/behaviors/datatable.mjs +76 -11
  37. package/dist/behaviors/date-picker.mjs +1 -0
  38. package/dist/behaviors/events.d.mts +10 -10
  39. package/dist/behaviors/events.mjs +11 -1
  40. package/dist/behaviors/form-validate.d.mts +10 -1
  41. package/dist/behaviors/form-validate.mjs +266 -4611
  42. package/dist/behaviors/i18n.d.mts +1 -0
  43. package/dist/behaviors/i18n.mjs +2 -0
  44. package/dist/behaviors/input.mjs +3 -0
  45. package/dist/behaviors/kb-access.d.mts +2 -2
  46. package/dist/behaviors/kb-access.mjs +8 -2
  47. package/dist/behaviors/kb-acl.d.mts +2 -2
  48. package/dist/behaviors/kb-acl.mjs +1 -0
  49. package/dist/behaviors/kb-answer.d.mts +2 -2
  50. package/dist/behaviors/kb-answer.mjs +56 -23
  51. package/dist/behaviors/kb-audit.d.mts +2 -2
  52. package/dist/behaviors/kb-audit.mjs +1 -0
  53. package/dist/behaviors/kb-canvas.d.mts +3 -3
  54. package/dist/behaviors/kb-canvas.mjs +1 -0
  55. package/dist/behaviors/kb-chain.d.mts +2 -2
  56. package/dist/behaviors/kb-chain.mjs +1 -0
  57. package/dist/behaviors/kb-checkpoint.d.mts +2 -2
  58. package/dist/behaviors/kb-checkpoint.mjs +1 -0
  59. package/dist/behaviors/kb-chunks.d.mts +2 -2
  60. package/dist/behaviors/kb-chunks.mjs +1 -0
  61. package/dist/behaviors/kb-citation.d.mts +2 -2
  62. package/dist/behaviors/kb-citation.mjs +20 -6
  63. package/dist/behaviors/kb-clarify.d.mts +2 -2
  64. package/dist/behaviors/kb-clarify.mjs +1 -0
  65. package/dist/behaviors/kb-conflict.d.mts +2 -2
  66. package/dist/behaviors/kb-conflict.mjs +1 -0
  67. package/dist/behaviors/kb-connector.d.mts +2 -2
  68. package/dist/behaviors/kb-connector.mjs +1 -0
  69. package/dist/behaviors/kb-core.d.mts +1 -1
  70. package/dist/behaviors/kb-core.mjs +1 -0
  71. package/dist/behaviors/kb-eval.d.mts +2 -2
  72. package/dist/behaviors/kb-eval.mjs +8 -2
  73. package/dist/behaviors/kb-explain.d.mts +2 -2
  74. package/dist/behaviors/kb-explain.mjs +13 -2
  75. package/dist/behaviors/kb-filter.d.mts +2 -2
  76. package/dist/behaviors/kb-filter.mjs +8 -2
  77. package/dist/behaviors/kb-gap.d.mts +2 -2
  78. package/dist/behaviors/kb-gap.mjs +1 -0
  79. package/dist/behaviors/kb-hittest.d.mts +2 -2
  80. package/dist/behaviors/kb-hittest.mjs +2 -1
  81. package/dist/behaviors/kb-metadata.mjs +6 -0
  82. package/dist/behaviors/kb-passage.d.mts +2 -2
  83. package/dist/behaviors/kb-passage.mjs +1 -0
  84. package/dist/behaviors/kb-pipeline.d.mts +2 -2
  85. package/dist/behaviors/kb-pipeline.mjs +1 -0
  86. package/dist/behaviors/kb-qa.mjs +1 -0
  87. package/dist/behaviors/kb-rerank.d.mts +2 -2
  88. package/dist/behaviors/kb-rerank.mjs +1 -0
  89. package/dist/behaviors/kb-retrieval.d.mts +2 -2
  90. package/dist/behaviors/kb-retrieval.mjs +18 -4
  91. package/dist/behaviors/kb-review.d.mts +2 -2
  92. package/dist/behaviors/kb-review.mjs +8 -2
  93. package/dist/behaviors/kb-sandbox.d.mts +2 -2
  94. package/dist/behaviors/kb-sandbox.mjs +7 -1
  95. package/dist/behaviors/kb-segment.d.mts +2 -2
  96. package/dist/behaviors/kb-segment.mjs +2 -1
  97. package/dist/behaviors/kb-select.mjs +8 -2
  98. package/dist/behaviors/kb-sources.d.mts +2 -2
  99. package/dist/behaviors/kb-sources.mjs +1 -0
  100. package/dist/behaviors/kb-sql.d.mts +2 -2
  101. package/dist/behaviors/kb-sql.mjs +1 -0
  102. package/dist/behaviors/kb-trace.d.mts +2 -2
  103. package/dist/behaviors/kb-trace.mjs +9 -3
  104. package/dist/behaviors/kb-visibility.d.mts +2 -2
  105. package/dist/behaviors/kb-visibility.mjs +1 -0
  106. package/dist/behaviors/kb-who-can.d.mts +2 -2
  107. package/dist/behaviors/kb-who-can.mjs +8 -2
  108. package/dist/behaviors/mentions.d.mts +1 -1
  109. package/dist/behaviors/mentions.mjs +1 -0
  110. package/dist/behaviors/modal.d.mts +1 -1
  111. package/dist/behaviors/modal.mjs +1 -0
  112. package/dist/behaviors/notification.d.mts +2 -2
  113. package/dist/behaviors/notification.mjs +14 -3
  114. package/dist/behaviors/panel.mjs +1 -0
  115. package/dist/behaviors/scroll-area.d.mts +17 -0
  116. package/dist/behaviors/scroll-area.mjs +59 -0
  117. package/dist/behaviors/select.mjs +8 -2
  118. package/dist/behaviors/slider.mjs +3 -3
  119. package/dist/behaviors/split-pane.mjs +2 -0
  120. package/dist/behaviors/suggestions.mjs +1 -0
  121. package/dist/behaviors/tabs.mjs +10 -0
  122. package/dist/behaviors/tag-input.mjs +1 -0
  123. package/dist/behaviors/theme-switcher.d.mts +2 -2
  124. package/dist/behaviors/theme-switcher.mjs +17 -2
  125. package/dist/behaviors/theme.mjs +16 -2
  126. package/dist/behaviors/toast.d.mts +2 -2
  127. package/dist/behaviors/toast.mjs +1 -0
  128. package/dist/behaviors/tree-select.mjs +14 -0
  129. package/dist/behaviors/tree.mjs +4 -4
  130. package/dist/behaviors/upload.mjs +1 -0
  131. package/dist/behaviors/url-safety.d.mts +8 -0
  132. package/dist/behaviors/url-safety.mjs +11 -0
  133. package/dist/components/accordion.d.mts +2 -0
  134. package/dist/components/ai-branch.d.mts +2 -0
  135. package/dist/components/ai-chat.css +40 -33
  136. package/dist/components/ai-chat.d.mts +2 -0
  137. package/dist/components/ai-composer.d.mts +2 -0
  138. package/dist/components/ai-context.d.mts +2 -0
  139. package/dist/components/ai-context.mjs +1 -0
  140. package/dist/components/ai-diff.css +6 -6
  141. package/dist/components/ai-diff.d.mts +2 -0
  142. package/dist/components/ai-feedback.d.mts +2 -0
  143. package/dist/components/ai-files.d.mts +2 -0
  144. package/dist/components/ai-message.d.mts +2 -0
  145. package/dist/components/ai-panel.css +12 -10
  146. package/dist/components/ai-reasoning.d.mts +2 -0
  147. package/dist/components/ai-subagent.d.mts +2 -0
  148. package/dist/components/ai-subagent.mjs +1 -0
  149. package/dist/components/ai-threads.d.mts +2 -0
  150. package/dist/components/ai-todo.d.mts +2 -0
  151. package/dist/components/ai-todo.mjs +1 -0
  152. package/dist/components/ai-tool-call.d.mts +2 -0
  153. package/dist/components/ai-tool-call.mjs +1 -0
  154. package/dist/components/ai-tool.css +16 -16
  155. package/dist/components/ai-usage.d.mts +2 -0
  156. package/dist/components/alert.d.mts +2 -0
  157. package/dist/components/autocomplete.css +12 -12
  158. package/dist/components/autocomplete.d.mts +2 -0
  159. package/dist/components/avatar.d.mts +2 -0
  160. package/dist/components/back-top.css +47 -16
  161. package/dist/components/back-top.d.mts +2 -0
  162. package/dist/components/badge.css +1 -1
  163. package/dist/components/badge.d.mts +2 -0
  164. package/dist/components/breadcrumb.css +26 -8
  165. package/dist/components/breadcrumb.d.mts +2 -0
  166. package/dist/components/btn.css +67 -62
  167. package/dist/components/btn.d.mts +2 -0
  168. package/dist/components/card.css +22 -22
  169. package/dist/components/card.d.mts +2 -0
  170. package/dist/components/carousel.d.mts +2 -0
  171. package/dist/components/cascader.css +56 -35
  172. package/dist/components/cascader.d.mts +2 -0
  173. package/dist/components/chart-area.css +46 -0
  174. package/dist/components/chart-area.d.mts +2 -0
  175. package/dist/components/chart-bar.css +66 -19
  176. package/dist/components/chart-bar.d.mts +2 -0
  177. package/dist/components/chart-calendar.d.mts +2 -0
  178. package/dist/components/chart-gauge.css +23 -3
  179. package/dist/components/chart-gauge.d.mts +2 -0
  180. package/dist/components/chart-graph.css +15 -3
  181. package/dist/components/chart-graph.d.mts +2 -0
  182. package/dist/components/chart-heatmap.css +117 -95
  183. package/dist/components/chart-heatmap.d.mts +2 -0
  184. package/dist/components/chart-line.css +54 -0
  185. package/dist/components/chart-line.d.mts +2 -0
  186. package/dist/components/chart-map.d.mts +2 -0
  187. package/dist/components/chart-pie.css +29 -0
  188. package/dist/components/chart-pie.d.mts +2 -0
  189. package/dist/components/chart-radar.css +50 -1
  190. package/dist/components/chart-radar.d.mts +2 -0
  191. package/dist/components/chart-scatter.css +39 -2
  192. package/dist/components/chart-scatter.d.mts +2 -0
  193. package/dist/components/chart-sparkline.css +20 -0
  194. package/dist/components/chart-sparkline.d.mts +2 -0
  195. package/dist/components/chart-stack.css +16 -5
  196. package/dist/components/chart-stack.d.mts +2 -0
  197. package/dist/components/charts.css +80 -35
  198. package/dist/components/charts.d.mts +2 -0
  199. package/dist/components/code-block.css +10 -8
  200. package/dist/components/code-block.d.mts +2 -0
  201. package/dist/components/command-palette.css +45 -38
  202. package/dist/components/command-palette.d.mts +2 -0
  203. package/dist/components/content.css +62 -57
  204. package/dist/components/context-menu.d.mts +2 -0
  205. package/dist/components/copy.css +10 -10
  206. package/dist/components/copy.d.mts +2 -0
  207. package/dist/components/datatable.css +72 -59
  208. package/dist/components/datatable.d.mts +2 -0
  209. package/dist/components/date-picker.css +26 -26
  210. package/dist/components/date-picker.d.mts +2 -0
  211. package/dist/components/desc.d.mts +2 -0
  212. package/dist/components/dropdown.d.mts +2 -0
  213. package/dist/components/empty.css +16 -16
  214. package/dist/components/empty.d.mts +2 -0
  215. package/dist/components/feedback.css +31 -29
  216. package/dist/components/form-validate.css +20 -6
  217. package/dist/components/form-validate.d.mts +2 -0
  218. package/dist/components/form.css +17 -14
  219. package/dist/components/form.d.mts +2 -0
  220. package/dist/components/icon.css +21 -10
  221. package/dist/components/icon.d.mts +2 -0
  222. package/dist/components/input.css +22 -22
  223. package/dist/components/input.d.mts +2 -0
  224. package/dist/components/kb-access.d.mts +2 -0
  225. package/dist/components/kb-acl.d.mts +2 -0
  226. package/dist/components/kb-agent.css +134 -55
  227. package/dist/components/kb-answer.d.mts +2 -0
  228. package/dist/components/kb-audit.d.mts +2 -0
  229. package/dist/components/kb-canvas.d.mts +2 -0
  230. package/dist/components/kb-chain.d.mts +2 -0
  231. package/dist/components/kb-checkpoint.d.mts +2 -0
  232. package/dist/components/kb-chunks.d.mts +2 -0
  233. package/dist/components/kb-citation.d.mts +2 -0
  234. package/dist/components/kb-clarify.d.mts +2 -0
  235. package/dist/components/kb-conflict.d.mts +2 -0
  236. package/dist/components/kb-connector.d.mts +2 -0
  237. package/dist/components/kb-data.css +192 -80
  238. package/dist/components/kb-eval.d.mts +2 -0
  239. package/dist/components/kb-explain.d.mts +2 -0
  240. package/dist/components/kb-filter.d.mts +2 -0
  241. package/dist/components/kb-gap.d.mts +2 -0
  242. package/dist/components/kb-ground.css +165 -73
  243. package/dist/components/kb-hittest.d.mts +2 -0
  244. package/dist/components/kb-ingest.css +308 -113
  245. package/dist/components/kb-metadata.d.mts +2 -0
  246. package/dist/components/kb-ops.css +189 -75
  247. package/dist/components/kb-passage.d.mts +2 -0
  248. package/dist/components/kb-perm.css +158 -87
  249. package/dist/components/kb-pipeline.d.mts +2 -0
  250. package/dist/components/kb-qa.d.mts +2 -0
  251. package/dist/components/kb-rerank.d.mts +2 -0
  252. package/dist/components/kb-retrieval.d.mts +2 -0
  253. package/dist/components/kb-review.d.mts +2 -0
  254. package/dist/components/kb-sandbox.d.mts +2 -0
  255. package/dist/components/kb-search.css +353 -116
  256. package/dist/components/kb-segment.d.mts +2 -0
  257. package/dist/components/kb-sources.d.mts +2 -0
  258. package/dist/components/kb-sql.d.mts +2 -0
  259. package/dist/components/kb-trace.d.mts +2 -0
  260. package/dist/components/kb-visibility.d.mts +2 -0
  261. package/dist/components/kb-who-can.d.mts +2 -0
  262. package/dist/components/kb.css +60 -36
  263. package/dist/components/kb.d.mts +2 -0
  264. package/dist/components/kbd.d.mts +2 -0
  265. package/dist/components/layout.css +10 -10
  266. package/dist/components/layout.d.mts +2 -0
  267. package/dist/components/list.d.mts +2 -0
  268. package/dist/components/material-chinese.css +10 -0
  269. package/dist/components/material-soviet.css +10 -0
  270. package/dist/components/material.css +61 -18
  271. package/dist/components/media-card.d.mts +2 -0
  272. package/dist/components/media.css +22 -27
  273. package/dist/components/mentions.css +18 -17
  274. package/dist/components/mentions.d.mts +2 -0
  275. package/dist/components/menu.css +32 -28
  276. package/dist/components/modal.css +30 -27
  277. package/dist/components/modal.d.mts +2 -0
  278. package/dist/components/nav.css +48 -44
  279. package/dist/components/nav.d.mts +2 -0
  280. package/dist/components/notification.css +31 -25
  281. package/dist/components/notification.d.mts +2 -0
  282. package/dist/components/pagination.css +15 -13
  283. package/dist/components/pagination.d.mts +2 -0
  284. package/dist/components/panel.css +19 -20
  285. package/dist/components/panel.d.mts +2 -0
  286. package/dist/components/pill.css +8 -7
  287. package/dist/components/pill.d.mts +2 -0
  288. package/dist/components/popover.css +32 -23
  289. package/dist/components/popover.d.mts +2 -0
  290. package/dist/components/progress.d.mts +2 -0
  291. package/dist/components/rating.d.mts +2 -0
  292. package/dist/components/result.d.mts +2 -0
  293. package/dist/components/scroll-area.css +84 -112
  294. package/dist/components/scroll-area.d.mts +2 -0
  295. package/dist/components/scroll-area.mjs +1 -0
  296. package/dist/components/segmented.css +47 -29
  297. package/dist/components/segmented.d.mts +2 -0
  298. package/dist/components/select.css +80 -41
  299. package/dist/components/select.d.mts +2 -0
  300. package/dist/components/sidebar.css +42 -38
  301. package/dist/components/sidebar.d.mts +2 -0
  302. package/dist/components/skeleton.d.mts +2 -0
  303. package/dist/components/slider.css +26 -22
  304. package/dist/components/slider.d.mts +2 -0
  305. package/dist/components/spinner.d.mts +2 -0
  306. package/dist/components/split-pane.css +16 -7
  307. package/dist/components/split-pane.d.mts +2 -0
  308. package/dist/components/stat.css +17 -16
  309. package/dist/components/stat.d.mts +2 -0
  310. package/dist/components/steps.css +26 -26
  311. package/dist/components/steps.d.mts +2 -0
  312. package/dist/components/switch.css +78 -39
  313. package/dist/components/switch.d.mts +2 -0
  314. package/dist/components/table.css +54 -32
  315. package/dist/components/table.d.mts +2 -0
  316. package/dist/components/tabs.css +53 -32
  317. package/dist/components/tabs.d.mts +2 -0
  318. package/dist/components/tag-input.css +12 -8
  319. package/dist/components/tag-input.d.mts +2 -0
  320. package/dist/components/tag.css +20 -12
  321. package/dist/components/tag.d.mts +2 -0
  322. package/dist/components/theme-switcher.css +7 -7
  323. package/dist/components/theme-switcher.d.mts +2 -0
  324. package/dist/components/timeline.d.mts +2 -0
  325. package/dist/components/toast.css +34 -30
  326. package/dist/components/toast.d.mts +2 -0
  327. package/dist/components/toolbar.css +19 -18
  328. package/dist/components/toolbar.d.mts +2 -0
  329. package/dist/components/tooltip.css +9 -8
  330. package/dist/components/tooltip.d.mts +2 -0
  331. package/dist/components/tree-select.css +63 -40
  332. package/dist/components/tree-select.d.mts +2 -0
  333. package/dist/components/tree.d.mts +2 -0
  334. package/dist/components/upload.css +9 -11
  335. package/dist/components/upload.d.mts +2 -0
  336. package/dist/index.d.mts +85 -84
  337. package/dist/index.mjs +14764 -14311
  338. package/dist/react.d.mts +18 -18
  339. package/dist/react.mjs +135 -36
  340. package/dist/registry.json +795 -64
  341. package/dist/tokens/colors.css +6 -0
  342. package/dist/tokens/design.css +130 -30
  343. package/dist/tokens/dtcg.json +6189 -1461
  344. package/dist/tokens/figma.variables.json +4107 -345
  345. package/dist/tokens/pixel.css +3 -0
  346. package/dist/tokens/scrollbars.css +40 -0
  347. package/dist/tokens/tokens-studio.json +5191 -271
  348. package/dist/tokens.css +304 -81
  349. package/dist/ui.css +4050 -2148
  350. package/dist/vue.d.mts +17 -17
  351. package/dist/vue.mjs +129 -34
  352. package/docs/spec/design-philosophy.md +5 -5
  353. package/docs/spec/design-primitives.md +10 -10
  354. package/docs/spec/scrollbars.md +46 -0
  355. package/package.json +16 -3
@@ -116,6 +116,7 @@ var ICEN_LOCALE_ZH_CN = {
116
116
  "aiChat.roleTool": "\u5DE5",
117
117
  "aiChat.roleUser": "\u6211",
118
118
  "aiChat.thinking": "\u601D\u8003 {p0}",
119
+ "aiChat.thinkingNow": "\u6B63\u5728\u601D\u8003\u2026",
119
120
  /* ── aiComposer ── */
120
121
  "aiComposer.attachAgent": "\u4EE3\u7406",
121
122
  "aiComposer.library": "\u6DFB\u52A0\u4E0A\u4E0B\u6587\u4E0E\u547D\u4EE4",
@@ -1163,7 +1164,8 @@ function withChrome(el, opts, hasLegend, paint, state, extras) {
1163
1164
  emitIcen(el, "icen:chart-legend-visibility", { visible: hidden ? false : true });
1164
1165
  });
1165
1166
  syncEye();
1166
- head.append(title, eye);
1167
+ if (hasLegend) head.append(title, eye);
1168
+ else head.append(title);
1167
1169
  el.append(head, body);
1168
1170
  paint(body);
1169
1171
  }
@@ -1264,7 +1266,7 @@ function renderVBar(el, opts) {
1264
1266
  wrap.appendChild(track);
1265
1267
  }
1266
1268
  body.append(wrap, labelsRow(labelsUsed));
1267
- if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name, label: s.name, tone: s.tone ?? (si === 0 ? opts.tone : void 0), si })), wrap));
1269
+ if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: `${si}:${s.name}`, label: s.name, tone: s.tone ?? (si === 0 ? opts.tone : void 0), si })), wrap));
1268
1270
  });
1269
1271
  attachChartInteraction(el, "vbar");
1270
1272
  return el;
@@ -1359,8 +1361,10 @@ function renderDonut(el, opts) {
1359
1361
  "stroke-width": "14"
1360
1362
  }));
1361
1363
  let offset = 0;
1364
+ const gap = opts.segments.length > 1 ? 2.5 : 0;
1362
1365
  opts.segments.forEach((seg, segIndex) => {
1363
- const dash = seg.value / total * circumference;
1366
+ const arcLength = Math.max(0, seg.value / total * circumference);
1367
+ const dash = arcLength - Math.min(gap, arcLength * 0.1);
1364
1368
  const arc = svgEl("circle", {
1365
1369
  cx: "60",
1366
1370
  cy: "60",
@@ -1374,7 +1378,7 @@ function renderDonut(el, opts) {
1374
1378
  });
1375
1379
  setMarkData(arc, { index: segIndex, label: seg.label, value: seg.value });
1376
1380
  svg.appendChild(arc);
1377
- offset += dash;
1381
+ offset += arcLength;
1378
1382
  });
1379
1383
  const legend = document.createElement("div");
1380
1384
  legend.className = "icen-chart-legend";
@@ -1409,8 +1413,9 @@ function renderLine(el, opts) {
1409
1413
  const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": chartAriaLabel(opts.title, t("charts.type.line")) });
1410
1414
  const defs = svgEl("defs", {});
1411
1415
  for (let i = 0; i < 4; i++) {
1412
- const y = pad + i * (height - pad * 2) / 3;
1416
+ const y = Math.round(pad + i * (height - pad * 2) / 3) + 0.5;
1413
1417
  svg.appendChild(svgEl("line", {
1418
+ class: i === 3 ? "icen-chart-grid icen-chart-baseline" : "icen-chart-grid",
1414
1419
  x1: String(pad),
1415
1420
  x2: String(width - pad),
1416
1421
  y1: String(y),
@@ -1494,7 +1499,7 @@ function renderLine(el, opts) {
1494
1499
  if (defs.firstChild) svg.insertBefore(defs, svg.firstChild);
1495
1500
  wrap.append(svg, pointLabelsRow(labelsUsed, width, pad));
1496
1501
  body.appendChild(wrap);
1497
- if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: s.name, label: s.name, tone: s.tone ?? (si === 0 ? opts.tone : void 0), si })), wrap));
1502
+ if (seriesList.length > 1) body.appendChild(toggleLegend(seriesList.map((s, si) => ({ key: `${si}:${s.name}`, label: s.name, tone: s.tone ?? (si === 0 ? opts.tone : void 0), si })), wrap));
1498
1503
  });
1499
1504
  if (el.dataset.icenChartBody == null) observeFluidWidth(el, () => renderLine(el, opts));
1500
1505
  attachChartInteraction(el, "line");
@@ -1601,7 +1606,7 @@ function renderRadar(el, opts) {
1601
1606
  setMarkData(dot, { index: i, seriesIndex: si, seriesName: s.name || void 0, label: axes[i] ?? "", value: s.values[i] ?? 0 });
1602
1607
  svg.appendChild(dot);
1603
1608
  }
1604
- legendItems.push({ key: s.name || t("charts.seriesName", { p0: si + 1 }), label: s.name || t("charts.seriesName", { p0: si + 1 }), tone: s.tone, si });
1609
+ legendItems.push({ key: `${si}:${s.name || t("charts.seriesName", { p0: si + 1 })}`, label: s.name || t("charts.seriesName", { p0: si + 1 }), tone: s.tone, si });
1605
1610
  });
1606
1611
  wrap.appendChild(svg);
1607
1612
  body.appendChild(wrap);
@@ -1623,6 +1628,11 @@ function niceCeil(v) {
1623
1628
  else nice = 10;
1624
1629
  return nice * base;
1625
1630
  }
1631
+ function tickPrecision(min, max, steps) {
1632
+ const span = Math.abs(max - min);
1633
+ if (!Number.isFinite(span) || span <= 0) return 0;
1634
+ return Math.max(0, Math.ceil(-Math.log10(span / steps)));
1635
+ }
1626
1636
  function isoDate(d) {
1627
1637
  const m = String(d.getMonth() + 1).padStart(2, "0");
1628
1638
  const day = String(d.getDate()).padStart(2, "0");
@@ -1718,7 +1728,8 @@ function renderSparkline(el, opts) {
1718
1728
  const tone = toneVar(opts.tone);
1719
1729
  const points = values.map((value, i) => ({
1720
1730
  x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
1721
- y: height - pad - value / max * (height - pad * 2),
1731
+ /* 负值钳到基线(与折线图同口径):viewBox 仅 28 高,反向射出更刺眼 */
1732
+ y: height - pad - Math.max(0, value) / max * (height - pad * 2),
1722
1733
  value
1723
1734
  }));
1724
1735
  const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
@@ -1843,6 +1854,7 @@ function renderCalendar(el, opts) {
1843
1854
  weekdayRow.appendChild(s);
1844
1855
  });
1845
1856
  const max = maxOf(entries.map((e) => e.value));
1857
+ entries.sort((a, b) => a.date.localeCompare(b.date));
1846
1858
  const pad = padFirstWeek(grid, entries[0].date, weekStart);
1847
1859
  let lastMonth = -1;
1848
1860
  entries.forEach((entry, i) => {
@@ -1901,8 +1913,9 @@ function renderScatter(el, opts) {
1901
1913
  const xAt = (x) => pad + (x - xMin) / (xMax - xMin) * (width - pad * 2);
1902
1914
  const yAt = (y) => height - pad - (y - yMin) / (yMax - yMin) * (height - pad * 2);
1903
1915
  for (let i = 0; i <= 3; i++) {
1904
- const y = pad + i * (height - pad * 2) / 3;
1916
+ const y = Math.round(pad + i * (height - pad * 2) / 3) + 0.5;
1905
1917
  svg.appendChild(svgEl("line", {
1918
+ class: i === 3 ? "icen-chart-grid icen-chart-baseline" : "icen-chart-grid",
1906
1919
  x1: String(pad),
1907
1920
  x2: String(width - pad),
1908
1921
  y1: String(y),
@@ -1918,12 +1931,13 @@ function renderScatter(el, opts) {
1918
1931
  "font-size": "8",
1919
1932
  fill: "var(--icen-token-text-faint)"
1920
1933
  });
1921
- txt.textContent = fmt(opts, Math.round(tv * 10) / 10);
1934
+ txt.textContent = fmt(opts, +tv.toFixed(tickPrecision(yMin, yMax, 3)));
1922
1935
  svg.appendChild(txt);
1923
1936
  }
1924
1937
  for (let i = 0; i <= 4; i++) {
1925
- const x = pad + i * (width - pad * 2) / 4;
1938
+ const x = Math.round(pad + i * (width - pad * 2) / 4) + 0.5;
1926
1939
  svg.appendChild(svgEl("line", {
1940
+ class: i === 0 ? "icen-chart-grid icen-chart-grid--v icen-chart-axis" : "icen-chart-grid icen-chart-grid--v",
1927
1941
  x1: String(x),
1928
1942
  x2: String(x),
1929
1943
  y1: String(pad),
@@ -1940,7 +1954,7 @@ function renderScatter(el, opts) {
1940
1954
  "font-size": "8",
1941
1955
  fill: "var(--icen-token-text-faint)"
1942
1956
  });
1943
- txt.textContent = fmt(opts, Math.round(tv * 10) / 10);
1957
+ txt.textContent = fmt(opts, +tv.toFixed(tickPrecision(xMin, xMax, 4)));
1944
1958
  svg.appendChild(txt);
1945
1959
  }
1946
1960
  if (opts.yLabel) {
@@ -2304,6 +2318,7 @@ function renderChartSkeleton(body) {
2304
2318
  body.appendChild(box);
2305
2319
  }
2306
2320
  var CHART_MARK_ACCENT = "var(--icen-token-accent)";
2321
+ var CHART_MARK_ACCENT_TEXT = "var(--icen-token-accent-text)";
2307
2322
  function markToneVar(tone) {
2308
2323
  switch (tone) {
2309
2324
  case "success":
@@ -2439,7 +2454,7 @@ function paintSvgChartMarks(root, view, marks, formatValue, warned) {
2439
2454
  y: String(clampNum(y - 10, 8, CHART_LINE_H - 4)),
2440
2455
  "text-anchor": "middle",
2441
2456
  "font-size": "8",
2442
- fill: CHART_MARK_ACCENT
2457
+ fill: CHART_MARK_ACCENT_TEXT
2443
2458
  });
2444
2459
  label.textContent = mark.text ?? mark.label;
2445
2460
  g.appendChild(label);
@@ -2997,6 +3012,12 @@ function renderChart(el, raw) {
2997
3012
  const graphic = root.querySelector('[role="img"]');
2998
3013
  if (graphic && !graphic.getAttribute("aria-label")) {
2999
3014
  graphic.setAttribute("aria-label", spec.title ? `${spec.title}\uFF08${type}\uFF09` : type);
3015
+ } else if (!graphic) {
3016
+ const dom = root.querySelector(".icen-chart-vbar, .icen-chart-hbar");
3017
+ if (dom) {
3018
+ dom.setAttribute("role", "img");
3019
+ dom.setAttribute("aria-label", spec.title ? `${spec.title}\uFF08${type}\uFF09` : type);
3020
+ }
3000
3021
  }
3001
3022
  }
3002
3023
  if (loading) return;
@@ -3498,7 +3519,13 @@ function onKeyDown(e) {
3498
3519
 
3499
3520
  // src/behaviors/datatable.ts
3500
3521
  function defaultCompare(a, b) {
3501
- if (typeof a === "number" && typeof b === "number") return a - b;
3522
+ if (typeof a === "number" && typeof b === "number") {
3523
+ if (Number.isNaN(a) || Number.isNaN(b)) {
3524
+ if (Number.isNaN(a) && Number.isNaN(b)) return 0;
3525
+ return Number.isNaN(a) ? 1 : -1;
3526
+ }
3527
+ return a - b;
3528
+ }
3502
3529
  return String(a ?? "").localeCompare(String(b ?? ""), "zh-CN", { numeric: true });
3503
3530
  }
3504
3531
  function toNum2(v) {
@@ -3787,11 +3814,13 @@ function createTable(el, opts) {
3787
3814
  const body = document.createElement("div");
3788
3815
  body.className = "icen-dt-body";
3789
3816
  body.setAttribute("role", "rowgroup");
3790
- const summaryEl = document.createElement("div");
3791
- summaryEl.className = "icen-dt-row icen-dt-row--summary";
3792
- summaryEl.setAttribute("role", "row");
3793
- summaryEl.hidden = true;
3794
- canvas.append(head, body, summaryEl);
3817
+ const summaryEl = opts.summaryRow ? document.createElement("div") : null;
3818
+ if (summaryEl) {
3819
+ summaryEl.className = "icen-dt-row icen-dt-row--summary";
3820
+ summaryEl.setAttribute("role", "row");
3821
+ summaryEl.hidden = true;
3822
+ }
3823
+ canvas.append(head, body, ...summaryEl ? [summaryEl] : []);
3795
3824
  scroll.appendChild(canvas);
3796
3825
  root.appendChild(scroll);
3797
3826
  const foot = document.createElement("div");
@@ -4604,7 +4633,8 @@ function createTable(el, opts) {
4604
4633
  else wrap.textContent = String(out);
4605
4634
  return wrap;
4606
4635
  }
4607
- function renderSummary() {
4636
+ function renderSummary2() {
4637
+ if (!summaryEl) return;
4608
4638
  summaryEl.textContent = "";
4609
4639
  summaryEl.hidden = true;
4610
4640
  const cfg = opts.summaryRow;
@@ -4801,9 +4831,9 @@ function createTable(el, opts) {
4801
4831
  }
4802
4832
  }
4803
4833
  let currentTotal = 0;
4804
- function visibleRange(total) {
4834
+ function visibleRange(total, avail) {
4805
4835
  if (!virtual) {
4806
- if (opts.remote || opts.pagination === false) return [0, total];
4836
+ if (opts.remote || opts.pagination === false) return [0, Math.min(total, avail)];
4807
4837
  const start2 = (page - 1) * pageSize;
4808
4838
  return [start2, Math.min(total, start2 + pageSize)];
4809
4839
  }
@@ -4818,6 +4848,10 @@ function createTable(el, opts) {
4818
4848
  const rows = pipeline();
4819
4849
  const entries = groupFn ? flatEntries() : null;
4820
4850
  const total = entries ? entries.length : opts.remote ? opts.totalCount ?? data.length : rows.length;
4851
+ if (!opts.remote && opts.pagination !== false) {
4852
+ const pages = Math.max(1, Math.ceil(total / pageSize));
4853
+ if (page > pages) page = pages;
4854
+ }
4821
4855
  navTotal = entries ? entries.length : rows.length;
4822
4856
  currentTotal = total;
4823
4857
  const keepTop = virtual ? scroll.scrollTop : 0;
@@ -4834,7 +4868,7 @@ function createTable(el, opts) {
4834
4868
  body.appendChild(sk);
4835
4869
  }
4836
4870
  renderFoot(total, rows.length);
4837
- renderSummary();
4871
+ renderSummary2();
4838
4872
  return;
4839
4873
  }
4840
4874
  if (total === 0) {
@@ -4843,10 +4877,10 @@ function createTable(el, opts) {
4843
4877
  empty.textContent = opts.emptyLabel ?? t("datatable.empty");
4844
4878
  body.appendChild(empty);
4845
4879
  renderFoot(total, rows.length);
4846
- renderSummary();
4880
+ renderSummary2();
4847
4881
  return;
4848
4882
  }
4849
- const [start, end] = visibleRange(total);
4883
+ const [start, end] = visibleRange(total, rows.length);
4850
4884
  const frag = document.createDocumentFragment();
4851
4885
  const offsets = frozenOffsets();
4852
4886
  if (virtual && start > 0) {
@@ -4880,7 +4914,7 @@ function createTable(el, opts) {
4880
4914
  if (virtual) scroll.scrollTop = keepTop;
4881
4915
  syncNavFocus();
4882
4916
  renderFoot(total, rows.length);
4883
- renderSummary();
4917
+ renderSummary2();
4884
4918
  }
4885
4919
  function renderFoot(total, dataCount = total) {
4886
4920
  foot.textContent = "";
@@ -4909,11 +4943,21 @@ function createTable(el, opts) {
4909
4943
  const pages = Math.ceil(total / pageSize);
4910
4944
  const pager = document.createElement("nav");
4911
4945
  pager.className = "icen-dt-pager";
4912
- pager.setAttribute("aria-label", t("datatable.paginationAria"));
4946
+ {
4947
+ const base = t("datatable.paginationAria");
4948
+ const used = /* @__PURE__ */ new Set();
4949
+ for (const el2 of document.querySelectorAll(".icen-dt-pager[aria-label]")) {
4950
+ if (!root.contains(el2)) used.add(el2.getAttribute("aria-label") ?? "");
4951
+ }
4952
+ let label = base, n = 2;
4953
+ while (used.has(label)) label = `${base} ${n++}`;
4954
+ pager.setAttribute("aria-label", label);
4955
+ }
4913
4956
  const sizeSel = document.createElement("select");
4914
4957
  sizeSel.className = "icen-dt-page-size";
4915
4958
  sizeSel.setAttribute("aria-label", t("datatable.pageSizeAria"));
4916
- for (const s of pageSizes) {
4959
+ const sizeOptions = pageSizes.includes(pageSize) ? pageSizes : [...pageSizes, pageSize].sort((a, b) => a - b);
4960
+ for (const s of sizeOptions) {
4917
4961
  const o = document.createElement("option");
4918
4962
  o.value = String(s);
4919
4963
  o.textContent = t("pagination.pageSize", { p0: s });
@@ -5005,6 +5049,7 @@ function createTable(el, opts) {
5005
5049
  if (destroyed) return;
5006
5050
  renderHead();
5007
5051
  renderBody();
5052
+ syncFrozenWidths();
5008
5053
  }
5009
5054
  rekey();
5010
5055
  renderAll();
@@ -5025,12 +5070,52 @@ function createTable(el, opts) {
5025
5070
  opts.onPageChange?.(page, pageSize);
5026
5071
  renderAll();
5027
5072
  }
5073
+ function hasFluidFrozen() {
5074
+ return cols().some((c) => frozenSide(c) != null && !/^(\d+(?:\.\d+)?)px/.test(colWidths.get(c.key) ?? c.width ?? ""));
5075
+ }
5076
+ function syncFrozenWidths() {
5077
+ if (!hasFluidFrozen()) return;
5078
+ const visible = cols();
5079
+ const headCells = Array.from(headRow.children);
5080
+ const lead = headCells.length - visible.length;
5081
+ const left = /* @__PURE__ */ new Map();
5082
+ const right = /* @__PURE__ */ new Map();
5083
+ let acc = headCells.slice(0, lead).reduce((s, c) => s + c.offsetWidth, 0);
5084
+ for (let i = 0; i < visible.length; i++) {
5085
+ const c = visible[i];
5086
+ if (frozenSide(c) === "left") left.set(c.key, acc);
5087
+ acc += headCells[lead + i]?.offsetWidth ?? 0;
5088
+ }
5089
+ let accR = 0;
5090
+ for (let i = visible.length - 1; i >= 0; i--) {
5091
+ const c = visible[i];
5092
+ if (frozenSide(c) === "right") right.set(c.key, accR);
5093
+ accR += headCells[lead + i]?.offsetWidth ?? 0;
5094
+ }
5095
+ const rows = [headRow, ...summaryEl ? [summaryEl] : [], ...Array.from(body.querySelectorAll(".icen-dt-row"))];
5096
+ for (const row of rows) {
5097
+ const cells = Array.from(row.children);
5098
+ if (cells.length !== headCells.length) continue;
5099
+ cells.forEach((cell, i) => {
5100
+ const col = i >= lead ? visible[i - lead] : void 0;
5101
+ if (!col) return;
5102
+ const l = left.get(col.key);
5103
+ if (l != null && cell.classList.contains("icen-dt-cell--frozen")) cell.style.left = `${l}px`;
5104
+ const r = right.get(col.key);
5105
+ if (r != null && cell.classList.contains("icen-dt-cell--frozen-right")) cell.style.right = `${r}px`;
5106
+ });
5107
+ }
5108
+ }
5028
5109
  return {
5029
5110
  setPage,
5030
5111
  setPageSize,
5031
5112
  setData(next) {
5032
5113
  if (editing) cancelEdit(true);
5033
5114
  data = next;
5115
+ if (typeof opts.rowKey !== "function" && typeof opts.rowKey !== "string" && selected.size > 0) {
5116
+ selected.clear();
5117
+ emitSelection();
5118
+ }
5034
5119
  rekey();
5035
5120
  invalidate();
5036
5121
  page = 1;
@@ -5107,6 +5192,823 @@ function createTable(el, opts) {
5107
5192
  };
5108
5193
  }
5109
5194
 
5195
+ // src/behaviors/form-validate.ts
5196
+ var DEFAULT_MESSAGES = {
5197
+ required: t("formValidate.required"),
5198
+ email: t("formValidate.email"),
5199
+ url: t("formValidate.url"),
5200
+ number: t("formValidate.number"),
5201
+ minlength: t("formValidate.minlength"),
5202
+ maxlength: t("formValidate.maxlength"),
5203
+ pattern: t("formValidate.pattern"),
5204
+ min: t("formValidate.min"),
5205
+ max: t("formValidate.max"),
5206
+ step: t("formValidate.step"),
5207
+ matches: t("formValidate.matches")
5208
+ };
5209
+ var messageTable = { ...DEFAULT_MESSAGES };
5210
+ function formatMessage(template, tokens) {
5211
+ return template.replace(/\{(\w+)\}/g, (m, key) => key in tokens ? String(tokens[key]) : m);
5212
+ }
5213
+ var customValidators = /* @__PURE__ */ new Map();
5214
+ function attrNumber(el, name) {
5215
+ const raw = el.getAttribute(name);
5216
+ if (raw == null || raw === "" || !Number.isFinite(Number(raw))) return null;
5217
+ return Number(raw);
5218
+ }
5219
+ function resolveFieldRules(field) {
5220
+ const rules = [];
5221
+ const type = (field.getAttribute("type") ?? "").toLowerCase();
5222
+ if (field.hasAttribute("required")) rules.push({ rule: "required" });
5223
+ if (type === "email" || type === "url") rules.push({ rule: type });
5224
+ if (type === "number" || type === "range") rules.push({ rule: "number" });
5225
+ const minlength = attrNumber(field, "minlength");
5226
+ if (minlength != null) rules.push({ rule: "minlength", attr: String(minlength) });
5227
+ const maxlength = attrNumber(field, "maxlength");
5228
+ if (maxlength != null) rules.push({ rule: "maxlength", attr: String(maxlength) });
5229
+ const pattern = field.getAttribute("pattern");
5230
+ if (pattern) rules.push({ rule: "pattern", attr: pattern });
5231
+ const min = field.getAttribute("min");
5232
+ if (min != null && min !== "") rules.push({ rule: "min", attr: min });
5233
+ const max = field.getAttribute("max");
5234
+ if (max != null && max !== "") rules.push({ rule: "max", attr: max });
5235
+ const step = field.getAttribute("step");
5236
+ if (step != null && step !== "" && step !== "any") rules.push({ rule: "step", attr: step });
5237
+ const extra = field.getAttribute("data-icen-validate") ?? "";
5238
+ for (const token of extra.trim().split(/\s+/)) {
5239
+ if (!token) continue;
5240
+ const idx = token.indexOf(":");
5241
+ const rule = idx === -1 ? token : token.slice(0, idx);
5242
+ const arg = idx === -1 ? void 0 : token.slice(idx + 1) || void 0;
5243
+ if (!rule || rule === "matches" && !arg) continue;
5244
+ rules.push({ rule, arg });
5245
+ }
5246
+ return rules;
5247
+ }
5248
+ function fieldValue(field) {
5249
+ if (field instanceof HTMLInputElement && (field.type === "checkbox" || field.type === "radio")) {
5250
+ return field.checked ? field.value || "on" : "";
5251
+ }
5252
+ return field.value;
5253
+ }
5254
+ function fieldLabel(field, fallback = "") {
5255
+ if (!field) return fallback;
5256
+ const wrap = field.closest(".icen-field, .icen-form-field");
5257
+ const labelEl = wrap?.querySelector(".icen-field-label, .icen-form-label, label");
5258
+ const text = labelEl?.textContent?.replace(/\s+/g, " ").trim();
5259
+ return text || field.getAttribute("aria-label") || field.getAttribute("placeholder") || field.getAttribute("name") || fallback;
5260
+ }
5261
+ function ruleMessage(field, rule, tokens) {
5262
+ const custom2 = field?.getAttribute(`data-icen-msg-${rule}`);
5263
+ const template = custom2 || messageTable[rule] || DEFAULT_MESSAGES[rule] || t("formValidate.invalid");
5264
+ return formatMessage(template, tokens);
5265
+ }
5266
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5267
+ var NUMBER_TYPES = /* @__PURE__ */ new Set(["number", "range"]);
5268
+ function compareValue(field, value, attr) {
5269
+ const numericAttr = Number.isFinite(Number(attr));
5270
+ const type = field?.getAttribute("type")?.toLowerCase() ?? "";
5271
+ if (numericAttr && (NUMBER_TYPES.has(type) || Number.isFinite(Number(value)))) {
5272
+ if (!Number.isFinite(Number(value))) return null;
5273
+ return Number(value) - Number(attr);
5274
+ }
5275
+ if (!numericAttr) return value < attr ? -1 : value > attr ? 1 : 0;
5276
+ return null;
5277
+ }
5278
+ async function applyRule(value, rule, ctx) {
5279
+ const field = ctx.field;
5280
+ const tokens = {
5281
+ min: rule.attr ?? "",
5282
+ max: rule.attr ?? "",
5283
+ step: rule.attr ?? "",
5284
+ length: value.length,
5285
+ label: fieldLabel(field, rule.arg ?? rule.rule)
5286
+ };
5287
+ switch (rule.rule) {
5288
+ case "required": {
5289
+ if (field instanceof HTMLInputElement && field.type === "checkbox") {
5290
+ return field.checked ? true : ruleMessage(field, "required", tokens);
5291
+ }
5292
+ if (field instanceof HTMLInputElement && field.type === "radio") {
5293
+ const form = ctx.form ?? field.form;
5294
+ const checked = form ? Array.from(form.elements).some(
5295
+ (el) => el instanceof HTMLInputElement && el.type === "radio" && el.name === field.name && el.checked
5296
+ ) : field.checked;
5297
+ return checked ? true : ruleMessage(field, "required", tokens);
5298
+ }
5299
+ return value.trim() !== "" ? true : ruleMessage(field, "required", tokens);
5300
+ }
5301
+ case "email":
5302
+ return value === "" || EMAIL_RE.test(value) ? true : ruleMessage(field, "email", tokens);
5303
+ case "url": {
5304
+ if (value === "") return true;
5305
+ try {
5306
+ new URL(value);
5307
+ return true;
5308
+ } catch {
5309
+ return ruleMessage(field, "url", tokens);
5310
+ }
5311
+ }
5312
+ case "number":
5313
+ return value === "" || Number.isFinite(Number(value)) ? true : ruleMessage(field, "number", tokens);
5314
+ case "minlength": {
5315
+ if (value === "") return true;
5316
+ const min = Number(rule.attr);
5317
+ return value.length >= min ? true : ruleMessage(field, "minlength", tokens);
5318
+ }
5319
+ case "maxlength": {
5320
+ if (value === "") return true;
5321
+ const max = Number(rule.attr);
5322
+ return value.length <= max ? true : ruleMessage(field, "maxlength", tokens);
5323
+ }
5324
+ case "pattern": {
5325
+ if (value === "") return true;
5326
+ try {
5327
+ return new RegExp(`^(?:${rule.attr})$`).test(value) ? true : ruleMessage(field, "pattern", tokens);
5328
+ } catch {
5329
+ return true;
5330
+ }
5331
+ }
5332
+ case "min": {
5333
+ if (value === "") return true;
5334
+ const cmp = compareValue(field, value, rule.attr ?? "");
5335
+ return cmp == null || cmp >= 0 ? true : ruleMessage(field, "min", tokens);
5336
+ }
5337
+ case "max": {
5338
+ if (value === "") return true;
5339
+ const cmp = compareValue(field, value, rule.attr ?? "");
5340
+ return cmp == null || cmp <= 0 ? true : ruleMessage(field, "max", tokens);
5341
+ }
5342
+ case "step": {
5343
+ if (value === "") return true;
5344
+ const step = Number(rule.attr);
5345
+ if (!Number.isFinite(step) || step <= 0 || !Number.isFinite(Number(value))) return true;
5346
+ const minAttr = field?.getAttribute("min");
5347
+ const base = minAttr != null && minAttr !== "" && Number.isFinite(Number(minAttr)) ? Number(minAttr) : 0;
5348
+ const rem = Math.abs((Number(value) - base) % step);
5349
+ return rem < 1e-9 || step - rem < 1e-9 ? true : ruleMessage(field, "step", tokens);
5350
+ }
5351
+ case "matches": {
5352
+ const form = ctx.form ?? field?.form ?? null;
5353
+ const other = form && rule.arg ? form.elements.namedItem(rule.arg) : null;
5354
+ const otherEl = other instanceof Element ? other : null;
5355
+ if (!otherEl) return true;
5356
+ return value === otherEl.value ? true : ruleMessage(field, "matches", tokens);
5357
+ }
5358
+ default: {
5359
+ const fn = customValidators.get(rule.rule);
5360
+ if (!fn || !field) return true;
5361
+ const result = await fn(value, field, ctx.form ?? field.form ?? null);
5362
+ return result === true ? true : typeof result === "string" ? result : true;
5363
+ }
5364
+ }
5365
+ }
5366
+ async function evaluateRules(value, rules, ctx = {}) {
5367
+ for (const rule of rules) {
5368
+ const fail = await applyRule(value, rule, ctx);
5369
+ if (fail !== true) return fail;
5370
+ }
5371
+ return true;
5372
+ }
5373
+ var DEFAULT_DEBOUNCE = 160;
5374
+ var uidSeq = 0;
5375
+ var liveValidators = /* @__PURE__ */ new WeakMap();
5376
+ var composingFlags = /* @__PURE__ */ new WeakMap();
5377
+ var compositionTracked = /* @__PURE__ */ new WeakSet();
5378
+ function trackComposition(el) {
5379
+ if (compositionTracked.has(el)) return;
5380
+ compositionTracked.add(el);
5381
+ el.addEventListener("compositionstart", () => composingFlags.set(el, true));
5382
+ el.addEventListener("compositionend", () => composingFlags.set(el, false));
5383
+ }
5384
+ function isComposing(el) {
5385
+ return composingFlags.get(el) === true;
5386
+ }
5387
+ function collectFormFields(form) {
5388
+ const out = [];
5389
+ for (const el of Array.from(form.elements)) {
5390
+ if (!(el instanceof HTMLInputElement) && !(el instanceof HTMLTextAreaElement) && !(el instanceof HTMLSelectElement)) {
5391
+ continue;
5392
+ }
5393
+ if (el.disabled || !el.name) continue;
5394
+ if (el instanceof HTMLInputElement) {
5395
+ const type = (el.getAttribute("type") ?? "").toLowerCase();
5396
+ if (["submit", "button", "reset", "image", "file"].includes(type)) continue;
5397
+ }
5398
+ out.push(el);
5399
+ }
5400
+ return out;
5401
+ }
5402
+ function findFormField(form, name) {
5403
+ const item = form.elements.namedItem(name);
5404
+ if (item instanceof Element && (item instanceof HTMLInputElement || item instanceof HTMLTextAreaElement || item instanceof HTMLSelectElement)) {
5405
+ return item;
5406
+ }
5407
+ if (item instanceof RadioNodeList) {
5408
+ for (const el of Array.from(item)) {
5409
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
5410
+ return el;
5411
+ }
5412
+ }
5413
+ }
5414
+ return null;
5415
+ }
5416
+ function fieldWrap(field) {
5417
+ return closestSelfOrAncestor(field, ".icen-field, .icen-form-field");
5418
+ }
5419
+ function closestSelfOrAncestor(el, selector) {
5420
+ return el.matches(selector) ? el : el.closest(selector);
5421
+ }
5422
+ function ensureControlId(field) {
5423
+ if (!field.id) field.id = `icen-fv-${++uidSeq}`;
5424
+ return field.id;
5425
+ }
5426
+ function findErrorEl(wrap) {
5427
+ for (const el of Array.from(wrap.children)) {
5428
+ if (el instanceof HTMLElement && el.classList.contains("icen-field-error")) return el;
5429
+ }
5430
+ return null;
5431
+ }
5432
+ function setValidating(field, on) {
5433
+ const wrap = fieldWrap(field);
5434
+ if (wrap) wrap.classList.toggle("is-validating", on);
5435
+ if (on) field.setAttribute("aria-busy", "true");
5436
+ else field.removeAttribute("aria-busy");
5437
+ }
5438
+ function presentField(field, outcome, showValid) {
5439
+ const wrap = fieldWrap(field);
5440
+ setValidating(field, false);
5441
+ if (!wrap) {
5442
+ if (!outcome.ok) field.setAttribute("aria-invalid", "true");
5443
+ else field.removeAttribute("aria-invalid");
5444
+ return;
5445
+ }
5446
+ if (!outcome.ok) {
5447
+ wrap.classList.add("is-error");
5448
+ wrap.setAttribute("data-icen-invalid", "true");
5449
+ wrap.classList.remove("is-valid");
5450
+ wrap.removeAttribute("data-icen-valid");
5451
+ let errEl2 = findErrorEl(wrap);
5452
+ if (!errEl2) {
5453
+ errEl2 = document.createElement("p");
5454
+ errEl2.className = "icen-field-error";
5455
+ errEl2.id = `icen-fv-err-${++uidSeq}`;
5456
+ errEl2.setAttribute("role", "alert");
5457
+ errEl2.setAttribute("data-icen-fv-auto", "");
5458
+ wrap.appendChild(errEl2);
5459
+ }
5460
+ errEl2.hidden = false;
5461
+ errEl2.textContent = outcome.message ?? "";
5462
+ field.setAttribute("aria-invalid", "true");
5463
+ const described = new Set((field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean));
5464
+ described.add(errEl2.id);
5465
+ field.setAttribute("aria-describedby", Array.from(described).join(" "));
5466
+ return;
5467
+ }
5468
+ wrap.classList.remove("is-error");
5469
+ wrap.removeAttribute("data-icen-invalid");
5470
+ field.removeAttribute("aria-invalid");
5471
+ const errEl = findErrorEl(wrap);
5472
+ if (errEl) {
5473
+ const rest = (field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean).filter((id) => id !== errEl.id);
5474
+ if (rest.length) field.setAttribute("aria-describedby", rest.join(" "));
5475
+ else field.removeAttribute("aria-describedby");
5476
+ if (errEl.hasAttribute("data-icen-fv-auto")) errEl.remove();
5477
+ else {
5478
+ errEl.textContent = "";
5479
+ errEl.hidden = true;
5480
+ }
5481
+ }
5482
+ if (showValid) {
5483
+ wrap.classList.add("is-valid");
5484
+ wrap.setAttribute("data-icen-valid", "true");
5485
+ } else {
5486
+ wrap.classList.remove("is-valid");
5487
+ wrap.removeAttribute("data-icen-valid");
5488
+ }
5489
+ }
5490
+ function clearFieldPresentation(field) {
5491
+ const wrap = fieldWrap(field);
5492
+ if (wrap) {
5493
+ wrap.classList.remove("is-error", "is-valid", "is-validating");
5494
+ wrap.removeAttribute("data-icen-invalid");
5495
+ wrap.removeAttribute("data-icen-valid");
5496
+ const errEl = findErrorEl(wrap);
5497
+ if (errEl?.hasAttribute("data-icen-fv-auto")) errEl.remove();
5498
+ else if (errEl) {
5499
+ errEl.textContent = "";
5500
+ errEl.hidden = true;
5501
+ }
5502
+ }
5503
+ field.removeAttribute("aria-invalid");
5504
+ field.removeAttribute("aria-busy");
5505
+ const rest = (field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean).filter((id) => !id.startsWith("icen-fv-err-"));
5506
+ if (rest.length) field.setAttribute("aria-describedby", rest.join(" "));
5507
+ else field.removeAttribute("aria-describedby");
5508
+ }
5509
+ function focusField(field) {
5510
+ try {
5511
+ field.focus();
5512
+ } catch {
5513
+ }
5514
+ if (typeof field.scrollIntoView === "function") {
5515
+ try {
5516
+ const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
5517
+ field.scrollIntoView({ block: "center", behavior: reduced ? "auto" : "smooth" });
5518
+ } catch {
5519
+ try {
5520
+ field.scrollIntoView();
5521
+ } catch {
5522
+ }
5523
+ }
5524
+ }
5525
+ }
5526
+ function renderSummary(form, errorsObj) {
5527
+ if (!form.hasAttribute("data-icen-validate-summary")) return;
5528
+ const names = Object.keys(errorsObj);
5529
+ const existing = form.querySelector(":scope > .icen-form-validate-summary");
5530
+ if (names.length === 0) {
5531
+ existing?.remove();
5532
+ return;
5533
+ }
5534
+ let box = existing;
5535
+ if (!box) {
5536
+ box = document.createElement("div");
5537
+ box.className = "icen-form-validate-summary";
5538
+ box.setAttribute("role", "alert");
5539
+ box.setAttribute("data-icen-fv-auto", "");
5540
+ box.addEventListener("click", (e) => {
5541
+ const a = e.target instanceof Element ? closestSelfOrAncestor(e.target, "a[data-icen-fv-name]") : null;
5542
+ if (!(a instanceof HTMLAnchorElement)) return;
5543
+ e.preventDefault();
5544
+ const field = findFormField(form, a.getAttribute("data-icen-fv-name") ?? "");
5545
+ if (field) focusField(field);
5546
+ });
5547
+ form.prepend(box);
5548
+ }
5549
+ box.textContent = "";
5550
+ const title = document.createElement("p");
5551
+ title.className = "icen-form-validate-summary-title";
5552
+ title.textContent = t("formValidate.summary", { p0: names.length });
5553
+ const list = document.createElement("ul");
5554
+ list.className = "icen-form-validate-summary-list";
5555
+ for (const name of names) {
5556
+ const field = findFormField(form, name);
5557
+ if (!field) continue;
5558
+ const li = document.createElement("li");
5559
+ const a = document.createElement("a");
5560
+ a.className = "icen-form-validate-summary-item";
5561
+ a.href = `#${ensureControlId(field)}`;
5562
+ a.setAttribute("data-icen-fv-name", name);
5563
+ a.textContent = `${fieldLabel(field, name)}\uFF1A${errorsObj[name]}`;
5564
+ li.appendChild(a);
5565
+ list.appendChild(li);
5566
+ }
5567
+ box.append(title, list);
5568
+ }
5569
+ function createFormValidator(form, opts = {}) {
5570
+ if (typeof document === "undefined") {
5571
+ const stub = () => void 0;
5572
+ return {
5573
+ form,
5574
+ validate: async () => true,
5575
+ validateField: async () => true,
5576
+ reset: stub,
5577
+ errors: () => ({}),
5578
+ destroy: stub
5579
+ };
5580
+ }
5581
+ const existing = liveValidators.get(form);
5582
+ if (existing) return existing;
5583
+ const state = {
5584
+ errors: /* @__PURE__ */ new Map(),
5585
+ touched: /* @__PURE__ */ new Set(),
5586
+ submitted: false,
5587
+ seq: /* @__PURE__ */ new Map(),
5588
+ debounceTimers: /* @__PURE__ */ new Map()
5589
+ };
5590
+ let pointerDown = false;
5591
+ const pendingPresent = /* @__PURE__ */ new Map();
5592
+ let pendingSummary = null;
5593
+ let flushScheduled = false;
5594
+ const showValidFor = (field) => field.hasAttribute("data-icen-validate-show-valid") || form.hasAttribute("data-icen-validate-show-valid");
5595
+ async function checkField(field) {
5596
+ const name = field.name;
5597
+ const rules = resolveFieldRules(field);
5598
+ if (rules.length === 0) return true;
5599
+ trackComposition(field);
5600
+ const seq = (state.seq.get(name) ?? 0) + 1;
5601
+ state.seq.set(name, seq);
5602
+ const hasAsync = rules.some((r) => customValidators.has(r.rule));
5603
+ if (hasAsync) setValidating(field, true);
5604
+ const result = await evaluateRules(fieldValue(field), rules, { field, form });
5605
+ if (state.seq.get(name) !== seq) {
5606
+ if (hasAsync) setValidating(field, false);
5607
+ return !state.errors.has(name);
5608
+ }
5609
+ if (hasAsync) setValidating(field, false);
5610
+ const outcome = result === true ? { ok: true } : { ok: false, message: result };
5611
+ if (outcome.ok) state.errors.delete(name);
5612
+ else state.errors.set(name, outcome.message ?? "");
5613
+ presentFieldGuarded(field, outcome, showValidFor(field));
5614
+ if (form.querySelector(":scope > .icen-form-validate-summary")) {
5615
+ renderSummaryGuarded(errors());
5616
+ }
5617
+ emitIcen(
5618
+ field,
5619
+ "icen:form-field-validate",
5620
+ outcome.ok ? { name, valid: true } : { name, valid: false, message: outcome.message }
5621
+ );
5622
+ return outcome.ok;
5623
+ }
5624
+ function debouncedCheck(field) {
5625
+ const name = field.name;
5626
+ const timer = state.debounceTimers.get(name);
5627
+ if (timer) clearTimeout(timer);
5628
+ const delay = opts.strategy?.debounceMs ?? DEFAULT_DEBOUNCE;
5629
+ state.debounceTimers.set(
5630
+ name,
5631
+ setTimeout(() => {
5632
+ state.debounceTimers.delete(name);
5633
+ void checkField(field);
5634
+ }, delay)
5635
+ );
5636
+ }
5637
+ function presentFieldPressed(field, outcome, showValid) {
5638
+ pendingPresent.set(field, { outcome, showValid });
5639
+ const wrap = fieldWrap(field);
5640
+ setValidating(field, false);
5641
+ if (!wrap) {
5642
+ if (!outcome.ok) field.setAttribute("aria-invalid", "true");
5643
+ else field.removeAttribute("aria-invalid");
5644
+ return;
5645
+ }
5646
+ if (!outcome.ok) {
5647
+ wrap.classList.add("is-error");
5648
+ wrap.setAttribute("data-icen-invalid", "true");
5649
+ wrap.classList.remove("is-valid");
5650
+ wrap.removeAttribute("data-icen-valid");
5651
+ field.setAttribute("aria-invalid", "true");
5652
+ const errEl = findErrorEl(wrap);
5653
+ if (errEl && !errEl.hidden) errEl.textContent = outcome.message ?? "";
5654
+ return;
5655
+ }
5656
+ wrap.classList.remove("is-error");
5657
+ wrap.removeAttribute("data-icen-invalid");
5658
+ field.removeAttribute("aria-invalid");
5659
+ if (showValid) {
5660
+ wrap.classList.add("is-valid");
5661
+ wrap.setAttribute("data-icen-valid", "true");
5662
+ } else {
5663
+ wrap.classList.remove("is-valid");
5664
+ wrap.removeAttribute("data-icen-valid");
5665
+ }
5666
+ }
5667
+ function presentFieldGuarded(field, outcome, showValid) {
5668
+ if (pointerDown) presentFieldPressed(field, outcome, showValid);
5669
+ else presentField(field, outcome, showValid);
5670
+ }
5671
+ function renderSummaryGuarded(errorsObj) {
5672
+ if (pointerDown) pendingSummary = errorsObj;
5673
+ else renderSummary(form, errorsObj);
5674
+ }
5675
+ function flushDeferredPresentation() {
5676
+ flushScheduled = false;
5677
+ if (destroyed) return;
5678
+ const items = Array.from(pendingPresent.entries());
5679
+ pendingPresent.clear();
5680
+ const summary = pendingSummary;
5681
+ pendingSummary = null;
5682
+ for (const [field, it] of items) presentField(field, it.outcome, it.showValid);
5683
+ if (summary) renderSummary(form, summary);
5684
+ }
5685
+ function scheduleFlush() {
5686
+ if (flushScheduled) return;
5687
+ flushScheduled = true;
5688
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(flushDeferredPresentation);
5689
+ else setTimeout(flushDeferredPresentation, 0);
5690
+ }
5691
+ function resolveEventField(target) {
5692
+ if (!(target instanceof Element)) return null;
5693
+ const el = closestSelfOrAncestor(target, "input, textarea, select");
5694
+ if (!(el instanceof HTMLInputElement) && !(el instanceof HTMLTextAreaElement) && !(el instanceof HTMLSelectElement)) {
5695
+ return null;
5696
+ }
5697
+ if (el.form !== form || el.disabled || !el.name) return null;
5698
+ if (el instanceof HTMLInputElement) {
5699
+ const type = (el.getAttribute("type") ?? "").toLowerCase();
5700
+ if (["submit", "button", "reset", "image", "file"].includes(type)) return null;
5701
+ }
5702
+ return el;
5703
+ }
5704
+ const onSubmit = (e) => {
5705
+ e.preventDefault();
5706
+ void validate();
5707
+ };
5708
+ const onFocusOut = (e) => {
5709
+ const field = resolveEventField(e.target);
5710
+ if (!field) return;
5711
+ state.touched.add(field.name);
5712
+ const mode = opts.strategy?.on;
5713
+ if (mode === "blur") void checkField(field);
5714
+ else if (!mode && state.submitted) void checkField(field);
5715
+ };
5716
+ const onInput = (e) => {
5717
+ const field = resolveEventField(e.target);
5718
+ if (!field) return;
5719
+ trackComposition(field);
5720
+ if (isComposing(field)) return;
5721
+ state.touched.add(field.name);
5722
+ const mode = opts.strategy?.on;
5723
+ if (mode === "input") debouncedCheck(field);
5724
+ else if (!mode && state.submitted && state.errors.has(field.name)) debouncedCheck(field);
5725
+ };
5726
+ async function validate() {
5727
+ state.submitted = true;
5728
+ const fields = collectFormFields(form);
5729
+ const names = [];
5730
+ let firstInvalid = null;
5731
+ for (const field of fields) {
5732
+ if (resolveFieldRules(field).length === 0) continue;
5733
+ names.push(field.name);
5734
+ const ok = await checkField(field);
5735
+ if (!ok && !firstInvalid) firstInvalid = field;
5736
+ }
5737
+ const errorsSnapshot = errors();
5738
+ if (firstInvalid) {
5739
+ renderSummaryGuarded(errorsSnapshot);
5740
+ emitIcen(form, "icen:form-invalid", { errors: errorsSnapshot, first: firstInvalid.name });
5741
+ if (opts.scrollToError !== false) focusField(firstInvalid);
5742
+ return false;
5743
+ }
5744
+ renderSummaryGuarded({});
5745
+ emitIcen(form, "icen:form-valid", { names });
5746
+ return true;
5747
+ }
5748
+ function errors() {
5749
+ return Object.fromEntries(state.errors);
5750
+ }
5751
+ function reset() {
5752
+ state.errors.clear();
5753
+ state.touched.clear();
5754
+ state.submitted = false;
5755
+ state.seq.clear();
5756
+ for (const timer of state.debounceTimers.values()) clearTimeout(timer);
5757
+ state.debounceTimers.clear();
5758
+ pendingPresent.clear();
5759
+ pendingSummary = null;
5760
+ for (const field of collectFormFields(form)) clearFieldPresentation(field);
5761
+ form.querySelector(":scope > .icen-form-validate-summary")?.remove();
5762
+ }
5763
+ const onFormPointerDown = (e) => {
5764
+ const btn = e.button;
5765
+ if (typeof btn === "number" && btn !== 0) return;
5766
+ pointerDown = true;
5767
+ };
5768
+ const onPressEnd = () => {
5769
+ if (!pointerDown) return;
5770
+ pointerDown = false;
5771
+ if (pendingPresent.size > 0 || pendingSummary !== null) scheduleFlush();
5772
+ };
5773
+ form.addEventListener("submit", onSubmit);
5774
+ form.addEventListener("focusout", onFocusOut);
5775
+ form.addEventListener("input", onInput);
5776
+ form.addEventListener("pointerdown", onFormPointerDown, true);
5777
+ document.addEventListener("pointerup", onPressEnd, true);
5778
+ document.addEventListener("pointercancel", onPressEnd, true);
5779
+ window.addEventListener("blur", onPressEnd);
5780
+ let destroyed = false;
5781
+ const handle = {
5782
+ form,
5783
+ validate,
5784
+ async validateField(name) {
5785
+ const field = findFormField(form, name);
5786
+ if (!field || field.disabled) return true;
5787
+ return checkField(field);
5788
+ },
5789
+ reset,
5790
+ errors,
5791
+ destroy() {
5792
+ if (destroyed) return;
5793
+ destroyed = true;
5794
+ form.removeEventListener("submit", onSubmit);
5795
+ form.removeEventListener("focusout", onFocusOut);
5796
+ form.removeEventListener("input", onInput);
5797
+ form.removeEventListener("pointerdown", onFormPointerDown, true);
5798
+ document.removeEventListener("pointerup", onPressEnd, true);
5799
+ document.removeEventListener("pointercancel", onPressEnd, true);
5800
+ window.removeEventListener("blur", onPressEnd);
5801
+ reset();
5802
+ liveValidators.delete(form);
5803
+ }
5804
+ };
5805
+ liveValidators.set(form, handle);
5806
+ return handle;
5807
+ }
5808
+ var SHOW_FORM_SCHEMA = {
5809
+ type: "object",
5810
+ properties: {
5811
+ title: { type: "string", description: t("formValidate.tool.title") },
5812
+ fields: {
5813
+ type: "array",
5814
+ description: t("formValidate.tool.fields"),
5815
+ items: {
5816
+ type: "object",
5817
+ properties: {
5818
+ name: { type: "string", description: t("formValidate.tool.name") },
5819
+ label: { type: "string", description: t("formValidate.tool.label") },
5820
+ type: {
5821
+ type: "string",
5822
+ enum: ["text", "email", "number", "url", "textarea", "select", "date"],
5823
+ description: t("formValidate.tool.type")
5824
+ },
5825
+ required: { type: "boolean", description: t("formValidate.tool.required") },
5826
+ pattern: { type: "string", description: t("formValidate.tool.pattern") },
5827
+ min: { type: "number", description: t("formValidate.tool.min") },
5828
+ max: { type: "number", description: t("formValidate.tool.max") },
5829
+ options: {
5830
+ type: "array",
5831
+ description: t("formValidate.tool.options"),
5832
+ items: {
5833
+ anyOf: [
5834
+ { type: "string" },
5835
+ {
5836
+ type: "object",
5837
+ properties: { label: { type: "string" }, value: { type: "string" } },
5838
+ required: ["label", "value"]
5839
+ }
5840
+ ]
5841
+ }
5842
+ },
5843
+ help: { type: "string", description: t("formValidate.tool.help") }
5844
+ },
5845
+ required: ["name", "label"]
5846
+ }
5847
+ },
5848
+ submitLabel: { type: "string", description: t("formValidate.tool.submitText") }
5849
+ },
5850
+ required: ["fields"]
5851
+ };
5852
+ function normalizeShowForm(input) {
5853
+ const raw = input ?? {};
5854
+ const fields = [];
5855
+ if (Array.isArray(raw.fields)) {
5856
+ for (const f of raw.fields) {
5857
+ if (!f || typeof f !== "object") continue;
5858
+ const name = typeof f.name === "string" ? f.name.trim() : "";
5859
+ if (!name) continue;
5860
+ const label = typeof f.label === "string" && f.label ? f.label : name;
5861
+ fields.push({
5862
+ name,
5863
+ label,
5864
+ type: f.type,
5865
+ required: f.required === true,
5866
+ pattern: typeof f.pattern === "string" ? f.pattern : void 0,
5867
+ min: typeof f.min === "number" ? f.min : void 0,
5868
+ max: typeof f.max === "number" ? f.max : void 0,
5869
+ options: Array.isArray(f.options) ? f.options : void 0,
5870
+ help: typeof f.help === "string" ? f.help : void 0
5871
+ });
5872
+ }
5873
+ }
5874
+ return {
5875
+ title: typeof raw.title === "string" && raw.title ? raw.title : void 0,
5876
+ fields,
5877
+ submitLabel: typeof raw.submitLabel === "string" && raw.submitLabel ? raw.submitLabel : void 0
5878
+ };
5879
+ }
5880
+ function renderShowFormField(spec) {
5881
+ const wrap = document.createElement("div");
5882
+ wrap.className = "icen-field";
5883
+ const id = `icen-ai-form-${++uidSeq}`;
5884
+ const label = document.createElement("label");
5885
+ label.className = "icen-field-label";
5886
+ label.htmlFor = id;
5887
+ label.textContent = spec.label;
5888
+ if (spec.required) {
5889
+ const req = document.createElement("span");
5890
+ req.className = "icen-req";
5891
+ req.textContent = "*";
5892
+ label.appendChild(req);
5893
+ }
5894
+ wrap.appendChild(label);
5895
+ let control;
5896
+ if (spec.type === "textarea") {
5897
+ const ta = document.createElement("textarea");
5898
+ ta.className = "icen-form-textarea";
5899
+ ta.setAttribute("rows", "3");
5900
+ control = ta;
5901
+ } else if (spec.type === "select") {
5902
+ const sel = document.createElement("select");
5903
+ sel.className = "icen-form-input";
5904
+ const placeholder = document.createElement("option");
5905
+ placeholder.value = "";
5906
+ placeholder.textContent = t("formValidate.selectPlaceholder");
5907
+ sel.appendChild(placeholder);
5908
+ for (const opt of spec.options ?? []) {
5909
+ const option = document.createElement("option");
5910
+ if (typeof opt === "string") {
5911
+ option.value = opt;
5912
+ option.textContent = opt;
5913
+ } else {
5914
+ option.value = opt.value;
5915
+ option.textContent = opt.label;
5916
+ }
5917
+ sel.appendChild(option);
5918
+ }
5919
+ control = sel;
5920
+ } else {
5921
+ const input = document.createElement("input");
5922
+ input.className = "icen-form-input";
5923
+ input.type = spec.type ?? "text";
5924
+ control = input;
5925
+ }
5926
+ control.id = id;
5927
+ control.name = spec.name;
5928
+ if (spec.required) control.required = true;
5929
+ if (spec.pattern) control.setAttribute("pattern", spec.pattern);
5930
+ if (spec.min != null) control.setAttribute("min", String(spec.min));
5931
+ if (spec.max != null) control.setAttribute("max", String(spec.max));
5932
+ wrap.appendChild(control);
5933
+ if (spec.help) {
5934
+ const hint = document.createElement("span");
5935
+ hint.className = "icen-field-hint";
5936
+ hint.textContent = spec.help;
5937
+ wrap.appendChild(hint);
5938
+ }
5939
+ return wrap;
5940
+ }
5941
+ function collectFormValues(form) {
5942
+ const out = {};
5943
+ for (const field of collectFormFields(form)) {
5944
+ if (field instanceof HTMLInputElement && field.type === "checkbox") {
5945
+ out[field.name] = field.checked;
5946
+ continue;
5947
+ }
5948
+ if (field instanceof HTMLInputElement && field.type === "radio") {
5949
+ if (!(field.name in out)) out[field.name] = field.checked ? field.value : "";
5950
+ else if (field.checked) out[field.name] = field.value;
5951
+ continue;
5952
+ }
5953
+ out[field.name] = field.value;
5954
+ }
5955
+ return out;
5956
+ }
5957
+ function runShowForm(input, mount) {
5958
+ return new Promise((resolve) => {
5959
+ if (typeof document === "undefined") {
5960
+ resolve({ cancelled: true });
5961
+ return;
5962
+ }
5963
+ const spec = normalizeShowForm(input);
5964
+ const root = document.createElement("div");
5965
+ root.className = "icen-ai-form";
5966
+ if (spec.title) {
5967
+ const title = document.createElement("p");
5968
+ title.className = "icen-ai-form-title";
5969
+ title.textContent = spec.title;
5970
+ root.appendChild(title);
5971
+ }
5972
+ const form = document.createElement("form");
5973
+ form.className = "icen-form icen-ai-form-fields";
5974
+ form.setAttribute("data-icen-validate", "");
5975
+ form.setAttribute("data-icen-validate-summary", "");
5976
+ form.setAttribute("novalidate", "");
5977
+ for (const field of spec.fields) form.appendChild(renderShowFormField(field));
5978
+ const actions = document.createElement("div");
5979
+ actions.className = "icen-ai-form-actions";
5980
+ const submit = document.createElement("button");
5981
+ submit.type = "submit";
5982
+ submit.className = "icen-btn icen-btn-primary";
5983
+ submit.textContent = spec.submitLabel ?? t("common.submit");
5984
+ const cancel = document.createElement("button");
5985
+ cancel.type = "button";
5986
+ cancel.className = "icen-btn icen-btn-ghost";
5987
+ cancel.setAttribute("data-icen-ai-form-cancel", "");
5988
+ cancel.textContent = t("common.cancel");
5989
+ actions.append(submit, cancel);
5990
+ form.appendChild(actions);
5991
+ root.appendChild(form);
5992
+ mount.appendChild(root);
5993
+ const handle = createFormValidator(form, { scrollToError: true });
5994
+ const finish = (value) => {
5995
+ form.removeEventListener("icen:form-valid", onValid);
5996
+ cancel.removeEventListener("click", onCancel);
5997
+ handle.destroy();
5998
+ root.remove();
5999
+ resolve(value);
6000
+ };
6001
+ const onValid = () => {
6002
+ finish(collectFormValues(form));
6003
+ };
6004
+ const onCancel = () => {
6005
+ finish({ cancelled: true });
6006
+ };
6007
+ form.addEventListener("icen:form-valid", onValid);
6008
+ cancel.addEventListener("click", onCancel);
6009
+ });
6010
+ }
6011
+
5110
6012
  // src/behaviors/ai-tools.ts
5111
6013
  var toolRegistry = /* @__PURE__ */ new Map();
5112
6014
  var LIVE_AREAS = /* @__PURE__ */ new Set();
@@ -5435,6 +6337,13 @@ registerAiTool({
5435
6337
  return { title, rows: rows.length };
5436
6338
  }
5437
6339
  });
6340
+ registerAiTool({
6341
+ name: "show_form",
6342
+ description: t("formValidate.tool.description"),
6343
+ inputSchema: SHOW_FORM_SCHEMA,
6344
+ present: "mount",
6345
+ run: (input, mount) => runShowForm(input, mount)
6346
+ });
5438
6347
  function isBrowserTools() {
5439
6348
  return typeof document !== "undefined" && typeof window !== "undefined";
5440
6349
  }