shadcn_view_component 0.3.3

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 (507) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +255 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +604 -0
  5. data/app/assets/stylesheets/shadcn-themes.css +1075 -0
  6. data/app/assets/stylesheets/shadcn.css +853 -0
  7. data/app/components/shadcn/accordion/component.rb +44 -0
  8. data/app/components/shadcn/accordion/content/component.rb +44 -0
  9. data/app/components/shadcn/accordion/item/component.rb +41 -0
  10. data/app/components/shadcn/accordion/preview.rb +16 -0
  11. data/app/components/shadcn/accordion/previews/default.html.erb +10 -0
  12. data/app/components/shadcn/accordion/previews/multiple.html.erb +13 -0
  13. data/app/components/shadcn/accordion/trigger/component.rb +52 -0
  14. data/app/components/shadcn/alert/component.rb +52 -0
  15. data/app/components/shadcn/alert/preview.rb +11 -0
  16. data/app/components/shadcn/alert/previews/default.html.erb +8 -0
  17. data/app/components/shadcn/alert.rb +17 -0
  18. data/app/components/shadcn/alert_dialog/action/component.rb +35 -0
  19. data/app/components/shadcn/alert_dialog/cancel/component.rb +16 -0
  20. data/app/components/shadcn/alert_dialog/component.rb +20 -0
  21. data/app/components/shadcn/alert_dialog/content/component.rb +55 -0
  22. data/app/components/shadcn/alert_dialog/header/component.rb +33 -0
  23. data/app/components/shadcn/alert_dialog/preview.rb +16 -0
  24. data/app/components/shadcn/alert_dialog/previews/default.html.erb +13 -0
  25. data/app/components/shadcn/alert_dialog/previews/small_with_media.html.erb +38 -0
  26. data/app/components/shadcn/alert_dialog.rb +32 -0
  27. data/app/components/shadcn/application_view_component.rb +225 -0
  28. data/app/components/shadcn/application_view_component_preview.rb +8 -0
  29. data/app/components/shadcn/aspect_ratio/component.rb +34 -0
  30. data/app/components/shadcn/aspect_ratio/preview.rb +16 -0
  31. data/app/components/shadcn/aspect_ratio/previews/default.html.erb +5 -0
  32. data/app/components/shadcn/aspect_ratio/previews/ratios.html.erb +12 -0
  33. data/app/components/shadcn/attachment/action/component.rb +19 -0
  34. data/app/components/shadcn/attachment/component.rb +76 -0
  35. data/app/components/shadcn/attachment/media/component.rb +56 -0
  36. data/app/components/shadcn/attachment/preview.rb +11 -0
  37. data/app/components/shadcn/attachment/previews/default.html.erb +80 -0
  38. data/app/components/shadcn/attachment/trigger/component.rb +31 -0
  39. data/app/components/shadcn/attachment.rb +42 -0
  40. data/app/components/shadcn/avatar/component.rb +43 -0
  41. data/app/components/shadcn/avatar/fallback/component.rb +23 -0
  42. data/app/components/shadcn/avatar/image/component.rb +20 -0
  43. data/app/components/shadcn/avatar/preview.rb +11 -0
  44. data/app/components/shadcn/avatar/previews/default.html.erb +13 -0
  45. data/app/components/shadcn/avatar.rb +32 -0
  46. data/app/components/shadcn/badge/component.rb +57 -0
  47. data/app/components/shadcn/badge/preview.rb +11 -0
  48. data/app/components/shadcn/badge/previews/default.html.erb +5 -0
  49. data/app/components/shadcn/breadcrumb/component.rb +15 -0
  50. data/app/components/shadcn/breadcrumb/ellipsis/component.rb +28 -0
  51. data/app/components/shadcn/breadcrumb/page/component.rb +25 -0
  52. data/app/components/shadcn/breadcrumb/preview.rb +16 -0
  53. data/app/components/shadcn/breadcrumb/previews/custom_separator.html.erb +17 -0
  54. data/app/components/shadcn/breadcrumb/previews/default.html.erb +15 -0
  55. data/app/components/shadcn/breadcrumb/separator/component.rb +31 -0
  56. data/app/components/shadcn/breadcrumb.rb +18 -0
  57. data/app/components/shadcn/bubble/component.rb +90 -0
  58. data/app/components/shadcn/bubble/preview.rb +16 -0
  59. data/app/components/shadcn/bubble/previews/alignment.html.erb +33 -0
  60. data/app/components/shadcn/bubble/previews/default.html.erb +22 -0
  61. data/app/components/shadcn/bubble/reactions/component.rb +54 -0
  62. data/app/components/shadcn/bubble.rb +24 -0
  63. data/app/components/shadcn/button/component.rb +85 -0
  64. data/app/components/shadcn/button/preview.rb +31 -0
  65. data/app/components/shadcn/button/previews/default.html.erb +8 -0
  66. data/app/components/shadcn/button/previews/sizes.html.erb +5 -0
  67. data/app/components/shadcn/button/previews/variants.html.erb +5 -0
  68. data/app/components/shadcn/button_group/component.rb +53 -0
  69. data/app/components/shadcn/button_group/preview.rb +15 -0
  70. data/app/components/shadcn/button_group/previews/default.html.erb +18 -0
  71. data/app/components/shadcn/button_group/previews/vertical.html.erb +5 -0
  72. data/app/components/shadcn/button_group/separator/component.rb +30 -0
  73. data/app/components/shadcn/button_group/text/component.rb +20 -0
  74. data/app/components/shadcn/calendar/component.rb +558 -0
  75. data/app/components/shadcn/calendar/month.rb +74 -0
  76. data/app/components/shadcn/calendar/preview.rb +64 -0
  77. data/app/components/shadcn/calendar/previews/booked.html.erb +18 -0
  78. data/app/components/shadcn/calendar/previews/custom_cell_size.html.erb +11 -0
  79. data/app/components/shadcn/calendar/previews/date_and_time.html.erb +31 -0
  80. data/app/components/shadcn/calendar/previews/default.html.erb +8 -0
  81. data/app/components/shadcn/calendar/previews/dropdown_caption.html.erb +11 -0
  82. data/app/components/shadcn/calendar/previews/in_a_form.html.erb +8 -0
  83. data/app/components/shadcn/calendar/previews/presets.html.erb +20 -0
  84. data/app/components/shadcn/calendar/previews/range.html.erb +10 -0
  85. data/app/components/shadcn/calendar/previews/week_numbers.html.erb +8 -0
  86. data/app/components/shadcn/card/component.rb +24 -0
  87. data/app/components/shadcn/card/header/component.rb +26 -0
  88. data/app/components/shadcn/card/preview.rb +16 -0
  89. data/app/components/shadcn/card/previews/default.html.erb +18 -0
  90. data/app/components/shadcn/card/previews/image.html.erb +37 -0
  91. data/app/components/shadcn/card.rb +21 -0
  92. data/app/components/shadcn/carousel/component.rb +61 -0
  93. data/app/components/shadcn/carousel/content/component.rb +86 -0
  94. data/app/components/shadcn/carousel/item/component.rb +42 -0
  95. data/app/components/shadcn/carousel/next/component.rb +59 -0
  96. data/app/components/shadcn/carousel/preview.rb +22 -0
  97. data/app/components/shadcn/carousel/previews/default.html.erb +22 -0
  98. data/app/components/shadcn/carousel/previews/sizes.html.erb +42 -0
  99. data/app/components/shadcn/carousel/previews/vertical.html.erb +27 -0
  100. data/app/components/shadcn/carousel/previous/component.rb +59 -0
  101. data/app/components/shadcn/chart/area/component.rb +43 -0
  102. data/app/components/shadcn/chart/bar/component.rb +75 -0
  103. data/app/components/shadcn/chart/cartesian/component.rb +152 -0
  104. data/app/components/shadcn/chart/component.rb +124 -0
  105. data/app/components/shadcn/chart/focusable.rb +38 -0
  106. data/app/components/shadcn/chart/legend/component.rb +55 -0
  107. data/app/components/shadcn/chart/line/component.rb +50 -0
  108. data/app/components/shadcn/chart/pie/component.rb +169 -0
  109. data/app/components/shadcn/chart/plot.rb +147 -0
  110. data/app/components/shadcn/chart/preview.rb +45 -0
  111. data/app/components/shadcn/chart/previews/area.html.erb +31 -0
  112. data/app/components/shadcn/chart/previews/bar.html.erb +32 -0
  113. data/app/components/shadcn/chart/previews/bar_stacked.html.erb +30 -0
  114. data/app/components/shadcn/chart/previews/default.html.erb +30 -0
  115. data/app/components/shadcn/chart/previews/donut.html.erb +16 -0
  116. data/app/components/shadcn/chart/previews/line.html.erb +30 -0
  117. data/app/components/shadcn/chart/previews/percentages.html.erb +17 -0
  118. data/app/components/shadcn/chart/table/component.rb +75 -0
  119. data/app/components/shadcn/chart/tooltip/component.rb +111 -0
  120. data/app/components/shadcn/checkbox/component.rb +109 -0
  121. data/app/components/shadcn/checkbox/preview.rb +16 -0
  122. data/app/components/shadcn/checkbox/previews/default.html.erb +14 -0
  123. data/app/components/shadcn/checkbox/previews/states.html.erb +23 -0
  124. data/app/components/shadcn/collapsible/component.rb +35 -0
  125. data/app/components/shadcn/collapsible/content/component.rb +20 -0
  126. data/app/components/shadcn/collapsible/preview.rb +11 -0
  127. data/app/components/shadcn/collapsible/previews/default.html.erb +9 -0
  128. data/app/components/shadcn/collapsible/trigger/component.rb +23 -0
  129. data/app/components/shadcn/combobox/chip/component.rb +60 -0
  130. data/app/components/shadcn/combobox/chip_input/component.rb +43 -0
  131. data/app/components/shadcn/combobox/chips/component.rb +54 -0
  132. data/app/components/shadcn/combobox/clear/component.rb +32 -0
  133. data/app/components/shadcn/combobox/component.rb +113 -0
  134. data/app/components/shadcn/combobox/content/component.rb +59 -0
  135. data/app/components/shadcn/combobox/empty/component.rb +23 -0
  136. data/app/components/shadcn/combobox/input/component.rb +85 -0
  137. data/app/components/shadcn/combobox/item/component.rb +69 -0
  138. data/app/components/shadcn/combobox/list/component.rb +27 -0
  139. data/app/components/shadcn/combobox/preview.rb +31 -0
  140. data/app/components/shadcn/combobox/previews/default.html.erb +20 -0
  141. data/app/components/shadcn/combobox/previews/grouped.html.erb +25 -0
  142. data/app/components/shadcn/combobox/previews/multiple.html.erb +28 -0
  143. data/app/components/shadcn/combobox/previews/with_clear.html.erb +16 -0
  144. data/app/components/shadcn/combobox/trigger/component.rb +54 -0
  145. data/app/components/shadcn/combobox.rb +16 -0
  146. data/app/components/shadcn/command/component.rb +67 -0
  147. data/app/components/shadcn/command/dialog/component.rb +72 -0
  148. data/app/components/shadcn/command/empty/component.rb +29 -0
  149. data/app/components/shadcn/command/group/component.rb +63 -0
  150. data/app/components/shadcn/command/input/component.rb +66 -0
  151. data/app/components/shadcn/command/item/component.rb +62 -0
  152. data/app/components/shadcn/command/list/component.rb +36 -0
  153. data/app/components/shadcn/command/preview.rb +26 -0
  154. data/app/components/shadcn/command/previews/default.html.erb +41 -0
  155. data/app/components/shadcn/command/previews/dialog.html.erb +33 -0
  156. data/app/components/shadcn/command/previews/ranking.html.erb +16 -0
  157. data/app/components/shadcn/command/separator/component.rb +33 -0
  158. data/app/components/shadcn/command.rb +14 -0
  159. data/app/components/shadcn/concerns/modal_root.rb +43 -0
  160. data/app/components/shadcn/concerns/submits_value.rb +81 -0
  161. data/app/components/shadcn/context_menu/component.rb +45 -0
  162. data/app/components/shadcn/context_menu/content/component.rb +40 -0
  163. data/app/components/shadcn/context_menu/preview.rb +16 -0
  164. data/app/components/shadcn/context_menu/previews/default.html.erb +34 -0
  165. data/app/components/shadcn/context_menu/previews/items.html.erb +35 -0
  166. data/app/components/shadcn/context_menu/sub/component.rb +47 -0
  167. data/app/components/shadcn/context_menu/sub_content/component.rb +31 -0
  168. data/app/components/shadcn/context_menu/trigger/component.rb +50 -0
  169. data/app/components/shadcn/context_menu.rb +43 -0
  170. data/app/components/shadcn/dialog/close/component.rb +20 -0
  171. data/app/components/shadcn/dialog/component.rb +20 -0
  172. data/app/components/shadcn/dialog/content/component.rb +78 -0
  173. data/app/components/shadcn/dialog/header/component.rb +23 -0
  174. data/app/components/shadcn/dialog/overlay/component.rb +28 -0
  175. data/app/components/shadcn/dialog/preview.rb +28 -0
  176. data/app/components/shadcn/dialog/previews/default.html.erb +17 -0
  177. data/app/components/shadcn/dialog/previews/inside_stacking_context.html.erb +25 -0
  178. data/app/components/shadcn/dialog/previews/no_close_button.html.erb +15 -0
  179. data/app/components/shadcn/dialog/previews/scrollable_content.html.erb +29 -0
  180. data/app/components/shadcn/dialog/trigger/component.rb +24 -0
  181. data/app/components/shadcn/dialog.rb +17 -0
  182. data/app/components/shadcn/drawer/component.rb +33 -0
  183. data/app/components/shadcn/drawer/content/component.rb +95 -0
  184. data/app/components/shadcn/drawer/header/component.rb +34 -0
  185. data/app/components/shadcn/drawer/preview.rb +29 -0
  186. data/app/components/shadcn/drawer/previews/default.html.erb +21 -0
  187. data/app/components/shadcn/drawer/previews/scrollable.html.erb +20 -0
  188. data/app/components/shadcn/drawer/previews/scrollable_side.html.erb +21 -0
  189. data/app/components/shadcn/drawer/previews/sides.html.erb +17 -0
  190. data/app/components/shadcn/drawer.rb +27 -0
  191. data/app/components/shadcn/dropdown_menu/checkbox_item/component.rb +61 -0
  192. data/app/components/shadcn/dropdown_menu/component.rb +48 -0
  193. data/app/components/shadcn/dropdown_menu/content/component.rb +49 -0
  194. data/app/components/shadcn/dropdown_menu/group/component.rb +16 -0
  195. data/app/components/shadcn/dropdown_menu/item/component.rb +50 -0
  196. data/app/components/shadcn/dropdown_menu/preview.rb +16 -0
  197. data/app/components/shadcn/dropdown_menu/previews/checkboxes_and_radio.html.erb +34 -0
  198. data/app/components/shadcn/dropdown_menu/previews/default.html.erb +23 -0
  199. data/app/components/shadcn/dropdown_menu/radio_group/component.rb +16 -0
  200. data/app/components/shadcn/dropdown_menu/radio_item/component.rb +63 -0
  201. data/app/components/shadcn/dropdown_menu/sub/component.rb +44 -0
  202. data/app/components/shadcn/dropdown_menu/sub_content/component.rb +45 -0
  203. data/app/components/shadcn/dropdown_menu/sub_trigger/component.rb +58 -0
  204. data/app/components/shadcn/dropdown_menu/trigger/component.rb +25 -0
  205. data/app/components/shadcn/dropdown_menu.rb +18 -0
  206. data/app/components/shadcn/empty/component.rb +17 -0
  207. data/app/components/shadcn/empty/media/component.rb +48 -0
  208. data/app/components/shadcn/empty/preview.rb +16 -0
  209. data/app/components/shadcn/empty/previews/default.html.erb +15 -0
  210. data/app/components/shadcn/empty/previews/media.html.erb +32 -0
  211. data/app/components/shadcn/empty.rb +26 -0
  212. data/app/components/shadcn/field/component.rb +61 -0
  213. data/app/components/shadcn/field/error/component.rb +49 -0
  214. data/app/components/shadcn/field/legend/component.rb +30 -0
  215. data/app/components/shadcn/field/preview.rb +17 -0
  216. data/app/components/shadcn/field/previews/default.html.erb +29 -0
  217. data/app/components/shadcn/field/previews/form_builder.html.erb +38 -0
  218. data/app/components/shadcn/field/separator/component.rb +35 -0
  219. data/app/components/shadcn/field.rb +44 -0
  220. data/app/components/shadcn/hover_card/component.rb +51 -0
  221. data/app/components/shadcn/hover_card/content/component.rb +45 -0
  222. data/app/components/shadcn/hover_card/preview.rb +16 -0
  223. data/app/components/shadcn/hover_card/previews/default.html.erb +36 -0
  224. data/app/components/shadcn/hover_card/previews/sides.html.erb +15 -0
  225. data/app/components/shadcn/hover_card/trigger/component.rb +33 -0
  226. data/app/components/shadcn/icon/component.rb +76 -0
  227. data/app/components/shadcn/icon.rb +43 -0
  228. data/app/components/shadcn/input/component.rb +26 -0
  229. data/app/components/shadcn/input/preview.rb +16 -0
  230. data/app/components/shadcn/input/previews/default.html.erb +5 -0
  231. data/app/components/shadcn/input/previews/states.html.erb +20 -0
  232. data/app/components/shadcn/input_group/addon/component.rb +65 -0
  233. data/app/components/shadcn/input_group/button/component.rb +40 -0
  234. data/app/components/shadcn/input_group/component.rb +43 -0
  235. data/app/components/shadcn/input_group/input/component.rb +25 -0
  236. data/app/components/shadcn/input_group/preview.rb +17 -0
  237. data/app/components/shadcn/input_group/previews/addons.html.erb +53 -0
  238. data/app/components/shadcn/input_group/previews/default.html.erb +25 -0
  239. data/app/components/shadcn/input_group/text/component.rb +20 -0
  240. data/app/components/shadcn/input_group/textarea/component.rb +21 -0
  241. data/app/components/shadcn/input_otp/component.rb +95 -0
  242. data/app/components/shadcn/input_otp/preview.rb +17 -0
  243. data/app/components/shadcn/input_otp/previews/default.html.erb +19 -0
  244. data/app/components/shadcn/input_otp/previews/variations.html.erb +33 -0
  245. data/app/components/shadcn/input_otp/separator/component.rb +21 -0
  246. data/app/components/shadcn/input_otp/slot/component.rb +60 -0
  247. data/app/components/shadcn/input_otp.rb +11 -0
  248. data/app/components/shadcn/item/component.rb +56 -0
  249. data/app/components/shadcn/item/group/component.rb +53 -0
  250. data/app/components/shadcn/item/media/component.rb +53 -0
  251. data/app/components/shadcn/item/preview.rb +20 -0
  252. data/app/components/shadcn/item/previews/default.html.erb +32 -0
  253. data/app/components/shadcn/item/previews/link.html.erb +39 -0
  254. data/app/components/shadcn/item/previews/outline.html.erb +8 -0
  255. data/app/components/shadcn/item/separator/component.rb +24 -0
  256. data/app/components/shadcn/item.rb +34 -0
  257. data/app/components/shadcn/kbd/component.rb +23 -0
  258. data/app/components/shadcn/kbd/group/component.rb +17 -0
  259. data/app/components/shadcn/kbd/preview.rb +11 -0
  260. data/app/components/shadcn/kbd/previews/default.html.erb +4 -0
  261. data/app/components/shadcn/label/component.rb +20 -0
  262. data/app/components/shadcn/label/preview.rb +11 -0
  263. data/app/components/shadcn/label/previews/default.html.erb +4 -0
  264. data/app/components/shadcn/marker/component.rb +46 -0
  265. data/app/components/shadcn/marker/icon/component.rb +23 -0
  266. data/app/components/shadcn/marker/preview.rb +17 -0
  267. data/app/components/shadcn/marker/previews/default.html.erb +18 -0
  268. data/app/components/shadcn/marker/previews/status.html.erb +32 -0
  269. data/app/components/shadcn/marker.rb +17 -0
  270. data/app/components/shadcn/menubar/component.rb +36 -0
  271. data/app/components/shadcn/menubar/content/component.rb +49 -0
  272. data/app/components/shadcn/menubar/menu/component.rb +39 -0
  273. data/app/components/shadcn/menubar/preview.rb +16 -0
  274. data/app/components/shadcn/menubar/previews/default.html.erb +52 -0
  275. data/app/components/shadcn/menubar/previews/items.html.erb +38 -0
  276. data/app/components/shadcn/menubar/sub/component.rb +43 -0
  277. data/app/components/shadcn/menubar/sub_content/component.rb +31 -0
  278. data/app/components/shadcn/menubar/sub_trigger/component.rb +31 -0
  279. data/app/components/shadcn/menubar/trigger/component.rb +41 -0
  280. data/app/components/shadcn/menubar.rb +47 -0
  281. data/app/components/shadcn/message/component.rb +33 -0
  282. data/app/components/shadcn/message/preview.rb +16 -0
  283. data/app/components/shadcn/message/previews/actions.html.erb +51 -0
  284. data/app/components/shadcn/message/previews/default.html.erb +35 -0
  285. data/app/components/shadcn/message.rb +35 -0
  286. data/app/components/shadcn/message_scroller/button/component.rb +78 -0
  287. data/app/components/shadcn/message_scroller/component.rb +59 -0
  288. data/app/components/shadcn/message_scroller/content/component.rb +50 -0
  289. data/app/components/shadcn/message_scroller/item/component.rb +45 -0
  290. data/app/components/shadcn/message_scroller/preview.rb +11 -0
  291. data/app/components/shadcn/message_scroller/previews/default.html.erb +72 -0
  292. data/app/components/shadcn/message_scroller/viewport/component.rb +41 -0
  293. data/app/components/shadcn/mode_switcher/component.rb +67 -0
  294. data/app/components/shadcn/mode_switcher/preview.rb +11 -0
  295. data/app/components/shadcn/mode_switcher/previews/default.html.erb +5 -0
  296. data/app/components/shadcn/mode_toggle/component.rb +75 -0
  297. data/app/components/shadcn/mode_toggle/preview.rb +11 -0
  298. data/app/components/shadcn/mode_toggle/previews/default.html.erb +20 -0
  299. data/app/components/shadcn/native_select/component.rb +59 -0
  300. data/app/components/shadcn/native_select/preview.rb +16 -0
  301. data/app/components/shadcn/native_select/previews/default.html.erb +9 -0
  302. data/app/components/shadcn/native_select/previews/groups.html.erb +26 -0
  303. data/app/components/shadcn/native_select.rb +16 -0
  304. data/app/components/shadcn/navigation_menu/component.rb +51 -0
  305. data/app/components/shadcn/navigation_menu/content/component.rb +58 -0
  306. data/app/components/shadcn/navigation_menu/preview.rb +11 -0
  307. data/app/components/shadcn/navigation_menu/previews/default.html.erb +49 -0
  308. data/app/components/shadcn/navigation_menu/trigger/component.rb +59 -0
  309. data/app/components/shadcn/navigation_menu.rb +28 -0
  310. data/app/components/shadcn/pagination/component.rb +19 -0
  311. data/app/components/shadcn/pagination/ellipsis/component.rb +28 -0
  312. data/app/components/shadcn/pagination/link/component.rb +37 -0
  313. data/app/components/shadcn/pagination/next/component.rb +31 -0
  314. data/app/components/shadcn/pagination/preview.rb +16 -0
  315. data/app/components/shadcn/pagination/previews/default.html.erb +18 -0
  316. data/app/components/shadcn/pagination/previews/simple.html.erb +39 -0
  317. data/app/components/shadcn/pagination/previous/component.rb +31 -0
  318. data/app/components/shadcn/pagination.rb +14 -0
  319. data/app/components/shadcn/parts.rb +38 -0
  320. data/app/components/shadcn/popover/anchor/component.rb +17 -0
  321. data/app/components/shadcn/popover/component.rb +42 -0
  322. data/app/components/shadcn/popover/content/component.rb +40 -0
  323. data/app/components/shadcn/popover/preview.rb +28 -0
  324. data/app/components/shadcn/popover/previews/align.html.erb +13 -0
  325. data/app/components/shadcn/popover/previews/default.html.erb +9 -0
  326. data/app/components/shadcn/popover/previews/inside_containing_block.html.erb +41 -0
  327. data/app/components/shadcn/popover/previews/inside_stacking_context.html.erb +30 -0
  328. data/app/components/shadcn/popover/trigger/component.rb +24 -0
  329. data/app/components/shadcn/popover.rb +16 -0
  330. data/app/components/shadcn/progress/component.rb +62 -0
  331. data/app/components/shadcn/progress/preview.rb +11 -0
  332. data/app/components/shadcn/progress/previews/default.html.erb +5 -0
  333. data/app/components/shadcn/radio_group/component.rb +51 -0
  334. data/app/components/shadcn/radio_group/item/component.rb +74 -0
  335. data/app/components/shadcn/radio_group/preview.rb +16 -0
  336. data/app/components/shadcn/radio_group/previews/default.html.erb +15 -0
  337. data/app/components/shadcn/radio_group/previews/states.html.erb +41 -0
  338. data/app/components/shadcn/resizable/component.rb +59 -0
  339. data/app/components/shadcn/resizable/handle/component.rb +70 -0
  340. data/app/components/shadcn/resizable/panel/component.rb +53 -0
  341. data/app/components/shadcn/resizable/preview.rb +23 -0
  342. data/app/components/shadcn/resizable/previews/default.html.erb +25 -0
  343. data/app/components/shadcn/resizable/previews/vertical.html.erb +19 -0
  344. data/app/components/shadcn/resizable/previews/with_handle.html.erb +17 -0
  345. data/app/components/shadcn/scroll_area/component.rb +84 -0
  346. data/app/components/shadcn/scroll_area/preview.rb +11 -0
  347. data/app/components/shadcn/scroll_area/previews/default.html.erb +36 -0
  348. data/app/components/shadcn/scroll_area/scrollbar/component.rb +72 -0
  349. data/app/components/shadcn/scroll_area/viewport/component.rb +70 -0
  350. data/app/components/shadcn/scroll_area.rb +14 -0
  351. data/app/components/shadcn/select/component.rb +67 -0
  352. data/app/components/shadcn/select/content/component.rb +157 -0
  353. data/app/components/shadcn/select/empty/component.rb +38 -0
  354. data/app/components/shadcn/select/group/component.rb +16 -0
  355. data/app/components/shadcn/select/item/component.rb +71 -0
  356. data/app/components/shadcn/select/list/component.rb +41 -0
  357. data/app/components/shadcn/select/preview.rb +24 -0
  358. data/app/components/shadcn/select/previews/default.html.erb +17 -0
  359. data/app/components/shadcn/select/previews/scrollable.html.erb +19 -0
  360. data/app/components/shadcn/select/previews/searchable.html.erb +18 -0
  361. data/app/components/shadcn/select/previews/states.html.erb +25 -0
  362. data/app/components/shadcn/select/search/component.rb +54 -0
  363. data/app/components/shadcn/select/trigger/component.rb +78 -0
  364. data/app/components/shadcn/select/value/component.rb +28 -0
  365. data/app/components/shadcn/select.rb +15 -0
  366. data/app/components/shadcn/separator/component.rb +41 -0
  367. data/app/components/shadcn/separator/preview.rb +11 -0
  368. data/app/components/shadcn/separator/previews/default.html.erb +9 -0
  369. data/app/components/shadcn/sheet/component.rb +16 -0
  370. data/app/components/shadcn/sheet/content/component.rb +98 -0
  371. data/app/components/shadcn/sheet/header/component.rb +23 -0
  372. data/app/components/shadcn/sheet/preview.rb +16 -0
  373. data/app/components/shadcn/sheet/previews/default.html.erb +16 -0
  374. data/app/components/shadcn/sheet/previews/no_close_button.html.erb +14 -0
  375. data/app/components/shadcn/sheet.rb +22 -0
  376. data/app/components/shadcn/sidebar/component.rb +232 -0
  377. data/app/components/shadcn/sidebar/input/component.rb +25 -0
  378. data/app/components/shadcn/sidebar/menu_action/component.rb +58 -0
  379. data/app/components/shadcn/sidebar/menu_button/component.rb +100 -0
  380. data/app/components/shadcn/sidebar/menu_skeleton/component.rb +51 -0
  381. data/app/components/shadcn/sidebar/menu_sub_button/component.rb +60 -0
  382. data/app/components/shadcn/sidebar/preview.rb +22 -0
  383. data/app/components/shadcn/sidebar/previews/default.html.erb +189 -0
  384. data/app/components/shadcn/sidebar/previews/floating.html.erb +39 -0
  385. data/app/components/shadcn/sidebar/previews/inset.html.erb +48 -0
  386. data/app/components/shadcn/sidebar/provider/component.rb +53 -0
  387. data/app/components/shadcn/sidebar/rail/component.rb +43 -0
  388. data/app/components/shadcn/sidebar/separator/component.rb +23 -0
  389. data/app/components/shadcn/sidebar/trigger/component.rb +39 -0
  390. data/app/components/shadcn/sidebar.rb +98 -0
  391. data/app/components/shadcn/skeleton/component.rb +14 -0
  392. data/app/components/shadcn/skeleton/preview.rb +11 -0
  393. data/app/components/shadcn/skeleton/previews/default.html.erb +7 -0
  394. data/app/components/shadcn/slider/component.rb +145 -0
  395. data/app/components/shadcn/slider/preview.rb +16 -0
  396. data/app/components/shadcn/slider/previews/default.html.erb +17 -0
  397. data/app/components/shadcn/slider/previews/vertical.html.erb +7 -0
  398. data/app/components/shadcn/slider/range/component.rb +51 -0
  399. data/app/components/shadcn/slider/thumb/component.rb +58 -0
  400. data/app/components/shadcn/slider/track/component.rb +32 -0
  401. data/app/components/shadcn/spinner/component.rb +21 -0
  402. data/app/components/shadcn/spinner/preview.rb +11 -0
  403. data/app/components/shadcn/spinner/previews/default.html.erb +7 -0
  404. data/app/components/shadcn/switch/component.rb +104 -0
  405. data/app/components/shadcn/switch/preview.rb +11 -0
  406. data/app/components/shadcn/switch/previews/default.html.erb +24 -0
  407. data/app/components/shadcn/table/component.rb +26 -0
  408. data/app/components/shadcn/table/preview.rb +16 -0
  409. data/app/components/shadcn/table/previews/default.html.erb +19 -0
  410. data/app/components/shadcn/table/previews/footer.html.erb +25 -0
  411. data/app/components/shadcn/table.rb +33 -0
  412. data/app/components/shadcn/tabs/component.rb +40 -0
  413. data/app/components/shadcn/tabs/content/component.rb +34 -0
  414. data/app/components/shadcn/tabs/list/component.rb +51 -0
  415. data/app/components/shadcn/tabs/preview.rb +16 -0
  416. data/app/components/shadcn/tabs/previews/default.html.erb +21 -0
  417. data/app/components/shadcn/tabs/previews/line.html.erb +23 -0
  418. data/app/components/shadcn/tabs/trigger/component.rb +66 -0
  419. data/app/components/shadcn/textarea/component.rb +24 -0
  420. data/app/components/shadcn/textarea/preview.rb +16 -0
  421. data/app/components/shadcn/textarea/previews/default.html.erb +3 -0
  422. data/app/components/shadcn/textarea/previews/states.html.erb +18 -0
  423. data/app/components/shadcn/theme_selector/component.rb +88 -0
  424. data/app/components/shadcn/theme_selector/preview.rb +17 -0
  425. data/app/components/shadcn/theme_selector/previews/all_themes.html.erb +20 -0
  426. data/app/components/shadcn/theme_selector/previews/default.html.erb +18 -0
  427. data/app/components/shadcn/toaster/component.rb +139 -0
  428. data/app/components/shadcn/toaster/preview.rb +16 -0
  429. data/app/components/shadcn/toaster/previews/default.html.erb +19 -0
  430. data/app/components/shadcn/toaster/previews/from_the_server.html.erb +15 -0
  431. data/app/components/shadcn/toaster/toast/component.rb +134 -0
  432. data/app/components/shadcn/toggle/component.rb +81 -0
  433. data/app/components/shadcn/toggle/preview.rb +11 -0
  434. data/app/components/shadcn/toggle/previews/default.html.erb +6 -0
  435. data/app/components/shadcn/toggle_group/component.rb +62 -0
  436. data/app/components/shadcn/toggle_group/item/component.rb +59 -0
  437. data/app/components/shadcn/toggle_group/preview.rb +16 -0
  438. data/app/components/shadcn/toggle_group/previews/default.html.erb +12 -0
  439. data/app/components/shadcn/toggle_group/previews/sizes.html.erb +37 -0
  440. data/app/components/shadcn/tooltip/component.rb +38 -0
  441. data/app/components/shadcn/tooltip/content/component.rb +62 -0
  442. data/app/components/shadcn/tooltip/preview.rb +21 -0
  443. data/app/components/shadcn/tooltip/previews/default.html.erb +4 -0
  444. data/app/components/shadcn/tooltip/previews/sides.html.erb +19 -0
  445. data/app/components/shadcn/tooltip/previews/with_keyboard_shortcut.html.erb +11 -0
  446. data/app/components/shadcn/tooltip/provider/component.rb +28 -0
  447. data/app/components/shadcn/tooltip/trigger/component.rb +25 -0
  448. data/app/helpers/shadcn_view_component/form_helper.rb +25 -0
  449. data/app/helpers/shadcn_view_component/theme_helper.rb +61 -0
  450. data/app/javascript/shadcn/animation.js +180 -0
  451. data/app/javascript/shadcn/command_score.js +110 -0
  452. data/app/javascript/shadcn/controllers/accordion_controller.js +127 -0
  453. data/app/javascript/shadcn/controllers/avatar_controller.js +32 -0
  454. data/app/javascript/shadcn/controllers/calendar_controller.js +633 -0
  455. data/app/javascript/shadcn/controllers/carousel_controller.js +139 -0
  456. data/app/javascript/shadcn/controllers/chart_controller.js +110 -0
  457. data/app/javascript/shadcn/controllers/checkbox_controller.js +61 -0
  458. data/app/javascript/shadcn/controllers/collapsible_controller.js +55 -0
  459. data/app/javascript/shadcn/controllers/combobox_controller.js +360 -0
  460. data/app/javascript/shadcn/controllers/command_controller.js +199 -0
  461. data/app/javascript/shadcn/controllers/dialog_controller.js +154 -0
  462. data/app/javascript/shadcn/controllers/drawer_controller.js +265 -0
  463. data/app/javascript/shadcn/controllers/dropdown_menu_controller.js +259 -0
  464. data/app/javascript/shadcn/controllers/hover_card_controller.js +85 -0
  465. data/app/javascript/shadcn/controllers/input_group_controller.js +17 -0
  466. data/app/javascript/shadcn/controllers/input_otp_controller.js +80 -0
  467. data/app/javascript/shadcn/controllers/menubar_controller.js +151 -0
  468. data/app/javascript/shadcn/controllers/message_scroller_controller.js +456 -0
  469. data/app/javascript/shadcn/controllers/navigation_menu_controller.js +211 -0
  470. data/app/javascript/shadcn/controllers/popover_controller.js +66 -0
  471. data/app/javascript/shadcn/controllers/radio_group_controller.js +69 -0
  472. data/app/javascript/shadcn/controllers/resizable_controller.js +167 -0
  473. data/app/javascript/shadcn/controllers/scroll_area_controller.js +253 -0
  474. data/app/javascript/shadcn/controllers/select_controller.js +320 -0
  475. data/app/javascript/shadcn/controllers/sidebar_controller.js +273 -0
  476. data/app/javascript/shadcn/controllers/slider_controller.js +230 -0
  477. data/app/javascript/shadcn/controllers/switch_controller.js +39 -0
  478. data/app/javascript/shadcn/controllers/tabs_controller.js +81 -0
  479. data/app/javascript/shadcn/controllers/theme_controller.js +67 -0
  480. data/app/javascript/shadcn/controllers/toaster_controller.js +281 -0
  481. data/app/javascript/shadcn/controllers/toggle_controller.js +24 -0
  482. data/app/javascript/shadcn/controllers/toggle_group_controller.js +73 -0
  483. data/app/javascript/shadcn/controllers/tooltip_controller.js +58 -0
  484. data/app/javascript/shadcn/direction.js +41 -0
  485. data/app/javascript/shadcn/dismiss.js +73 -0
  486. data/app/javascript/shadcn/floating.js +207 -0
  487. data/app/javascript/shadcn/focus.js +86 -0
  488. data/app/javascript/shadcn/id.js +14 -0
  489. data/app/javascript/shadcn/index.js +110 -0
  490. data/app/javascript/shadcn/popper.js +262 -0
  491. data/app/javascript/shadcn/scroll_geometry.js +267 -0
  492. data/app/javascript/shadcn/theme.js +161 -0
  493. data/app/javascript/shadcn/top_layer.js +59 -0
  494. data/app/javascript/shadcn/typeahead.js +99 -0
  495. data/config/importmap.rb +6 -0
  496. data/config/locales/en.yml +62 -0
  497. data/lib/generators/shadcn_view_component/install/install_generator.rb +149 -0
  498. data/lib/shadcn_view_component/engine.rb +73 -0
  499. data/lib/shadcn_view_component/form_builder.rb +264 -0
  500. data/lib/shadcn_view_component/icon_registry.rb +66 -0
  501. data/lib/shadcn_view_component/icons.rb +32 -0
  502. data/lib/shadcn_view_component/themes.rb +1065 -0
  503. data/lib/shadcn_view_component/version.rb +3 -0
  504. data/lib/shadcn_view_component.rb +82 -0
  505. data/lib/tasks/icons.rake +53 -0
  506. data/lib/tasks/themes.rake +171 -0
  507. metadata +608 -0
