@adia-ai/web-components 0.6.33 → 0.6.35

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 (391) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/color/index.js +1 -1
  3. package/components/accordion/accordion-item.yaml +2 -2
  4. package/components/accordion/accordion.css +2 -2
  5. package/components/accordion/accordion.js +1 -1
  6. package/components/action-list/action-item.yaml +2 -2
  7. package/components/action-list/action-list.css +2 -2
  8. package/components/action-list/action-list.js +1 -1
  9. package/components/agent-artifact/{class.js → agent-artifact.class.js} +1 -1
  10. package/components/agent-artifact/agent-artifact.css +31 -31
  11. package/components/agent-artifact/agent-artifact.js +1 -1
  12. package/components/agent-feedback-bar/agent-feedback-bar.css +10 -10
  13. package/components/agent-feedback-bar/agent-feedback-bar.js +1 -1
  14. package/components/agent-questions/agent-questions.css +57 -57
  15. package/components/agent-questions/agent-questions.js +1 -1
  16. package/components/agent-reasoning/agent-reasoning.css +62 -62
  17. package/components/agent-reasoning/agent-reasoning.js +1 -1
  18. package/components/agent-suggestions/agent-suggestions.css +4 -4
  19. package/components/agent-suggestions/agent-suggestions.js +1 -1
  20. package/components/agent-trace/agent-trace.css +53 -53
  21. package/components/alert/alert.a2ui.json +64 -1
  22. package/components/alert/{class.js → alert.class.js} +189 -2
  23. package/components/alert/alert.css +119 -41
  24. package/components/alert/alert.d.ts +14 -0
  25. package/components/alert/alert.js +1 -1
  26. package/components/alert/alert.test.js +184 -0
  27. package/components/alert/alert.yaml +114 -1
  28. package/components/avatar/avatar-group.yaml +2 -2
  29. package/components/avatar/avatar.css +27 -27
  30. package/components/avatar/avatar.js +1 -1
  31. package/components/badge/badge.css +27 -27
  32. package/components/badge/badge.js +1 -1
  33. package/components/block/block.css +16 -16
  34. package/components/block/block.js +1 -1
  35. package/components/breadcrumb/breadcrumb.css +23 -23
  36. package/components/breadcrumb/breadcrumb.js +1 -1
  37. package/components/button/button.css +101 -91
  38. package/components/button/button.js +1 -1
  39. package/components/calendar-grid/calendar-grid.a2ui.json +146 -0
  40. package/components/calendar-grid/calendar-grid.class.js +326 -0
  41. package/components/calendar-grid/calendar-grid.css +246 -0
  42. package/components/calendar-grid/calendar-grid.d.ts +41 -0
  43. package/components/calendar-grid/calendar-grid.js +17 -0
  44. package/components/calendar-grid/calendar-grid.yaml +136 -0
  45. package/components/calendar-picker/calendar-picker.css +139 -139
  46. package/components/calendar-picker/calendar-picker.js +1 -1
  47. package/components/canvas/canvas.css +12 -12
  48. package/components/card/card.css +83 -83
  49. package/components/card/card.js +1 -1
  50. package/components/chart/chart.css +224 -224
  51. package/components/chart/chart.js +1 -1
  52. package/components/chart-legend/chart-legend.css +26 -26
  53. package/components/chart-legend/chart-legend.js +1 -1
  54. package/components/chat-thread/chat-input.a2ui.json +1 -1
  55. package/components/chat-thread/chat-input.js +6 -1
  56. package/components/chat-thread/chat-input.yaml +4 -1
  57. package/components/chat-thread/chat-thread.js +1 -1
  58. package/components/check/check.css +40 -40
  59. package/components/check/check.js +1 -1
  60. package/components/code/code.css +125 -125
  61. package/components/code/code.js +1 -1
  62. package/components/col/col.css +15 -15
  63. package/components/col/col.js +1 -1
  64. package/components/color-input/color-input.js +1 -1
  65. package/components/color-picker/color-picker.css +55 -55
  66. package/components/color-picker/color-picker.js +1 -1
  67. package/components/combobox/combobox.a2ui.json +363 -0
  68. package/components/combobox/combobox.class.js +861 -0
  69. package/components/combobox/combobox.css +244 -0
  70. package/components/combobox/combobox.d.ts +113 -0
  71. package/components/combobox/combobox.examples.md +59 -0
  72. package/components/combobox/combobox.js +17 -0
  73. package/components/combobox/combobox.test.js +181 -0
  74. package/components/combobox/combobox.yaml +369 -0
  75. package/components/command/command.css +90 -90
  76. package/components/command/command.js +1 -1
  77. package/components/date-range-picker/date-range-picker.a2ui.json +300 -0
  78. package/components/date-range-picker/date-range-picker.class.js +791 -0
  79. package/components/date-range-picker/date-range-picker.css +224 -0
  80. package/components/date-range-picker/date-range-picker.d.ts +82 -0
  81. package/components/date-range-picker/date-range-picker.examples.md +37 -0
  82. package/components/date-range-picker/date-range-picker.js +17 -0
  83. package/components/date-range-picker/date-range-picker.test.js +387 -0
  84. package/components/date-range-picker/date-range-picker.yaml +285 -0
  85. package/components/datetime-picker/datetime-picker.a2ui.json +334 -0
  86. package/components/datetime-picker/datetime-picker.class.js +706 -0
  87. package/components/datetime-picker/datetime-picker.css +150 -0
  88. package/components/datetime-picker/datetime-picker.d.ts +86 -0
  89. package/components/datetime-picker/datetime-picker.examples.md +46 -0
  90. package/components/datetime-picker/datetime-picker.js +17 -0
  91. package/components/datetime-picker/datetime-picker.test.js +454 -0
  92. package/components/datetime-picker/datetime-picker.yaml +332 -0
  93. package/components/demo-toggle/demo-toggle.css +27 -27
  94. package/components/demo-toggle/demo-toggle.js +1 -1
  95. package/components/description-list/description-list.css +18 -18
  96. package/components/description-list/description-list.js +1 -1
  97. package/components/divider/divider.css +24 -24
  98. package/components/divider/divider.js +1 -1
  99. package/components/drawer/drawer.js +1 -1
  100. package/components/embed/embed.css +6 -6
  101. package/components/embed/embed.js +1 -1
  102. package/components/empty-state/empty-state.css +27 -27
  103. package/components/empty-state/empty-state.js +1 -1
  104. package/components/feed/feed.css +12 -12
  105. package/components/feed/feed.js +1 -1
  106. package/components/field/field.css +28 -28
  107. package/components/field/field.js +1 -1
  108. package/components/field/field.test.js +1 -1
  109. package/components/fields/fields.css +5 -5
  110. package/components/fields/fields.js +1 -1
  111. package/components/grid/grid.css +5 -5
  112. package/components/grid/grid.js +1 -1
  113. package/components/heatmap/heatmap.css +63 -63
  114. package/components/heatmap/heatmap.js +1 -1
  115. package/components/icon/icon.css +12 -12
  116. package/components/icon/icon.js +1 -1
  117. package/components/image/image.css +14 -14
  118. package/components/image/image.js +1 -1
  119. package/components/index.js +11 -0
  120. package/components/inline-message/inline-message.a2ui.json +143 -0
  121. package/components/inline-message/inline-message.class.js +169 -0
  122. package/components/inline-message/inline-message.css +75 -0
  123. package/components/inline-message/inline-message.d.ts +31 -0
  124. package/components/inline-message/inline-message.examples.md +19 -0
  125. package/components/inline-message/inline-message.js +17 -0
  126. package/components/inline-message/inline-message.test.js +203 -0
  127. package/components/inline-message/inline-message.yaml +205 -0
  128. package/components/input/input.css +67 -67
  129. package/components/input/input.js +1 -1
  130. package/components/input/input.yaml +5 -4
  131. package/components/inspector/inspector.css +6 -6
  132. package/components/inspector/inspector.js +1 -1
  133. package/components/integration-card/integration-card.a2ui.json +268 -0
  134. package/components/integration-card/integration-card.class.js +410 -0
  135. package/components/integration-card/integration-card.css +169 -0
  136. package/components/integration-card/integration-card.d.ts +63 -0
  137. package/components/integration-card/integration-card.examples.md +41 -0
  138. package/components/integration-card/integration-card.js +17 -0
  139. package/components/integration-card/integration-card.test.js +306 -0
  140. package/components/integration-card/integration-card.yaml +280 -0
  141. package/components/kbd/kbd.css +32 -32
  142. package/components/kbd/kbd.js +1 -1
  143. package/components/link/link.css +12 -12
  144. package/components/link/link.js +1 -1
  145. package/components/list/list-item.yaml +2 -2
  146. package/components/list/list.css +8 -8
  147. package/components/list/list.js +1 -1
  148. package/components/list-window/list-window.a2ui.json +277 -0
  149. package/components/list-window/list-window.class.js +688 -0
  150. package/components/list-window/list-window.css +124 -0
  151. package/components/list-window/list-window.d.ts +84 -0
  152. package/components/list-window/list-window.examples.md +73 -0
  153. package/components/list-window/list-window.js +17 -0
  154. package/components/list-window/list-window.test.js +303 -0
  155. package/components/list-window/list-window.yaml +270 -0
  156. package/components/loading-overlay/loading-overlay.a2ui.json +176 -0
  157. package/components/loading-overlay/loading-overlay.class.js +203 -0
  158. package/components/loading-overlay/loading-overlay.css +81 -0
  159. package/components/loading-overlay/loading-overlay.d.ts +24 -0
  160. package/components/loading-overlay/loading-overlay.examples.md +50 -0
  161. package/components/loading-overlay/loading-overlay.js +17 -0
  162. package/components/loading-overlay/loading-overlay.test.js +257 -0
  163. package/components/loading-overlay/loading-overlay.yaml +260 -0
  164. package/components/menu/menu-divider.yaml +1 -1
  165. package/components/menu/menu-item.yaml +1 -1
  166. package/components/menu/menu.a2ui.json +3 -0
  167. package/components/menu/menu.css +8 -8
  168. package/components/menu/menu.js +1 -1
  169. package/components/menu/menu.yaml +7 -0
  170. package/components/modal/{class.js → modal.class.js} +12 -1
  171. package/components/modal/modal.css +54 -44
  172. package/components/modal/modal.js +1 -1
  173. package/components/nav/nav.css +40 -40
  174. package/components/nav/nav.js +1 -1
  175. package/components/nav-group/nav-group.css +52 -52
  176. package/components/nav-group/nav-group.js +1 -1
  177. package/components/nav-item/nav-item.css +44 -44
  178. package/components/nav-item/nav-item.js +1 -1
  179. package/components/noodles/noodles.css +31 -31
  180. package/components/noodles/noodles.js +1 -1
  181. package/components/option-card/option-card.css +69 -69
  182. package/components/option-card/option-card.js +1 -1
  183. package/components/otp-input/otp-input.css +30 -30
  184. package/components/otp-input/otp-input.js +1 -1
  185. package/components/page/page.css +18 -18
  186. package/components/page/page.js +1 -1
  187. package/components/pagination/pagination.css +61 -61
  188. package/components/pagination/pagination.js +1 -1
  189. package/components/pane/pane.css +57 -57
  190. package/components/pane/pane.js +1 -1
  191. package/components/pipeline-status/pipeline-status.css +65 -65
  192. package/components/pipeline-status/pipeline-status.js +1 -1
  193. package/components/popover/popover.a2ui.json +8 -1
  194. package/components/popover/popover.css +17 -17
  195. package/components/popover/popover.js +1 -1
  196. package/components/popover/popover.yaml +14 -1
  197. package/components/progress/progress.css +23 -23
  198. package/components/progress/progress.js +1 -1
  199. package/components/progress-row/progress-row.css +17 -17
  200. package/components/progress-row/progress-row.js +1 -1
  201. package/components/radio/radio.css +39 -39
  202. package/components/radio/radio.js +1 -1
  203. package/components/range/range.css +55 -55
  204. package/components/range/range.js +1 -1
  205. package/components/rating/rating.css +28 -28
  206. package/components/rating/rating.js +1 -1
  207. package/components/richtext/richtext.css +133 -133
  208. package/components/richtext/richtext.js +1 -1
  209. package/components/row/row.css +19 -19
  210. package/components/row/row.js +1 -1
  211. package/components/search/search.css +5 -5
  212. package/components/search/search.js +1 -1
  213. package/components/segment/segment.css +24 -24
  214. package/components/segment/segment.js +1 -1
  215. package/components/segmented/segmented.css +25 -25
  216. package/components/segmented/segmented.js +1 -1
  217. package/components/select/select.a2ui.json +58 -4
  218. package/components/select/{class.js → select.class.js} +415 -6
  219. package/components/select/select.css +242 -84
  220. package/components/select/select.d.ts +31 -1
  221. package/components/select/select.js +1 -1
  222. package/components/select/select.test.js +202 -0
  223. package/components/select/select.yaml +126 -5
  224. package/components/skeleton/skeleton.css +14 -14
  225. package/components/skeleton/skeleton.js +1 -1
  226. package/components/slider/slider.css +46 -46
  227. package/components/slider/slider.js +1 -1
  228. package/components/spinner/spinner.a2ui.json +198 -0
  229. package/components/spinner/spinner.class.js +99 -0
  230. package/components/spinner/spinner.css +221 -0
  231. package/components/spinner/spinner.d.ts +26 -0
  232. package/components/spinner/spinner.examples.md +26 -0
  233. package/components/spinner/spinner.js +17 -0
  234. package/components/spinner/spinner.test.js +272 -0
  235. package/components/spinner/spinner.yaml +238 -0
  236. package/components/stack/stack.css +11 -11
  237. package/components/stack/stack.js +1 -1
  238. package/components/stat/stat.css +25 -25
  239. package/components/step-progress/step-progress.css +20 -20
  240. package/components/step-progress/step-progress.js +1 -1
  241. package/components/stepper/stepper-item.yaml +1 -1
  242. package/components/stepper/stepper.css +29 -29
  243. package/components/stepper/stepper.js +1 -1
  244. package/components/stream/stream.css +12 -12
  245. package/components/stream/stream.js +1 -1
  246. package/components/swatch/swatch.css +68 -68
  247. package/components/swatch/swatch.js +1 -1
  248. package/components/swiper/swiper.css +57 -57
  249. package/components/swiper/swiper.js +1 -1
  250. package/components/switch/switch.css +52 -52
  251. package/components/switch/switch.js +1 -1
  252. package/components/table/table.css +163 -163
  253. package/components/table/table.js +1 -1
  254. package/components/table-toolbar/{class.js → table-toolbar.class.js} +1 -1
  255. package/components/table-toolbar/table-toolbar.css +32 -32
  256. package/components/table-toolbar/table-toolbar.js +1 -1
  257. package/components/tabs/tab.yaml +2 -2
  258. package/components/tabs/tabs.css +51 -51
  259. package/components/tabs/tabs.js +1 -1
  260. package/components/tag/tag.css +48 -48
  261. package/components/tag/tag.js +1 -1
  262. package/components/tags-input/tags-input.a2ui.json +337 -0
  263. package/components/tags-input/tags-input.class.js +776 -0
  264. package/components/tags-input/tags-input.css +201 -0
  265. package/components/tags-input/tags-input.d.ts +120 -0
  266. package/components/tags-input/tags-input.examples.md +92 -0
  267. package/components/tags-input/tags-input.js +17 -0
  268. package/components/tags-input/tags-input.test.js +368 -0
  269. package/components/tags-input/tags-input.yaml +367 -0
  270. package/components/text/text.css +44 -44
  271. package/components/text/text.js +1 -1
  272. package/components/textarea/textarea.a2ui.json +1 -1
  273. package/components/textarea/textarea.css +46 -46
  274. package/components/textarea/textarea.js +1 -1
  275. package/components/textarea/textarea.yaml +11 -8
  276. package/components/time-picker/time-picker.a2ui.json +267 -0
  277. package/components/time-picker/time-picker.class.js +693 -0
  278. package/components/time-picker/time-picker.css +122 -0
  279. package/components/time-picker/time-picker.d.ts +75 -0
  280. package/components/time-picker/time-picker.examples.md +35 -0
  281. package/components/time-picker/time-picker.js +17 -0
  282. package/components/time-picker/time-picker.test.js +287 -0
  283. package/components/time-picker/time-picker.yaml +256 -0
  284. package/components/timeline/timeline-item.yaml +2 -2
  285. package/components/timeline/{class.js → timeline.class.js} +1 -1
  286. package/components/timeline/timeline.css +50 -50
  287. package/components/timeline/timeline.js +1 -1
  288. package/components/toast/toast.css +58 -58
  289. package/components/toast/toast.js +1 -1
  290. package/components/toggle-group/toggle-group.css +6 -6
  291. package/components/toggle-group/toggle-group.js +1 -1
  292. package/components/toggle-group/toggle-option.yaml +1 -1
  293. package/components/toggle-scheme/toggle-scheme.css +2 -2
  294. package/components/toggle-scheme/toggle-scheme.js +1 -1
  295. package/components/toolbar/toolbar-group.yaml +1 -1
  296. package/components/toolbar/toolbar.css +17 -17
  297. package/components/toolbar/toolbar.js +1 -1
  298. package/components/tooltip/tooltip.css +2 -2
  299. package/components/tooltip/tooltip.js +1 -1
  300. package/components/tree/tree-item.yaml +1 -1
  301. package/components/tree/tree.css +37 -37
  302. package/components/tree/tree.js +1 -1
  303. package/components/upload/upload.css +49 -49
  304. package/components/upload/upload.js +1 -1
  305. package/dist/web-components.min.css +1 -1
  306. package/dist/web-components.min.js +146 -87
  307. package/package.json +3 -3
  308. package/styles/components.css +11 -0
  309. /package/components/accordion/{class.js → accordion.class.js} +0 -0
  310. /package/components/action-list/{class.js → action-list.class.js} +0 -0
  311. /package/components/agent-feedback-bar/{class.js → agent-feedback-bar.class.js} +0 -0
  312. /package/components/agent-questions/{class.js → agent-questions.class.js} +0 -0
  313. /package/components/agent-reasoning/{class.js → agent-reasoning.class.js} +0 -0
  314. /package/components/agent-suggestions/{class.js → agent-suggestions.class.js} +0 -0
  315. /package/components/avatar/{class.js → avatar.class.js} +0 -0
  316. /package/components/badge/{class.js → badge.class.js} +0 -0
  317. /package/components/block/{class.js → block.class.js} +0 -0
  318. /package/components/breadcrumb/{class.js → breadcrumb.class.js} +0 -0
  319. /package/components/button/{class.js → button.class.js} +0 -0
  320. /package/components/calendar-picker/{class.js → calendar-picker.class.js} +0 -0
  321. /package/components/card/{class.js → card.class.js} +0 -0
  322. /package/components/chart/{class.js → chart.class.js} +0 -0
  323. /package/components/chart-legend/{class.js → chart-legend.class.js} +0 -0
  324. /package/components/chat-thread/{class.js → chat-thread.class.js} +0 -0
  325. /package/components/check/{class.js → check.class.js} +0 -0
  326. /package/components/code/{class.js → code.class.js} +0 -0
  327. /package/components/col/{class.js → col.class.js} +0 -0
  328. /package/components/color-input/{class.js → color-input.class.js} +0 -0
  329. /package/components/color-picker/{class.js → color-picker.class.js} +0 -0
  330. /package/components/command/{class.js → command.class.js} +0 -0
  331. /package/components/demo-toggle/{class.js → demo-toggle.class.js} +0 -0
  332. /package/components/description-list/{class.js → description-list.class.js} +0 -0
  333. /package/components/divider/{class.js → divider.class.js} +0 -0
  334. /package/components/drawer/{class.js → drawer.class.js} +0 -0
  335. /package/components/embed/{class.js → embed.class.js} +0 -0
  336. /package/components/empty-state/{class.js → empty-state.class.js} +0 -0
  337. /package/components/feed/{class.js → feed.class.js} +0 -0
  338. /package/components/field/{class.js → field.class.js} +0 -0
  339. /package/components/fields/{class.js → fields.class.js} +0 -0
  340. /package/components/grid/{class.js → grid.class.js} +0 -0
  341. /package/components/heatmap/{class.js → heatmap.class.js} +0 -0
  342. /package/components/icon/{class.js → icon.class.js} +0 -0
  343. /package/components/image/{class.js → image.class.js} +0 -0
  344. /package/components/input/{class.js → input.class.js} +0 -0
  345. /package/components/inspector/{class.js → inspector.class.js} +0 -0
  346. /package/components/kbd/{class.js → kbd.class.js} +0 -0
  347. /package/components/link/{class.js → link.class.js} +0 -0
  348. /package/components/list/{class.js → list.class.js} +0 -0
  349. /package/components/menu/{class.js → menu.class.js} +0 -0
  350. /package/components/nav/{class.js → nav.class.js} +0 -0
  351. /package/components/nav-group/{class.js → nav-group.class.js} +0 -0
  352. /package/components/nav-item/{class.js → nav-item.class.js} +0 -0
  353. /package/components/noodles/{class.js → noodles.class.js} +0 -0
  354. /package/components/option-card/{class.js → option-card.class.js} +0 -0
  355. /package/components/otp-input/{class.js → otp-input.class.js} +0 -0
  356. /package/components/page/{class.js → page.class.js} +0 -0
  357. /package/components/pagination/{class.js → pagination.class.js} +0 -0
  358. /package/components/pane/{class.js → pane.class.js} +0 -0
  359. /package/components/pipeline-status/{class.js → pipeline-status.class.js} +0 -0
  360. /package/components/popover/{class.js → popover.class.js} +0 -0
  361. /package/components/progress/{class.js → progress.class.js} +0 -0
  362. /package/components/progress-row/{class.js → progress-row.class.js} +0 -0
  363. /package/components/radio/{class.js → radio.class.js} +0 -0
  364. /package/components/range/{class.js → range.class.js} +0 -0
  365. /package/components/rating/{class.js → rating.class.js} +0 -0
  366. /package/components/richtext/{class.js → richtext.class.js} +0 -0
  367. /package/components/row/{class.js → row.class.js} +0 -0
  368. /package/components/search/{class.js → search.class.js} +0 -0
  369. /package/components/segment/{class.js → segment.class.js} +0 -0
  370. /package/components/segmented/{class.js → segmented.class.js} +0 -0
  371. /package/components/skeleton/{class.js → skeleton.class.js} +0 -0
  372. /package/components/slider/{class.js → slider.class.js} +0 -0
  373. /package/components/stack/{class.js → stack.class.js} +0 -0
  374. /package/components/step-progress/{class.js → step-progress.class.js} +0 -0
  375. /package/components/stepper/{class.js → stepper.class.js} +0 -0
  376. /package/components/stream/{class.js → stream.class.js} +0 -0
  377. /package/components/swatch/{class.js → swatch.class.js} +0 -0
  378. /package/components/swiper/{class.js → swiper.class.js} +0 -0
  379. /package/components/switch/{class.js → switch.class.js} +0 -0
  380. /package/components/table/{class.js → table.class.js} +0 -0
  381. /package/components/tabs/{class.js → tabs.class.js} +0 -0
  382. /package/components/tag/{class.js → tag.class.js} +0 -0
  383. /package/components/text/{class.js → text.class.js} +0 -0
  384. /package/components/textarea/{class.js → textarea.class.js} +0 -0
  385. /package/components/toast/{class.js → toast.class.js} +0 -0
  386. /package/components/toggle-group/{class.js → toggle-group.class.js} +0 -0
  387. /package/components/toggle-scheme/{class.js → toggle-scheme.class.js} +0 -0
  388. /package/components/toolbar/{class.js → toolbar.class.js} +0 -0
  389. /package/components/tooltip/{class.js → tooltip.class.js} +0 -0
  390. /package/components/tree/{class.js → tree.class.js} +0 -0
  391. /package/components/upload/{class.js → upload.class.js} +0 -0
