shadcn-rails 0.1.0

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 (315) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +40 -0
  3. data/CHANGELOG.md +54 -0
  4. data/CLAUDE.md +463 -0
  5. data/PROGRESS.md +485 -0
  6. data/README.md +1483 -0
  7. data/Rakefile +29 -0
  8. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +13 -0
  9. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +46 -0
  10. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +111 -0
  11. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +27 -0
  12. data/__tests__/controllers/accordion_controller.test.js +904 -0
  13. data/__tests__/controllers/calendar_controller.test.js +1370 -0
  14. data/__tests__/controllers/carousel_controller.test.js +912 -0
  15. data/__tests__/controllers/checkbox_controller.test.js +454 -0
  16. data/__tests__/controllers/collapsible_controller.test.js +407 -0
  17. data/__tests__/controllers/combobox_controller.test.js +966 -0
  18. data/__tests__/controllers/context_menu_controller.test.js +627 -0
  19. data/__tests__/controllers/date_picker_controller.test.js +636 -0
  20. data/__tests__/controllers/dialog_controller.test.js +878 -0
  21. data/__tests__/controllers/drawer_controller.test.js +995 -0
  22. data/__tests__/controllers/menubar_controller.test.js +736 -0
  23. data/__tests__/controllers/navigation_menu_controller.test.js +598 -0
  24. data/__tests__/controllers/popover_controller.test.js +1007 -0
  25. data/__tests__/controllers/radio_group_controller.test.js +640 -0
  26. data/__tests__/controllers/resizable_controller.test.js +680 -0
  27. data/__tests__/controllers/select_controller.test.js +674 -0
  28. data/__tests__/controllers/sheet_controller.test.js +986 -0
  29. data/__tests__/controllers/slider_controller.test.js +1036 -0
  30. data/__tests__/controllers/switch_controller.test.js +424 -0
  31. data/__tests__/controllers/tabs_controller.test.js +907 -0
  32. data/__tests__/controllers/toggle_group_controller.test.js +839 -0
  33. data/__tests__/controllers/tooltip_controller.test.js +808 -0
  34. data/__tests__/helpers/stimulus-test-helper.js +203 -0
  35. data/app/assets/config/manifest.js +1 -0
  36. data/app/assets/javascripts/shadcn/controllers/accordion_controller.d.ts +53 -0
  37. data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +140 -0
  38. data/app/assets/javascripts/shadcn/controllers/avatar_controller.d.ts +22 -0
  39. data/app/assets/javascripts/shadcn/controllers/avatar_controller.js +26 -0
  40. data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +592 -0
  41. data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +263 -0
  42. data/app/assets/javascripts/shadcn/controllers/checkbox_controller.d.ts +31 -0
  43. data/app/assets/javascripts/shadcn/controllers/checkbox_controller.js +48 -0
  44. data/app/assets/javascripts/shadcn/controllers/collapsible_controller.d.ts +43 -0
  45. data/app/assets/javascripts/shadcn/controllers/collapsible_controller.js +73 -0
  46. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +234 -0
  47. data/app/assets/javascripts/shadcn/controllers/command_controller.js +141 -0
  48. data/app/assets/javascripts/shadcn/controllers/command_dialog_controller.js +162 -0
  49. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +202 -0
  50. data/app/assets/javascripts/shadcn/controllers/date_picker_controller.js +282 -0
  51. data/app/assets/javascripts/shadcn/controllers/dialog_controller.d.ts +67 -0
  52. data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +187 -0
  53. data/app/assets/javascripts/shadcn/controllers/drawer_controller.d.ts +58 -0
  54. data/app/assets/javascripts/shadcn/controllers/drawer_controller.js +112 -0
  55. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.d.ts +83 -0
  56. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +225 -0
  57. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.d.ts +59 -0
  58. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +143 -0
  59. data/app/assets/javascripts/shadcn/controllers/input_otp_controller.d.ts +44 -0
  60. data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +206 -0
  61. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +323 -0
  62. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +251 -0
  63. data/app/assets/javascripts/shadcn/controllers/popover_controller.d.ts +56 -0
  64. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +141 -0
  65. data/app/assets/javascripts/shadcn/controllers/radio_group_controller.d.ts +47 -0
  66. data/app/assets/javascripts/shadcn/controllers/radio_group_controller.js +108 -0
  67. data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +272 -0
  68. data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.d.ts +44 -0
  69. data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.js +74 -0
  70. data/app/assets/javascripts/shadcn/controllers/select_controller.d.ts +84 -0
  71. data/app/assets/javascripts/shadcn/controllers/select_controller.js +222 -0
  72. data/app/assets/javascripts/shadcn/controllers/sheet_controller.d.ts +60 -0
  73. data/app/assets/javascripts/shadcn/controllers/sheet_controller.js +151 -0
  74. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +148 -0
  75. data/app/assets/javascripts/shadcn/controllers/slider_controller.d.ts +102 -0
  76. data/app/assets/javascripts/shadcn/controllers/slider_controller.js +364 -0
  77. data/app/assets/javascripts/shadcn/controllers/switch_controller.d.ts +46 -0
  78. data/app/assets/javascripts/shadcn/controllers/switch_controller.js +78 -0
  79. data/app/assets/javascripts/shadcn/controllers/tabs_controller.d.ts +51 -0
  80. data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +126 -0
  81. data/app/assets/javascripts/shadcn/controllers/toast_controller.d.ts +37 -0
  82. data/app/assets/javascripts/shadcn/controllers/toast_controller.js +58 -0
  83. data/app/assets/javascripts/shadcn/controllers/toggle_controller.d.ts +27 -0
  84. data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +42 -0
  85. data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.d.ts +44 -0
  86. data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +68 -0
  87. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.d.ts +56 -0
  88. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +117 -0
  89. data/app/assets/javascripts/shadcn/index.d.ts +74 -0
  90. data/app/assets/javascripts/shadcn/index.js +133 -0
  91. data/app/assets/stylesheets/.keep +0 -0
  92. data/app/assets/stylesheets/shadcn/base.css +445 -0
  93. data/app/assets/stylesheets/shadcn/components.css +513 -0
  94. data/app/assets/stylesheets/shadcn/index.css +18 -0
  95. data/app/assets/stylesheets/shadcn/themes/gray.css +68 -0
  96. data/app/assets/stylesheets/shadcn/themes/slate.css +68 -0
  97. data/app/assets/stylesheets/shadcn/themes/stone.css +68 -0
  98. data/app/assets/stylesheets/shadcn/themes/zinc.css +68 -0
  99. data/app/components/shadcn/accordion_component.rb +63 -0
  100. data/app/components/shadcn/accordion_content_component.rb +29 -0
  101. data/app/components/shadcn/accordion_item_component.rb +40 -0
  102. data/app/components/shadcn/accordion_trigger_component.rb +49 -0
  103. data/app/components/shadcn/alert_component.rb +75 -0
  104. data/app/components/shadcn/alert_description_component.rb +12 -0
  105. data/app/components/shadcn/alert_dialog_action_component.rb +24 -0
  106. data/app/components/shadcn/alert_dialog_cancel_component.rb +24 -0
  107. data/app/components/shadcn/alert_dialog_component.rb +71 -0
  108. data/app/components/shadcn/alert_dialog_content_component.rb +57 -0
  109. data/app/components/shadcn/alert_dialog_description_component.rb +12 -0
  110. data/app/components/shadcn/alert_dialog_footer_component.rb +19 -0
  111. data/app/components/shadcn/alert_dialog_header_component.rb +19 -0
  112. data/app/components/shadcn/alert_dialog_title_component.rb +12 -0
  113. data/app/components/shadcn/alert_title_component.rb +12 -0
  114. data/app/components/shadcn/aspect_ratio_component.rb +49 -0
  115. data/app/components/shadcn/avatar_component.rb +107 -0
  116. data/app/components/shadcn/avatar_fallback_component.rb +17 -0
  117. data/app/components/shadcn/badge_component.rb +49 -0
  118. data/app/components/shadcn/base_component.rb +100 -0
  119. data/app/components/shadcn/breadcrumb_component.rb +70 -0
  120. data/app/components/shadcn/breadcrumb_item_component.rb +50 -0
  121. data/app/components/shadcn/button_component.rb +141 -0
  122. data/app/components/shadcn/button_group_component.rb +69 -0
  123. data/app/components/shadcn/calendar_component.rb +337 -0
  124. data/app/components/shadcn/card_action_component.rb +10 -0
  125. data/app/components/shadcn/card_component.rb +63 -0
  126. data/app/components/shadcn/card_content_component.rb +19 -0
  127. data/app/components/shadcn/card_description_component.rb +12 -0
  128. data/app/components/shadcn/card_footer_component.rb +12 -0
  129. data/app/components/shadcn/card_header_component.rb +24 -0
  130. data/app/components/shadcn/card_title_component.rb +18 -0
  131. data/app/components/shadcn/carousel_component.rb +275 -0
  132. data/app/components/shadcn/checkbox_component.rb +103 -0
  133. data/app/components/shadcn/collapsible_component.rb +66 -0
  134. data/app/components/shadcn/collapsible_content_component.rb +28 -0
  135. data/app/components/shadcn/combobox_component.rb +322 -0
  136. data/app/components/shadcn/command_component.rb +52 -0
  137. data/app/components/shadcn/command_dialog_component.rb +76 -0
  138. data/app/components/shadcn/command_empty_component.rb +12 -0
  139. data/app/components/shadcn/command_group_component.rb +34 -0
  140. data/app/components/shadcn/command_input_component.rb +59 -0
  141. data/app/components/shadcn/command_item_component.rb +48 -0
  142. data/app/components/shadcn/command_list_component.rb +38 -0
  143. data/app/components/shadcn/command_separator_component.rb +12 -0
  144. data/app/components/shadcn/command_shortcut_component.rb +12 -0
  145. data/app/components/shadcn/context_menu_component.rb +64 -0
  146. data/app/components/shadcn/context_menu_content_component.rb +44 -0
  147. data/app/components/shadcn/context_menu_item_component.rb +63 -0
  148. data/app/components/shadcn/context_menu_label_component.rb +18 -0
  149. data/app/components/shadcn/context_menu_separator_component.rb +12 -0
  150. data/app/components/shadcn/context_menu_shortcut_component.rb +12 -0
  151. data/app/components/shadcn/date_picker_component.rb +368 -0
  152. data/app/components/shadcn/dialog_component.rb +77 -0
  153. data/app/components/shadcn/dialog_content_component.rb +91 -0
  154. data/app/components/shadcn/dialog_description_component.rb +12 -0
  155. data/app/components/shadcn/dialog_footer_component.rb +12 -0
  156. data/app/components/shadcn/dialog_header_component.rb +19 -0
  157. data/app/components/shadcn/dialog_title_component.rb +12 -0
  158. data/app/components/shadcn/drawer_component.rb +72 -0
  159. data/app/components/shadcn/drawer_content_component.rb +76 -0
  160. data/app/components/shadcn/drawer_description_component.rb +12 -0
  161. data/app/components/shadcn/drawer_footer_component.rb +12 -0
  162. data/app/components/shadcn/drawer_header_component.rb +19 -0
  163. data/app/components/shadcn/drawer_title_component.rb +12 -0
  164. data/app/components/shadcn/dropdown_menu_component.rb +75 -0
  165. data/app/components/shadcn/dropdown_menu_content_component.rb +49 -0
  166. data/app/components/shadcn/dropdown_menu_group_component.rb +10 -0
  167. data/app/components/shadcn/dropdown_menu_item_component.rb +63 -0
  168. data/app/components/shadcn/dropdown_menu_label_component.rb +18 -0
  169. data/app/components/shadcn/dropdown_menu_separator_component.rb +12 -0
  170. data/app/components/shadcn/dropdown_menu_shortcut_component.rb +12 -0
  171. data/app/components/shadcn/empty_component.rb +48 -0
  172. data/app/components/shadcn/empty_content_component.rb +12 -0
  173. data/app/components/shadcn/empty_description_component.rb +12 -0
  174. data/app/components/shadcn/empty_header_component.rb +29 -0
  175. data/app/components/shadcn/empty_media_component.rb +21 -0
  176. data/app/components/shadcn/empty_title_component.rb +12 -0
  177. data/app/components/shadcn/field_component.rb +113 -0
  178. data/app/components/shadcn/hover_card_component.rb +64 -0
  179. data/app/components/shadcn/hover_card_content_component.rb +36 -0
  180. data/app/components/shadcn/input_component.rb +108 -0
  181. data/app/components/shadcn/input_group_component.rb +70 -0
  182. data/app/components/shadcn/input_otp_component.rb +183 -0
  183. data/app/components/shadcn/item_actions_component.rb +12 -0
  184. data/app/components/shadcn/item_component.rb +98 -0
  185. data/app/components/shadcn/item_content_component.rb +24 -0
  186. data/app/components/shadcn/item_description_component.rb +12 -0
  187. data/app/components/shadcn/item_footer_component.rb +12 -0
  188. data/app/components/shadcn/item_group_component.rb +24 -0
  189. data/app/components/shadcn/item_header_component.rb +12 -0
  190. data/app/components/shadcn/item_media_component.rb +22 -0
  191. data/app/components/shadcn/item_separator_component.rb +12 -0
  192. data/app/components/shadcn/item_title_component.rb +12 -0
  193. data/app/components/shadcn/kbd_component.rb +36 -0
  194. data/app/components/shadcn/label_component.rb +49 -0
  195. data/app/components/shadcn/menubar_checkbox_item_component.rb +76 -0
  196. data/app/components/shadcn/menubar_component.rb +56 -0
  197. data/app/components/shadcn/menubar_content_component.rb +64 -0
  198. data/app/components/shadcn/menubar_item_component.rb +65 -0
  199. data/app/components/shadcn/menubar_label_component.rb +27 -0
  200. data/app/components/shadcn/menubar_menu_component.rb +34 -0
  201. data/app/components/shadcn/menubar_radio_group_component.rb +42 -0
  202. data/app/components/shadcn/menubar_radio_item_component.rb +76 -0
  203. data/app/components/shadcn/menubar_separator_component.rb +22 -0
  204. data/app/components/shadcn/menubar_shortcut_component.rb +21 -0
  205. data/app/components/shadcn/menubar_sub_component.rb +38 -0
  206. data/app/components/shadcn/menubar_sub_content_component.rb +45 -0
  207. data/app/components/shadcn/menubar_sub_trigger_component.rb +59 -0
  208. data/app/components/shadcn/menubar_trigger_component.rb +31 -0
  209. data/app/components/shadcn/native_select_component.rb +150 -0
  210. data/app/components/shadcn/navigation_menu_component.rb +76 -0
  211. data/app/components/shadcn/navigation_menu_content_component.rb +30 -0
  212. data/app/components/shadcn/navigation_menu_item_component.rb +39 -0
  213. data/app/components/shadcn/navigation_menu_link_component.rb +38 -0
  214. data/app/components/shadcn/navigation_menu_list_component.rb +29 -0
  215. data/app/components/shadcn/navigation_menu_trigger_component.rb +59 -0
  216. data/app/components/shadcn/pagination_component.rb +195 -0
  217. data/app/components/shadcn/pagination_content_component.rb +47 -0
  218. data/app/components/shadcn/pagination_ellipsis_component.rb +30 -0
  219. data/app/components/shadcn/pagination_item_component.rb +53 -0
  220. data/app/components/shadcn/pagination_next_component.rb +48 -0
  221. data/app/components/shadcn/pagination_previous_component.rb +48 -0
  222. data/app/components/shadcn/popover_component.rb +76 -0
  223. data/app/components/shadcn/popover_content_component.rb +25 -0
  224. data/app/components/shadcn/progress_component.rb +77 -0
  225. data/app/components/shadcn/radio_group_component.rb +129 -0
  226. data/app/components/shadcn/radio_group_item_component.rb +109 -0
  227. data/app/components/shadcn/resizable_handle_component.rb +98 -0
  228. data/app/components/shadcn/resizable_panel_component.rb +56 -0
  229. data/app/components/shadcn/resizable_panel_group_component.rb +94 -0
  230. data/app/components/shadcn/scroll_area_component.rb +110 -0
  231. data/app/components/shadcn/select_component.rb +151 -0
  232. data/app/components/shadcn/select_group_component.rb +32 -0
  233. data/app/components/shadcn/select_item_component.rb +59 -0
  234. data/app/components/shadcn/select_separator_component.rb +12 -0
  235. data/app/components/shadcn/separator_component.rb +54 -0
  236. data/app/components/shadcn/sheet_component.rb +82 -0
  237. data/app/components/shadcn/sheet_content_component.rb +95 -0
  238. data/app/components/shadcn/sheet_description_component.rb +12 -0
  239. data/app/components/shadcn/sheet_footer_component.rb +12 -0
  240. data/app/components/shadcn/sheet_header_component.rb +19 -0
  241. data/app/components/shadcn/sheet_title_component.rb +12 -0
  242. data/app/components/shadcn/sidebar_component.rb +180 -0
  243. data/app/components/shadcn/sidebar_content_component.rb +32 -0
  244. data/app/components/shadcn/sidebar_footer_component.rb +24 -0
  245. data/app/components/shadcn/sidebar_group_action_component.rb +26 -0
  246. data/app/components/shadcn/sidebar_group_component.rb +38 -0
  247. data/app/components/shadcn/sidebar_group_content_component.rb +32 -0
  248. data/app/components/shadcn/sidebar_group_label_component.rb +25 -0
  249. data/app/components/shadcn/sidebar_header_component.rb +24 -0
  250. data/app/components/shadcn/sidebar_inset_component.rb +25 -0
  251. data/app/components/shadcn/sidebar_menu_action_component.rb +37 -0
  252. data/app/components/shadcn/sidebar_menu_badge_component.rb +25 -0
  253. data/app/components/shadcn/sidebar_menu_button_component.rb +52 -0
  254. data/app/components/shadcn/sidebar_menu_component.rb +32 -0
  255. data/app/components/shadcn/sidebar_menu_item_component.rb +41 -0
  256. data/app/components/shadcn/sidebar_menu_skeleton_component.rb +46 -0
  257. data/app/components/shadcn/sidebar_menu_sub_button_component.rb +43 -0
  258. data/app/components/shadcn/sidebar_menu_sub_component.rb +33 -0
  259. data/app/components/shadcn/sidebar_menu_sub_item_component.rb +30 -0
  260. data/app/components/shadcn/sidebar_provider_component.rb +57 -0
  261. data/app/components/shadcn/sidebar_rail_component.rb +30 -0
  262. data/app/components/shadcn/sidebar_separator_component.rb +24 -0
  263. data/app/components/shadcn/sidebar_trigger_component.rb +51 -0
  264. data/app/components/shadcn/skeleton_component.rb +29 -0
  265. data/app/components/shadcn/slider_component.rb +76 -0
  266. data/app/components/shadcn/spinner_component.rb +67 -0
  267. data/app/components/shadcn/switch_component.rb +147 -0
  268. data/app/components/shadcn/table_body_component.rb +16 -0
  269. data/app/components/shadcn/table_caption_component.rb +12 -0
  270. data/app/components/shadcn/table_cell_component.rb +12 -0
  271. data/app/components/shadcn/table_component.rb +57 -0
  272. data/app/components/shadcn/table_footer_component.rb +16 -0
  273. data/app/components/shadcn/table_head_component.rb +12 -0
  274. data/app/components/shadcn/table_header_component.rb +16 -0
  275. data/app/components/shadcn/table_row_component.rb +40 -0
  276. data/app/components/shadcn/tabs_component.rb +78 -0
  277. data/app/components/shadcn/tabs_content_component.rb +32 -0
  278. data/app/components/shadcn/tabs_list_component.rb +30 -0
  279. data/app/components/shadcn/tabs_trigger_component.rb +37 -0
  280. data/app/components/shadcn/textarea_component.rb +84 -0
  281. data/app/components/shadcn/toast_action_component.rb +18 -0
  282. data/app/components/shadcn/toast_component.rb +114 -0
  283. data/app/components/shadcn/toast_description_component.rb +12 -0
  284. data/app/components/shadcn/toast_title_component.rb +12 -0
  285. data/app/components/shadcn/toast_viewport_component.rb +12 -0
  286. data/app/components/shadcn/toggle_component.rb +77 -0
  287. data/app/components/shadcn/toggle_group_component.rb +96 -0
  288. data/app/components/shadcn/toggle_group_item_component.rb +62 -0
  289. data/app/components/shadcn/tooltip_component.rb +89 -0
  290. data/app/components/shadcn/typography_component.rb +112 -0
  291. data/babel.config.cjs +5 -0
  292. data/bin/console +11 -0
  293. data/bin/setup +8 -0
  294. data/config/importmap.rb +5 -0
  295. data/fly.toml +26 -0
  296. data/jest.config.js +19 -0
  297. data/jest.setup.js +8 -0
  298. data/lib/generators/shadcn/component/component_generator.rb +188 -0
  299. data/lib/generators/shadcn/install/install_generator.rb +140 -0
  300. data/lib/generators/shadcn/install/templates/initializer.rb.tt +35 -0
  301. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +35 -0
  302. data/lib/generators/shadcn/theme/theme_generator.rb +128 -0
  303. data/lib/shadcn/rails/class_merger.rb +228 -0
  304. data/lib/shadcn/rails/configuration.rb +341 -0
  305. data/lib/shadcn/rails/engine.rb +59 -0
  306. data/lib/shadcn/rails/helpers/class_name_helper.rb +35 -0
  307. data/lib/shadcn/rails/helpers/component_helper.rb +60 -0
  308. data/lib/shadcn/rails/helpers/pagination_helper.rb +187 -0
  309. data/lib/shadcn/rails/version.rb +7 -0
  310. data/lib/shadcn/rails.rb +179 -0
  311. data/package-lock.json +7415 -0
  312. data/package.json +68 -0
  313. data/rollup.config.js +29 -0
  314. data/sig/shadcn/rails.rbs +6 -0
  315. metadata +526 -0
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Combobox component - autocomplete input with searchable dropdown
5
+ # Composes Popover and Command components for a searchable select
6
+ #
7
+ # @example Basic combobox
8
+ # <%= render Shadcn::ComboboxComponent.new(
9
+ # items: [
10
+ # { value: "next", label: "Next.js" },
11
+ # { value: "remix", label: "Remix" },
12
+ # { value: "rails", label: "Ruby on Rails" }
13
+ # ],
14
+ # placeholder: "Select framework...",
15
+ # search_placeholder: "Search framework..."
16
+ # ) %>
17
+ #
18
+ # @example With selected value
19
+ # <%= render Shadcn::ComboboxComponent.new(
20
+ # items: frameworks,
21
+ # value: "rails",
22
+ # placeholder: "Select framework..."
23
+ # ) %>
24
+ #
25
+ # @example With name for form submission
26
+ # <%= render Shadcn::ComboboxComponent.new(
27
+ # items: frameworks,
28
+ # name: "project[framework]",
29
+ # placeholder: "Select framework..."
30
+ # ) %>
31
+ #
32
+ class ComboboxComponent < BaseComponent
33
+ TRIGGER_CLASSES = "w-[200px] justify-between"
34
+ POPOVER_CONTENT_CLASSES = "w-[200px] p-0"
35
+ CHEVRON_CLASSES = "ml-2 h-4 w-4 shrink-0 opacity-50"
36
+ CHECK_CLASSES = "mr-2 h-4 w-4"
37
+
38
+ # @param items [Array<Hash>] Array of items with :value and :label keys
39
+ # @param value [String, nil] Currently selected value
40
+ # @param placeholder [String] Placeholder text when no value selected
41
+ # @param search_placeholder [String] Placeholder text for search input
42
+ # @param empty_text [String] Text shown when no results found
43
+ # @param name [String, nil] Form field name for hidden input
44
+ # @param disabled [Boolean] Whether the combobox is disabled
45
+ # @param width [String] Width class for the component
46
+ def initialize(
47
+ items: [],
48
+ value: nil,
49
+ placeholder: "Select option...",
50
+ search_placeholder: "Search...",
51
+ empty_text: "No results found.",
52
+ name: nil,
53
+ disabled: false,
54
+ width: "w-[200px]",
55
+ **options
56
+ )
57
+ super(**options)
58
+ @items = items
59
+ @value = value
60
+ @placeholder = placeholder
61
+ @search_placeholder = search_placeholder
62
+ @empty_text = empty_text
63
+ @name = name
64
+ @disabled = disabled
65
+ @width = width
66
+ end
67
+
68
+ def call
69
+ content_tag(:div, combobox_content, **combobox_attributes)
70
+ end
71
+
72
+ private
73
+
74
+ def combobox_content
75
+ safe_join([
76
+ hidden_input,
77
+ trigger_button,
78
+ popover_content_template
79
+ ].compact)
80
+ end
81
+
82
+ def hidden_input
83
+ return unless @name
84
+
85
+ tag.input(
86
+ type: "hidden",
87
+ name: @name,
88
+ value: @value,
89
+ data: { "shadcn--combobox-target": "hiddenInput" }
90
+ )
91
+ end
92
+
93
+ def trigger_button
94
+ content_tag(:button, button_content, **trigger_attributes)
95
+ end
96
+
97
+ def button_content
98
+ safe_join([
99
+ display_value,
100
+ chevron_icon
101
+ ])
102
+ end
103
+
104
+ def display_value
105
+ label = selected_label
106
+ content_tag(:span, label || @placeholder,
107
+ class: label ? nil : "text-muted-foreground",
108
+ data: { "shadcn--combobox-target": "displayValue" }
109
+ )
110
+ end
111
+
112
+ def selected_label
113
+ return nil unless @value
114
+
115
+ item = @items.find { |i| i[:value].to_s == @value.to_s }
116
+ item&.dig(:label)
117
+ end
118
+
119
+ def chevron_icon
120
+ content_tag(:svg,
121
+ class: CHEVRON_CLASSES,
122
+ xmlns: "http://www.w3.org/2000/svg",
123
+ width: "24",
124
+ height: "24",
125
+ viewBox: "0 0 24 24",
126
+ fill: "none",
127
+ stroke: "currentColor",
128
+ "stroke-width": "2",
129
+ "stroke-linecap": "round",
130
+ "stroke-linejoin": "round"
131
+ ) do
132
+ safe_join([
133
+ tag.path(d: "m7 15 5 5 5-5"),
134
+ tag.path(d: "m7 9 5-5 5 5")
135
+ ])
136
+ end
137
+ end
138
+
139
+ def trigger_attributes
140
+ {
141
+ type: "button",
142
+ role: "combobox",
143
+ class: cn(
144
+ "inline-flex items-center whitespace-nowrap rounded-md text-sm font-medium transition-colors",
145
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
146
+ "disabled:pointer-events-none disabled:opacity-50",
147
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
148
+ "h-9 px-4 py-2",
149
+ @width,
150
+ "justify-between"
151
+ ),
152
+ disabled: @disabled || nil,
153
+ "aria-expanded": "false",
154
+ data: {
155
+ "shadcn--combobox-target": "trigger",
156
+ action: "click->shadcn--combobox#toggle"
157
+ }
158
+ }
159
+ end
160
+
161
+ def popover_content_template
162
+ content_tag(:div, popover_inner_content, **popover_content_attributes)
163
+ end
164
+
165
+ def popover_inner_content
166
+ content_tag(:div, command_content, **command_wrapper_attributes)
167
+ end
168
+
169
+ def command_content
170
+ safe_join([
171
+ search_input,
172
+ items_list
173
+ ])
174
+ end
175
+
176
+ def search_input
177
+ content_tag(:div, class: "flex items-center border-b px-3") do
178
+ safe_join([
179
+ search_icon,
180
+ tag.input(
181
+ type: "text",
182
+ placeholder: @search_placeholder,
183
+ class: "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
184
+ data: {
185
+ "shadcn--combobox-target": "input",
186
+ action: "input->shadcn--combobox#filter"
187
+ }
188
+ )
189
+ ])
190
+ end
191
+ end
192
+
193
+ def search_icon
194
+ content_tag(:svg,
195
+ class: "mr-2 h-4 w-4 shrink-0 opacity-50",
196
+ xmlns: "http://www.w3.org/2000/svg",
197
+ width: "24",
198
+ height: "24",
199
+ viewBox: "0 0 24 24",
200
+ fill: "none",
201
+ stroke: "currentColor",
202
+ "stroke-width": "2",
203
+ "stroke-linecap": "round",
204
+ "stroke-linejoin": "round"
205
+ ) do
206
+ safe_join([
207
+ tag.circle(cx: "11", cy: "11", r: "8"),
208
+ tag.path(d: "m21 21-4.3-4.3")
209
+ ])
210
+ end
211
+ end
212
+
213
+ def items_list
214
+ content_tag(:div, items_content, class: "max-h-[300px] overflow-y-auto overflow-x-hidden", data: { "shadcn--combobox-target": "list" })
215
+ end
216
+
217
+ def items_content
218
+ safe_join([
219
+ empty_state,
220
+ items_group
221
+ ])
222
+ end
223
+
224
+ def empty_state
225
+ # Always start hidden - will show only when user types and no results match
226
+ content_tag(:div, @empty_text,
227
+ class: "py-6 text-center text-sm text-muted-foreground",
228
+ hidden: true,
229
+ data: { "shadcn--combobox-target": "empty" }
230
+ )
231
+ end
232
+
233
+ def items_group
234
+ content_tag(:div, class: "overflow-hidden p-1 text-foreground") do
235
+ safe_join(@items.map { |item| render_item(item) })
236
+ end
237
+ end
238
+
239
+ def render_item(item)
240
+ is_selected = @value.to_s == item[:value].to_s
241
+
242
+ content_tag(:div, **item_attributes(item, is_selected)) do
243
+ safe_join([
244
+ check_icon(is_selected),
245
+ item[:label]
246
+ ])
247
+ end
248
+ end
249
+
250
+ def check_icon(is_selected)
251
+ content_tag(:svg,
252
+ class: cn(CHECK_CLASSES, is_selected ? "opacity-100" : "opacity-0"),
253
+ xmlns: "http://www.w3.org/2000/svg",
254
+ width: "24",
255
+ height: "24",
256
+ viewBox: "0 0 24 24",
257
+ fill: "none",
258
+ stroke: "currentColor",
259
+ "stroke-width": "2",
260
+ "stroke-linecap": "round",
261
+ "stroke-linejoin": "round"
262
+ ) do
263
+ tag.path(d: "M20 6 9 17l-5-5")
264
+ end
265
+ end
266
+
267
+ def item_attributes(item, is_selected)
268
+ {
269
+ class: cn(
270
+ "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
271
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
272
+ "data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
273
+ "hover:bg-accent hover:text-accent-foreground cursor-pointer"
274
+ ),
275
+ role: "option",
276
+ tabindex: "0",
277
+ data: {
278
+ "shadcn--combobox-target": "item",
279
+ value: item[:value],
280
+ label: item[:label],
281
+ selected: is_selected,
282
+ action: "click->shadcn--combobox#select"
283
+ }
284
+ }
285
+ end
286
+
287
+ def popover_content_attributes
288
+ {
289
+ class: cn(
290
+ "absolute z-50 mt-1 rounded-md border bg-popover text-popover-foreground shadow-md outline-none",
291
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
292
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
293
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
294
+ @width,
295
+ "p-0"
296
+ ),
297
+ hidden: true,
298
+ data: {
299
+ "shadcn--combobox-target": "content",
300
+ state: "closed"
301
+ }
302
+ }
303
+ end
304
+
305
+ def command_wrapper_attributes
306
+ {
307
+ class: "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground"
308
+ }
309
+ end
310
+
311
+ def combobox_attributes
312
+ {
313
+ class: cn("relative inline-block", class_name),
314
+ data: {
315
+ controller: "shadcn--combobox",
316
+ "shadcn--combobox-value-value": @value,
317
+ action: "keydown.escape->shadcn--combobox#close click@window->shadcn--combobox#handleClickOutside"
318
+ }
319
+ }.merge(html_options).merge(build_data)
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Command component - a command palette for searching and selecting items
5
+ # Matches shadcn/ui Command component (cmdk pattern)
6
+ #
7
+ # @example Basic command
8
+ # <%= render Shadcn::CommandComponent.new do |command| %>
9
+ # <% command.with_input(placeholder: "Type a command...") %>
10
+ # <% command.with_list do |list| %>
11
+ # <% list.with_empty { "No results found." } %>
12
+ # <% list.with_group(heading: "Suggestions") do |group| %>
13
+ # <% group.with_item(value: "calendar") { "Calendar" } %>
14
+ # <% group.with_item(value: "search") { "Search" } %>
15
+ # <% end %>
16
+ # <% end %>
17
+ # <% end %>
18
+ #
19
+ class CommandComponent < BaseComponent
20
+ BASE_CLASSES = "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground"
21
+
22
+ # Input slot for search
23
+ renders_one :input, lambda { |placeholder: "Type a command or search...", **options|
24
+ CommandInputComponent.new(placeholder: placeholder, **options)
25
+ }
26
+
27
+ # List slot containing groups and items
28
+ renders_one :list, lambda { |**options|
29
+ CommandListComponent.new(**options)
30
+ }
31
+
32
+ def call
33
+ content_tag(:div, command_content, **command_attributes)
34
+ end
35
+
36
+ private
37
+
38
+ def command_content
39
+ safe_join([input, list, content].compact)
40
+ end
41
+
42
+ def command_attributes
43
+ {
44
+ class: merge_classes(BASE_CLASSES),
45
+ data: {
46
+ controller: "shadcn--command",
47
+ action: "keydown->shadcn--command#handleKeydown"
48
+ }
49
+ }.merge(html_options).merge(build_data)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Command Dialog component - command palette in a modal dialog
5
+ # Combines Dialog and Command components
6
+ #
7
+ # @example Basic command dialog
8
+ # <%= render Shadcn::CommandDialogComponent.new do |dialog| %>
9
+ # <% dialog.with_trigger do %>
10
+ # <button>Open Command</button>
11
+ # <% end %>
12
+ # <% dialog.with_command do |command| %>
13
+ # <% command.with_input(placeholder: "Type a command...") %>
14
+ # <% command.with_list do |list| %>
15
+ # <% list.with_empty { "No results found." } %>
16
+ # <% list.with_group(heading: "Suggestions") do |group| %>
17
+ # <% group.with_item(value: "calendar") { "Calendar" } %>
18
+ # <% end %>
19
+ # <% end %>
20
+ # <% end %>
21
+ # <% end %>
22
+ #
23
+ class CommandDialogComponent < BaseComponent
24
+ OVERLAY_CLASSES = "fixed inset-0 z-50 bg-black/80"
25
+ CONTENT_CLASSES = "fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%] shadow-lg"
26
+ COMMAND_CLASSES = "[&_[data-shadcn--command-target='input']]:h-12"
27
+
28
+ # Trigger slot - simple wrapper that renders content
29
+ renders_one :trigger, lambda { |**options, &block|
30
+ content_tag(:div, data: { "shadcn--command-dialog-target": "trigger", action: "click->shadcn--command-dialog#open" }, **options, &block)
31
+ }
32
+
33
+ # Command slot
34
+ renders_one :command, lambda { |**options|
35
+ CommandComponent.new(class_name: "rounded-lg border shadow-md md:min-w-[450px] #{COMMAND_CLASSES}", **options)
36
+ }
37
+
38
+ # @param shortcut [String] Keyboard shortcut to open (e.g., "k" for Cmd+K)
39
+ def initialize(shortcut: nil, **options)
40
+ super(**options)
41
+ @shortcut = shortcut
42
+ end
43
+
44
+ def call
45
+ content_tag(:div, dialog_content, **dialog_attributes)
46
+ end
47
+
48
+ private
49
+
50
+ def dialog_content
51
+ safe_join([
52
+ trigger,
53
+ dialog_template
54
+ ].compact)
55
+ end
56
+
57
+ def dialog_template
58
+ content_tag(:template, data: { "shadcn--command-dialog-target": "template" }) do
59
+ safe_join([
60
+ content_tag(:div, "", class: OVERLAY_CLASSES, data: { "shadcn--command-dialog-target": "overlay", action: "click->shadcn--command-dialog#close" }),
61
+ content_tag(:div, command || content, class: CONTENT_CLASSES, data: { "shadcn--command-dialog-target": "content" })
62
+ ])
63
+ end
64
+ end
65
+
66
+ def dialog_attributes
67
+ attrs = {
68
+ data: {
69
+ controller: "shadcn--command-dialog",
70
+ "shadcn--command-dialog-shortcut-value": @shortcut
71
+ }.compact
72
+ }
73
+ attrs.merge(html_options).merge(build_data)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Command Empty component - shown when no results match
5
+ class CommandEmptyComponent < BaseComponent
6
+ BASE_CLASSES = "py-6 text-center text-sm text-muted-foreground"
7
+
8
+ def call
9
+ content_tag(:div, content.presence || "No results found.", class: merge_classes(BASE_CLASSES), data: { "shadcn--command-target": "empty" }, **html_options.merge(build_data))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Command Group component - groups related command items
5
+ class CommandGroupComponent < BaseComponent
6
+ BASE_CLASSES = "overflow-hidden p-1 text-foreground"
7
+ HEADING_CLASSES = "px-2 py-1.5 text-xs font-medium text-muted-foreground"
8
+
9
+ # Items in this group
10
+ renders_many :items, lambda { |value: nil, disabled: false, **options|
11
+ CommandItemComponent.new(value: value, disabled: disabled, **options)
12
+ }
13
+
14
+ # @param heading [String, nil] Optional heading for the group
15
+ def initialize(heading: nil, **options)
16
+ super(**options)
17
+ @heading = heading
18
+ end
19
+
20
+ def call
21
+ content_tag(:div, group_content, class: merge_classes(BASE_CLASSES), role: "group", data: { "shadcn--command-target": "group" }, **html_options.merge(build_data))
22
+ end
23
+
24
+ private
25
+
26
+ def group_content
27
+ parts = []
28
+ parts << content_tag(:div, @heading, class: HEADING_CLASSES, "aria-hidden": true) if @heading.present?
29
+ parts << safe_join(items)
30
+ parts << content
31
+ safe_join(parts.compact)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Command Input component - search input for command palette
5
+ class CommandInputComponent < BaseComponent
6
+ WRAPPER_CLASSES = "flex items-center border-b px-3"
7
+ INPUT_CLASSES = "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
8
+ ICON_CLASSES = "mr-2 h-4 w-4 shrink-0 opacity-50"
9
+
10
+ # @param placeholder [String] Placeholder text
11
+ # @param autofocus [Boolean] Whether to autofocus the input
12
+ def initialize(placeholder: "Type a command or search...", autofocus: false, **options)
13
+ super(**options)
14
+ @placeholder = placeholder
15
+ @autofocus = autofocus
16
+ end
17
+
18
+ def call
19
+ content_tag(:div, class: WRAPPER_CLASSES) do
20
+ safe_join([
21
+ search_icon,
22
+ tag.input(
23
+ type: "text",
24
+ placeholder: @placeholder,
25
+ autofocus: @autofocus || nil,
26
+ class: merge_classes(INPUT_CLASSES),
27
+ data: {
28
+ "shadcn--command-target": "input",
29
+ action: "input->shadcn--command#filter"
30
+ },
31
+ **html_options.except(:class)
32
+ )
33
+ ])
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def search_icon
40
+ content_tag(:svg,
41
+ class: ICON_CLASSES,
42
+ xmlns: "http://www.w3.org/2000/svg",
43
+ width: "24",
44
+ height: "24",
45
+ viewBox: "0 0 24 24",
46
+ fill: "none",
47
+ stroke: "currentColor",
48
+ "stroke-width": "2",
49
+ "stroke-linecap": "round",
50
+ "stroke-linejoin": "round"
51
+ ) do
52
+ safe_join([
53
+ tag.circle(cx: "11", cy: "11", r: "8"),
54
+ tag.path(d: "m21 21-4.3-4.3")
55
+ ])
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Command Item component - individual selectable command
5
+ class CommandItemComponent < BaseComponent
6
+ BASE_CLASSES = "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0"
7
+
8
+ # Shortcut slot
9
+ renders_one :shortcut, lambda { |**options|
10
+ CommandShortcutComponent.new(**options)
11
+ }
12
+
13
+ # @param value [String, nil] The searchable value (defaults to content text)
14
+ # @param disabled [Boolean] Whether the item is disabled
15
+ # @param on_select [String] JavaScript to execute on select
16
+ def initialize(value: nil, disabled: false, on_select: nil, **options)
17
+ super(**options)
18
+ @value = value
19
+ @disabled = disabled
20
+ @on_select = on_select
21
+ end
22
+
23
+ def call
24
+ content_tag(:div, item_content, **item_attributes)
25
+ end
26
+
27
+ private
28
+
29
+ def item_content
30
+ safe_join([content, shortcut].compact)
31
+ end
32
+
33
+ def item_attributes
34
+ {
35
+ class: merge_classes(BASE_CLASSES),
36
+ role: "option",
37
+ tabindex: @disabled ? nil : "0",
38
+ data: {
39
+ "shadcn--command-target": "item",
40
+ value: @value,
41
+ disabled: @disabled || nil,
42
+ selected: false,
43
+ action: @disabled ? nil : "click->shadcn--command#select"
44
+ }.compact
45
+ }.merge(html_options).merge(build_data)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Command List component - container for command groups and items
5
+ class CommandListComponent < BaseComponent
6
+ BASE_CLASSES = "max-h-[300px] overflow-y-auto overflow-x-hidden"
7
+
8
+ # Empty state slot
9
+ renders_one :empty, lambda { |**options|
10
+ CommandEmptyComponent.new(**options)
11
+ }
12
+
13
+ # Groups of items
14
+ renders_many :groups, lambda { |heading: nil, **options|
15
+ CommandGroupComponent.new(heading: heading, **options)
16
+ }
17
+
18
+ # Direct items (without group)
19
+ renders_many :items, lambda { |value: nil, disabled: false, **options|
20
+ CommandItemComponent.new(value: value, disabled: disabled, **options)
21
+ }
22
+
23
+ # Separators
24
+ renders_many :separators, lambda { |**options|
25
+ CommandSeparatorComponent.new(**options)
26
+ }
27
+
28
+ def call
29
+ content_tag(:div, list_content, class: merge_classes(BASE_CLASSES), data: { "shadcn--command-target": "list" }, **html_options.merge(build_data))
30
+ end
31
+
32
+ private
33
+
34
+ def list_content
35
+ safe_join([empty, groups, items, separators, content].flatten.compact)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Command Separator component - visual divider between groups
5
+ class CommandSeparatorComponent < BaseComponent
6
+ BASE_CLASSES = "-mx-1 h-px bg-border"
7
+
8
+ def call
9
+ content_tag(:div, "", class: merge_classes(BASE_CLASSES), role: "separator", **html_options.merge(build_data))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Command Shortcut component - displays keyboard shortcut hint
5
+ class CommandShortcutComponent < BaseComponent
6
+ BASE_CLASSES = "ml-auto text-xs tracking-widest text-muted-foreground"
7
+
8
+ def call
9
+ content_tag(:span, content, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data))
10
+ end
11
+ end
12
+ end