@@ -0,0 +1,360 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { uniqueId } from "shadcn/id"
3
+ import { FloatingLayer } from "shadcn/floating"
4
+
5
+ // Base UI's Combobox: a text field that filters a listbox and keeps the caret.
6
+ //
7
+ // It is the only family here answerable to Base UI rather than Radix, and the
8
+ // difference is in the names rather than in the behaviour — `data-open` where
9
+ // everything else writes `data-state="open"`, `--anchor-width` where everything
10
+ // else reads `--radix-popper-anchor-width`. The arrangement itself is the
11
+ // searchable select's: virtual focus through `aria-activedescendant`, the caret
12
+ // never leaving the input.
13
+ export default class extends Controller {
14
+ static targets = [ "search", "content", "list", "item", "indicator", "input", "trigger", "clear",
15
+ "chip", "chips", "chipTemplate" ]
16
+
17
+ static values = {
18
+ open: Boolean,
19
+ value: String,
20
+ multiple: Boolean,
21
+ values: Array,
22
+ name: String,
23
+ side: { type: String, default: "bottom" },
24
+ align: { type: String, default: "start" },
25
+ sideOffset: { type: Number, default: 6 },
26
+ alignOffset: { type: Number, default: 0 }
27
+ }
28
+
29
+ connect() {
30
+ if (!this.hasContentTarget || !this.hasSearchTarget) return
31
+
32
+ this.contentTarget.id ||= uniqueId("shadcn-combobox-content")
33
+ this.searchTarget.setAttribute("aria-controls", this.contentTarget.id)
34
+
35
+ this.layer = new FloatingLayer({
36
+ trigger: this.searchTarget,
37
+ content: this.contentTarget,
38
+ prefix: "combobox",
39
+ side: this.sideValue,
40
+ align: this.alignValue,
41
+ sideOffset: this.sideOffsetValue,
42
+ alignOffset: this.alignOffsetValue,
43
+ matchAnchorWidth: true,
44
+ // The four unprefixed names Base UI publishes and this component's own
45
+ // classes read: `w-(--anchor-width)`, `max-w-(--available-width)`,
46
+ // `origin-(--transform-origin)`. Asked for by this family and by no
47
+ // other, so a host's page gets them only where a combobox is.
48
+ publishBaseUiVariables: true,
49
+ onOpen: () => this.opened(),
50
+ onClose: () => this.closed()
51
+ })
52
+
53
+ // Kept so it can be put back when the last chip goes. Read once, before
54
+ // anything has had a chance to blank it.
55
+ this.placeholder = this.searchTarget.getAttribute("placeholder") ?? ""
56
+ this.syncPlaceholder()
57
+ }
58
+
59
+ // Upstream's multiple example writes `placeholder={value.length > 0 ? '' :
60
+ // 'e.g. TypeScript'}` — the placeholder belongs to an empty control, and once
61
+ // there are chips they are what the field is saying. There the caller does
62
+ // it, because there the caller re-renders; here the chips come and go under
63
+ // the controller, so the controller owns it.
64
+ //
65
+ // Called from `connect` as well, not only after a change: the server can
66
+ // render a field that already has chips beside it, and it did — the preview
67
+ // starts with two.
68
+ syncPlaceholder() {
69
+ if (!this.multipleValue) return
70
+
71
+ this.searchTarget.setAttribute("placeholder", this.valuesValue.length > 0 ? "" : this.placeholder)
72
+ }
73
+
74
+ disconnect() {
75
+ this.layer?.destroy()
76
+ }
77
+
78
+ toggle(event) {
79
+ event.preventDefault()
80
+ this.openValue ? this.close() : this.open()
81
+ }
82
+
83
+ open() {
84
+ if (this.openValue) return
85
+
86
+ this.openValue = true
87
+ this.layer.show()
88
+ }
89
+
90
+ close() {
91
+ if (!this.openValue) return
92
+
93
+ this.openValue = false
94
+ this.layer.hide()
95
+ }
96
+
97
+ opened() {
98
+ // Base UI's own state attributes, which are what the panel's classes read.
99
+ this.contentTarget.dataset.open = ""
100
+ delete this.contentTarget.dataset.closed
101
+ this.searchTarget.setAttribute("aria-expanded", "true")
102
+ this.highlight(this.chosenItem || this.visibleItems[0])
103
+ }
104
+
105
+ closed() {
106
+ this.contentTarget.dataset.closed = ""
107
+ delete this.contentTarget.dataset.open
108
+ this.searchTarget.setAttribute("aria-expanded", "false")
109
+ this.searchTarget.removeAttribute("aria-activedescendant")
110
+ }
111
+
112
+ // Base UI's default filter is a contains match, which is also what the
113
+ // searchable select does — a combobox completes a value the caller already
114
+ // ordered, where the palette ranks answers. Same reasoning, opposite ends.
115
+ search() {
116
+ const query = this.searchTarget.value.trim().toLowerCase()
117
+
118
+ for (const item of this.itemTargets) {
119
+ item.hidden = query !== "" && !this.textOf(item).toLowerCase().includes(query)
120
+ }
121
+
122
+ // `data-empty` on the *content*, because that is what the empty state's
123
+ // own class reads: `group-data-empty/combobox-content:flex`.
124
+ if (this.visibleItems.length === 0) this.contentTarget.dataset.empty = ""
125
+ else delete this.contentTarget.dataset.empty
126
+
127
+ this.open()
128
+ this.highlight(this.visibleItems[0])
129
+ }
130
+
131
+ keydown(event) {
132
+ const keys = {
133
+ ArrowDown: () => (this.openValue ? this.move(1) : this.open()),
134
+ ArrowUp: () => (this.openValue ? this.move(-1) : this.open()),
135
+ Home: () => this.highlight(this.visibleItems[0]),
136
+ End: () => this.highlight(this.visibleItems.at(-1)),
137
+ Enter: () => this.take(this.highlighted),
138
+ Escape: () => this.close()
139
+ }
140
+
141
+ // Backspace on an empty field takes the last chip back off. This one is
142
+ // **ours**: Base UI's documentation does not describe a Backspace
143
+ // behaviour, and Base UI itself is not vendored here, so it could not be
144
+ // checked the way the rest of this family was. It is the convention every
145
+ // token field on the web follows, and it is the only way to undo a chip
146
+ // from the keyboard — the X is a pointer target.
147
+ if (this.multipleValue && event.key === "Backspace" && this.searchTarget.value === "") {
148
+ const last = this.chipTargets.at(-1)
149
+ if (last) {
150
+ event.preventDefault()
151
+ this.drop(last.dataset.value ?? "")
152
+ }
153
+ return
154
+ }
155
+
156
+ const act = keys[event.key]
157
+ if (!act) return
158
+
159
+ // Tab is deliberately absent: it leaves the field, which is what Tab does.
160
+ event.preventDefault()
161
+ act()
162
+ }
163
+
164
+ hover(event) {
165
+ this.highlight(event.currentTarget)
166
+ }
167
+
168
+ choose(event) {
169
+ this.take(event.currentTarget)
170
+ }
171
+
172
+ clear(event) {
173
+ event.preventDefault()
174
+ this.searchTarget.value = ""
175
+
176
+ if (this.multipleValue) {
177
+ // Every chip, not just the field: in multiple mode the chosen set *is*
178
+ // what the control holds, and leaving the tokens behind would make Clear
179
+ // look like it had done nothing.
180
+ for (const chip of this.chipTargets) chip.remove()
181
+ this.valuesValue = []
182
+ this.writeInputs()
183
+ this.syncPlaceholder()
184
+ } else {
185
+ this.valueValue = ""
186
+ if (this.hasInputTarget) this.inputTarget.value = ""
187
+ }
188
+
189
+ this.mark()
190
+ this.search()
191
+ this.searchTarget.focus()
192
+ }
193
+
194
+ // A chip taken back off, by its X.
195
+ remove(event) {
196
+ event.preventDefault()
197
+ const chip = event.currentTarget.closest("[data-slot=combobox-chip]")
198
+ if (!chip) return
199
+
200
+ this.drop(chip.dataset.value ?? "", chip)
201
+ }
202
+
203
+ take(item) {
204
+ if (!item || item.dataset.disabled !== undefined) return
205
+
206
+ const value = item.dataset.value ?? ""
207
+
208
+ if (this.multipleValue) return this.toggle_value(value, item)
209
+
210
+ this.valueValue = value
211
+ this.searchTarget.value = item.dataset.label || this.textOf(item)
212
+ if (this.hasInputTarget) this.inputTarget.value = this.valueValue
213
+
214
+ this.mark()
215
+ this.close()
216
+ this.searchTarget.focus()
217
+ this.dispatch("select", { detail: { value: this.valueValue, item } })
218
+ }
219
+
220
+ // Multiple selection. Taking an option that is already taken puts it back —
221
+ // which is what the tick in the list invites, and what leaves the list and
222
+ // the chips describing the same set. Base UI's documentation does not say
223
+ // either way, and Base UI is not vendored here, so this is **ours**.
224
+ //
225
+ // The panel stays open and the field is emptied, which the documentation's
226
+ // own multiple example does show.
227
+ toggle_value(value, item) {
228
+ if (this.valuesValue.includes(value)) return this.drop(value)
229
+
230
+ this.valuesValue = [ ...this.valuesValue, value ]
231
+ this.addChip(value, item.dataset.label || this.textOf(item))
232
+ this.writeInputs()
233
+ this.syncPlaceholder()
234
+
235
+ this.searchTarget.value = ""
236
+ this.search()
237
+ this.mark()
238
+ this.searchTarget.focus()
239
+ this.dispatch("select", { detail: { value, item } })
240
+ }
241
+
242
+ drop(value, chip = null) {
243
+ const token = chip || this.chipTargets.find((c) => (c.dataset.value ?? "") === value)
244
+ token?.remove()
245
+
246
+ if (this.multipleValue) {
247
+ this.valuesValue = this.valuesValue.filter((v) => v !== value)
248
+ this.writeInputs()
249
+ this.syncPlaceholder()
250
+ this.mark()
251
+ }
252
+
253
+ this.dispatch("remove", { detail: { value } })
254
+ this.searchTarget.focus()
255
+ }
256
+
257
+ // A chip per chosen value, cloned from the `<template>` the Chips component
258
+ // renders — so the class strings stay in Ruby, where `parity_spec` reads
259
+ // them, instead of being copied into this file.
260
+ addChip(value, label) {
261
+ if (!this.hasChipsTarget || !this.hasChipTemplateTarget) return
262
+
263
+ const chip = this.chipTemplateTarget.content.firstElementChild.cloneNode(true)
264
+ chip.dataset.value = value
265
+
266
+ const remove = chip.querySelector("[data-slot=combobox-chip-remove]")
267
+ if (remove) remove.dataset.value = value
268
+
269
+ // Before the remove button, which the Chip component renders last.
270
+ chip.insertBefore(document.createTextNode(label), remove)
271
+
272
+ // Before the field, not before the `<template>`. The template is the last
273
+ // child, so inserting there put every new chip *after* the input: the ones
274
+ // the server rendered sat to its left and the ones taken since to its
275
+ // right, with the field stranded in the middle. The field is the anchor —
276
+ // upstream's chips box is a row of tokens with the input taking whatever is
277
+ // left of the last line.
278
+ this.chipsTarget.insertBefore(chip, this.searchTarget)
279
+ }
280
+
281
+ // One hidden input per chosen value, rewritten whenever the set changes. The
282
+ // first input the component rendered carries the empty value that keeps the
283
+ // parameter present when every chip is gone, and is left alone — it has no
284
+ // target, so it is not in `inputTargets` to begin with.
285
+ writeInputs() {
286
+ if (!this.hasNameValue) return
287
+
288
+ for (const input of this.inputTargets) input.remove()
289
+
290
+ for (const value of this.valuesValue) {
291
+ const input = document.createElement("input")
292
+ input.type = "hidden"
293
+ input.name = this.nameValue
294
+ input.value = value
295
+ input.autocomplete = "off"
296
+ // `setAttribute`, not `dataset`: the controller's identifier contains a
297
+ // double dash, and `data-shadcn--combobox-target` has no camelCase form
298
+ // `dataset` round-trips to.
299
+ input.setAttribute("data-shadcn--combobox-target", "input")
300
+ this.element.appendChild(input)
301
+ }
302
+ }
303
+
304
+ // The tick, which is the only thing that says which ones were taken once the
305
+ // panel is closed and reopened.
306
+ mark() {
307
+ for (const item of this.itemTargets) {
308
+ const value = item.dataset.value ?? ""
309
+ const chosen = this.multipleValue
310
+ ? this.valuesValue.includes(value)
311
+ : value === this.valueValue && this.valueValue !== ""
312
+
313
+ item.setAttribute("aria-selected", String(chosen))
314
+ const indicator = item.querySelector("[data-slot=combobox-item-indicator]")
315
+ if (indicator) indicator.hidden = !chosen
316
+ }
317
+ }
318
+
319
+ move(by) {
320
+ const items = this.visibleItems
321
+ if (items.length === 0) return
322
+
323
+ const at = items.indexOf(this.highlighted)
324
+ this.highlight(items[(at + by + items.length) % items.length])
325
+ }
326
+
327
+ highlight(item) {
328
+ for (const other of this.itemTargets) {
329
+ if (other === item) other.dataset.highlighted = ""
330
+ else delete other.dataset.highlighted
331
+ }
332
+
333
+ // The Item component gives every server-rendered option an id, so this only
334
+ // catches the ones a host adds later — but that is exactly the case that was
335
+ // broken, and it failed without a sound.
336
+ if (item) {
337
+ item.id ||= uniqueId("shadcn-combobox-item")
338
+ this.searchTarget.setAttribute("aria-activedescendant", item.id)
339
+ }
340
+ else this.searchTarget.removeAttribute("aria-activedescendant")
341
+
342
+ item?.scrollIntoView({ block: "nearest" })
343
+ }
344
+
345
+ get highlighted() {
346
+ return this.itemTargets.find((item) => item.dataset.highlighted !== undefined)
347
+ }
348
+
349
+ get chosenItem() {
350
+ return this.itemTargets.find((item) => (item.dataset.value ?? "") === this.valueValue && this.valueValue !== "")
351
+ }
352
+
353
+ get visibleItems() {
354
+ return this.itemTargets.filter((item) => !item.hidden && item.dataset.disabled === undefined)
355
+ }
356
+
357
+ textOf(item) {
358
+ return item.dataset.label || item.textContent.trim().replace(/\s+/g, " ")
359
+ }
360
+ }
@@ -0,0 +1,199 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { commandScore } from "shadcn/command_score"
3
+ import { uniqueId } from "shadcn/id"
4
+
5
+ // The palette. Items are rendered by the server and this filters, ranks and
6
+ // walks them — which is what `cmdk` does, minus the part where it also owns the
7
+ // markup.
8
+ //
9
+ // Ranking rather than filtering is the difference between this and the
10
+ // searchable select, and it is deliberate: a select filters a list its caller
11
+ // ordered and keeps that order, while a palette answers "what did you mean" and
12
+ // has to put the answer first.
13
+ export default class extends Controller {
14
+ static targets = [ "search", "list", "item", "group", "separator", "empty" ]
15
+
16
+ connect() {
17
+ // The order the server rendered, which is what an empty query goes back to.
18
+ this.order = new Map(this.itemTargets.map((item, index) => [ item, index ]))
19
+ this.select(this.visibleItems[0])
20
+ }
21
+
22
+ search() {
23
+ const query = this.searchTarget.value.trim()
24
+
25
+ for (const item of this.itemTargets) {
26
+ const score = query === "" ? 1 : commandScore(this.textOf(item), query, this.keywordsOf(item))
27
+
28
+ item.dataset.score = score
29
+ item.hidden = score === 0
30
+ }
31
+
32
+ this.rank(query)
33
+ this.settle()
34
+ }
35
+
36
+ // Within a group first, then the groups themselves by their best item: an
37
+ // answer buried under a heading nobody is looking at is not an answer.
38
+ rank(query) {
39
+ for (const container of this.containers) {
40
+ // `:scope >`, because the list holds the groups as well as any loose
41
+ // items: without it every item in the document matched, and appending
42
+ // them to the list pulled them out of the groups they belonged to.
43
+ const items = [ ...container.querySelectorAll(":scope > [cmdk-item]") ]
44
+ const sorted = query === ""
45
+ ? items.sort((a, b) => this.order.get(a) - this.order.get(b))
46
+ : items.sort((a, b) => this.scoreOf(b) - this.scoreOf(a) || this.order.get(a) - this.order.get(b))
47
+
48
+ for (const item of sorted) container.appendChild(item)
49
+ }
50
+
51
+ if (this.groupTargets.length < 2) return
52
+
53
+ const groups = [ ...this.groupTargets ].sort((a, b) =>
54
+ query === "" ? this.order.get(this.firstOf(a)) - this.order.get(this.firstOf(b)) : this.bestOf(b) - this.bestOf(a))
55
+
56
+ for (const group of groups) group.parentElement.appendChild(group)
57
+ }
58
+
59
+ // What is left showing decides the rest: an empty state, the groups that
60
+ // still have something in them, and where the selection goes.
61
+ settle() {
62
+ for (const group of this.groupTargets) {
63
+ group.hidden = [ ...group.querySelectorAll("[cmdk-item]") ].every((item) => item.hidden)
64
+ }
65
+
66
+ // A rule between two things is a rule between nothing once one of them has
67
+ // gone.
68
+ for (const separator of this.separatorTargets) {
69
+ separator.hidden = this.groupTargets.length > 0 && this.groupTargets.every((group) => group.hidden)
70
+ }
71
+
72
+ const matches = this.visibleItems
73
+ // The empty state lives *inside* the list — upstream's own example puts it
74
+ // there — so hiding the list when nothing matches hides the message that
75
+ // says nothing matches. The select hides its list because its empty state
76
+ // is a sibling; this one is not.
77
+ for (const empty of this.emptyTargets) empty.hidden = matches.length > 0
78
+
79
+ this.select(matches.includes(this.selected) ? this.selected : matches[0])
80
+ }
81
+
82
+ keydown(event) {
83
+ const keys = {
84
+ ArrowDown: () => this.move(1),
85
+ ArrowUp: () => this.move(-1),
86
+ Home: () => this.select(this.visibleItems[0]),
87
+ End: () => this.select(this.visibleItems.at(-1)),
88
+ Enter: () => this.choose()
89
+ }
90
+ const move = keys[event.key]
91
+ if (!move) return
92
+
93
+ event.preventDefault()
94
+ move()
95
+ }
96
+
97
+ // The pointer selects what it is over, as cmdk does — so the keyboard and the
98
+ // pointer never disagree about which item Enter would take.
99
+ hover(event) {
100
+ this.select(event.currentTarget)
101
+ }
102
+
103
+ choose(event) {
104
+ const item = event?.currentTarget ?? this.selected
105
+ if (!item || item.dataset.disabled === "true") return
106
+
107
+ this.select(item)
108
+ this.dispatch("select", { detail: { value: this.valueOf(item), item } })
109
+ }
110
+
111
+ move(by) {
112
+ const items = this.visibleItems
113
+ if (items.length === 0) return
114
+
115
+ const at = items.indexOf(this.selected)
116
+ // Wrapping, which is cmdk's default (`loop` turns it off there, and the
117
+ // palette is a ring: the first item is one press up from the last).
118
+ this.select(items[(at + by + items.length) % items.length])
119
+ }
120
+
121
+ select(item) {
122
+ for (const other of this.itemTargets) {
123
+ const chosen = other === item
124
+
125
+ other.setAttribute("aria-selected", String(chosen))
126
+ other.dataset.selected = String(chosen)
127
+ }
128
+
129
+ this.selected = item
130
+ this.publish(item)
131
+ item?.scrollIntoView({ block: "nearest" })
132
+ }
133
+
134
+ // The virtual focus: the input keeps the caret and the list is walked by id,
135
+ // which is the same arrangement the searchable select uses.
136
+ publish(item) {
137
+ for (const element of [ this.hasSearchTarget && this.searchTarget, this.hasListTarget && this.listTarget ]) {
138
+ if (!element) continue
139
+
140
+ if (item) {
141
+ // As in the select and the combobox: the id is assigned where it is
142
+ // read, so an item added after `connect` still has one to point at.
143
+ item.id ||= uniqueId("shadcn-command-item")
144
+ element.setAttribute("aria-activedescendant", item.id)
145
+ }
146
+ else element.removeAttribute("aria-activedescendant")
147
+ }
148
+ }
149
+
150
+ get selected() {
151
+ return this._selected
152
+ }
153
+
154
+ set selected(item) {
155
+ this._selected = item
156
+ }
157
+
158
+ get visibleItems() {
159
+ return this.itemTargets.filter((item) => !item.hidden && item.dataset.disabled !== "true" && !this.insideHiddenGroup(item))
160
+ }
161
+
162
+ insideHiddenGroup(item) {
163
+ return this.groupTargets.some((group) => group.hidden && group.contains(item))
164
+ }
165
+
166
+ // Where items live: the wrapper inside each group, and the list itself for
167
+ // the ones written outside a group.
168
+ get containers() {
169
+ const groups = this.groupTargets.map((group) => group.querySelector("[cmdk-group-items]") || group)
170
+
171
+ return this.hasListTarget ? [ ...groups, this.listTarget ] : groups
172
+ }
173
+
174
+ firstOf(group) {
175
+ return group.querySelector("[cmdk-item]")
176
+ }
177
+
178
+ bestOf(group) {
179
+ return [ ...group.querySelectorAll("[cmdk-item]") ].reduce((best, item) => Math.max(best, this.scoreOf(item)), 0)
180
+ }
181
+
182
+ scoreOf(item) {
183
+ return Number(item.dataset.score ?? 0)
184
+ }
185
+
186
+ // What is searched: the value a caller gave, otherwise the text a reader
187
+ // sees — cmdk's own rule.
188
+ textOf(item) {
189
+ return item.dataset.value || item.textContent.trim().replace(/\s+/g, " ")
190
+ }
191
+
192
+ keywordsOf(item) {
193
+ return (item.dataset.keywords || "").split(/\s+/).filter(Boolean)
194
+ }
195
+
196
+ valueOf(item) {
197
+ return item.dataset.value || this.textOf(item)
198
+ }
199
+ }