@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,861 @@
1
+ /**
2
+ * Non-side-effect class export for `<combobox-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/combobox`
9
+ * (which imports this file + calls `defineIfFree()`).
10
+ *
11
+ * @see ../../USAGE.md#registration--auto-vs-explicit
12
+ */
13
+
14
+ /**
15
+ * `<combobox-ui>` — SPEC-036.
16
+ *
17
+ * Typeahead-filterable single-select per WAI-APG "Combobox With List
18
+ * Autocomplete (manual selection)". Constrained-choice by default:
19
+ * `value` must come from the options list. `[free-text]` opts into the
20
+ * autocomplete-style hybrid; `[creatable]` adds a create-flow on top.
21
+ *
22
+ * No native `<input>` wrap (per ADR-0025). The host carries
23
+ * `role="combobox"` and the editable surface is a `contenteditable`
24
+ * span. Form participation via `UIFormElement` + `ElementInternals`.
25
+ *
26
+ * <combobox-ui name="country" placeholder="Select country…">
27
+ * <option value="us">United States</option>
28
+ * <option value="gb">United Kingdom</option>
29
+ * </combobox-ui>
30
+ *
31
+ * <combobox-ui id="member" placeholder="Search members…"></combobox-ui>
32
+ * <script>
33
+ * document.getElementById('member').addEventListener('input', async e => {
34
+ * const res = await fetch(`/api/members?q=${e.detail.query}`);
35
+ * e.target.options = await res.json();
36
+ * });
37
+ * </script>
38
+ */
39
+
40
+ import { UIFormElement } from '../../core/form.js';
41
+ import { anchorPopover } from '../../core/anchor.js';
42
+ import { untracked } from '../../core/signals.js';
43
+
44
+ function escapeHTML(s) {
45
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
46
+ }
47
+
48
+ function escapeRegExp(s) {
49
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
50
+ }
51
+
52
+ let cbInstanceSeq = 0;
53
+
54
+ export class UICombobox extends UIFormElement {
55
+ // `label` here is first-class — rendered above the field with proper
56
+ // `aria-labelledby` wiring on the editable surface. Opt out of the
57
+ // base-class deprecation warning that targets inert above-field labels.
58
+ static labelDeprecated = false;
59
+
60
+ static requiredIcons = ['caret-down', 'x-circle', 'check'];
61
+
62
+ static properties = {
63
+ ...UIFormElement.properties,
64
+ placeholder: { type: String, default: 'Select...', reflect: true },
65
+ label: { type: String, default: '', reflect: true },
66
+ open: { type: Boolean, default: false, reflect: true },
67
+ freeText: { type: Boolean, default: false, reflect: true, attribute: 'free-text' },
68
+ creatable: { type: Boolean, default: false, reflect: true },
69
+ clearable: { type: Boolean, default: false, reflect: true },
70
+ filterMode: { type: String, default: 'substring', reflect: true, attribute: 'filter-mode' },
71
+ loading: { type: Boolean, default: false, reflect: true },
72
+ maxOptions: { type: Number, default: 100, reflect: true, attribute: 'max-options' },
73
+ highlightMatch: { type: Boolean, default: true, reflect: true, attribute: 'highlight-match' },
74
+ };
75
+
76
+ static template = () => null;
77
+
78
+ // ── Instance state ──
79
+ #options = [];
80
+ #lastCommittedValue = '';
81
+ #query = '';
82
+ #activeIndex = -1;
83
+ #suppressInput = false; // guard contenteditable re-syncs during programmatic updates
84
+
85
+ // ── Refs ──
86
+ #inputEl = null;
87
+ #labelEl = null;
88
+ #listbox = null;
89
+ #emptyEl = null;
90
+ #loadingEl = null;
91
+ #footerEl = null;
92
+ #clearBtn = null;
93
+ #suffixEl = null;
94
+ #anchorCleanup = null;
95
+ #rafId = null;
96
+ #instanceId = `combobox-${++cbInstanceSeq}`;
97
+
98
+ // ── Stable handler refs (so removeEventListener finds them) ──
99
+ #onInputEvent = () => this.#handleInput();
100
+ #onKeydown = (e) => this.#handleKeydown(e);
101
+ #onFocus = () => this.#handleFocus();
102
+ #onBlur = (e) => this.#handleBlur(e);
103
+ #onOptionClick = (e) => this.#handleOptionClick(e);
104
+ #onOptionMouseEnter = (e) => this.#handleOptionMouseEnter(e);
105
+ #onClearClick = (e) => this.#handleClearClick(e);
106
+ #onSuffixClick = (e) => this.#handleSuffixClick(e);
107
+ #onOutside = (e) => this.#handleOutside(e);
108
+
109
+ // ── Lifecycle ──
110
+
111
+ connected() {
112
+ super.connected();
113
+ // Host carries presentation. role="combobox" + ARIA expansion state
114
+ // live on the inner [data-input] per WAI-APG combobox-list-autocomplete
115
+ // (the editable surface IS the combobox; the host wraps the input +
116
+ // popover into one form-bearing custom element).
117
+
118
+ // Parse declarative <option> / <optgroup> children once on connect,
119
+ // unless options were already set programmatically.
120
+ if (this.#options.length === 0) {
121
+ this.#parseOptions();
122
+ }
123
+
124
+ this.#lastCommittedValue = this.value || '';
125
+
126
+ this.#stampShell();
127
+ this.#renderOptions();
128
+ this.#syncInputDisplay();
129
+ }
130
+
131
+ disconnected() {
132
+ super.disconnected();
133
+ this.#teardownListeners();
134
+ this.#anchorCleanup?.();
135
+ this.#anchorCleanup = null;
136
+ if (this.#rafId != null) {
137
+ cancelAnimationFrame(this.#rafId);
138
+ this.#rafId = null;
139
+ }
140
+ document.removeEventListener('pointerdown', this.#onOutside);
141
+ this.#listbox?.hidePopover?.();
142
+ this.#listbox = null;
143
+ this.#inputEl = null;
144
+ this.#labelEl = null;
145
+ this.#emptyEl = null;
146
+ this.#loadingEl = null;
147
+ this.#footerEl = null;
148
+ this.#clearBtn = null;
149
+ this.#suffixEl = null;
150
+ }
151
+
152
+ render() {
153
+ if (!this.#inputEl) return;
154
+
155
+ // Sync ARIA expanded on the inner combobox surface (WAI-APG).
156
+ this.#inputEl.setAttribute('aria-expanded', String(this.open));
157
+
158
+ // Disabled / readonly state on the editable surface.
159
+ if (this.disabled || this.readonly) {
160
+ this.#inputEl.contentEditable = 'false';
161
+ } else {
162
+ this.#inputEl.contentEditable = 'plaintext-only';
163
+ }
164
+
165
+ if (this.#labelEl) {
166
+ this.#labelEl.textContent = this.label || '';
167
+ this.#labelEl.style.display = this.label ? '' : 'none';
168
+ }
169
+
170
+ // aria-label fallback when no [label] is set.
171
+ if (this.label) {
172
+ this.removeAttribute('aria-label');
173
+ } else if (this.placeholder) {
174
+ this.setAttribute('aria-label', this.placeholder);
175
+ } else {
176
+ this.removeAttribute('aria-label');
177
+ }
178
+
179
+ // Toggle clear-button visibility when [clearable].
180
+ if (this.#clearBtn) {
181
+ this.#clearBtn.style.display = this.clearable && this.value ? '' : 'none';
182
+ }
183
+
184
+ // Show/hide the loading indicator + empty state in the popover.
185
+ if (this.#loadingEl) {
186
+ this.#loadingEl.style.display = this.loading ? '' : 'none';
187
+ }
188
+
189
+ // Popover open / close.
190
+ if (this.open) {
191
+ this.#openPopover();
192
+ } else {
193
+ this.#closePopover();
194
+ }
195
+ }
196
+
197
+ // ── Shell ──
198
+
199
+ #stampShell() {
200
+ if (this.querySelector(':scope > [data-field]')) {
201
+ // Already stamped (re-connect path); just re-query refs.
202
+ this.#queryRefs();
203
+ this.#bindListeners();
204
+ return;
205
+ }
206
+
207
+ const labelId = `${this.#instanceId}-label`;
208
+ const listboxId = `${this.#instanceId}-listbox`;
209
+ const inputId = `${this.#instanceId}-input`;
210
+
211
+ // Capture consumer-supplied slotted children before innerHTML wipes them.
212
+ const prefixNodes = Array.from(this.querySelectorAll(':scope > [slot="prefix"]'));
213
+ const suffixNodes = Array.from(this.querySelectorAll(':scope > [slot="suffix"]'));
214
+ const emptyNodes = Array.from(this.querySelectorAll(':scope > [slot="empty"]'));
215
+ const loadingNodes = Array.from(this.querySelectorAll(':scope > [slot="loading"]'));
216
+ const footerNodes = Array.from(this.querySelectorAll(':scope > [slot="footer"]'));
217
+
218
+ this.innerHTML = `
219
+ <span data-label id="${labelId}"${this.label ? '' : ' style="display:none"'}>${escapeHTML(this.label || '')}</span>
220
+ <div data-field>
221
+ <span data-prefix></span>
222
+ <span data-input
223
+ id="${inputId}"
224
+ contenteditable="plaintext-only"
225
+ role="combobox"
226
+ tabindex="0"
227
+ aria-autocomplete="list"
228
+ aria-expanded="false"
229
+ aria-controls="${listboxId}"
230
+ aria-labelledby="${labelId}"
231
+ data-placeholder="${escapeHTML(this.placeholder || '')}"></span>
232
+ <button type="button" data-clear aria-label="Clear" tabindex="-1" style="display:none">
233
+ <icon-ui name="x-circle"></icon-ui>
234
+ </button>
235
+ <span data-suffix>
236
+ <icon-ui name="caret-down"></icon-ui>
237
+ </span>
238
+ </div>
239
+ <div data-listbox id="${listboxId}" role="listbox" popover="manual" aria-labelledby="${labelId}">
240
+ <div data-loading style="display:none"><spinner-ui></spinner-ui></div>
241
+ <div data-options></div>
242
+ <div data-empty>No matches</div>
243
+ <div data-footer></div>
244
+ </div>
245
+ `;
246
+
247
+ this.#queryRefs();
248
+
249
+ // Re-insert consumer-supplied slot nodes into the appropriate region.
250
+ if (prefixNodes.length) {
251
+ const prefix = this.querySelector(':scope > [data-field] > [data-prefix]');
252
+ prefix.replaceChildren(...prefixNodes);
253
+ }
254
+ if (suffixNodes.length) {
255
+ // Consumer-supplied suffix replaces the default caret-down.
256
+ this.#suffixEl.replaceChildren(...suffixNodes);
257
+ }
258
+ if (emptyNodes.length) {
259
+ this.#emptyEl.replaceChildren(...emptyNodes);
260
+ }
261
+ if (loadingNodes.length) {
262
+ this.#loadingEl.replaceChildren(...loadingNodes);
263
+ }
264
+ if (footerNodes.length) {
265
+ this.#footerEl.replaceChildren(...footerNodes);
266
+ }
267
+
268
+ this.#bindListeners();
269
+ }
270
+
271
+ #queryRefs() {
272
+ this.#inputEl = this.querySelector(':scope > [data-field] > [data-input]');
273
+ this.#labelEl = this.querySelector(':scope > [data-label]');
274
+ this.#clearBtn = this.querySelector(':scope > [data-field] > [data-clear]');
275
+ this.#suffixEl = this.querySelector(':scope > [data-field] > [data-suffix]');
276
+ this.#listbox = this.querySelector(':scope > [data-listbox]');
277
+ this.#loadingEl = this.querySelector(':scope > [data-listbox] > [data-loading]');
278
+ this.#emptyEl = this.querySelector(':scope > [data-listbox] > [data-empty]');
279
+ this.#footerEl = this.querySelector(':scope > [data-listbox] > [data-footer]');
280
+ }
281
+
282
+ #bindListeners() {
283
+ if (this.#inputEl) {
284
+ this.#inputEl.addEventListener('input', this.#onInputEvent);
285
+ this.#inputEl.addEventListener('keydown', this.#onKeydown);
286
+ this.#inputEl.addEventListener('focus', this.#onFocus);
287
+ this.#inputEl.addEventListener('blur', this.#onBlur);
288
+ }
289
+ if (this.#clearBtn) {
290
+ this.#clearBtn.addEventListener('click', this.#onClearClick);
291
+ }
292
+ if (this.#suffixEl) {
293
+ this.#suffixEl.addEventListener('click', this.#onSuffixClick);
294
+ }
295
+ }
296
+
297
+ #teardownListeners() {
298
+ if (this.#inputEl) {
299
+ this.#inputEl.removeEventListener('input', this.#onInputEvent);
300
+ this.#inputEl.removeEventListener('keydown', this.#onKeydown);
301
+ this.#inputEl.removeEventListener('focus', this.#onFocus);
302
+ this.#inputEl.removeEventListener('blur', this.#onBlur);
303
+ }
304
+ if (this.#clearBtn) {
305
+ this.#clearBtn.removeEventListener('click', this.#onClearClick);
306
+ }
307
+ if (this.#suffixEl) {
308
+ this.#suffixEl.removeEventListener('click', this.#onSuffixClick);
309
+ }
310
+ }
311
+
312
+ // ── Options ──
313
+
314
+ /** Parse <option> + <optgroup> children into the internal option model. */
315
+ #parseOptions() {
316
+ const opts = [];
317
+ let preSelected = '';
318
+ for (const child of Array.from(this.children)) {
319
+ if (child.tagName === 'OPTGROUP') {
320
+ const group = { label: child.label || child.getAttribute('label') || '', options: [] };
321
+ for (const opt of child.querySelectorAll('option')) {
322
+ group.options.push({ value: opt.value, label: opt.textContent.trim(), disabled: opt.disabled });
323
+ if (opt.hasAttribute('selected') && !preSelected) preSelected = opt.value;
324
+ }
325
+ opts.push(group);
326
+ } else if (child.tagName === 'OPTION') {
327
+ opts.push({ value: child.value, label: child.textContent.trim(), disabled: child.disabled });
328
+ if (child.hasAttribute('selected') && !preSelected) preSelected = child.value;
329
+ }
330
+ }
331
+ // Remove the parsed nodes so they don't compete with the stamped shell.
332
+ for (const child of [...this.querySelectorAll(':scope > option, :scope > optgroup')]) child.remove();
333
+ this.#options = opts;
334
+ if (!this.value && preSelected) this.value = preSelected;
335
+ }
336
+
337
+ set options(list) {
338
+ // Reads of `this.value` happen below (for aria-selected wiring). Wrap
339
+ // the rebuild in `untracked` so calling-effect contexts don't subscribe
340
+ // to value via this setter — mirrors the select-ui pattern from
341
+ // FEEDBACK-22 / v0.5.18.
342
+ untracked(() => {
343
+ this.#options = Array.isArray(list) ? list : [];
344
+ if (this.#listbox) this.#renderOptions();
345
+ this.#syncInputDisplay();
346
+ });
347
+ }
348
+
349
+ get options() { return this.#options; }
350
+
351
+ #flatOptions() {
352
+ const flat = [];
353
+ for (const item of this.#options) {
354
+ if (item && Array.isArray(item.options)) {
355
+ for (const opt of item.options) flat.push(opt);
356
+ } else if (item) {
357
+ flat.push(item);
358
+ }
359
+ }
360
+ return flat;
361
+ }
362
+
363
+ #findOption(value) {
364
+ return this.#flatOptions().find((o) => o.value === value) || null;
365
+ }
366
+
367
+ // ── Render the listbox ──
368
+
369
+ #renderOptions() {
370
+ if (!this.#listbox) return;
371
+ const container = this.querySelector(':scope > [data-listbox] > [data-options]');
372
+ if (!container) return;
373
+ container.replaceChildren();
374
+
375
+ const flat = this.#flatOptions();
376
+ const renderable = this.#applyFilter(flat).slice(0, Number(this.maxOptions) || 100);
377
+
378
+ let optionCounter = 0;
379
+ const renderOpt = (opt) => {
380
+ optionCounter += 1;
381
+ const el = document.createElement('div');
382
+ el.setAttribute('role', 'option');
383
+ el.setAttribute('id', `${this.#instanceId}-opt-${optionCounter}`);
384
+ el.dataset.value = opt.value ?? '';
385
+ if (opt.disabled) el.setAttribute('aria-disabled', 'true');
386
+ if (opt.value === this.value) el.setAttribute('aria-selected', 'true');
387
+ el.innerHTML = this.#renderOptionLabel(opt.label ?? opt.value);
388
+ el.addEventListener('click', this.#onOptionClick);
389
+ el.addEventListener('mouseenter', this.#onOptionMouseEnter);
390
+ return el;
391
+ };
392
+
393
+ if (this.#options.some((o) => o && Array.isArray(o.options))) {
394
+ // Grouped path — render groups with section headers, filtered.
395
+ for (const item of this.#options) {
396
+ if (item && Array.isArray(item.options)) {
397
+ const groupOpts = this.#applyFilter(item.options);
398
+ if (!groupOpts.length) continue;
399
+ const groupEl = document.createElement('div');
400
+ groupEl.setAttribute('role', 'group');
401
+ const header = document.createElement('div');
402
+ header.setAttribute('data-group-label', '');
403
+ header.textContent = item.label || '';
404
+ groupEl.appendChild(header);
405
+ for (const opt of groupOpts) groupEl.appendChild(renderOpt(opt));
406
+ container.appendChild(groupEl);
407
+ } else if (item) {
408
+ // Flat option mixed into a grouped list — still render it.
409
+ if (this.#matches(item)) container.appendChild(renderOpt(item));
410
+ }
411
+ }
412
+ } else {
413
+ for (const opt of renderable) container.appendChild(renderOpt(opt));
414
+ }
415
+
416
+ // Update the empty-state visibility based on what got rendered.
417
+ const visible = container.querySelectorAll('[role="option"]').length;
418
+ if (this.#emptyEl) {
419
+ this.#emptyEl.style.display = visible || this.loading ? 'none' : '';
420
+ }
421
+
422
+ // Clear any stale active-descendant pointer.
423
+ this.#activeIndex = -1;
424
+ this.#inputEl?.removeAttribute('aria-activedescendant');
425
+ }
426
+
427
+ #renderOptionLabel(label) {
428
+ const text = String(label ?? '');
429
+ if (!this.highlightMatch || !this.#query) return escapeHTML(text);
430
+ const q = this.#query;
431
+ const re = new RegExp(escapeRegExp(q), 'i');
432
+ const match = text.match(re);
433
+ if (!match) return escapeHTML(text);
434
+ const start = match.index ?? 0;
435
+ const end = start + match[0].length;
436
+ return (
437
+ escapeHTML(text.slice(0, start)) +
438
+ '<mark>' +
439
+ escapeHTML(text.slice(start, end)) +
440
+ '</mark>' +
441
+ escapeHTML(text.slice(end))
442
+ );
443
+ }
444
+
445
+ // ── Filtering ──
446
+
447
+ #matches(opt) {
448
+ if (!this.#query) return true;
449
+ const q = this.#query.toLowerCase();
450
+ const label = String(opt.label ?? opt.value ?? '').toLowerCase();
451
+ switch (this.filterMode) {
452
+ case 'prefix':
453
+ return label.startsWith(q);
454
+ case 'fuzzy': {
455
+ let li = 0;
456
+ for (const ch of q) {
457
+ li = label.indexOf(ch, li);
458
+ if (li === -1) return false;
459
+ li += 1;
460
+ }
461
+ return true;
462
+ }
463
+ case 'substring':
464
+ default:
465
+ return label.includes(q);
466
+ }
467
+ }
468
+
469
+ #applyFilter(list) {
470
+ if (!this.#query) return list.slice();
471
+ return list.filter((o) => this.#matches(o));
472
+ }
473
+
474
+ // ── Input handling ──
475
+
476
+ #handleInput() {
477
+ if (this.#suppressInput) return;
478
+ const text = this.#inputEl.textContent || '';
479
+ this.#query = text.trim();
480
+
481
+ if (!this.open) {
482
+ this.open = true;
483
+ this.dispatchEvent(new CustomEvent('open', { bubbles: true, detail: { trigger: 'typing' } }));
484
+ }
485
+
486
+ this.#renderOptions();
487
+
488
+ this.dispatchEvent(new CustomEvent('input', {
489
+ bubbles: true,
490
+ detail: { value: text, query: this.#query.toLowerCase() },
491
+ }));
492
+ }
493
+
494
+ #handleKeydown(e) {
495
+ const k = e.key;
496
+
497
+ if (k === 'ArrowDown') {
498
+ e.preventDefault();
499
+ if (!this.open) {
500
+ this.open = true;
501
+ this.dispatchEvent(new CustomEvent('open', { bubbles: true, detail: { trigger: 'keyboard' } }));
502
+ }
503
+ this.#moveActive(1);
504
+ return;
505
+ }
506
+ if (k === 'ArrowUp') {
507
+ if (!this.open) return;
508
+ e.preventDefault();
509
+ this.#moveActive(-1);
510
+ return;
511
+ }
512
+ if (k === 'Home' && this.open) {
513
+ e.preventDefault();
514
+ this.#setActiveIndex(0);
515
+ return;
516
+ }
517
+ if (k === 'End' && this.open) {
518
+ e.preventDefault();
519
+ const opts = this.#renderedOptions();
520
+ this.#setActiveIndex(opts.length - 1);
521
+ return;
522
+ }
523
+ if (k === 'Enter') {
524
+ e.preventDefault();
525
+ this.#commitOnEnter();
526
+ return;
527
+ }
528
+ if (k === 'Escape') {
529
+ if (this.open) {
530
+ e.preventDefault();
531
+ this.#closeAndRestore('escape');
532
+ }
533
+ return;
534
+ }
535
+ if (k === 'Tab') {
536
+ // Tab: commit active option if any; otherwise per free-text policy.
537
+ this.#commitOnTab();
538
+ return;
539
+ }
540
+ if (k === 'Backspace') {
541
+ // When clearable + the input is empty + a value is committed, clear.
542
+ const empty = !(this.#inputEl.textContent || '').length;
543
+ if (this.clearable && empty && this.value) {
544
+ e.preventDefault();
545
+ this.#clear('keyboard');
546
+ }
547
+ return;
548
+ }
549
+ }
550
+
551
+ #handleFocus() {
552
+ // No auto-open on focus; user opens with Arrow / click / typing.
553
+ }
554
+
555
+ #handleBlur(e) {
556
+ // Closing on blur is handled by the document-level outside-click handler
557
+ // (so clicks on listbox options don't close before they fire). Just
558
+ // restore the input display if free-text is off and the typed query
559
+ // doesn't resolve to a real value.
560
+ if (!this.freeText && this.#query && !this.value) {
561
+ // No commit happened; restore last committed value into the display.
562
+ this.#syncInputDisplay();
563
+ this.#query = '';
564
+ }
565
+ void e;
566
+ }
567
+
568
+ // ── Active-descendant management ──
569
+
570
+ #renderedOptions() {
571
+ if (!this.#listbox) return [];
572
+ return Array.from(this.#listbox.querySelectorAll('[role="option"]:not([aria-disabled="true"])'));
573
+ }
574
+
575
+ #moveActive(dir) {
576
+ const opts = this.#renderedOptions();
577
+ if (!opts.length) return;
578
+ if (this.#activeIndex < 0) {
579
+ this.#setActiveIndex(dir > 0 ? 0 : opts.length - 1);
580
+ return;
581
+ }
582
+ const next = (this.#activeIndex + dir + opts.length) % opts.length;
583
+ this.#setActiveIndex(next);
584
+ }
585
+
586
+ #setActiveIndex(idx) {
587
+ const opts = this.#renderedOptions();
588
+ if (!opts.length) {
589
+ this.#activeIndex = -1;
590
+ this.#inputEl?.removeAttribute('aria-activedescendant');
591
+ return;
592
+ }
593
+ // Clear previous data-active marker.
594
+ for (const o of opts) o.removeAttribute('data-active');
595
+ const clamped = Math.max(0, Math.min(idx, opts.length - 1));
596
+ const el = opts[clamped];
597
+ if (!el) return;
598
+ el.setAttribute('data-active', '');
599
+ el.scrollIntoView({ block: 'nearest' });
600
+ this.#activeIndex = clamped;
601
+ this.#inputEl?.setAttribute('aria-activedescendant', el.id);
602
+ }
603
+
604
+ // ── Commit paths ──
605
+
606
+ #handleOptionClick(e) {
607
+ const el = e.currentTarget;
608
+ if (el.getAttribute('aria-disabled') === 'true') return;
609
+ const value = el.dataset.value || '';
610
+ this.#commitOption(value, 'click');
611
+ }
612
+
613
+ #handleOptionMouseEnter(e) {
614
+ const el = e.currentTarget;
615
+ const opts = this.#renderedOptions();
616
+ const idx = opts.indexOf(el);
617
+ if (idx >= 0) this.#setActiveIndex(idx);
618
+ }
619
+
620
+ #commitOnEnter() {
621
+ if (!this.open) {
622
+ // Closed: open the popover for the user.
623
+ this.open = true;
624
+ this.dispatchEvent(new CustomEvent('open', { bubbles: true, detail: { trigger: 'keyboard' } }));
625
+ return;
626
+ }
627
+ const opts = this.#renderedOptions();
628
+ const active = this.#activeIndex >= 0 ? opts[this.#activeIndex] : null;
629
+ if (active) {
630
+ this.#commitOption(active.dataset.value || '', 'keyboard');
631
+ return;
632
+ }
633
+ const typed = (this.#inputEl.textContent || '').trim();
634
+ if (!typed) {
635
+ this.#closePopover('select');
636
+ return;
637
+ }
638
+ if (this.creatable) {
639
+ this.dispatchEvent(new CustomEvent('create', { bubbles: true, detail: { value: typed } }));
640
+ // Free-text commit also fires for creatable (consumer can refresh options after).
641
+ this.#commitFreeText(typed, 'keyboard');
642
+ return;
643
+ }
644
+ if (this.freeText) {
645
+ this.#commitFreeText(typed, 'keyboard');
646
+ return;
647
+ }
648
+ // Constrained-choice + no match: invalid.
649
+ this.dispatchEvent(new CustomEvent('invalid', { bubbles: true, detail: { query: typed } }));
650
+ this.#syncInputDisplay();
651
+ this.#query = '';
652
+ this.#renderOptions();
653
+ }
654
+
655
+ #commitOnTab() {
656
+ if (!this.open) return;
657
+ const opts = this.#renderedOptions();
658
+ const active = this.#activeIndex >= 0 ? opts[this.#activeIndex] : null;
659
+ if (active) {
660
+ this.#commitOption(active.dataset.value || '', 'keyboard');
661
+ return;
662
+ }
663
+ const typed = (this.#inputEl.textContent || '').trim();
664
+ if (typed && (this.freeText || this.creatable)) {
665
+ if (this.creatable && !this.#findOption(typed)) {
666
+ this.dispatchEvent(new CustomEvent('create', { bubbles: true, detail: { value: typed } }));
667
+ }
668
+ this.#commitFreeText(typed, 'keyboard');
669
+ return;
670
+ }
671
+ // No active + no free-text: restore + close.
672
+ this.#syncInputDisplay();
673
+ this.#query = '';
674
+ this.#closePopover('blur');
675
+ }
676
+
677
+ #commitOption(value, source) {
678
+ if (this.readonly) {
679
+ this.#closePopover('select');
680
+ return;
681
+ }
682
+ const opt = this.#findOption(value);
683
+ untracked(() => {
684
+ this.value = value;
685
+ });
686
+ this.#lastCommittedValue = value;
687
+ this.#query = '';
688
+ this.syncValue(value);
689
+ this.#syncInputDisplay();
690
+ this.#renderOptions();
691
+ this.dispatchEvent(new CustomEvent('change', {
692
+ bubbles: true,
693
+ detail: { value, option: opt, source },
694
+ }));
695
+ this.open = false;
696
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, detail: { reason: 'select' } }));
697
+ }
698
+
699
+ #commitFreeText(value, source) {
700
+ if (this.readonly) {
701
+ this.#closePopover('select');
702
+ return;
703
+ }
704
+ untracked(() => {
705
+ this.value = value;
706
+ });
707
+ this.#lastCommittedValue = value;
708
+ this.#query = '';
709
+ this.syncValue(value);
710
+ this.#syncInputDisplay();
711
+ this.#renderOptions();
712
+ this.dispatchEvent(new CustomEvent('change', {
713
+ bubbles: true,
714
+ detail: { value, option: null, source },
715
+ }));
716
+ this.open = false;
717
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, detail: { reason: 'select' } }));
718
+ }
719
+
720
+ // ── Clear ──
721
+
722
+ #handleClearClick(e) {
723
+ e.preventDefault();
724
+ e.stopPropagation();
725
+ this.#clear('click');
726
+ this.#inputEl?.focus();
727
+ }
728
+
729
+ #clear(source) {
730
+ untracked(() => { this.value = ''; });
731
+ this.#lastCommittedValue = '';
732
+ this.#query = '';
733
+ this.syncValue('');
734
+ this.#syncInputDisplay();
735
+ this.#renderOptions();
736
+ this.dispatchEvent(new CustomEvent('change', {
737
+ bubbles: true,
738
+ detail: { value: '', option: null, source },
739
+ }));
740
+ }
741
+
742
+ clear() { this.#clear('programmatic'); }
743
+
744
+ // ── Suffix (caret) toggles popover ──
745
+
746
+ #handleSuffixClick(e) {
747
+ e.preventDefault();
748
+ e.stopPropagation();
749
+ if (this.disabled) return;
750
+ if (this.open) {
751
+ this.open = false;
752
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, detail: { reason: 'outside' } }));
753
+ } else {
754
+ this.open = true;
755
+ this.dispatchEvent(new CustomEvent('open', { bubbles: true, detail: { trigger: 'click' } }));
756
+ this.#inputEl?.focus();
757
+ }
758
+ }
759
+
760
+ // ── Outside-click + popover anchor ──
761
+
762
+ #handleOutside(e) {
763
+ const insideHost = this.contains(e.target);
764
+ const insidePopover = this.#listbox && e.composedPath().includes(this.#listbox);
765
+ if (!insideHost && !insidePopover) {
766
+ // Restore the displayed text to the last committed value if there
767
+ // was an in-flight unmatched query.
768
+ if (this.#query && !this.freeText) {
769
+ this.#syncInputDisplay();
770
+ this.#query = '';
771
+ }
772
+ this.open = false;
773
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, detail: { reason: 'outside' } }));
774
+ }
775
+ }
776
+
777
+ #closeAndRestore(reason) {
778
+ this.#syncInputDisplay();
779
+ this.#query = '';
780
+ this.#renderOptions();
781
+ this.open = false;
782
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, detail: { reason } }));
783
+ }
784
+
785
+ #openPopover() {
786
+ if (!this.#listbox) return;
787
+ this.#listbox.showPopover?.();
788
+ const trigger = this.querySelector(':scope > [data-field]') || this;
789
+ this.#listbox.style.minWidth = `${trigger.getBoundingClientRect().width}px`;
790
+ this.#anchorCleanup?.();
791
+ this.#anchorCleanup = anchorPopover(trigger, this.#listbox, {
792
+ placement: this.getAttribute('placement') || 'bottom-start',
793
+ gap: 4,
794
+ matchWidth: false,
795
+ });
796
+ // Defer registration so the current click that opened us doesn't
797
+ // immediately close us.
798
+ if (this.#rafId != null) cancelAnimationFrame(this.#rafId);
799
+ this.#rafId = requestAnimationFrame(() => {
800
+ this.#rafId = null;
801
+ document.addEventListener('pointerdown', this.#onOutside);
802
+ });
803
+ // Initial active option: the selected one if visible, else first.
804
+ const opts = this.#renderedOptions();
805
+ if (!opts.length) return;
806
+ const selectedIdx = opts.findIndex((o) => o.dataset.value === this.value);
807
+ this.#setActiveIndex(selectedIdx >= 0 ? selectedIdx : 0);
808
+ }
809
+
810
+ #closePopover(reason) {
811
+ if (!this.#listbox) return;
812
+ this.#anchorCleanup?.();
813
+ this.#anchorCleanup = null;
814
+ this.#listbox.hidePopover?.();
815
+ if (this.#rafId != null) {
816
+ cancelAnimationFrame(this.#rafId);
817
+ this.#rafId = null;
818
+ }
819
+ document.removeEventListener('pointerdown', this.#onOutside);
820
+ this.#activeIndex = -1;
821
+ this.#inputEl?.removeAttribute('aria-activedescendant');
822
+ if (reason) {
823
+ // Note: caller usually dispatches close; this is a no-op signal path.
824
+ }
825
+ }
826
+
827
+ // ── Display sync ──
828
+
829
+ #syncInputDisplay() {
830
+ if (!this.#inputEl) return;
831
+ const opt = this.#findOption(this.value);
832
+ const text = opt ? (opt.label ?? opt.value ?? '') : (this.value || '');
833
+ if ((this.#inputEl.textContent || '') !== text) {
834
+ this.#suppressInput = true;
835
+ this.#inputEl.textContent = text;
836
+ this.#suppressInput = false;
837
+ }
838
+ this.#inputEl.toggleAttribute('data-empty', !text);
839
+ }
840
+
841
+ // ── Public methods ──
842
+
843
+ open$() { this.open = true; }
844
+ close$() { this.open = false; }
845
+
846
+ focus() { this.#inputEl?.focus(); }
847
+
848
+ selectOption(value) {
849
+ const opt = this.#findOption(value);
850
+ if (!opt) return false;
851
+ this.#commitOption(value, 'programmatic');
852
+ return true;
853
+ }
854
+
855
+ // ── Form value sync override ──
856
+
857
+ syncValue(val) {
858
+ const v = val ?? this.value ?? '';
859
+ super.syncValue(String(v));
860
+ }
861
+ }