@hypoth-ui/cli 0.0.1 → 0.1.1

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 (375) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -115
  3. package/dist/{add-PDBC4JTE.js → add-V5PW73GC.js} +29 -17
  4. package/dist/{chunk-5LTQ2XVL.js → chunk-27CLUUVC.js} +0 -2
  5. package/dist/{chunk-YPKFYE45.js → chunk-NWIRSZUQ.js} +6 -13
  6. package/dist/{chunk-GJ6JOQ3Q.js → chunk-PBK72SJJ.js} +1 -1
  7. package/dist/{diff-BQEXG7HU.js → diff-776UATCA.js} +2 -2
  8. package/dist/index.js +5 -5
  9. package/dist/{init-7AZXYAPJ.js → init-GDU2PW7K.js} +10 -13
  10. package/dist/{list-X6ZLM2NQ.js → list-XDP5I537.js} +3 -3
  11. package/package.json +16 -12
  12. package/registry/components.json +1820 -206
  13. package/templates/accordion/index.tsx +266 -0
  14. package/templates/accordion/wc/accordion-content.ts +113 -0
  15. package/templates/accordion/wc/accordion-item.ts +111 -0
  16. package/templates/accordion/wc/accordion-trigger.ts +105 -0
  17. package/templates/accordion/wc/accordion.ts +213 -0
  18. package/templates/accordion/wc/index.ts +12 -0
  19. package/templates/alert/index.tsx +177 -0
  20. package/templates/alert/wc/alert.ts +167 -0
  21. package/templates/alert/wc/index.ts +1 -0
  22. package/templates/alert-dialog/index.tsx +360 -0
  23. package/templates/alert-dialog/wc/alert-dialog-action.ts +43 -0
  24. package/templates/alert-dialog/wc/alert-dialog-cancel.ts +43 -0
  25. package/templates/alert-dialog/wc/alert-dialog-content.ts +42 -0
  26. package/templates/alert-dialog/wc/alert-dialog-description.ts +34 -0
  27. package/templates/alert-dialog/wc/alert-dialog-footer.ts +25 -0
  28. package/templates/alert-dialog/wc/alert-dialog-header.ts +25 -0
  29. package/templates/alert-dialog/wc/alert-dialog-title.ts +34 -0
  30. package/templates/alert-dialog/wc/alert-dialog-trigger.ts +46 -0
  31. package/templates/alert-dialog/wc/alert-dialog.ts +302 -0
  32. package/templates/alert-dialog/wc/index.ts +13 -0
  33. package/templates/aspect-ratio/index.tsx +50 -0
  34. package/templates/aspect-ratio/wc/aspect-ratio.ts +78 -0
  35. package/templates/aspect-ratio/wc/index.ts +5 -0
  36. package/templates/avatar/avatar-group.tsx +88 -0
  37. package/templates/avatar/avatar.tsx +124 -0
  38. package/templates/avatar/index.tsx +33 -0
  39. package/templates/avatar/wc/avatar-group.ts +112 -0
  40. package/templates/avatar/wc/avatar.ts +184 -0
  41. package/templates/avatar/wc/index.ts +5 -0
  42. package/templates/badge/index.tsx +140 -0
  43. package/templates/badge/wc/badge.ts +119 -0
  44. package/templates/badge/wc/index.ts +9 -0
  45. package/templates/breadcrumb/index.tsx +157 -0
  46. package/templates/breadcrumb/wc/breadcrumb-item.ts +30 -0
  47. package/templates/breadcrumb/wc/breadcrumb-link.ts +70 -0
  48. package/templates/breadcrumb/wc/breadcrumb-list.ts +30 -0
  49. package/templates/breadcrumb/wc/breadcrumb-page.ts +32 -0
  50. package/templates/breadcrumb/wc/breadcrumb-separator.ts +31 -0
  51. package/templates/breadcrumb/wc/breadcrumb.ts +55 -0
  52. package/templates/breadcrumb/wc/index.ts +10 -0
  53. package/templates/button/button.tsx +119 -0
  54. package/templates/button/index.ts +1 -0
  55. package/templates/button/wc/button.ts +169 -0
  56. package/templates/calendar/index.tsx +149 -0
  57. package/templates/calendar/wc/calendar.ts +316 -0
  58. package/templates/calendar/wc/index.ts +4 -0
  59. package/templates/card/index.tsx +108 -0
  60. package/templates/card/wc/card-content.ts +25 -0
  61. package/templates/card/wc/card-footer.ts +25 -0
  62. package/templates/card/wc/card-header.ts +25 -0
  63. package/templates/card/wc/card.ts +43 -0
  64. package/templates/card/wc/index.ts +8 -0
  65. package/templates/checkbox/checkbox.tsx +85 -0
  66. package/templates/checkbox/wc/checkbox.ts +247 -0
  67. package/templates/collapsible/index.tsx +172 -0
  68. package/templates/collapsible/wc/collapsible-content.ts +97 -0
  69. package/templates/collapsible/wc/collapsible-trigger.ts +39 -0
  70. package/templates/collapsible/wc/collapsible.ts +143 -0
  71. package/templates/collapsible/wc/index.ts +7 -0
  72. package/templates/combobox/combobox-content.tsx +141 -0
  73. package/templates/combobox/combobox-context.ts +36 -0
  74. package/templates/combobox/combobox-empty.tsx +38 -0
  75. package/templates/combobox/combobox-input.tsx +159 -0
  76. package/templates/combobox/combobox-loading.tsx +38 -0
  77. package/templates/combobox/combobox-option.tsx +99 -0
  78. package/templates/combobox/combobox-root.tsx +207 -0
  79. package/templates/combobox/combobox-tag.tsx +62 -0
  80. package/templates/combobox/index.ts +62 -0
  81. package/templates/combobox/wc/combobox-content.ts +97 -0
  82. package/templates/combobox/wc/combobox-input.ts +134 -0
  83. package/templates/combobox/wc/combobox-option.ts +111 -0
  84. package/templates/combobox/wc/combobox-tag.ts +103 -0
  85. package/templates/combobox/wc/combobox.ts +981 -0
  86. package/templates/combobox/wc/index.ts +5 -0
  87. package/templates/command/index.tsx +279 -0
  88. package/templates/command/wc/command-empty.ts +24 -0
  89. package/templates/command/wc/command-group.ts +60 -0
  90. package/templates/command/wc/command-input.ts +136 -0
  91. package/templates/command/wc/command-item.ts +78 -0
  92. package/templates/command/wc/command-list.ts +103 -0
  93. package/templates/command/wc/command-loading.ts +24 -0
  94. package/templates/command/wc/command-separator.ts +23 -0
  95. package/templates/command/wc/command.ts +176 -0
  96. package/templates/context-menu/index.tsx +262 -0
  97. package/templates/context-menu/wc/context-menu-content.ts +41 -0
  98. package/templates/context-menu/wc/context-menu-item.ts +83 -0
  99. package/templates/context-menu/wc/context-menu-label.ts +30 -0
  100. package/templates/context-menu/wc/context-menu-separator.ts +28 -0
  101. package/templates/context-menu/wc/context-menu.ts +324 -0
  102. package/templates/context-menu/wc/index.ts +9 -0
  103. package/templates/data-table/index.tsx +263 -0
  104. package/templates/data-table/wc/data-table.ts +405 -0
  105. package/templates/data-table/wc/index.ts +10 -0
  106. package/templates/date-picker/date-picker-calendar.tsx +352 -0
  107. package/templates/date-picker/date-picker-content.tsx +121 -0
  108. package/templates/date-picker/date-picker-context.ts +46 -0
  109. package/templates/date-picker/date-picker-root.tsx +201 -0
  110. package/templates/date-picker/date-picker-trigger.tsx +95 -0
  111. package/templates/date-picker/index.ts +44 -0
  112. package/templates/date-picker/wc/date-picker-calendar.ts +457 -0
  113. package/templates/date-picker/wc/date-picker.ts +592 -0
  114. package/templates/date-picker/wc/date-utils.ts +467 -0
  115. package/templates/date-picker/wc/index.ts +3 -0
  116. package/templates/dialog/dialog-close.tsx +57 -0
  117. package/templates/dialog/dialog-content.tsx +106 -0
  118. package/templates/dialog/dialog-context.ts +24 -0
  119. package/templates/dialog/dialog-description.tsx +51 -0
  120. package/templates/dialog/dialog-root.tsx +104 -0
  121. package/templates/dialog/dialog-title.tsx +38 -0
  122. package/templates/dialog/dialog-trigger.tsx +94 -0
  123. package/templates/dialog/index.ts +52 -0
  124. package/templates/dialog/wc/dialog-content.ts +59 -0
  125. package/templates/dialog/wc/dialog-description.ts +58 -0
  126. package/templates/dialog/wc/dialog-title.ts +56 -0
  127. package/templates/dialog/wc/dialog.ts +411 -0
  128. package/templates/drawer/index.tsx +263 -0
  129. package/templates/drawer/wc/drawer-content.ts +150 -0
  130. package/templates/drawer/wc/drawer-description.ts +34 -0
  131. package/templates/drawer/wc/drawer-footer.ts +25 -0
  132. package/templates/drawer/wc/drawer-header.ts +25 -0
  133. package/templates/drawer/wc/drawer-title.ts +34 -0
  134. package/templates/drawer/wc/drawer.ts +348 -0
  135. package/templates/drawer/wc/index.ts +10 -0
  136. package/templates/dropdown-menu/index.tsx +454 -0
  137. package/templates/dropdown-menu/wc/dropdown-menu-checkbox-item.ts +93 -0
  138. package/templates/dropdown-menu/wc/dropdown-menu-content.ts +43 -0
  139. package/templates/dropdown-menu/wc/dropdown-menu-item.ts +85 -0
  140. package/templates/dropdown-menu/wc/dropdown-menu-label.ts +31 -0
  141. package/templates/dropdown-menu/wc/dropdown-menu-radio-group.ts +80 -0
  142. package/templates/dropdown-menu/wc/dropdown-menu-radio-item.ts +101 -0
  143. package/templates/dropdown-menu/wc/dropdown-menu-separator.ts +28 -0
  144. package/templates/dropdown-menu/wc/dropdown-menu.ts +358 -0
  145. package/templates/dropdown-menu/wc/index.ts +12 -0
  146. package/templates/field/field-description.tsx +39 -0
  147. package/templates/field/field-error.tsx +37 -0
  148. package/templates/field/field.tsx +46 -0
  149. package/templates/field/index.ts +4 -0
  150. package/templates/field/label.tsx +40 -0
  151. package/templates/field/wc/field-description.ts +42 -0
  152. package/templates/field/wc/field-error.ts +46 -0
  153. package/templates/field/wc/field.ts +210 -0
  154. package/templates/field/wc/label.ts +54 -0
  155. package/templates/file-upload/file-upload-context.ts +26 -0
  156. package/templates/file-upload/file-upload-dropzone.tsx +111 -0
  157. package/templates/file-upload/file-upload-input.tsx +86 -0
  158. package/templates/file-upload/file-upload-item.tsx +105 -0
  159. package/templates/file-upload/file-upload-root.tsx +115 -0
  160. package/templates/file-upload/index.ts +50 -0
  161. package/templates/file-upload/wc/file-upload.ts +380 -0
  162. package/templates/file-upload/wc/index.ts +1 -0
  163. package/templates/hover-card/index.tsx +203 -0
  164. package/templates/hover-card/wc/hover-card-content.ts +50 -0
  165. package/templates/hover-card/wc/hover-card.ts +382 -0
  166. package/templates/hover-card/wc/index.ts +6 -0
  167. package/templates/icon/icon.tsx +76 -0
  168. package/templates/icon/wc/icon-adapter.ts +108 -0
  169. package/templates/icon/wc/icon.ts +161 -0
  170. package/templates/input/input.tsx +130 -0
  171. package/templates/input/wc/input.ts +216 -0
  172. package/templates/layout/app-shell.tsx +177 -0
  173. package/templates/layout/box.tsx +53 -0
  174. package/templates/layout/center.tsx +42 -0
  175. package/templates/layout/container.tsx +43 -0
  176. package/templates/layout/flow.tsx +83 -0
  177. package/templates/layout/grid.tsx +79 -0
  178. package/templates/layout/index.ts +33 -0
  179. package/templates/layout/inline.tsx +16 -0
  180. package/templates/layout/page.tsx +43 -0
  181. package/templates/layout/section.tsx +39 -0
  182. package/templates/layout/spacer.tsx +30 -0
  183. package/templates/layout/split.tsx +47 -0
  184. package/templates/layout/stack.tsx +16 -0
  185. package/templates/layout/wc/app-shell.ts +58 -0
  186. package/templates/layout/wc/box.ts +117 -0
  187. package/templates/layout/wc/center.ts +78 -0
  188. package/templates/layout/wc/container.ts +77 -0
  189. package/templates/layout/wc/flow.ts +149 -0
  190. package/templates/layout/wc/footer.ts +57 -0
  191. package/templates/layout/wc/grid.ts +142 -0
  192. package/templates/layout/wc/header.ts +57 -0
  193. package/templates/layout/wc/index.ts +41 -0
  194. package/templates/layout/wc/main.ts +46 -0
  195. package/templates/layout/wc/page.ts +81 -0
  196. package/templates/layout/wc/section.ts +65 -0
  197. package/templates/layout/wc/spacer.ts +77 -0
  198. package/templates/layout/wc/split.ts +94 -0
  199. package/templates/layout/wc/wrap.ts +93 -0
  200. package/templates/layout/wrap.tsx +46 -0
  201. package/templates/link/link.tsx +109 -0
  202. package/templates/link/wc/link.ts +124 -0
  203. package/templates/list/index.tsx +55 -0
  204. package/templates/list/list-item.tsx +117 -0
  205. package/templates/list/list.tsx +115 -0
  206. package/templates/list/wc/index.ts +5 -0
  207. package/templates/list/wc/list-item.ts +127 -0
  208. package/templates/list/wc/list.ts +114 -0
  209. package/templates/menu/index.ts +49 -0
  210. package/templates/menu/menu-content.tsx +109 -0
  211. package/templates/menu/menu-context.ts +17 -0
  212. package/templates/menu/menu-item.tsx +108 -0
  213. package/templates/menu/menu-label.tsx +32 -0
  214. package/templates/menu/menu-root.tsx +108 -0
  215. package/templates/menu/menu-separator.tsx +24 -0
  216. package/templates/menu/menu-trigger.tsx +104 -0
  217. package/templates/menu/wc/menu-content.ts +67 -0
  218. package/templates/menu/wc/menu-item.ts +109 -0
  219. package/templates/menu/wc/menu.ts +449 -0
  220. package/templates/navigation-menu/index.tsx +328 -0
  221. package/templates/navigation-menu/wc/index.ts +12 -0
  222. package/templates/navigation-menu/wc/navigation-menu-content.ts +30 -0
  223. package/templates/navigation-menu/wc/navigation-menu-indicator.ts +30 -0
  224. package/templates/navigation-menu/wc/navigation-menu-item.ts +60 -0
  225. package/templates/navigation-menu/wc/navigation-menu-link.ts +97 -0
  226. package/templates/navigation-menu/wc/navigation-menu-list.ts +30 -0
  227. package/templates/navigation-menu/wc/navigation-menu-trigger.ts +110 -0
  228. package/templates/navigation-menu/wc/navigation-menu-viewport.ts +85 -0
  229. package/templates/navigation-menu/wc/navigation-menu.ts +272 -0
  230. package/templates/number-input/index.ts +46 -0
  231. package/templates/number-input/number-input-context.ts +38 -0
  232. package/templates/number-input/number-input-decrement.tsx +53 -0
  233. package/templates/number-input/number-input-field.tsx +93 -0
  234. package/templates/number-input/number-input-increment.tsx +53 -0
  235. package/templates/number-input/number-input-root.tsx +137 -0
  236. package/templates/number-input/wc/index.ts +1 -0
  237. package/templates/number-input/wc/number-input.ts +283 -0
  238. package/templates/pagination/index.tsx +198 -0
  239. package/templates/pagination/wc/index.ts +11 -0
  240. package/templates/pagination/wc/pagination-content.ts +30 -0
  241. package/templates/pagination/wc/pagination-ellipsis.ts +28 -0
  242. package/templates/pagination/wc/pagination-item.ts +30 -0
  243. package/templates/pagination/wc/pagination-link.ts +76 -0
  244. package/templates/pagination/wc/pagination-next.ts +69 -0
  245. package/templates/pagination/wc/pagination-previous.ts +69 -0
  246. package/templates/pagination/wc/pagination.ts +156 -0
  247. package/templates/pin-input/index.ts +39 -0
  248. package/templates/pin-input/pin-input-context.ts +30 -0
  249. package/templates/pin-input/pin-input-field.tsx +186 -0
  250. package/templates/pin-input/pin-input-root.tsx +120 -0
  251. package/templates/pin-input/wc/index.ts +1 -0
  252. package/templates/pin-input/wc/pin-input.ts +259 -0
  253. package/templates/popover/popover.tsx +121 -0
  254. package/templates/popover/wc/popover-content.ts +66 -0
  255. package/templates/popover/wc/popover.ts +343 -0
  256. package/templates/progress/index.tsx +117 -0
  257. package/templates/progress/wc/index.ts +4 -0
  258. package/templates/progress/wc/progress.ts +174 -0
  259. package/templates/radio/radio.tsx +43 -0
  260. package/templates/radio/wc/radio-group.ts +261 -0
  261. package/templates/radio/wc/radio.ts +145 -0
  262. package/templates/scroll-area/index.tsx +144 -0
  263. package/templates/scroll-area/wc/index.ts +8 -0
  264. package/templates/scroll-area/wc/scroll-area-scrollbar.ts +143 -0
  265. package/templates/scroll-area/wc/scroll-area-thumb.ts +225 -0
  266. package/templates/scroll-area/wc/scroll-area-viewport.ts +120 -0
  267. package/templates/scroll-area/wc/scroll-area.ts +63 -0
  268. package/templates/select/index.ts +57 -0
  269. package/templates/select/select-content.tsx +243 -0
  270. package/templates/select/select-context.ts +30 -0
  271. package/templates/select/select-group.tsx +53 -0
  272. package/templates/select/select-label.tsx +34 -0
  273. package/templates/select/select-option.tsx +97 -0
  274. package/templates/select/select-root.tsx +153 -0
  275. package/templates/select/select-separator.tsx +27 -0
  276. package/templates/select/select-trigger.tsx +112 -0
  277. package/templates/select/select-value.tsx +48 -0
  278. package/templates/select/wc/index.ts +6 -0
  279. package/templates/select/wc/select-content.ts +89 -0
  280. package/templates/select/wc/select-group.ts +82 -0
  281. package/templates/select/wc/select-label.ts +49 -0
  282. package/templates/select/wc/select-option.ts +111 -0
  283. package/templates/select/wc/select-trigger.ts +101 -0
  284. package/templates/select/wc/select.ts +840 -0
  285. package/templates/separator/index.tsx +49 -0
  286. package/templates/separator/wc/index.ts +5 -0
  287. package/templates/separator/wc/separator.ts +60 -0
  288. package/templates/sheet/index.tsx +291 -0
  289. package/templates/sheet/wc/index.ts +12 -0
  290. package/templates/sheet/wc/sheet-close.ts +43 -0
  291. package/templates/sheet/wc/sheet-content.ts +47 -0
  292. package/templates/sheet/wc/sheet-description.ts +34 -0
  293. package/templates/sheet/wc/sheet-footer.ts +25 -0
  294. package/templates/sheet/wc/sheet-header.ts +25 -0
  295. package/templates/sheet/wc/sheet-overlay.ts +23 -0
  296. package/templates/sheet/wc/sheet-title.ts +34 -0
  297. package/templates/sheet/wc/sheet.ts +336 -0
  298. package/templates/skeleton/index.tsx +131 -0
  299. package/templates/skeleton/wc/index.ts +10 -0
  300. package/templates/skeleton/wc/skeleton.ts +107 -0
  301. package/templates/slider/index.ts +41 -0
  302. package/templates/slider/slider-context.ts +36 -0
  303. package/templates/slider/slider-range.tsx +59 -0
  304. package/templates/slider/slider-root.tsx +166 -0
  305. package/templates/slider/slider-thumb.tsx +213 -0
  306. package/templates/slider/slider-track.tsx +113 -0
  307. package/templates/slider/wc/index.ts +1 -0
  308. package/templates/slider/wc/slider.ts +465 -0
  309. package/templates/spinner/spinner.tsx +64 -0
  310. package/templates/spinner/wc/spinner.ts +70 -0
  311. package/templates/stepper/index.tsx +230 -0
  312. package/templates/stepper/wc/index.ts +12 -0
  313. package/templates/stepper/wc/stepper-content.ts +30 -0
  314. package/templates/stepper/wc/stepper-description.ts +25 -0
  315. package/templates/stepper/wc/stepper-indicator.ts +30 -0
  316. package/templates/stepper/wc/stepper-item.ts +55 -0
  317. package/templates/stepper/wc/stepper-separator.ts +29 -0
  318. package/templates/stepper/wc/stepper-title.ts +25 -0
  319. package/templates/stepper/wc/stepper-trigger.ts +67 -0
  320. package/templates/stepper/wc/stepper.ts +164 -0
  321. package/templates/switch/switch.tsx +90 -0
  322. package/templates/switch/wc/switch.ts +228 -0
  323. package/templates/table/body.tsx +21 -0
  324. package/templates/table/cell.tsx +44 -0
  325. package/templates/table/head.tsx +112 -0
  326. package/templates/table/header.tsx +21 -0
  327. package/templates/table/index.tsx +93 -0
  328. package/templates/table/root.tsx +82 -0
  329. package/templates/table/row.tsx +36 -0
  330. package/templates/table/wc/index.ts +9 -0
  331. package/templates/table/wc/table-body.ts +32 -0
  332. package/templates/table/wc/table-cell.ts +58 -0
  333. package/templates/table/wc/table-head.ts +129 -0
  334. package/templates/table/wc/table-header.ts +32 -0
  335. package/templates/table/wc/table-row.ts +50 -0
  336. package/templates/table/wc/table.ts +93 -0
  337. package/templates/tabs/index.tsx +222 -0
  338. package/templates/tabs/wc/index.ts +8 -0
  339. package/templates/tabs/wc/tabs-content.ts +82 -0
  340. package/templates/tabs/wc/tabs-list.ts +56 -0
  341. package/templates/tabs/wc/tabs-trigger.ts +136 -0
  342. package/templates/tabs/wc/tabs.ts +202 -0
  343. package/templates/tag/index.tsx +186 -0
  344. package/templates/tag/wc/index.ts +4 -0
  345. package/templates/tag/wc/tag.ts +166 -0
  346. package/templates/text/text.tsx +100 -0
  347. package/templates/text/wc/text.ts +94 -0
  348. package/templates/textarea/textarea.tsx +134 -0
  349. package/templates/textarea/wc/textarea.ts +280 -0
  350. package/templates/time-picker/index.ts +42 -0
  351. package/templates/time-picker/time-picker-context.ts +28 -0
  352. package/templates/time-picker/time-picker-root.tsx +113 -0
  353. package/templates/time-picker/time-picker-segment.tsx +91 -0
  354. package/templates/time-picker/wc/index.ts +1 -0
  355. package/templates/time-picker/wc/time-picker.ts +221 -0
  356. package/templates/toast/index.tsx +71 -0
  357. package/templates/toast/provider.tsx +228 -0
  358. package/templates/toast/toast.tsx +142 -0
  359. package/templates/toast/use-toast.ts +89 -0
  360. package/templates/toast/wc/index.ts +15 -0
  361. package/templates/toast/wc/toast-controller.ts +282 -0
  362. package/templates/toast/wc/toast-provider.ts +161 -0
  363. package/templates/toast/wc/toast.ts +165 -0
  364. package/templates/tooltip/tooltip.tsx +62 -0
  365. package/templates/tooltip/wc/tooltip-content.ts +64 -0
  366. package/templates/tooltip/wc/tooltip.ts +289 -0
  367. package/templates/tree/index.tsx +60 -0
  368. package/templates/tree/tree-item.tsx +131 -0
  369. package/templates/tree/tree.tsx +138 -0
  370. package/templates/tree/wc/index.ts +11 -0
  371. package/templates/tree/wc/tree-item.ts +273 -0
  372. package/templates/tree/wc/tree-utils.ts +143 -0
  373. package/templates/tree/wc/tree.ts +139 -0
  374. package/templates/visually-hidden/visually-hidden.tsx +45 -0
  375. package/templates/visually-hidden/wc/visually-hidden.ts +64 -0