@@ -0,0 +1,776 @@
1
+ /**
2
+ * Non-side-effect class export for `<tags-input-ui>`.
3
+ *
4
+ * Importing this file gives you the class(es) without auto-registering the tag.
5
+ * Useful for test isolation, subclassing with tag-name override, or selective
6
+ * composition.
7
+ *
8
+ * The auto-register path stays at `@adia-ai/web-components/components/tags-input`
9
+ * (which imports this file + calls `defineIfFree()`).
10
+ *
11
+ * @see ../../USAGE.md#registration--auto-vs-explicit
12
+ */
13
+
14
+ /**
15
+ * `<tags-input-ui>` — SPEC-042.
16
+ *
17
+ * Free-form token / chip input. Type a value, press Enter (or the
18
+ * configured delimiter), and the typed value commits as a chip;
19
+ * Backspace from the empty inline input removes the last chip.
20
+ * Distinct from `<select-ui multiple>` (SPEC-040), which gates against
21
+ * a fixed options list — this primitive is for OPEN sets (labels,
22
+ * email recipients, keyword inputs).
23
+ *
24
+ * Per ADR-0025 the inline editor is a `contenteditable` surface; there
25
+ * is no `<input>` element wrapped inside. Form participation goes
26
+ * through `UIFormElement` + `ElementInternals`; the form value is a
27
+ * JSON-serialized string array under `name`.
28
+ *
29
+ * <tags-input-ui name="labels" placeholder="Add a label…"
30
+ * transform="lowercase" max="10"></tags-input-ui>
31
+ *
32
+ * <tags-input-ui id="to" name="to" placeholder="Email…"></tags-input-ui>
33
+ * <script>
34
+ * const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
35
+ * document.getElementById('to').validateFn = (value) => EMAIL_RE.test(value);
36
+ * </script>
37
+ */
38
+
39
+ import { UIFormElement } from '../../core/form.js';
40
+ import { untracked } from '../../core/signals.js';
41
+
42
+ let tagsInstanceSeq = 0;
43
+
44
+ function escapeRegExp(s) {
45
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
46
+ }
47
+
48
+ export class UITagsInput extends UIFormElement {
49
+ // §198-pattern: above-field labels handled by <field-ui>; the host
50
+ // does not render an inert label itself. Opt out of the base-class
51
+ // deprecation warning that targets per-control labels.
52
+ static labelDeprecated = false;
53
+
54
+ static requiredIcons = ['circle-notch'];
55
+
56
+ // NOTE: we intentionally exclude `value` from the reactive-property
57
+ // table even though UIFormElement declares it. `installProps()` runs
58
+ // `Object.defineProperty(this, 'value', …)` on the instance, which
59
+ // would clobber the array-shaped `get/set value` accessors below
60
+ // (instance descriptors win over prototype accessors). We re-implement
61
+ // the attribute-routing for `value` in `attributeChangedCallback`.
62
+ static get properties() {
63
+ const { value: _drop, ...rest } = UIFormElement.properties;
64
+ void _drop;
65
+ return {
66
+ ...rest,
67
+ placeholder: { type: String, default: 'Add tag…', reflect: true },
68
+ delimiter: { type: String, default: ',', reflect: true },
69
+ pasteSplit: { type: String, default: ',\n', reflect: true, attribute: 'paste-split' },
70
+ max: { type: Number, default: 0, reflect: true },
71
+ min: { type: Number, default: 0, reflect: true },
72
+ minLength: { type: Number, default: 1, reflect: true, attribute: 'min-length' },
73
+ maxLength: { type: Number, default: 0, reflect: true, attribute: 'max-length' },
74
+ unique: { type: Boolean, default: true, reflect: true },
75
+ transform: { type: String, default: '', reflect: true },
76
+ size: { type: String, default: 'md', reflect: true },
77
+ };
78
+ }
79
+
80
+ static get observedAttributes() {
81
+ // Re-add `value` so attribute-side writes route through our setter.
82
+ const base = super.observedAttributes;
83
+ return base.includes('value') ? base : [...base, 'value'];
84
+ }
85
+
86
+ attributeChangedCallback(name, oldV, newV) {
87
+ if (name === 'value') {
88
+ // String → array routing for declarative `value="…"` writes.
89
+ if (newV != null && oldV !== newV) {
90
+ const parsed = this.#parseValueAttr(newV);
91
+ if (parsed) this.value = parsed;
92
+ }
93
+ return;
94
+ }
95
+ super.attributeChangedCallback(name, oldV, newV);
96
+ }
97
+
98
+ static template = () => null;
99
+
100
+ // ── Instance state ──
101
+ #value = []; // string[] — the committed tokens
102
+ #suggestions = []; // string[] — autocomplete hints
103
+ #validateFn = null; // (value, index) => boolean|Promise<boolean>
104
+ #activeSuggestion = -1;
105
+ #suppressInput = false; // guard contenteditable resyncs during programmatic ops
106
+ #pendingValidationId = 0; // async-validator generation counter
107
+
108
+ // ── Refs ──
109
+ #chipList = null;
110
+ #inputEl = null;
111
+ #suggestEl = null;
112
+ #spinnerEl = null;
113
+ #instanceId = `tags-input-${++tagsInstanceSeq}`;
114
+
115
+ // ── Stable handler refs (so removeEventListener finds them) ──
116
+ #onInputEvent = () => this.#handleInput();
117
+ #onKeydown = (e) => this.#handleKeydown(e);
118
+ #onPaste = (e) => this.#handlePaste(e);
119
+ #onChipRemove = (e) => this.#handleChipRemove(e);
120
+ #onHostMousedown = (e) => this.#handleHostMousedown(e);
121
+ #onSuggestionClick = (e) => this.#handleSuggestionClick(e);
122
+
123
+ // ── Lifecycle ──
124
+
125
+ connected() {
126
+ super.connected();
127
+ // Read `value` attribute as JSON array (when set declaratively).
128
+ if (!this.#value.length && this.hasAttribute('value')) {
129
+ const parsed = this.#parseValueAttr(this.getAttribute('value'));
130
+ if (parsed) this.#value = parsed;
131
+ }
132
+ this.#stampShell();
133
+ this.#renderChips();
134
+ this.#syncStateAttrs();
135
+ this.#syncFormValue();
136
+ }
137
+
138
+ disconnected() {
139
+ super.disconnected();
140
+ this.#teardownListeners();
141
+ this.#chipList = null;
142
+ this.#inputEl = null;
143
+ this.#suggestEl = null;
144
+ this.#spinnerEl = null;
145
+ }
146
+
147
+ render() {
148
+ if (!this.#inputEl) return;
149
+ // Disabled / readonly toggle the editable surface.
150
+ if (this.disabled || this.readonly) {
151
+ this.#inputEl.contentEditable = 'false';
152
+ } else {
153
+ this.#inputEl.contentEditable = 'plaintext-only';
154
+ }
155
+ this.#inputEl.setAttribute('data-placeholder', this.placeholder || '');
156
+ // ARIA: combobox when suggestions wired, group otherwise.
157
+ if (this.#suggestions.length) {
158
+ this.setAttribute('role', 'combobox');
159
+ this.setAttribute('aria-haspopup', 'listbox');
160
+ this.setAttribute('aria-expanded', String(!!this.hasAttribute('suggesting')));
161
+ } else {
162
+ this.setAttribute('role', 'group');
163
+ this.removeAttribute('aria-haspopup');
164
+ this.removeAttribute('aria-expanded');
165
+ }
166
+ if (!this.hasAttribute('aria-label')) {
167
+ this.setAttribute('aria-label', this.placeholder || 'Tags');
168
+ }
169
+ }
170
+
171
+ // ── Public properties ──
172
+
173
+ /** Current token list. Setting replaces the entire list. */
174
+ set value(list) {
175
+ untracked(() => {
176
+ const next = Array.isArray(list)
177
+ ? list.map((s) => String(s ?? ''))
178
+ : (typeof list === 'string' ? this.#parseValueAttr(list) || [] : []);
179
+ const before = this.#value.slice();
180
+ this.#value = next;
181
+ this.#renderChips();
182
+ this.#syncStateAttrs();
183
+ this.#syncFormValue();
184
+ this.#announceChange(before, next, 'programmatic');
185
+ });
186
+ }
187
+
188
+ get value() { return this.#value.slice(); }
189
+
190
+ set suggestions(list) {
191
+ untracked(() => {
192
+ this.#suggestions = Array.isArray(list) ? list.map((s) => String(s ?? '')) : [];
193
+ this.#renderSuggestions();
194
+ this.render();
195
+ });
196
+ }
197
+
198
+ get suggestions() { return this.#suggestions.slice(); }
199
+
200
+ set validateFn(fn) {
201
+ this.#validateFn = typeof fn === 'function' ? fn : null;
202
+ }
203
+
204
+ get validateFn() { return this.#validateFn; }
205
+
206
+ // ── Public methods ──
207
+
208
+ /** Commit a token programmatically. Returns true on success. */
209
+ addToken(text) {
210
+ return this.#commitToken(String(text ?? ''), 'programmatic');
211
+ }
212
+
213
+ /** Remove a token by index. */
214
+ removeToken(index) {
215
+ const i = Number(index);
216
+ if (!Number.isInteger(i) || i < 0 || i >= this.#value.length) return;
217
+ this.#removeByIndex(i, 'programmatic');
218
+ }
219
+
220
+ /** Clear all tokens. */
221
+ clear() {
222
+ if (this.#value.length === 0) return;
223
+ const before = this.#value.slice();
224
+ this.#value = [];
225
+ this.#renderChips();
226
+ this.#syncStateAttrs();
227
+ this.#syncFormValue();
228
+ this.#announceChange(before, [], 'programmatic');
229
+ }
230
+
231
+ focus() { this.#inputEl?.focus(); }
232
+
233
+ // ── Shell ──
234
+
235
+ #stampShell() {
236
+ if (this.querySelector(':scope > [data-chip-list]')) {
237
+ this.#queryRefs();
238
+ this.#bindListeners();
239
+ return;
240
+ }
241
+ const suggestId = `${this.#instanceId}-suggestions`;
242
+ const inputId = `${this.#instanceId}-input`;
243
+ this.innerHTML = `
244
+ <div data-chip-list role="list" aria-label="Tags"></div>
245
+ <span data-inline-input
246
+ id="${inputId}"
247
+ contenteditable="plaintext-only"
248
+ role="searchbox"
249
+ tabindex="0"
250
+ spellcheck="false"
251
+ aria-autocomplete="list"
252
+ aria-controls="${suggestId}"
253
+ data-placeholder="${this.placeholder || ''}"
254
+ data-empty></span>
255
+ <span data-spinner hidden aria-hidden="true">
256
+ <icon-ui name="circle-notch"></icon-ui>
257
+ </span>
258
+ <div data-suggestions id="${suggestId}" role="listbox" hidden></div>
259
+ `;
260
+ this.#queryRefs();
261
+ this.#bindListeners();
262
+ }
263
+
264
+ #queryRefs() {
265
+ this.#chipList = this.querySelector(':scope > [data-chip-list]');
266
+ this.#inputEl = this.querySelector(':scope > [data-inline-input]');
267
+ this.#spinnerEl = this.querySelector(':scope > [data-spinner]');
268
+ this.#suggestEl = this.querySelector(':scope > [data-suggestions]');
269
+ }
270
+
271
+ #bindListeners() {
272
+ if (this.#inputEl) {
273
+ this.#inputEl.addEventListener('input', this.#onInputEvent);
274
+ this.#inputEl.addEventListener('keydown', this.#onKeydown);
275
+ this.#inputEl.addEventListener('paste', this.#onPaste);
276
+ }
277
+ if (this.#chipList) {
278
+ this.#chipList.addEventListener('remove', this.#onChipRemove);
279
+ }
280
+ if (this.#suggestEl) {
281
+ this.#suggestEl.addEventListener('click', this.#onSuggestionClick);
282
+ }
283
+ this.addEventListener('mousedown', this.#onHostMousedown);
284
+ }
285
+
286
+ #teardownListeners() {
287
+ if (this.#inputEl) {
288
+ this.#inputEl.removeEventListener('input', this.#onInputEvent);
289
+ this.#inputEl.removeEventListener('keydown', this.#onKeydown);
290
+ this.#inputEl.removeEventListener('paste', this.#onPaste);
291
+ }
292
+ if (this.#chipList) {
293
+ this.#chipList.removeEventListener('remove', this.#onChipRemove);
294
+ }
295
+ if (this.#suggestEl) {
296
+ this.#suggestEl.removeEventListener('click', this.#onSuggestionClick);
297
+ }
298
+ this.removeEventListener('mousedown', this.#onHostMousedown);
299
+ }
300
+
301
+ // ── Render chips ──
302
+
303
+ #renderChips() {
304
+ if (!this.#chipList) return;
305
+ // Reconcile in place: existing chips that still match keep their nodes
306
+ // (so the user's :hover / :focus / animation states don't flicker on
307
+ // each typed-character render); excess chips get dropped; new chips
308
+ // append at the end.
309
+ const existing = Array.from(this.#chipList.querySelectorAll(':scope > tag-ui'));
310
+ for (let i = 0; i < this.#value.length; i++) {
311
+ const v = this.#value[i];
312
+ let chip = existing[i];
313
+ if (!chip) {
314
+ chip = document.createElement('tag-ui');
315
+ chip.setAttribute('role', 'listitem');
316
+ this.#chipList.appendChild(chip);
317
+ }
318
+ chip.setAttribute('text', v);
319
+ if (this.readonly || this.disabled) {
320
+ chip.removeAttribute('removable');
321
+ } else {
322
+ chip.setAttribute('removable', '');
323
+ }
324
+ chip.setAttribute('data-index', String(i));
325
+ }
326
+ // Drop overflow chips.
327
+ for (let i = this.#value.length; i < existing.length; i++) {
328
+ existing[i].remove();
329
+ }
330
+ }
331
+
332
+ #renderSuggestions() {
333
+ if (!this.#suggestEl) return;
334
+ const typed = this.#inputEl ? (this.#inputEl.textContent || '').trim().toLowerCase() : '';
335
+ const list = this.#suggestions
336
+ .filter((s) => !this.#value.includes(s))
337
+ .filter((s) => !typed || s.toLowerCase().includes(typed));
338
+ this.#suggestEl.replaceChildren();
339
+ list.forEach((s, idx) => {
340
+ const item = document.createElement('div');
341
+ item.setAttribute('role', 'option');
342
+ item.setAttribute('id', `${this.#instanceId}-opt-${idx}`);
343
+ item.dataset.value = s;
344
+ item.textContent = s;
345
+ this.#suggestEl.appendChild(item);
346
+ });
347
+ const shouldShow = this.#suggestions.length > 0 && list.length > 0 && this.hasAttribute('editing');
348
+ this.#suggestEl.hidden = !shouldShow;
349
+ this.toggleAttribute('suggesting', shouldShow);
350
+ if (shouldShow) {
351
+ this.setAttribute('aria-expanded', 'true');
352
+ } else {
353
+ this.removeAttribute('aria-expanded');
354
+ }
355
+ // Reset active option when the rendered list changes.
356
+ this.#activeSuggestion = -1;
357
+ this.#inputEl?.removeAttribute('aria-activedescendant');
358
+ for (const node of this.#suggestEl.children) node.removeAttribute('data-active');
359
+ }
360
+
361
+ // ── State attrs ──
362
+
363
+ #syncStateAttrs() {
364
+ this.toggleAttribute('populated', this.#value.length > 0);
365
+ if (!this.#inputEl) return;
366
+ const empty = !(this.#inputEl.textContent || '').length;
367
+ this.#inputEl.toggleAttribute('data-empty', empty);
368
+ this.toggleAttribute('editing', !empty);
369
+ }
370
+
371
+ #syncFormValue() {
372
+ // Empty array → empty FormData entry (per spec §11). Non-empty → JSON.
373
+ const payload = this.#value.length ? JSON.stringify(this.#value) : '';
374
+ super.syncValue(payload);
375
+ // Required-validation: empty array fails when [required].
376
+ if (this.required && this.#value.length === 0) {
377
+ this.internals.setValidity(
378
+ { valueMissing: true },
379
+ this.getAttribute('data-msg-required') || 'Please add at least one tag.',
380
+ this,
381
+ );
382
+ } else if (this.min > 0 && this.#value.length < this.min) {
383
+ this.internals.setValidity(
384
+ { tooShort: true },
385
+ this.getAttribute('data-msg-min')
386
+ || `Please add at least ${this.min} tag${this.min === 1 ? '' : 's'}.`,
387
+ this,
388
+ );
389
+ } else {
390
+ this.internals.setValidity({});
391
+ }
392
+ }
393
+
394
+ // ── Input handling ──
395
+
396
+ #handleInput() {
397
+ if (this.#suppressInput) return;
398
+ const text = (this.#inputEl.textContent || '');
399
+ // Delimiter-driven commit (when the user types the delimiter inline,
400
+ // commit the substring before it and leave whatever's after as the new
401
+ // typed buffer). Skipped when delimiter is the `enter` sentinel.
402
+ if (this.delimiter && this.delimiter !== 'enter' && text.includes(this.delimiter)) {
403
+ const parts = text.split(this.delimiter);
404
+ const trailing = parts.pop();
405
+ let anyCommitted = false;
406
+ for (const fragment of parts) {
407
+ if (fragment.length) {
408
+ const committed = this.#commitToken(fragment, 'delimiter');
409
+ if (committed) anyCommitted = true;
410
+ }
411
+ }
412
+ // Anything committed (or not), reset the contenteditable to the trailing
413
+ // remainder so subsequent keystrokes append after the delimiter.
414
+ this.#suppressInput = true;
415
+ this.#inputEl.textContent = trailing || '';
416
+ this.#suppressInput = false;
417
+ this.#placeCaretAtEnd();
418
+ // Fall through to fire `input` with the new query state.
419
+ this.#syncStateAttrs();
420
+ this.#renderSuggestions();
421
+ this.dispatchEvent(new CustomEvent('input', {
422
+ bubbles: true,
423
+ detail: { query: trailing || '' },
424
+ }));
425
+ return;
426
+ }
427
+ this.#syncStateAttrs();
428
+ this.#renderSuggestions();
429
+ this.dispatchEvent(new CustomEvent('input', {
430
+ bubbles: true,
431
+ detail: { query: text },
432
+ }));
433
+ }
434
+
435
+ #handleKeydown(e) {
436
+ const k = e.key;
437
+ if (this.disabled || this.readonly) return;
438
+ const text = this.#inputEl ? (this.#inputEl.textContent || '') : '';
439
+ if (k === 'Enter') {
440
+ e.preventDefault();
441
+ // If a suggestion is highlighted, commit it; otherwise commit typed.
442
+ if (this.#activeSuggestion >= 0) {
443
+ const opt = this.#suggestEl?.children[this.#activeSuggestion];
444
+ if (opt) {
445
+ this.#commitToken(opt.dataset.value || '', 'suggestion');
446
+ return;
447
+ }
448
+ }
449
+ if (text) this.#commitToken(text, 'keyboard');
450
+ return;
451
+ }
452
+ if (k === 'Backspace' && text.length === 0) {
453
+ if (this.#value.length > 0) {
454
+ e.preventDefault();
455
+ this.#removeByIndex(this.#value.length - 1, 'backspace');
456
+ }
457
+ return;
458
+ }
459
+ if (k === 'ArrowDown') {
460
+ if (!this.hasAttribute('suggesting')) return;
461
+ e.preventDefault();
462
+ this.#moveActiveSuggestion(1);
463
+ return;
464
+ }
465
+ if (k === 'ArrowUp') {
466
+ if (!this.hasAttribute('suggesting')) return;
467
+ e.preventDefault();
468
+ this.#moveActiveSuggestion(-1);
469
+ return;
470
+ }
471
+ if (k === 'Escape') {
472
+ if (this.hasAttribute('suggesting')) {
473
+ e.preventDefault();
474
+ this.#closeSuggestions();
475
+ }
476
+ }
477
+ }
478
+
479
+ #handlePaste(e) {
480
+ if (this.disabled || this.readonly) return;
481
+ const raw = e.clipboardData?.getData('text/plain') || '';
482
+ if (!raw) return;
483
+ if (!this.pasteSplit) {
484
+ // No splitting — let the default contenteditable paste happen.
485
+ return;
486
+ }
487
+ e.preventDefault();
488
+ const splitRE = new RegExp(`[${escapeRegExp(this.pasteSplit)}]+`);
489
+ const fragments = raw.split(splitRE).map((s) => s.trim()).filter(Boolean);
490
+ if (fragments.length === 0) return;
491
+ if (fragments.length === 1) {
492
+ // Single fragment: insert as plain text rather than auto-committing —
493
+ // pasting "foo" into the input should let the user keep typing.
494
+ document.execCommand('insertText', false, fragments[0]);
495
+ this.#syncStateAttrs();
496
+ this.#renderSuggestions();
497
+ return;
498
+ }
499
+ // Multi-fragment paste — commit each, leave the input empty afterwards.
500
+ let lastSurplus = '';
501
+ for (let i = 0; i < fragments.length; i++) {
502
+ const f = fragments[i];
503
+ const ok = this.#commitToken(f, 'paste');
504
+ // If the LAST fragment was rejected (e.g. count cap), surface it as
505
+ // the new typed buffer so the user can correct it.
506
+ if (!ok && i === fragments.length - 1) lastSurplus = f;
507
+ }
508
+ if (lastSurplus) {
509
+ this.#suppressInput = true;
510
+ this.#inputEl.textContent = lastSurplus;
511
+ this.#suppressInput = false;
512
+ this.#placeCaretAtEnd();
513
+ this.#syncStateAttrs();
514
+ this.#renderSuggestions();
515
+ }
516
+ }
517
+
518
+ #handleChipRemove(e) {
519
+ const chip = e.target.closest('tag-ui[data-index]');
520
+ if (!chip) return;
521
+ // Stop the bubbling `remove` from <tag-ui> — tag-ui auto-removes the node
522
+ // itself on dispatch, but we own the list. Rebuild to keep the source of
523
+ // truth in `#value`.
524
+ e.stopPropagation();
525
+ const idx = Number(chip.dataset.index);
526
+ if (!Number.isInteger(idx)) return;
527
+ this.#removeByIndex(idx, 'chip-click');
528
+ }
529
+
530
+ #handleHostMousedown(e) {
531
+ // Click anywhere on the host that isn't a chip's remove button focuses
532
+ // the inline input. Mirrors gmail-style "tap-on-recipient-row" UX.
533
+ if (this.disabled) return;
534
+ const onRemove = e.target.closest('[slot="dismiss"], tag-ui');
535
+ const onSuggestion = e.target.closest('[role="option"]');
536
+ if (onRemove || onSuggestion) return;
537
+ if (e.target === this.#inputEl) return;
538
+ e.preventDefault(); // don't shift focus from the input mid-type
539
+ this.#inputEl?.focus();
540
+ this.#placeCaretAtEnd();
541
+ }
542
+
543
+ #handleSuggestionClick(e) {
544
+ const opt = e.target.closest('[role="option"]');
545
+ if (!opt) return;
546
+ this.#commitToken(opt.dataset.value || '', 'suggestion');
547
+ }
548
+
549
+ // ── Commit / remove ──
550
+
551
+ #applyTransform(raw) {
552
+ const t = this.transform;
553
+ if (t === 'lowercase') return raw.toLowerCase();
554
+ if (t === 'trim') return raw.trim();
555
+ if (t === 'strip-spaces') return raw.replace(/\s+/g, '');
556
+ return raw;
557
+ }
558
+
559
+ #commitToken(rawInput, source) {
560
+ if (this.disabled || this.readonly) return false;
561
+ // Always strip surrounding whitespace before transform — typed strings
562
+ // from the contenteditable surface routinely carry trailing spaces.
563
+ let candidate = this.#applyTransform(rawInput.trim());
564
+ if (!candidate) {
565
+ // Empty after transform — silently drop without firing invalid.
566
+ this.#clearInputBuffer();
567
+ return false;
568
+ }
569
+
570
+ // ── built-in validators ──
571
+ if (candidate.length < this.minLength) {
572
+ this.#fireInvalid(candidate, 'too-short');
573
+ return false;
574
+ }
575
+ if (this.maxLength > 0 && candidate.length > this.maxLength) {
576
+ this.#fireInvalid(candidate, 'too-long');
577
+ return false;
578
+ }
579
+ if (this.unique && this.#value.includes(candidate)) {
580
+ // Silent dedup per spec §A2UI rules — clear buffer, no invalid event.
581
+ this.#clearInputBuffer();
582
+ return false;
583
+ }
584
+ if (this.max > 0 && this.#value.length >= this.max) {
585
+ this.#fireInvalid(candidate, 'max');
586
+ return false;
587
+ }
588
+
589
+ // commit event (cancelable)
590
+ const commitEvent = new CustomEvent('commit', {
591
+ bubbles: true,
592
+ cancelable: true,
593
+ detail: { value: candidate, accepted: true },
594
+ });
595
+ const accepted = this.dispatchEvent(commitEvent);
596
+ if (!accepted) {
597
+ this.#fireInvalid(candidate, 'validator');
598
+ return false;
599
+ }
600
+
601
+ // ── consumer-supplied validateFn (sync or async) ──
602
+ if (this.#validateFn) {
603
+ try {
604
+ const result = this.#validateFn(candidate, this.#value.length);
605
+ if (result && typeof result.then === 'function') {
606
+ // Async — flip to [validating], resolve later.
607
+ this.#runAsyncValidation(candidate, result, source);
608
+ return false; // not yet committed
609
+ }
610
+ if (result === false) {
611
+ this.#fireInvalid(candidate, 'validator');
612
+ return false;
613
+ }
614
+ } catch (err) {
615
+ this.#fireInvalid(candidate, 'validator');
616
+ return false;
617
+ }
618
+ }
619
+
620
+ this.#applyCommit(candidate, source);
621
+ return true;
622
+ }
623
+
624
+ #runAsyncValidation(candidate, promise, source) {
625
+ const id = ++this.#pendingValidationId;
626
+ this.setAttribute('validating', '');
627
+ if (this.#spinnerEl) this.#spinnerEl.hidden = false;
628
+ promise.then(
629
+ (ok) => {
630
+ if (id !== this.#pendingValidationId || !this.isConnected) return;
631
+ this.removeAttribute('validating');
632
+ if (this.#spinnerEl) this.#spinnerEl.hidden = true;
633
+ if (ok === false) {
634
+ this.#fireInvalid(candidate, 'validator');
635
+ return;
636
+ }
637
+ this.#applyCommit(candidate, source);
638
+ },
639
+ () => {
640
+ if (id !== this.#pendingValidationId || !this.isConnected) return;
641
+ this.removeAttribute('validating');
642
+ if (this.#spinnerEl) this.#spinnerEl.hidden = true;
643
+ this.#fireInvalid(candidate, 'validator');
644
+ },
645
+ );
646
+ }
647
+
648
+ #applyCommit(token, source) {
649
+ const before = this.#value.slice();
650
+ this.#value = [...this.#value, token];
651
+ this.#renderChips();
652
+ this.#clearInputBuffer();
653
+ this.#syncStateAttrs();
654
+ this.#syncFormValue();
655
+ this.#announceChange(before, this.#value, source);
656
+ // Keep focus on the inline input post-commit (spec §5 focus model).
657
+ if (source !== 'programmatic') this.#inputEl?.focus();
658
+ }
659
+
660
+ #removeByIndex(index, source) {
661
+ if (index < 0 || index >= this.#value.length) return;
662
+ const before = this.#value.slice();
663
+ const next = before.slice();
664
+ next.splice(index, 1);
665
+ this.#value = next;
666
+ this.#renderChips();
667
+ this.#syncStateAttrs();
668
+ this.#syncFormValue();
669
+ this.#announceChange(before, next, source);
670
+ if (source !== 'programmatic') this.#inputEl?.focus();
671
+ }
672
+
673
+ #announceChange(before, after, source) {
674
+ // Compute added / removed against `before` snapshot.
675
+ const beforeSet = new Set(before);
676
+ const afterSet = new Set(after);
677
+ const added = after.filter((t) => !beforeSet.has(t));
678
+ const removed = before.filter((t) => !afterSet.has(t));
679
+ if (added.length === 0 && removed.length === 0) return;
680
+ this.dispatchEvent(new CustomEvent('change', {
681
+ bubbles: true,
682
+ detail: { value: after.slice(), added, removed, source },
683
+ }));
684
+ }
685
+
686
+ #fireInvalid(value, reason) {
687
+ this.dispatchEvent(new CustomEvent('invalid', {
688
+ bubbles: true,
689
+ detail: { value, reason },
690
+ }));
691
+ }
692
+
693
+ #clearInputBuffer() {
694
+ if (!this.#inputEl) return;
695
+ this.#suppressInput = true;
696
+ this.#inputEl.textContent = '';
697
+ this.#suppressInput = false;
698
+ this.#inputEl.setAttribute('data-empty', '');
699
+ this.removeAttribute('editing');
700
+ this.#renderSuggestions();
701
+ }
702
+
703
+ #placeCaretAtEnd() {
704
+ if (!this.#inputEl || !this.#inputEl.isConnected) return;
705
+ try {
706
+ const sel = window.getSelection();
707
+ const range = document.createRange();
708
+ range.selectNodeContents(this.#inputEl);
709
+ range.collapse(false);
710
+ sel.removeAllRanges();
711
+ sel.addRange(range);
712
+ } catch { /* happy-dom may not support selection — ignore */ }
713
+ }
714
+
715
+ // ── Suggestions navigation ──
716
+
717
+ #moveActiveSuggestion(dir) {
718
+ if (!this.#suggestEl) return;
719
+ const opts = Array.from(this.#suggestEl.querySelectorAll('[role="option"]'));
720
+ if (!opts.length) return;
721
+ if (this.#activeSuggestion < 0) {
722
+ this.#activeSuggestion = dir > 0 ? 0 : opts.length - 1;
723
+ } else {
724
+ this.#activeSuggestion = (this.#activeSuggestion + dir + opts.length) % opts.length;
725
+ }
726
+ for (const node of opts) node.removeAttribute('data-active');
727
+ const active = opts[this.#activeSuggestion];
728
+ active.setAttribute('data-active', '');
729
+ active.scrollIntoView({ block: 'nearest' });
730
+ this.#inputEl?.setAttribute('aria-activedescendant', active.id);
731
+ }
732
+
733
+ #closeSuggestions() {
734
+ this.removeAttribute('suggesting');
735
+ if (this.#suggestEl) this.#suggestEl.hidden = true;
736
+ this.#activeSuggestion = -1;
737
+ this.#inputEl?.removeAttribute('aria-activedescendant');
738
+ this.removeAttribute('aria-expanded');
739
+ }
740
+
741
+ // ── Value attribute parsing ──
742
+
743
+ #parseValueAttr(raw) {
744
+ if (raw == null) return null;
745
+ const s = String(raw).trim();
746
+ if (!s) return [];
747
+ try {
748
+ const parsed = JSON.parse(s);
749
+ if (Array.isArray(parsed)) return parsed.map((t) => String(t ?? ''));
750
+ } catch { /* fall through to comma-fallback */ }
751
+ // Comma-fallback: be charitable to consumers who pass "a,b,c" — but
752
+ // emit `invalid` so the A2UI authoring contract surfaces the smell.
753
+ if (s.includes(',')) {
754
+ this.#fireInvalid(s, 'validator');
755
+ return s.split(',').map((t) => t.trim()).filter(Boolean);
756
+ }
757
+ return [s];
758
+ }
759
+
760
+ // ── Form value sync override ──
761
+
762
+ syncValue(val) {
763
+ // Subclasses that override `value` setter route through `#syncFormValue`,
764
+ // which is the canonical path. Direct calls from the base class
765
+ // (e.g. attributeChangedCallback for `value` attribute writes) land
766
+ // here — parse JSON, reroute through `set value`.
767
+ if (typeof val === 'string') {
768
+ const parsed = this.#parseValueAttr(val);
769
+ if (parsed) {
770
+ this.value = parsed;
771
+ return;
772
+ }
773
+ }
774
+ super.syncValue(val);
775
+ }
776
+ }