@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,840 @@
1
+ import {
2
+ type AnchorPosition,
3
+ type DismissableLayer,
4
+ type Option,
5
+ type Placement,
6
+ type Presence,
7
+ type RovingFocus,
8
+ type SelectBehavior,
9
+ type TypeAhead,
10
+ type VirtualizedList,
11
+ createAnchorPosition,
12
+ createDismissableLayer,
13
+ createPresence,
14
+ createRovingFocus,
15
+ createSelectBehavior,
16
+ createTypeAhead,
17
+ prefersReducedMotion,
18
+ } from "@hypoth-ui/primitives-dom";
19
+ import { html, nothing } from "lit";
20
+ import type { PropertyValues } from "lit";
21
+ import { property, state } from "lit/decorators.js";
22
+ import { repeat } from "lit/directives/repeat.js";
23
+ import { DSElement } from "../../base/ds-element.js";
24
+ import { FormAssociatedMixin } from "../../base/form-associated.js";
25
+ import type { ValidationFlags } from "../../base/form-associated.js";
26
+ import { StandardEvents, emitEvent } from "../../events/emit.js";
27
+ import { define } from "../../registry/define.js";
28
+
29
+ // Import child components to ensure they're registered
30
+ import type { DsSelectContent } from "./select-content.js";
31
+ import type { DsSelectOption } from "./select-option.js";
32
+ import type { DsSelectTrigger } from "./select-trigger.js";
33
+ import "./select-content.js";
34
+ import "./select-option.js";
35
+ import "./select-trigger.js";
36
+ import "./select-group.js";
37
+ import "./select-label.js";
38
+
39
+ /**
40
+ * Select component with keyboard navigation, type-ahead, and native form participation.
41
+ *
42
+ * Uses ElementInternals for form association - the selected value is submitted with the form
43
+ * and the select participates in constraint validation.
44
+ *
45
+ * Implements WAI-ARIA Listbox pattern with:
46
+ * - Arrow key navigation between options
47
+ * - Type-ahead search to jump to options
48
+ * - Enter/Space/Click to select options
49
+ * - Escape to close
50
+ *
51
+ * @element ds-select
52
+ *
53
+ * @slot trigger - Trigger element (ds-select-trigger with button inside)
54
+ * @slot - Select content (ds-select-content with ds-select-option children)
55
+ *
56
+ * @fires ds:open-change - Fired when open state changes (detail: { open, reason })
57
+ * @fires ds:change - Fired when value changes (detail: { value, label })
58
+ * @fires ds:invalid - Fired when customValidation is true and validation fails
59
+ *
60
+ * @example
61
+ * ```html
62
+ * <form>
63
+ * <ds-select name="fruit" required>
64
+ * <ds-select-trigger slot="trigger">
65
+ * <button>Select fruit</button>
66
+ * </ds-select-trigger>
67
+ * <ds-select-content>
68
+ * <ds-select-option value="apple">Apple</ds-select-option>
69
+ * <ds-select-option value="banana">Banana</ds-select-option>
70
+ * </ds-select-content>
71
+ * </ds-select>
72
+ * <button type="submit">Submit</button>
73
+ * </form>
74
+ * ```
75
+ */
76
+ export class DsSelect extends FormAssociatedMixin(DSElement) {
77
+ /** Whether the select is open */
78
+ @property({ type: Boolean, reflect: true })
79
+ open = false;
80
+
81
+ /** Current selected value */
82
+ @property({ type: String, reflect: true })
83
+ value = "";
84
+
85
+ /** Placement relative to trigger */
86
+ @property({ type: String, reflect: true })
87
+ placement: Placement = "bottom-start";
88
+
89
+ /** Offset distance from trigger in pixels */
90
+ @property({ type: Number })
91
+ offset = 4;
92
+
93
+ /** Whether to flip placement when near viewport edge */
94
+ @property({ type: Boolean })
95
+ flip = true;
96
+
97
+ /** Whether to animate open/close transitions */
98
+ @property({ type: Boolean })
99
+ animated = true;
100
+
101
+ /** Whether the select is disabled */
102
+ @property({ type: Boolean, reflect: true })
103
+ disabled = false;
104
+
105
+ /** Whether the select is read-only */
106
+ @property({ type: Boolean, reflect: true })
107
+ readonly = false;
108
+
109
+ /** Whether to enable type-ahead search */
110
+ @property({ type: Boolean })
111
+ searchable = true;
112
+
113
+ /** Whether to show clear button */
114
+ @property({ type: Boolean })
115
+ clearable = false;
116
+
117
+ /** Enable virtualization for large lists */
118
+ @property({ type: Boolean })
119
+ virtualize = false;
120
+
121
+ /** Virtualization threshold (default: 100) */
122
+ @property({ type: Number, attribute: "virtualization-threshold" })
123
+ virtualizationThreshold = 100;
124
+
125
+ /** Whether the select is in a loading state (e.g., fetching options) */
126
+ @property({ type: Boolean, reflect: true })
127
+ loading = false;
128
+
129
+ /** Text to display/announce during loading */
130
+ @property({ type: String, attribute: "loading-text" })
131
+ loadingText = "Loading...";
132
+
133
+ /**
134
+ * Data-driven options array. Use this for programmatic option rendering.
135
+ * When provided, options will be rendered from this array instead of slots.
136
+ */
137
+ @property({ attribute: false })
138
+ items: Option<string>[] = [];
139
+
140
+ /** Visible item IDs for virtualization */
141
+ @state()
142
+ private visibleItemIds = new Set<string>();
143
+
144
+ /** Default value for form reset */
145
+ private _defaultValue = "";
146
+
147
+ private behavior: SelectBehavior<string> | null = null;
148
+ private anchorPosition: AnchorPosition | null = null;
149
+ private dismissLayer: DismissableLayer | null = null;
150
+ private presence: Presence | null = null;
151
+ private rovingFocus: RovingFocus | null = null;
152
+ private typeAhead: TypeAhead | null = null;
153
+ private virtualizedList: VirtualizedList | null = null;
154
+ private resizeObserver: ResizeObserver | null = null;
155
+ private scrollHandler: (() => void) | null = null;
156
+ private focusFirstOnOpen: "first" | "last" | "selected" | null = null;
157
+
158
+ override connectedCallback(): void {
159
+ // Store default value for form reset
160
+ this._defaultValue = this.value;
161
+
162
+ super.connectedCallback();
163
+
164
+ // Initialize behavior
165
+ this.behavior = createSelectBehavior({
166
+ defaultValue: this.value || null,
167
+ disabled: this.disabled,
168
+ readOnly: this.readonly,
169
+ searchable: this.searchable,
170
+ clearable: this.clearable,
171
+ onValueChange: (value) => {
172
+ this.value = value ?? "";
173
+ const option = this.getOptionByValue(value ?? "");
174
+ emitEvent(this, StandardEvents.CHANGE, {
175
+ detail: {
176
+ value: value ?? "",
177
+ label: option?.getLabel() ?? "",
178
+ },
179
+ });
180
+ },
181
+ onOpenChange: (open) => {
182
+ this.open = open;
183
+ emitEvent(this, StandardEvents.OPEN_CHANGE, {
184
+ detail: { open, reason: "trigger" },
185
+ });
186
+ },
187
+ });
188
+
189
+ // Listen for trigger interactions
190
+ this.addEventListener("click", this.handleTriggerClick);
191
+ this.addEventListener("keydown", this.handleKeyDown);
192
+
193
+ // Setup after first render
194
+ this.updateComplete.then(() => {
195
+ this.setupTriggerAccessibility();
196
+ this.registerOptions();
197
+ this.updateOptionStates();
198
+ });
199
+ }
200
+
201
+ override disconnectedCallback(): void {
202
+ super.disconnectedCallback();
203
+ this.removeEventListener("click", this.handleTriggerClick);
204
+ this.removeEventListener("keydown", this.handleKeyDown);
205
+ this.cleanup();
206
+ this.behavior?.destroy();
207
+ this.behavior = null;
208
+ }
209
+
210
+ /**
211
+ * Opens the select.
212
+ */
213
+ public show(): void {
214
+ if (this.open || this.disabled || this.loading) return;
215
+ this.behavior?.open();
216
+ }
217
+
218
+ /**
219
+ * Closes the select.
220
+ */
221
+ public close(): void {
222
+ if (!this.open) return;
223
+
224
+ const content = this.querySelector("ds-select-content") as DsSelectContent | null;
225
+
226
+ // If animated, use presence for exit animation
227
+ if (this.animated && content && !prefersReducedMotion()) {
228
+ // Cleanup dismiss layer so it doesn't re-trigger
229
+ this.dismissLayer?.deactivate();
230
+ this.dismissLayer = null;
231
+
232
+ // Create presence for exit animation
233
+ this.presence = createPresence({
234
+ onExitComplete: () => {
235
+ this.completeClose();
236
+ },
237
+ });
238
+ this.presence.hide(content);
239
+ } else {
240
+ // No animation - close immediately
241
+ this.cleanup();
242
+ this.behavior?.close();
243
+
244
+ // Return focus to trigger
245
+ this.getTriggerElement()?.focus();
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Completes the close after exit animation.
251
+ */
252
+ private completeClose(): void {
253
+ this.cleanup();
254
+ this.behavior?.close();
255
+
256
+ // Return focus to trigger
257
+ this.getTriggerElement()?.focus();
258
+ }
259
+
260
+ /**
261
+ * Toggles the select open/closed state.
262
+ */
263
+ public toggle(): void {
264
+ if (this.open) {
265
+ this.close();
266
+ } else {
267
+ this.show();
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Selects a value programmatically.
273
+ */
274
+ public select(value: string): void {
275
+ if (this.disabled || this.readonly) return;
276
+ this.behavior?.select(value);
277
+ }
278
+
279
+ /**
280
+ * Clears the selection.
281
+ */
282
+ public clear(): void {
283
+ if (!this.clearable || this.disabled || this.readonly) return;
284
+ this.behavior?.clear();
285
+ this.updateOptionStates();
286
+ }
287
+
288
+ private getTriggerElement(): HTMLElement | null {
289
+ const trigger = this.querySelector("ds-select-trigger") as DsSelectTrigger | null;
290
+ return trigger?.getTriggerElement() ?? null;
291
+ }
292
+
293
+ private getOptions(): DsSelectOption[] {
294
+ const content = this.querySelector("ds-select-content");
295
+ if (!content) return [];
296
+ return Array.from(content.querySelectorAll<DsSelectOption>("ds-select-option"));
297
+ }
298
+
299
+ private getEnabledOptions(): DsSelectOption[] {
300
+ return this.getOptions().filter((opt) => !opt.disabled);
301
+ }
302
+
303
+ private getOptionByValue(value: string): DsSelectOption | null {
304
+ return this.getOptions().find((opt) => opt.value === value) ?? null;
305
+ }
306
+
307
+ private registerOptions(): void {
308
+ const options = this.getOptions();
309
+ const items = options.map((opt) => ({
310
+ value: opt.value,
311
+ disabled: opt.disabled,
312
+ }));
313
+ this.behavior?.setItems(items);
314
+ this.behavior?.setOptionCount(options.length);
315
+ }
316
+
317
+ private updateOptionStates(): void {
318
+ const currentValue = this.behavior?.state.value ?? this.value;
319
+ const highlightedValue = this.behavior?.state.highlightedValue;
320
+
321
+ for (const option of this.getOptions()) {
322
+ option.setSelected(option.value === currentValue);
323
+ option.setHighlighted(option.value === highlightedValue);
324
+ }
325
+ }
326
+
327
+ private handleTriggerClick = (event: Event): void => {
328
+ const target = event.target as HTMLElement;
329
+ const trigger = target.closest("ds-select-trigger");
330
+
331
+ if (trigger && this.contains(trigger)) {
332
+ event.preventDefault();
333
+ if (this.disabled || this.loading) return;
334
+ this.focusFirstOnOpen = this.value ? "selected" : "first";
335
+ this.toggle();
336
+ }
337
+
338
+ // Handle option click
339
+ const option = target.closest("ds-select-option") as DsSelectOption | null;
340
+ if (option && this.contains(option) && !option.disabled) {
341
+ event.preventDefault();
342
+ this.selectOption(option);
343
+ }
344
+ };
345
+
346
+ private handleKeyDown = (event: KeyboardEvent): void => {
347
+ const target = event.target as HTMLElement;
348
+
349
+ // Handle trigger keys when closed
350
+ if (!this.open) {
351
+ const trigger = target.closest("ds-select-trigger");
352
+ if (trigger && this.contains(trigger)) {
353
+ this.handleTriggerKeyDown(event);
354
+ }
355
+ return;
356
+ }
357
+
358
+ // Handle content keys when open
359
+ this.handleContentKeyDown(event);
360
+ };
361
+
362
+ private handleTriggerKeyDown(event: KeyboardEvent): void {
363
+ if (this.disabled || this.loading) return;
364
+
365
+ switch (event.key) {
366
+ case "Enter":
367
+ case " ":
368
+ event.preventDefault();
369
+ this.focusFirstOnOpen = this.value ? "selected" : "first";
370
+ this.show();
371
+ break;
372
+ case "ArrowDown":
373
+ event.preventDefault();
374
+ this.focusFirstOnOpen = "first";
375
+ this.show();
376
+ break;
377
+ case "ArrowUp":
378
+ event.preventDefault();
379
+ this.focusFirstOnOpen = "last";
380
+ this.show();
381
+ break;
382
+ }
383
+ }
384
+
385
+ private handleContentKeyDown(event: KeyboardEvent): void {
386
+ switch (event.key) {
387
+ case "Enter":
388
+ case " ":
389
+ event.preventDefault();
390
+ this.selectHighlighted();
391
+ break;
392
+ case "Escape":
393
+ event.preventDefault();
394
+ this.close();
395
+ break;
396
+ case "ArrowDown":
397
+ event.preventDefault();
398
+ this.behavior?.highlightNext();
399
+ this.updateOptionStates();
400
+ this.updateTriggerAria();
401
+ break;
402
+ case "ArrowUp":
403
+ event.preventDefault();
404
+ this.behavior?.highlightPrev();
405
+ this.updateOptionStates();
406
+ this.updateTriggerAria();
407
+ break;
408
+ case "Home":
409
+ event.preventDefault();
410
+ this.behavior?.highlightFirst();
411
+ this.updateOptionStates();
412
+ this.updateTriggerAria();
413
+ break;
414
+ case "End":
415
+ event.preventDefault();
416
+ this.behavior?.highlightLast();
417
+ this.updateOptionStates();
418
+ this.updateTriggerAria();
419
+ break;
420
+ case "Tab":
421
+ // Close on tab without preventing default
422
+ this.close();
423
+ break;
424
+ }
425
+ }
426
+
427
+ private selectOption(option: DsSelectOption): void {
428
+ this.behavior?.select(option.value);
429
+ this.updateOptionStates();
430
+ this.close();
431
+ }
432
+
433
+ private selectHighlighted(): void {
434
+ const highlightedValue = this.behavior?.state.highlightedValue;
435
+ if (highlightedValue) {
436
+ this.behavior?.select(highlightedValue);
437
+ this.updateOptionStates();
438
+ this.close();
439
+ }
440
+ }
441
+
442
+ private handleDismiss = (): void => {
443
+ this.close();
444
+ };
445
+
446
+ private setupTriggerAccessibility(): void {
447
+ const trigger = this.querySelector("ds-select-trigger") as DsSelectTrigger | null;
448
+ const content = this.querySelector("ds-select-content") as DsSelectContent | null;
449
+
450
+ if (trigger && content) {
451
+ trigger.disabled = this.disabled;
452
+ trigger.updateAria(this.open, undefined, content.id);
453
+ }
454
+
455
+ // Set aria-busy on the trigger element when loading
456
+ this.updateLoadingState();
457
+ }
458
+
459
+ private updateLoadingState(): void {
460
+ const triggerElement = this.getTriggerElement();
461
+ if (triggerElement) {
462
+ if (this.loading) {
463
+ triggerElement.setAttribute("aria-busy", "true");
464
+ } else {
465
+ triggerElement.removeAttribute("aria-busy");
466
+ }
467
+ }
468
+ }
469
+
470
+ private updateTriggerAria(): void {
471
+ const trigger = this.querySelector("ds-select-trigger") as DsSelectTrigger | null;
472
+ const content = this.querySelector("ds-select-content") as DsSelectContent | null;
473
+
474
+ if (trigger && content) {
475
+ const highlightedValue = this.behavior?.state.highlightedValue;
476
+ const highlightedOption = highlightedValue ? this.getOptionByValue(highlightedValue) : null;
477
+ trigger.updateAria(this.open, highlightedOption?.id, content.id);
478
+ }
479
+ }
480
+
481
+ private setupPositioning(): void {
482
+ const trigger = this.getTriggerElement();
483
+ const content = this.querySelector("ds-select-content") as HTMLElement | null;
484
+
485
+ if (!trigger || !content) return;
486
+
487
+ // Setup anchor positioning
488
+ this.anchorPosition = createAnchorPosition({
489
+ anchor: trigger,
490
+ floating: content,
491
+ placement: this.placement,
492
+ offset: this.offset,
493
+ flip: this.flip,
494
+ onPositionChange: (pos) => {
495
+ content.setAttribute("data-placement", pos.placement);
496
+ },
497
+ });
498
+
499
+ // Setup resize observer for repositioning
500
+ this.resizeObserver = new ResizeObserver(() => {
501
+ this.anchorPosition?.update();
502
+ });
503
+ this.resizeObserver.observe(trigger);
504
+ this.resizeObserver.observe(content);
505
+
506
+ // Setup scroll handler for repositioning
507
+ this.scrollHandler = () => {
508
+ this.anchorPosition?.update();
509
+ };
510
+ window.addEventListener("scroll", this.scrollHandler, { passive: true });
511
+ window.addEventListener("resize", this.scrollHandler, { passive: true });
512
+ }
513
+
514
+ private setupDismissLayer(): void {
515
+ const content = this.querySelector("ds-select-content") as HTMLElement | null;
516
+ const trigger = this.getTriggerElement();
517
+
518
+ if (!content) return;
519
+
520
+ this.dismissLayer = createDismissableLayer({
521
+ container: content,
522
+ excludeElements: trigger ? [trigger] : [],
523
+ onDismiss: this.handleDismiss,
524
+ closeOnEscape: true,
525
+ closeOnOutsideClick: true,
526
+ });
527
+ this.dismissLayer.activate();
528
+ }
529
+
530
+ private setupRovingFocus(): void {
531
+ const content = this.querySelector("ds-select-content") as HTMLElement | null;
532
+
533
+ if (!content) return;
534
+
535
+ this.rovingFocus = createRovingFocus({
536
+ container: content,
537
+ selector: "ds-select-option:not([disabled])",
538
+ direction: "vertical",
539
+ loop: true,
540
+ skipDisabled: true,
541
+ });
542
+ }
543
+
544
+ private setupTypeAhead(): void {
545
+ if (!this.searchable) return;
546
+
547
+ const content = this.querySelector("ds-select-content") as HTMLElement | null;
548
+ if (!content) return;
549
+
550
+ this.typeAhead = createTypeAhead({
551
+ items: () => this.getEnabledOptions() as HTMLElement[],
552
+ getText: (item) => (item as DsSelectOption).getLabel(),
553
+ onMatch: (item) => {
554
+ const option = item as DsSelectOption;
555
+ this.behavior?.highlight(option.value);
556
+ this.updateOptionStates();
557
+ this.updateTriggerAria();
558
+ // Scroll into view
559
+ option.scrollIntoView({ block: "nearest" });
560
+ },
561
+ });
562
+
563
+ // Wire type-ahead to keydown events
564
+ content.addEventListener("keydown", this.handleTypeAheadKeyDown);
565
+ }
566
+
567
+ private handleTypeAheadKeyDown = (event: KeyboardEvent): void => {
568
+ this.typeAhead?.handleKeyDown(event);
569
+ };
570
+
571
+ private focusInitialItem(): void {
572
+ const content = this.querySelector("ds-select-content");
573
+ if (!content) return;
574
+
575
+ const options = this.getEnabledOptions();
576
+ if (options.length === 0) return;
577
+
578
+ let initialIndex = 0;
579
+
580
+ if (this.focusFirstOnOpen === "last") {
581
+ initialIndex = options.length - 1;
582
+ } else if (this.focusFirstOnOpen === "selected" && this.value) {
583
+ const selectedIndex = options.findIndex((opt) => opt.value === this.value);
584
+ if (selectedIndex >= 0) {
585
+ initialIndex = selectedIndex;
586
+ }
587
+ }
588
+
589
+ // Highlight the initial option
590
+ const initialOption = options[initialIndex];
591
+ if (initialOption) {
592
+ this.behavior?.highlight(initialOption.value);
593
+ this.updateOptionStates();
594
+ this.rovingFocus?.setFocusedIndex(initialIndex);
595
+ initialOption.scrollIntoView({ block: "nearest" });
596
+ }
597
+
598
+ this.focusFirstOnOpen = null;
599
+ }
600
+
601
+ private cleanup(): void {
602
+ const content = this.querySelector("ds-select-content") as HTMLElement | null;
603
+
604
+ // Cleanup type-ahead listener
605
+ if (content) {
606
+ content.removeEventListener("keydown", this.handleTypeAheadKeyDown);
607
+ }
608
+
609
+ // Cleanup anchor positioning
610
+ this.anchorPosition?.destroy();
611
+ this.anchorPosition = null;
612
+
613
+ // Cleanup dismiss layer
614
+ this.dismissLayer?.deactivate();
615
+ this.dismissLayer = null;
616
+
617
+ // Cleanup presence
618
+ this.presence?.destroy();
619
+ this.presence = null;
620
+
621
+ // Cleanup roving focus
622
+ this.rovingFocus?.destroy();
623
+ this.rovingFocus = null;
624
+
625
+ // Cleanup type-ahead
626
+ this.typeAhead?.reset();
627
+ this.typeAhead = null;
628
+
629
+ // Cleanup virtualized list
630
+ this.virtualizedList?.destroy();
631
+ this.virtualizedList = null;
632
+
633
+ // Cleanup resize observer
634
+ this.resizeObserver?.disconnect();
635
+ this.resizeObserver = null;
636
+
637
+ // Cleanup scroll handler
638
+ if (this.scrollHandler) {
639
+ window.removeEventListener("scroll", this.scrollHandler);
640
+ window.removeEventListener("resize", this.scrollHandler);
641
+ this.scrollHandler = null;
642
+ }
643
+ }
644
+
645
+ override async updated(changedProperties: Map<string, unknown>): Promise<void> {
646
+ super.updated(changedProperties);
647
+
648
+ if (changedProperties.has("open")) {
649
+ this.updateTriggerAria();
650
+
651
+ const content = this.querySelector("ds-select-content") as DsSelectContent | null;
652
+
653
+ if (this.open) {
654
+ // Re-register options in case they changed
655
+ this.registerOptions();
656
+
657
+ // Show content
658
+ content?.removeAttribute("hidden");
659
+
660
+ // Set data-state to open for entry animation
661
+ if (content) {
662
+ content.dataState = "open";
663
+ }
664
+
665
+ // Wait for DOM update
666
+ await this.updateComplete;
667
+
668
+ // Setup all behaviors
669
+ this.setupPositioning();
670
+ this.setupDismissLayer();
671
+ this.setupRovingFocus();
672
+ this.setupTypeAhead();
673
+
674
+ // Focus initial item
675
+ this.focusInitialItem();
676
+ } else {
677
+ // Set data-state to closed for exit animation
678
+ if (content) {
679
+ content.dataState = "closed";
680
+ }
681
+ // Hide content
682
+ content?.setAttribute("hidden", "");
683
+ }
684
+ }
685
+
686
+ if (changedProperties.has("value")) {
687
+ this.updateOptionStates();
688
+ }
689
+
690
+ if (changedProperties.has("disabled")) {
691
+ const trigger = this.querySelector("ds-select-trigger") as DsSelectTrigger | null;
692
+ if (trigger) {
693
+ trigger.disabled = this.disabled;
694
+ }
695
+ if (this.disabled && this.open) {
696
+ this.close();
697
+ }
698
+ }
699
+
700
+ // Handle loading state changes
701
+ if (changedProperties.has("loading")) {
702
+ this.updateLoadingState();
703
+ // Close dropdown if loading starts while open
704
+ if (this.loading && this.open) {
705
+ this.close();
706
+ }
707
+ }
708
+
709
+ // Update positioning if placement or offset changes while open
710
+ if (this.open && (changedProperties.has("placement") || changedProperties.has("offset"))) {
711
+ this.cleanup();
712
+ this.setupPositioning();
713
+ this.setupDismissLayer();
714
+ this.setupRovingFocus();
715
+ this.setupTypeAhead();
716
+ }
717
+ }
718
+
719
+ // Form association implementation
720
+
721
+ protected getFormValue(): string | null {
722
+ return this.value || null;
723
+ }
724
+
725
+ protected getValidationAnchor(): HTMLElement | undefined {
726
+ return this.getTriggerElement() as HTMLElement | undefined;
727
+ }
728
+
729
+ protected getValidationFlags(): ValidationFlags {
730
+ if (this.required && !this.value) {
731
+ return { valueMissing: true };
732
+ }
733
+ return {};
734
+ }
735
+
736
+ protected getValidationMessage(flags: ValidationFlags): string {
737
+ if (flags.valueMissing) {
738
+ return "Please select an option";
739
+ }
740
+ return "";
741
+ }
742
+
743
+ protected shouldUpdateFormValue(changedProperties: PropertyValues): boolean {
744
+ return changedProperties.has("value");
745
+ }
746
+
747
+ protected shouldUpdateValidity(changedProperties: PropertyValues): boolean {
748
+ return changedProperties.has("value");
749
+ }
750
+
751
+ protected onFormReset(): void {
752
+ this.value = this._defaultValue;
753
+ this.updateOptionStates();
754
+ }
755
+
756
+ protected onFormStateRestore(
757
+ state: string | File | FormData | null,
758
+ _mode: "restore" | "autocomplete"
759
+ ): void {
760
+ if (typeof state === "string") {
761
+ this.value = state;
762
+ this.updateOptionStates();
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Returns true if using data-driven rendering.
768
+ */
769
+ private get isDataDriven(): boolean {
770
+ return this.items.length > 0;
771
+ }
772
+
773
+ /**
774
+ * Renders a single option from data.
775
+ */
776
+ private renderDataOption(item: Option<string>) {
777
+ const currentValue = this.behavior?.state.value ?? this.value;
778
+ const highlightedValue = this.behavior?.state.highlightedValue;
779
+ const isSelected = item.value === currentValue;
780
+ const isHighlighted = item.value === highlightedValue;
781
+
782
+ return html`
783
+ <ds-select-option
784
+ value=${item.value}
785
+ ?disabled=${item.disabled}
786
+ data-selected=${isSelected || nothing}
787
+ data-highlighted=${isHighlighted || nothing}
788
+ >
789
+ ${item.label}
790
+ </ds-select-option>
791
+ `;
792
+ }
793
+
794
+ /**
795
+ * Renders data-driven options with optional virtualization.
796
+ */
797
+ private renderDataOptions() {
798
+ // Use virtualization for large lists
799
+ if (this.virtualize && this.items.length > this.virtualizationThreshold) {
800
+ return html`
801
+ <div class="ds-select__virtualized" style="height: ${Math.min(this.items.length * 40, 300)}px; overflow-y: auto;">
802
+ ${repeat(
803
+ this.items,
804
+ (item) => item.value,
805
+ (item) => this.renderDataOption(item)
806
+ )}
807
+ </div>
808
+ `;
809
+ }
810
+
811
+ return repeat(
812
+ this.items,
813
+ (item) => item.value,
814
+ (item) => this.renderDataOption(item)
815
+ );
816
+ }
817
+
818
+ override render() {
819
+ return html`
820
+ <slot name="trigger"></slot>
821
+ ${
822
+ this.isDataDriven
823
+ ? html`
824
+ <ds-select-content>
825
+ ${this.renderDataOptions()}
826
+ </ds-select-content>
827
+ `
828
+ : html`<slot></slot>`
829
+ }
830
+ `;
831
+ }
832
+ }
833
+
834
+ define("ds-select", DsSelect);
835
+
836
+ declare global {
837
+ interface HTMLElementTagNameMap {
838
+ "ds-select": DsSelect;
839
+ }
840
+ }