@@ -0,0 +1,981 @@
1
+ import {
2
+ type AnchorPosition,
3
+ type ComboboxBehavior,
4
+ type DismissableLayer,
5
+ type Option,
6
+ type Placement,
7
+ type Presence,
8
+ type RovingFocus,
9
+ type VirtualizedList,
10
+ createAnchorPosition,
11
+ createComboboxBehavior,
12
+ createDismissableLayer,
13
+ createPresence,
14
+ createRovingFocus,
15
+ prefersReducedMotion,
16
+ } from "@hypoth-ui/primitives-dom";
17
+ import { html, nothing } from "lit";
18
+ import type { PropertyValues } from "lit";
19
+ import { property, state } from "lit/decorators.js";
20
+ import { repeat } from "lit/directives/repeat.js";
21
+ import { DSElement } from "../../base/ds-element.js";
22
+ import { FormAssociatedMixin } from "../../base/form-associated.js";
23
+ import type { ValidationFlags } from "../../base/form-associated.js";
24
+ import { StandardEvents, emitEvent } from "../../events/emit.js";
25
+ import { define } from "../../registry/define.js";
26
+
27
+ // Import child components to ensure they're registered
28
+ import type { DsComboboxContent } from "./combobox-content.js";
29
+ import type { DsComboboxInput } from "./combobox-input.js";
30
+ import type { DsComboboxOption } from "./combobox-option.js";
31
+ import "./combobox-content.js";
32
+ import "./combobox-input.js";
33
+ import "./combobox-option.js";
34
+ import "./combobox-tag.js";
35
+
36
+ /**
37
+ * Combobox component with async loading, multi-select, keyboard navigation, and native form participation.
38
+ *
39
+ * Uses ElementInternals for form association - the selected value(s) are submitted with the form
40
+ * and the combobox participates in constraint validation.
41
+ *
42
+ * Implements WAI-ARIA Combobox pattern with:
43
+ * - Text input with autocomplete suggestions
44
+ * - Arrow key navigation between options
45
+ * - Async loading with debounce
46
+ * - Multi-select with tag display
47
+ * - Enter to select, Escape to close
48
+ *
49
+ * @element ds-combobox
50
+ *
51
+ * @slot input - Input element (ds-combobox-input with input inside)
52
+ * @slot tags - Selected value tags (for multi-select)
53
+ * @slot - Combobox content (ds-combobox-content with ds-combobox-option children)
54
+ *
55
+ * @fires ds:open - Fired when combobox opens
56
+ * @fires ds:close - Fired when combobox closes
57
+ * @fires ds:change - Fired when value changes (detail: { value, label } or { values, labels })
58
+ * @fires ds:input - Fired when input value changes (detail: { value })
59
+ * @fires ds:invalid - Fired when customValidation is true and validation fails
60
+ *
61
+ * @example
62
+ * ```html
63
+ * <form>
64
+ * <ds-combobox name="fruit" required>
65
+ * <ds-combobox-input slot="input">
66
+ * <input placeholder="Search fruits..." />
67
+ * </ds-combobox-input>
68
+ * <ds-combobox-content>
69
+ * <ds-combobox-option value="apple">Apple</ds-combobox-option>
70
+ * <ds-combobox-option value="banana">Banana</ds-combobox-option>
71
+ * </ds-combobox-content>
72
+ * </ds-combobox>
73
+ * <button type="submit">Submit</button>
74
+ * </form>
75
+ * ```
76
+ */
77
+ export class DsCombobox extends FormAssociatedMixin(DSElement) {
78
+ /** Whether the combobox is open */
79
+ @property({ type: Boolean, reflect: true })
80
+ open = false;
81
+
82
+ /** Current selected value (single-select mode) */
83
+ @property({ type: String, reflect: true })
84
+ value = "";
85
+
86
+ /** Current selected values (multi-select mode) */
87
+ @property({ type: Array })
88
+ values: string[] = [];
89
+
90
+ /** Placement relative to input */
91
+ @property({ type: String, reflect: true })
92
+ placement: Placement = "bottom-start";
93
+
94
+ /** Offset distance from input in pixels */
95
+ @property({ type: Number })
96
+ offset = 4;
97
+
98
+ /** Whether to flip placement when near viewport edge */
99
+ @property({ type: Boolean })
100
+ flip = true;
101
+
102
+ /** Whether to animate open/close transitions */
103
+ @property({ type: Boolean })
104
+ animated = true;
105
+
106
+ /** Whether the combobox is disabled */
107
+ @property({ type: Boolean, reflect: true })
108
+ disabled = false;
109
+
110
+ /** Enable multi-select mode */
111
+ @property({ type: Boolean, reflect: true })
112
+ multiple = false;
113
+
114
+ /** Allow creating new values */
115
+ @property({ type: Boolean })
116
+ creatable = false;
117
+
118
+ /** Debounce delay for async loading in milliseconds */
119
+ @property({ type: Number })
120
+ debounce = 300;
121
+
122
+ /** Virtualization threshold (default: 100) */
123
+ @property({ type: Number, attribute: "virtualization-threshold" })
124
+ virtualizationThreshold = 100;
125
+
126
+ /** Enable virtualization for large lists */
127
+ @property({ type: Boolean })
128
+ virtualize = false;
129
+
130
+ /**
131
+ * Async loading function. Called when input changes (after debounce).
132
+ * Return an array of Option objects: { value: string, label: string, disabled?: boolean }
133
+ */
134
+ @property({ attribute: false })
135
+ loadItems?: (query: string, signal: AbortSignal) => Promise<Option<string>[]>;
136
+
137
+ /**
138
+ * Data-driven options array. Use this for programmatic option rendering.
139
+ * When provided, options will be rendered from this array instead of slots.
140
+ */
141
+ @property({ attribute: false })
142
+ items: Option<string>[] = [];
143
+
144
+ /** Whether async loading is in progress (read-only) */
145
+ @state()
146
+ loading = false;
147
+
148
+ /** Error message from async loading (read-only) */
149
+ @state()
150
+ loadError: string | null = null;
151
+
152
+ /** Filtered items for display */
153
+ @state()
154
+ private filteredItems: Option<string>[] = [];
155
+
156
+ /** Visible item IDs for virtualization */
157
+ @state()
158
+ private visibleItemIds = new Set<string>();
159
+
160
+ /** Default value for form reset (single-select) */
161
+ private _defaultValue = "";
162
+
163
+ /** Default values for form reset (multi-select) */
164
+ private _defaultValues: string[] = [];
165
+
166
+ private behavior: ComboboxBehavior<string, false> | ComboboxBehavior<string, true> | null = null;
167
+ private anchorPosition: AnchorPosition | null = null;
168
+ private dismissLayer: DismissableLayer | null = null;
169
+ private presence: Presence | null = null;
170
+ private rovingFocus: RovingFocus | null = null;
171
+ private virtualizedList: VirtualizedList | null = null;
172
+ private resizeObserver: ResizeObserver | null = null;
173
+ private scrollHandler: (() => void) | null = null;
174
+ private loadAbortController: AbortController | null = null;
175
+ private debounceTimeout: number | null = null;
176
+
177
+ override connectedCallback(): void {
178
+ // Store default value(s) for form reset
179
+ this._defaultValue = this.value;
180
+ this._defaultValues = [...this.values];
181
+
182
+ super.connectedCallback();
183
+
184
+ // Initialize behavior based on multiple prop
185
+ this.initBehavior();
186
+
187
+ // Listen for input interactions
188
+ this.addEventListener("input", this.handleInput);
189
+ this.addEventListener("keydown", this.handleKeyDown);
190
+ this.addEventListener("click", this.handleClick);
191
+
192
+ // Listen for tag removal
193
+ this.addEventListener("ds:remove", this.handleTagRemove);
194
+
195
+ // Setup after first render
196
+ this.updateComplete.then(() => {
197
+ this.setupInputAccessibility();
198
+ this.registerOptions();
199
+ this.updateOptionStates();
200
+ });
201
+ }
202
+
203
+ override disconnectedCallback(): void {
204
+ super.disconnectedCallback();
205
+ this.removeEventListener("input", this.handleInput);
206
+ this.removeEventListener("keydown", this.handleKeyDown);
207
+ this.removeEventListener("click", this.handleClick);
208
+ this.removeEventListener("ds:remove", this.handleTagRemove);
209
+ this.cleanup();
210
+ this.behavior?.destroy();
211
+ this.behavior = null;
212
+ }
213
+
214
+ private initBehavior(): void {
215
+ if (this.multiple) {
216
+ this.behavior = createComboboxBehavior<string, true>({
217
+ defaultValue: this.values,
218
+ multiple: true,
219
+ creatable: this.creatable,
220
+ debounce: this.debounce,
221
+ virtualizationThreshold: this.virtualizationThreshold,
222
+ disabled: this.disabled,
223
+ onValueChange: (values) => {
224
+ this.values = values;
225
+ const options = this.getOptions();
226
+ const labels = values.map((v) => {
227
+ const opt = options.find((o) => o.value === v);
228
+ return opt?.getLabel() ?? v;
229
+ });
230
+ emitEvent(this, StandardEvents.CHANGE, { detail: { values, labels } });
231
+ },
232
+ onInputChange: (query) => {
233
+ emitEvent(this, "input", { detail: { value: query } });
234
+ this.filterOptions(query);
235
+ },
236
+ onCreateValue: (value) => {
237
+ emitEvent(this, "create", { detail: { value } });
238
+ },
239
+ });
240
+ } else {
241
+ this.behavior = createComboboxBehavior<string, false>({
242
+ defaultValue: this.value || null,
243
+ multiple: false,
244
+ creatable: this.creatable,
245
+ debounce: this.debounce,
246
+ virtualizationThreshold: this.virtualizationThreshold,
247
+ disabled: this.disabled,
248
+ onValueChange: (value) => {
249
+ this.value = value ?? "";
250
+ const option = this.getOptionByValue(value ?? "");
251
+ emitEvent(this, StandardEvents.CHANGE, {
252
+ detail: {
253
+ value: value ?? "",
254
+ label: option?.getLabel() ?? "",
255
+ },
256
+ });
257
+ },
258
+ onInputChange: (query) => {
259
+ emitEvent(this, "input", { detail: { value: query } });
260
+ this.filterOptions(query);
261
+ },
262
+ onCreateValue: (value) => {
263
+ emitEvent(this, "create", { detail: { value } });
264
+ },
265
+ });
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Opens the combobox.
271
+ */
272
+ public show(): void {
273
+ if (this.open || this.disabled) return;
274
+ this.behavior?.open();
275
+ this.open = true;
276
+ emitEvent(this, StandardEvents.OPEN);
277
+ }
278
+
279
+ /**
280
+ * Closes the combobox.
281
+ */
282
+ public close(): void {
283
+ if (!this.open) return;
284
+
285
+ const content = this.querySelector("ds-combobox-content") as DsComboboxContent | null;
286
+
287
+ // If animated, use presence for exit animation
288
+ if (this.animated && content && !prefersReducedMotion()) {
289
+ this.dismissLayer?.deactivate();
290
+ this.dismissLayer = null;
291
+
292
+ this.presence = createPresence({
293
+ onExitComplete: () => {
294
+ this.completeClose();
295
+ },
296
+ });
297
+ this.presence.hide(content);
298
+ } else {
299
+ this.cleanup();
300
+ this.behavior?.close();
301
+ this.open = false;
302
+ emitEvent(this, StandardEvents.CLOSE);
303
+ }
304
+ }
305
+
306
+ private completeClose(): void {
307
+ this.cleanup();
308
+ this.behavior?.close();
309
+ this.open = false;
310
+ emitEvent(this, StandardEvents.CLOSE);
311
+ }
312
+
313
+ /**
314
+ * Selects a value programmatically.
315
+ */
316
+ public select(value: string): void {
317
+ if (this.disabled) return;
318
+ this.behavior?.select(value);
319
+ this.updateOptionStates();
320
+ if (!this.multiple) {
321
+ this.close();
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Removes a value (multi-select mode).
327
+ */
328
+ public removeValue(value: string): void {
329
+ if (this.disabled || !this.multiple) return;
330
+ (this.behavior as ComboboxBehavior<string, true>)?.remove(value);
331
+ this.updateOptionStates();
332
+ }
333
+
334
+ /**
335
+ * Clears all selections.
336
+ */
337
+ public clear(): void {
338
+ if (this.disabled) return;
339
+ this.behavior?.clear();
340
+ this.updateOptionStates();
341
+ }
342
+
343
+ /**
344
+ * Creates a new value (creatable mode).
345
+ */
346
+ public create(value: string): void {
347
+ if (!this.creatable || this.disabled) return;
348
+ this.behavior?.create(value);
349
+ }
350
+
351
+ private getInputWrapper(): DsComboboxInput | null {
352
+ return this.querySelector("ds-combobox-input") as DsComboboxInput | null;
353
+ }
354
+
355
+ private getInputElement(): HTMLInputElement | null {
356
+ return this.getInputWrapper()?.getInputElement() ?? null;
357
+ }
358
+
359
+ private getOptions(): DsComboboxOption[] {
360
+ const content = this.querySelector("ds-combobox-content");
361
+ if (!content) return [];
362
+ return Array.from(content.querySelectorAll<DsComboboxOption>("ds-combobox-option"));
363
+ }
364
+
365
+ private getEnabledOptions(): DsComboboxOption[] {
366
+ return this.getOptions().filter((opt) => !opt.disabled);
367
+ }
368
+
369
+ private getOptionByValue(value: string): DsComboboxOption | null {
370
+ return this.getOptions().find((opt) => opt.value === value) ?? null;
371
+ }
372
+
373
+ private registerOptions(): void {
374
+ const options = this.getOptions();
375
+ const _items: Option<string>[] = options.map((opt) => ({
376
+ value: opt.value,
377
+ label: opt.getLabel(),
378
+ disabled: opt.disabled,
379
+ }));
380
+ // Set static items on behavior (for filtering)
381
+ // Note: behavior stores items internally via the items option
382
+ }
383
+
384
+ private filterOptions(query: string): void {
385
+ const options = this.getOptions();
386
+ const lowerQuery = query.toLowerCase();
387
+
388
+ for (const option of options) {
389
+ const label = option.getLabel().toLowerCase();
390
+ const matches = !query || label.includes(lowerQuery);
391
+
392
+ if (matches) {
393
+ option.removeAttribute("hidden");
394
+ } else {
395
+ option.setAttribute("hidden", "");
396
+ }
397
+ }
398
+
399
+ // Auto-highlight first visible option
400
+ const visibleOptions = this.getEnabledOptions().filter((opt) => !opt.hasAttribute("hidden"));
401
+ if (visibleOptions.length > 0 && visibleOptions[0]) {
402
+ this.behavior?.highlightFirst();
403
+ this.updateOptionStates();
404
+ }
405
+ }
406
+
407
+ private updateOptionStates(): void {
408
+ const currentValue = this.multiple
409
+ ? ((this.behavior as ComboboxBehavior<string, true>)?.state.value ?? this.values)
410
+ : ([this.behavior?.state.value ?? this.value].filter(Boolean) as string[]);
411
+ const highlightedValue = this.behavior?.state.highlightedValue;
412
+
413
+ for (const option of this.getOptions()) {
414
+ const isSelected = currentValue.includes(option.value);
415
+ option.setSelected(isSelected);
416
+ option.setHighlighted(option.value === highlightedValue);
417
+ }
418
+ }
419
+
420
+ private handleInput = (event: Event): void => {
421
+ const target = event.target as HTMLInputElement;
422
+ if (target.tagName !== "INPUT") return;
423
+
424
+ const value = target.value;
425
+ this.behavior?.setInputValue(value);
426
+
427
+ // Auto-open on input
428
+ if (!this.open && value) {
429
+ this.show();
430
+ }
431
+
432
+ // Handle async loading with debounce
433
+ if (this.loadItems) {
434
+ this.scheduleAsyncLoad(value);
435
+ }
436
+ };
437
+
438
+ /**
439
+ * Schedules async loading with debounce.
440
+ */
441
+ private scheduleAsyncLoad(query: string): void {
442
+ // Clear previous debounce
443
+ if (this.debounceTimeout !== null) {
444
+ window.clearTimeout(this.debounceTimeout);
445
+ }
446
+
447
+ // Cancel previous request
448
+ this.loadAbortController?.abort();
449
+
450
+ this.debounceTimeout = window.setTimeout(() => {
451
+ this.executeAsyncLoad(query);
452
+ }, this.debounce);
453
+ }
454
+
455
+ /**
456
+ * Executes the async load.
457
+ */
458
+ private async executeAsyncLoad(query: string): Promise<void> {
459
+ if (!this.loadItems) return;
460
+
461
+ // Create new abort controller
462
+ this.loadAbortController = new AbortController();
463
+ const signal = this.loadAbortController.signal;
464
+
465
+ this.loading = true;
466
+ this.loadError = null;
467
+
468
+ try {
469
+ const results = await this.loadItems(query, signal);
470
+
471
+ // Check if aborted
472
+ if (signal.aborted) return;
473
+
474
+ this.items = results;
475
+ this.filteredItems = results;
476
+ this.loading = false;
477
+
478
+ // Update highlighting
479
+ if (results.length > 0) {
480
+ this.behavior?.highlightFirst();
481
+ this.updateOptionStates();
482
+ }
483
+ } catch (error) {
484
+ // Ignore abort errors
485
+ if (error instanceof Error && error.name === "AbortError") {
486
+ return;
487
+ }
488
+
489
+ this.loading = false;
490
+ this.loadError = error instanceof Error ? error.message : "Failed to load items";
491
+ this.items = [];
492
+ this.filteredItems = [];
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Manually trigger async loading (useful for initial load).
498
+ */
499
+ public async load(query = ""): Promise<void> {
500
+ if (!this.loadItems) return;
501
+ await this.executeAsyncLoad(query);
502
+ }
503
+
504
+ private handleClick = (event: Event): void => {
505
+ const target = event.target as HTMLElement;
506
+
507
+ // Handle option click
508
+ const option = target.closest("ds-combobox-option") as DsComboboxOption | null;
509
+ if (option && this.contains(option) && !option.disabled && !option.hasAttribute("hidden")) {
510
+ event.preventDefault();
511
+ this.select(option.value);
512
+ }
513
+ };
514
+
515
+ private handleKeyDown = (event: KeyboardEvent): void => {
516
+ const target = event.target as HTMLElement;
517
+ const isInput = target.tagName === "INPUT";
518
+
519
+ if (!this.open) {
520
+ // Open on arrow down when focused on input
521
+ if (isInput && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
522
+ event.preventDefault();
523
+ this.show();
524
+ }
525
+ return;
526
+ }
527
+
528
+ switch (event.key) {
529
+ case "Enter": {
530
+ event.preventDefault();
531
+ const highlightedValue = this.behavior?.state.highlightedValue;
532
+ if (highlightedValue) {
533
+ this.select(highlightedValue);
534
+ } else if (this.creatable && isInput) {
535
+ const inputValue = (target as HTMLInputElement).value.trim();
536
+ if (inputValue) {
537
+ this.create(inputValue);
538
+ }
539
+ }
540
+ break;
541
+ }
542
+ case "Escape":
543
+ event.preventDefault();
544
+ this.close();
545
+ this.getInputElement()?.focus();
546
+ break;
547
+ case "ArrowDown":
548
+ event.preventDefault();
549
+ this.behavior?.highlightNext();
550
+ this.updateOptionStates();
551
+ this.updateInputAria();
552
+ this.scrollHighlightedIntoView();
553
+ break;
554
+ case "ArrowUp":
555
+ event.preventDefault();
556
+ this.behavior?.highlightPrev();
557
+ this.updateOptionStates();
558
+ this.updateInputAria();
559
+ this.scrollHighlightedIntoView();
560
+ break;
561
+ case "Home":
562
+ if (isInput && (target as HTMLInputElement).selectionStart === 0) {
563
+ event.preventDefault();
564
+ this.behavior?.highlightFirst();
565
+ this.updateOptionStates();
566
+ this.updateInputAria();
567
+ this.scrollHighlightedIntoView();
568
+ }
569
+ break;
570
+ case "End":
571
+ if (
572
+ isInput &&
573
+ (target as HTMLInputElement).selectionEnd === (target as HTMLInputElement).value.length
574
+ ) {
575
+ event.preventDefault();
576
+ this.behavior?.highlightLast();
577
+ this.updateOptionStates();
578
+ this.updateInputAria();
579
+ this.scrollHighlightedIntoView();
580
+ }
581
+ break;
582
+ case "Backspace":
583
+ if (this.multiple && isInput && (target as HTMLInputElement).value === "") {
584
+ // Remove last tag
585
+ (this.behavior as ComboboxBehavior<string, true>)?.removeLastTag();
586
+ this.updateOptionStates();
587
+ }
588
+ break;
589
+ case "Tab":
590
+ // Close on tab without preventing default
591
+ this.close();
592
+ break;
593
+ }
594
+ };
595
+
596
+ private handleTagRemove = (event: Event): void => {
597
+ const customEvent = event as CustomEvent<{ value: string }>;
598
+ this.removeValue(customEvent.detail.value);
599
+ };
600
+
601
+ private scrollHighlightedIntoView(): void {
602
+ const highlightedValue = this.behavior?.state.highlightedValue;
603
+ if (highlightedValue) {
604
+ const option = this.getOptionByValue(highlightedValue);
605
+ option?.scrollIntoView({ block: "nearest" });
606
+ }
607
+ }
608
+
609
+ private handleDismiss = (): void => {
610
+ this.close();
611
+ };
612
+
613
+ private setupInputAccessibility(): void {
614
+ const inputWrapper = this.getInputWrapper();
615
+ const content = this.querySelector("ds-combobox-content") as DsComboboxContent | null;
616
+
617
+ if (inputWrapper && content) {
618
+ inputWrapper.disabled = this.disabled;
619
+ inputWrapper.updateAria(this.open, undefined, content.id);
620
+ }
621
+ }
622
+
623
+ private updateInputAria(): void {
624
+ const inputWrapper = this.getInputWrapper();
625
+ const content = this.querySelector("ds-combobox-content") as DsComboboxContent | null;
626
+
627
+ if (inputWrapper && content) {
628
+ const highlightedValue = this.behavior?.state.highlightedValue;
629
+ const highlightedOption = highlightedValue ? this.getOptionByValue(highlightedValue) : null;
630
+ inputWrapper.updateAria(this.open, highlightedOption?.id, content.id);
631
+ }
632
+ }
633
+
634
+ private setupPositioning(): void {
635
+ const input = this.getInputElement();
636
+ const content = this.querySelector("ds-combobox-content") as HTMLElement | null;
637
+
638
+ if (!input || !content) return;
639
+
640
+ this.anchorPosition = createAnchorPosition({
641
+ anchor: input,
642
+ floating: content,
643
+ placement: this.placement,
644
+ offset: this.offset,
645
+ flip: this.flip,
646
+ onPositionChange: (pos) => {
647
+ content.setAttribute("data-placement", pos.placement);
648
+ },
649
+ });
650
+
651
+ this.resizeObserver = new ResizeObserver(() => {
652
+ this.anchorPosition?.update();
653
+ });
654
+ this.resizeObserver.observe(input);
655
+ this.resizeObserver.observe(content);
656
+
657
+ this.scrollHandler = () => {
658
+ this.anchorPosition?.update();
659
+ };
660
+ window.addEventListener("scroll", this.scrollHandler, { passive: true });
661
+ window.addEventListener("resize", this.scrollHandler, { passive: true });
662
+ }
663
+
664
+ private setupDismissLayer(): void {
665
+ const content = this.querySelector("ds-combobox-content") as HTMLElement | null;
666
+ const input = this.getInputElement();
667
+
668
+ if (!content) return;
669
+
670
+ this.dismissLayer = createDismissableLayer({
671
+ container: content,
672
+ excludeElements: input ? [input] : [],
673
+ onDismiss: this.handleDismiss,
674
+ closeOnEscape: true,
675
+ closeOnOutsideClick: true,
676
+ });
677
+ this.dismissLayer.activate();
678
+ }
679
+
680
+ private setupRovingFocus(): void {
681
+ const content = this.querySelector("ds-combobox-content") as HTMLElement | null;
682
+
683
+ if (!content) return;
684
+
685
+ this.rovingFocus = createRovingFocus({
686
+ container: content,
687
+ selector: "ds-combobox-option:not([disabled]):not([hidden])",
688
+ direction: "vertical",
689
+ loop: true,
690
+ skipDisabled: true,
691
+ });
692
+ }
693
+
694
+ private cleanup(): void {
695
+ this.anchorPosition?.destroy();
696
+ this.anchorPosition = null;
697
+
698
+ this.dismissLayer?.deactivate();
699
+ this.dismissLayer = null;
700
+
701
+ this.presence?.destroy();
702
+ this.presence = null;
703
+
704
+ this.rovingFocus?.destroy();
705
+ this.rovingFocus = null;
706
+
707
+ this.virtualizedList?.destroy();
708
+ this.virtualizedList = null;
709
+
710
+ this.resizeObserver?.disconnect();
711
+ this.resizeObserver = null;
712
+
713
+ if (this.scrollHandler) {
714
+ window.removeEventListener("scroll", this.scrollHandler);
715
+ window.removeEventListener("resize", this.scrollHandler);
716
+ this.scrollHandler = null;
717
+ }
718
+
719
+ // Cancel any pending async operations
720
+ if (this.debounceTimeout !== null) {
721
+ window.clearTimeout(this.debounceTimeout);
722
+ this.debounceTimeout = null;
723
+ }
724
+ this.loadAbortController?.abort();
725
+ this.loadAbortController = null;
726
+ }
727
+
728
+ override async updated(changedProperties: Map<string, unknown>): Promise<void> {
729
+ super.updated(changedProperties);
730
+
731
+ if (changedProperties.has("open")) {
732
+ this.updateInputAria();
733
+
734
+ const content = this.querySelector("ds-combobox-content") as DsComboboxContent | null;
735
+
736
+ if (this.open) {
737
+ this.registerOptions();
738
+
739
+ content?.removeAttribute("hidden");
740
+
741
+ if (content) {
742
+ content.dataState = "open";
743
+ }
744
+
745
+ await this.updateComplete;
746
+
747
+ this.setupPositioning();
748
+ this.setupDismissLayer();
749
+ this.setupRovingFocus();
750
+
751
+ // Highlight first option or current value
752
+ const visibleOptions = this.getEnabledOptions().filter(
753
+ (opt) => !opt.hasAttribute("hidden")
754
+ );
755
+ if (visibleOptions.length > 0) {
756
+ this.behavior?.highlightFirst();
757
+ this.updateOptionStates();
758
+ }
759
+ } else {
760
+ if (content) {
761
+ content.dataState = "closed";
762
+ }
763
+ content?.setAttribute("hidden", "");
764
+
765
+ // Clear filter when closing
766
+ for (const option of this.getOptions()) {
767
+ option.removeAttribute("hidden");
768
+ }
769
+ }
770
+ }
771
+
772
+ if (changedProperties.has("value") || changedProperties.has("values")) {
773
+ this.updateOptionStates();
774
+ }
775
+
776
+ if (changedProperties.has("disabled")) {
777
+ const inputWrapper = this.getInputWrapper();
778
+ if (inputWrapper) {
779
+ inputWrapper.disabled = this.disabled;
780
+ }
781
+ if (this.disabled && this.open) {
782
+ this.close();
783
+ }
784
+ }
785
+
786
+ if (this.open && (changedProperties.has("placement") || changedProperties.has("offset"))) {
787
+ this.cleanup();
788
+ this.setupPositioning();
789
+ this.setupDismissLayer();
790
+ this.setupRovingFocus();
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Returns true if using data-driven rendering.
796
+ */
797
+ private get isDataDriven(): boolean {
798
+ return this.items.length > 0 || this.loadItems !== undefined;
799
+ }
800
+
801
+ /**
802
+ * Renders a single option from data.
803
+ */
804
+ private renderDataOption(item: Option<string>) {
805
+ const currentValue = this.multiple
806
+ ? ((this.behavior as ComboboxBehavior<string, true>)?.state.value ?? this.values)
807
+ : ([this.behavior?.state.value ?? this.value].filter(Boolean) as string[]);
808
+ const highlightedValue = this.behavior?.state.highlightedValue;
809
+ const isSelected = currentValue.includes(item.value);
810
+ const isHighlighted = item.value === highlightedValue;
811
+
812
+ return html`
813
+ <ds-combobox-option
814
+ value=${item.value}
815
+ ?disabled=${item.disabled}
816
+ data-selected=${isSelected || nothing}
817
+ data-highlighted=${isHighlighted || nothing}
818
+ >
819
+ ${item.label}
820
+ </ds-combobox-option>
821
+ `;
822
+ }
823
+
824
+ /**
825
+ * Renders loading state.
826
+ */
827
+ private renderLoading() {
828
+ return html`
829
+ <div class="ds-combobox__loading" role="status" aria-live="polite">
830
+ <slot name="loading">Loading...</slot>
831
+ </div>
832
+ `;
833
+ }
834
+
835
+ /**
836
+ * Renders error state.
837
+ */
838
+ private renderError() {
839
+ return html`
840
+ <div class="ds-combobox__error" role="alert">
841
+ <slot name="error">${this.loadError}</slot>
842
+ </div>
843
+ `;
844
+ }
845
+
846
+ /**
847
+ * Renders empty state.
848
+ */
849
+ private renderEmpty() {
850
+ return html`
851
+ <div class="ds-combobox__empty">
852
+ <slot name="empty">No results found</slot>
853
+ </div>
854
+ `;
855
+ }
856
+
857
+ // Form association implementation
858
+
859
+ protected getFormValue(): FormData | string | null {
860
+ if (this.multiple) {
861
+ // For multi-select, submit as FormData with multiple values
862
+ if (this.values.length === 0) return null;
863
+ const formData = new FormData();
864
+ for (const val of this.values) {
865
+ formData.append(this.name, val);
866
+ }
867
+ return formData;
868
+ }
869
+ return this.value || null;
870
+ }
871
+
872
+ protected getValidationAnchor(): HTMLElement | undefined {
873
+ return this.getInputElement() as HTMLElement | undefined;
874
+ }
875
+
876
+ protected getValidationFlags(): ValidationFlags {
877
+ const hasValue = this.multiple ? this.values.length > 0 : Boolean(this.value);
878
+ if (this.required && !hasValue) {
879
+ return { valueMissing: true };
880
+ }
881
+ return {};
882
+ }
883
+
884
+ protected getValidationMessage(flags: ValidationFlags): string {
885
+ if (flags.valueMissing) {
886
+ return "Please select an option";
887
+ }
888
+ return "";
889
+ }
890
+
891
+ protected shouldUpdateFormValue(changedProperties: PropertyValues): boolean {
892
+ return changedProperties.has("value") || changedProperties.has("values");
893
+ }
894
+
895
+ protected shouldUpdateValidity(changedProperties: PropertyValues): boolean {
896
+ return changedProperties.has("value") || changedProperties.has("values");
897
+ }
898
+
899
+ protected onFormReset(): void {
900
+ if (this.multiple) {
901
+ this.values = [...this._defaultValues];
902
+ } else {
903
+ this.value = this._defaultValue;
904
+ }
905
+ this.updateOptionStates();
906
+ }
907
+
908
+ protected onFormStateRestore(
909
+ state: string | File | FormData | null,
910
+ _mode: "restore" | "autocomplete"
911
+ ): void {
912
+ if (this.multiple && state instanceof FormData) {
913
+ this.values = state.getAll(this.name) as string[];
914
+ } else if (typeof state === "string") {
915
+ this.value = state;
916
+ }
917
+ this.updateOptionStates();
918
+ }
919
+
920
+ /**
921
+ * Renders data-driven options.
922
+ */
923
+ private renderDataOptions() {
924
+ const items = this.filteredItems.length > 0 ? this.filteredItems : this.items;
925
+
926
+ if (this.loading) {
927
+ return this.renderLoading();
928
+ }
929
+
930
+ if (this.loadError) {
931
+ return this.renderError();
932
+ }
933
+
934
+ if (items.length === 0) {
935
+ return this.renderEmpty();
936
+ }
937
+
938
+ // Use virtualization for large lists
939
+ if (this.virtualize && items.length > this.virtualizationThreshold) {
940
+ return html`
941
+ <div class="ds-combobox__virtualized" style="height: ${Math.min(items.length * 40, 300)}px; overflow-y: auto;">
942
+ ${repeat(
943
+ items,
944
+ (item) => item.value,
945
+ (item) => this.renderDataOption(item)
946
+ )}
947
+ </div>
948
+ `;
949
+ }
950
+
951
+ return repeat(
952
+ items,
953
+ (item) => item.value,
954
+ (item) => this.renderDataOption(item)
955
+ );
956
+ }
957
+
958
+ override render() {
959
+ return html`
960
+ <slot name="tags"></slot>
961
+ <slot name="input"></slot>
962
+ ${
963
+ this.isDataDriven
964
+ ? html`
965
+ <ds-combobox-content>
966
+ ${this.renderDataOptions()}
967
+ </ds-combobox-content>
968
+ `
969
+ : html`<slot></slot>`
970
+ }
971
+ `;
972
+ }
973
+ }
974
+
975
+ define("ds-combobox", DsCombobox);
976
+
977
+ declare global {
978
+ interface HTMLElementTagNameMap {
979
+ "ds-combobox": DsCombobox;
980
+ }
981
+ }