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,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Menubar Radio Group component
5
+ # Group of mutually exclusive radio items
6
+ class MenubarRadioGroupComponent < BaseComponent
7
+ renders_many :items, lambda { |**options, &block|
8
+ MenubarRadioItemComponent.new(**options, &block)
9
+ }
10
+
11
+ # @param value [String] Currently selected value
12
+ def initialize(value: nil, **options, &block)
13
+ super(**options, &block)
14
+ @value = value
15
+ end
16
+
17
+ def call
18
+ content_tag(:div, group_content, group_attributes)
19
+ end
20
+
21
+ private
22
+
23
+ def group_content
24
+ if items.any?
25
+ safe_join(items)
26
+ else
27
+ content
28
+ end
29
+ end
30
+
31
+ def group_attributes
32
+ attrs = {
33
+ class: class_name,
34
+ role: "group",
35
+ "data-value": @value
36
+ }
37
+ attrs.merge!(html_options)
38
+ attrs.merge!(build_data)
39
+ attrs.compact
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Menubar Radio Item component
5
+ # A radio button within a radio group
6
+ class MenubarRadioItemComponent < BaseComponent
7
+ BASE_CLASSES = "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
8
+
9
+ renders_one :shortcut, lambda { |**options|
10
+ MenubarShortcutComponent.new(**options)
11
+ }
12
+
13
+ # @param value [String] Value of this radio item
14
+ # @param checked [Boolean] Whether item is selected
15
+ # @param disabled [Boolean] Whether item is disabled
16
+ def initialize(value: nil, checked: false, disabled: false, **options, &block)
17
+ super(**options, &block)
18
+ @value = value
19
+ @checked = checked
20
+ @disabled = disabled
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([
31
+ radio_indicator,
32
+ content,
33
+ shortcut
34
+ ].compact)
35
+ end
36
+
37
+ def radio_indicator
38
+ content_tag(:span, radio_icon, class: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center")
39
+ end
40
+
41
+ def radio_icon
42
+ return "" unless @checked
43
+
44
+ content_tag(:svg, circle_svg, {
45
+ xmlns: "http://www.w3.org/2000/svg",
46
+ width: "16",
47
+ height: "16",
48
+ viewBox: "0 0 24 24",
49
+ fill: "currentColor",
50
+ stroke: "none",
51
+ class: "h-4 w-4"
52
+ })
53
+ end
54
+
55
+ def circle_svg
56
+ content_tag(:circle, "", cx: "12", cy: "12", r: "6")
57
+ end
58
+
59
+ def item_attributes
60
+ attrs = {
61
+ class: cn(BASE_CLASSES, class_name),
62
+ role: "menuitemradio",
63
+ "aria-checked": @checked.to_s,
64
+ tabindex: @disabled ? nil : "-1",
65
+ "data-disabled": @disabled ? "" : nil,
66
+ "data-state": @checked ? "checked" : "unchecked",
67
+ "data-value": @value,
68
+ "data-shadcn--menubar-target": "item",
69
+ "data-action": "click->shadcn--menubar#selectRadio"
70
+ }
71
+ attrs.merge!(html_options)
72
+ attrs.merge!(build_data)
73
+ attrs.compact
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Menubar Separator component
5
+ # Visual separator between menu items
6
+ class MenubarSeparatorComponent < BaseComponent
7
+ BASE_CLASSES = "-mx-1 my-1 h-px bg-muted"
8
+
9
+ def call
10
+ content_tag(:div, "", separator_attributes)
11
+ end
12
+
13
+ private
14
+
15
+ def separator_attributes
16
+ {
17
+ class: cn(BASE_CLASSES, class_name),
18
+ role: "separator"
19
+ }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Menubar Shortcut component
5
+ # Displays keyboard shortcuts next to menu items
6
+ class MenubarShortcutComponent < BaseComponent
7
+ BASE_CLASSES = "ml-auto text-xs tracking-widest text-muted-foreground"
8
+
9
+ def call
10
+ content_tag(:span, content, shortcut_attributes)
11
+ end
12
+
13
+ private
14
+
15
+ def shortcut_attributes
16
+ {
17
+ class: cn(BASE_CLASSES, class_name)
18
+ }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Menubar Sub component
5
+ # Creates nested/submenu functionality
6
+ class MenubarSubComponent < BaseComponent
7
+ renders_one :trigger, lambda { |**options, &block|
8
+ MenubarSubTriggerComponent.new(**options, &block)
9
+ }
10
+ # Note: Named content_slot because 'content' is a reserved ViewComponent method
11
+ renders_one :content_slot, lambda { |**options|
12
+ MenubarSubContentComponent.new(**options)
13
+ }
14
+
15
+ # Alias for more intuitive API
16
+ alias_method :with_content, :with_content_slot
17
+
18
+ def call
19
+ content_tag(:div, sub_content, sub_attributes)
20
+ end
21
+
22
+ private
23
+
24
+ def sub_content
25
+ safe_join([trigger, content_slot, content].compact)
26
+ end
27
+
28
+ def sub_attributes
29
+ attrs = {
30
+ class: cn("relative", class_name),
31
+ "data-shadcn--menubar-target": "sub"
32
+ }
33
+ attrs.merge!(html_options)
34
+ attrs.merge!(build_data)
35
+ attrs.compact
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Menubar Sub Content component
5
+ # Container for submenu items
6
+ class MenubarSubContentComponent < BaseComponent
7
+ BASE_CLASSES = "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
8
+
9
+ renders_many :items, lambda { |**options, &block|
10
+ MenubarItemComponent.new(**options, &block)
11
+ }
12
+ renders_many :separators, lambda { |**options|
13
+ MenubarSeparatorComponent.new(**options)
14
+ }
15
+
16
+ def call
17
+ content_tag(:div, sub_content, content_attributes)
18
+ end
19
+
20
+ private
21
+
22
+ def sub_content
23
+ if items.any? || separators.any?
24
+ safe_join([items, separators, content].flatten.compact)
25
+ else
26
+ content
27
+ end
28
+ end
29
+
30
+ def content_attributes
31
+ attrs = {
32
+ class: cn(BASE_CLASSES, class_name),
33
+ role: "menu",
34
+ "aria-orientation": "vertical",
35
+ "data-state": "closed",
36
+ "data-shadcn--menubar-target": "subContent",
37
+ "data-action": "mouseenter->shadcn--menubar#cancelCloseSubTimer mouseleave->shadcn--menubar#startCloseSubTimer",
38
+ hidden: true
39
+ }
40
+ attrs.merge!(html_options)
41
+ attrs.merge!(build_data)
42
+ attrs.compact
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Menubar Sub Trigger component
5
+ # Button that opens a submenu
6
+ class MenubarSubTriggerComponent < BaseComponent
7
+ BASE_CLASSES = "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground"
8
+
9
+ # @param inset [Boolean] Whether to add left padding
10
+ def initialize(inset: false, **options, &block)
11
+ super(**options, &block)
12
+ @inset = inset
13
+ end
14
+
15
+ def call
16
+ content_tag(:div, trigger_content, trigger_attributes)
17
+ end
18
+
19
+ private
20
+
21
+ def trigger_content
22
+ safe_join([content, chevron_icon])
23
+ end
24
+
25
+ def chevron_icon
26
+ content_tag(:svg, chevron_path, {
27
+ xmlns: "http://www.w3.org/2000/svg",
28
+ width: "16",
29
+ height: "16",
30
+ viewBox: "0 0 24 24",
31
+ fill: "none",
32
+ stroke: "currentColor",
33
+ "stroke-width": "2",
34
+ "stroke-linecap": "round",
35
+ "stroke-linejoin": "round",
36
+ class: "ml-auto h-4 w-4"
37
+ })
38
+ end
39
+
40
+ def chevron_path
41
+ content_tag(:polyline, "", points: "9 18 15 12 9 6")
42
+ end
43
+
44
+ def trigger_attributes
45
+ attrs = {
46
+ class: cn(BASE_CLASSES, @inset ? "pl-8" : "", class_name),
47
+ role: "menuitem",
48
+ "aria-haspopup": "menu",
49
+ "aria-expanded": "false",
50
+ "data-state": "closed",
51
+ "data-shadcn--menubar-target": "subTrigger",
52
+ "data-action": "mouseenter->shadcn--menubar#openSub mouseleave->shadcn--menubar#startCloseSubTimer"
53
+ }
54
+ attrs.merge!(html_options)
55
+ attrs.merge!(build_data)
56
+ attrs.compact
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Menubar Trigger component
5
+ # Button that opens the menu dropdown
6
+ class MenubarTriggerComponent < BaseComponent
7
+ BASE_CLASSES = "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground"
8
+
9
+ def call
10
+ content_tag(:button, content, trigger_attributes)
11
+ end
12
+
13
+ private
14
+
15
+ def trigger_attributes
16
+ attrs = {
17
+ class: cn(BASE_CLASSES, class_name),
18
+ type: "button",
19
+ role: "menuitem",
20
+ "aria-haspopup": "menu",
21
+ "aria-expanded": "false",
22
+ "data-state": "closed",
23
+ "data-shadcn--menubar-target": "trigger",
24
+ "data-action": "click->shadcn--menubar#toggle mouseenter->shadcn--menubar#hoverOpen"
25
+ }
26
+ attrs.merge!(html_options)
27
+ attrs.merge!(build_data)
28
+ attrs.compact
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Native Select component for styled native HTML selects
5
+ # Matches shadcn/ui Native Select component
6
+ #
7
+ # @example Basic select
8
+ # <%= render Shadcn::NativeSelectComponent.new(name: "country") do |select| %>
9
+ # <% select.with_option(value: "", disabled: true, selected: true) { "Select a country" } %>
10
+ # <% select.with_option(value: "us") { "United States" } %>
11
+ # <% select.with_option(value: "uk") { "United Kingdom" } %>
12
+ # <% select.with_option(value: "ca") { "Canada" } %>
13
+ # <% end %>
14
+ #
15
+ # @example With optgroups
16
+ # <%= render Shadcn::NativeSelectComponent.new(name: "car") do |select| %>
17
+ # <% select.with_optgroup(label: "Swedish Cars") do |group| %>
18
+ # <% group.with_option(value: "volvo") { "Volvo" } %>
19
+ # <% group.with_option(value: "saab") { "Saab" } %>
20
+ # <% end %>
21
+ # <% select.with_optgroup(label: "German Cars") do |group| %>
22
+ # <% group.with_option(value: "mercedes") { "Mercedes" } %>
23
+ # <% group.with_option(value: "audi") { "Audi" } %>
24
+ # <% end %>
25
+ # <% end %>
26
+ #
27
+ # @example Disabled
28
+ # <%= render Shadcn::NativeSelectComponent.new(name: "status", disabled: true) do |select| %>
29
+ # <% select.with_option(value: "active") { "Active" } %>
30
+ # <% end %>
31
+ #
32
+ class NativeSelectComponent < BaseComponent
33
+ # Select wrapper classes for positioning the chevron icon
34
+ WRAPPER_CLASSES = "relative"
35
+
36
+ # Native select element classes
37
+ SELECT_CLASSES = "h-9 w-full cursor-pointer appearance-none rounded-md border border-input bg-transparent pl-3 pr-8 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
38
+
39
+ # Chevron icon classes (positioned absolutely)
40
+ CHEVRON_CLASSES = "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
41
+
42
+ # Option slot
43
+ renders_many :options, "OptionComponent"
44
+
45
+ # Optgroup slot
46
+ renders_many :optgroups, "OptgroupComponent"
47
+
48
+ # @param name [String, nil] Select name attribute
49
+ # @param id [String, nil] Select ID attribute
50
+ # @param disabled [Boolean] Whether the select is disabled
51
+ # @param required [Boolean] Whether the select is required
52
+ def initialize(name: nil, id: nil, disabled: false, required: false, **options)
53
+ super(**options)
54
+ @name = name
55
+ @id = id
56
+ @disabled = disabled
57
+ @required = required
58
+ end
59
+
60
+ def call
61
+ tag.div(class: WRAPPER_CLASSES) do
62
+ safe_join([
63
+ tag.select(**select_attributes) do
64
+ if optgroups.any?
65
+ safe_join(optgroups)
66
+ else
67
+ safe_join(options)
68
+ end
69
+ end,
70
+ chevron_icon
71
+ ])
72
+ end
73
+ end
74
+
75
+ def chevron_icon
76
+ tag.svg(
77
+ xmlns: "http://www.w3.org/2000/svg",
78
+ width: "16",
79
+ height: "16",
80
+ viewBox: "0 0 24 24",
81
+ fill: "none",
82
+ stroke: "currentColor",
83
+ stroke_width: "2",
84
+ stroke_linecap: "round",
85
+ stroke_linejoin: "round",
86
+ class: CHEVRON_CLASSES
87
+ ) do
88
+ tag.path(d: "m6 9 6 6 6-6")
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def select_attributes
95
+ {
96
+ name: @name,
97
+ id: @id,
98
+ disabled: @disabled || nil,
99
+ required: @required || nil,
100
+ class: merge_classes(SELECT_CLASSES)
101
+ }.merge(html_options).merge(build_data).compact
102
+ end
103
+
104
+ # Option subcomponent
105
+ class OptionComponent < BaseComponent
106
+ # @param value [String] Option value
107
+ # @param disabled [Boolean] Whether the option is disabled
108
+ # @param selected [Boolean] Whether the option is selected
109
+ def initialize(value: nil, disabled: false, selected: false, **options)
110
+ super(**options)
111
+ @value = value
112
+ @disabled = disabled
113
+ @selected = selected
114
+ end
115
+
116
+ def call
117
+ tag.option(content, **option_attributes)
118
+ end
119
+
120
+ private
121
+
122
+ def option_attributes
123
+ {
124
+ value: @value,
125
+ disabled: @disabled || nil,
126
+ selected: @selected || nil
127
+ }.merge(html_options).merge(build_data).compact
128
+ end
129
+ end
130
+
131
+ # Optgroup subcomponent
132
+ class OptgroupComponent < BaseComponent
133
+ renders_many :options, OptionComponent
134
+
135
+ # @param label [String] Optgroup label
136
+ # @param disabled [Boolean] Whether the optgroup is disabled
137
+ def initialize(label:, disabled: false, **options)
138
+ super(**options)
139
+ @label = label
140
+ @disabled = disabled
141
+ end
142
+
143
+ def call
144
+ tag.optgroup(label: @label, disabled: @disabled || nil, **html_options.merge(build_data).compact) do
145
+ safe_join(options)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # NavigationMenu component
5
+ # Matches shadcn/ui NavigationMenu component
6
+ # A collection of links for navigating websites
7
+ #
8
+ # @example Basic navigation menu
9
+ # <%= render Shadcn::NavigationMenuComponent.new do |nav| %>
10
+ # <% nav.with_list do |list| %>
11
+ # <% list.with_item do |item| %>
12
+ # <% item.with_trigger { "Getting Started" } %>
13
+ # <% item.with_content do %>
14
+ # <ul class="grid gap-3 p-4 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
15
+ # <li class="row-span-3">Featured content</li>
16
+ # <li><a href="#">Introduction</a></li>
17
+ # <li><a href="#">Installation</a></li>
18
+ # </ul>
19
+ # <% end %>
20
+ # <% end %>
21
+ # <% list.with_item do |item| %>
22
+ # <% item.with_link(href: "/docs") { "Documentation" } %>
23
+ # <% end %>
24
+ # <% end %>
25
+ # <% end %>
26
+ #
27
+ class NavigationMenuComponent < BaseComponent
28
+ BASE_CLASSES = "relative z-10 flex max-w-max flex-1 items-center justify-center"
29
+
30
+ renders_one :list, lambda { |**options|
31
+ NavigationMenuListComponent.new(**options)
32
+ }
33
+
34
+ def call
35
+ content_tag(:nav, navigation_content, navigation_attributes)
36
+ end
37
+
38
+ private
39
+
40
+ def navigation_content
41
+ safe_join([list, viewport].compact)
42
+ end
43
+
44
+ def viewport
45
+ content_tag(:div, viewport_inner, class: "absolute left-0 top-full flex justify-center")
46
+ end
47
+
48
+ def viewport_inner
49
+ content_tag(:div, "", viewport_attributes)
50
+ end
51
+
52
+ def viewport_attributes
53
+ {
54
+ class: cn(
55
+ "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow",
56
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90",
57
+ "md:w-[var(--radix-navigation-menu-viewport-width)]"
58
+ ),
59
+ "data-shadcn--navigation-menu-target": "viewport",
60
+ "data-state": "closed",
61
+ hidden: true
62
+ }
63
+ end
64
+
65
+ def navigation_attributes
66
+ attrs = {
67
+ class: cn(BASE_CLASSES, class_name),
68
+ "data-controller": "shadcn--navigation-menu",
69
+ "aria-label": "Main"
70
+ }
71
+ attrs.merge!(html_options)
72
+ attrs.merge!(build_data)
73
+ attrs.compact
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Content area that appears when a navigation trigger is activated
5
+ class NavigationMenuContentComponent < BaseComponent
6
+ CONTENT_CLASSES = [
7
+ "left-0 top-0 w-full",
8
+ "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out",
9
+ "data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out",
10
+ "data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52",
11
+ "data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52",
12
+ "md:absolute md:w-auto"
13
+ ].join(" ").freeze
14
+
15
+ def call
16
+ content_tag(:div, content, content_attributes)
17
+ end
18
+
19
+ private
20
+
21
+ def content_attributes
22
+ {
23
+ class: cn(CONTENT_CLASSES, class_name),
24
+ "data-shadcn--navigation-menu-target": "content",
25
+ "data-state": "closed",
26
+ hidden: true
27
+ }.merge(html_options).compact
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Individual item in the navigation menu
5
+ class NavigationMenuItemComponent < BaseComponent
6
+ renders_one :trigger, lambda { |**options, &block|
7
+ NavigationMenuTriggerComponent.new(**options, &block)
8
+ }
9
+
10
+ renders_one :dropdown, lambda { |**options, &block|
11
+ NavigationMenuContentComponent.new(**options, &block)
12
+ }
13
+
14
+ renders_one :link, lambda { |href:, active: false, **options, &block|
15
+ NavigationMenuLinkComponent.new(href: href, active: active, **options, &block)
16
+ }
17
+
18
+ def call
19
+ content_tag(:li, item_content, item_attributes)
20
+ end
21
+
22
+ private
23
+
24
+ def item_content
25
+ if link
26
+ link
27
+ else
28
+ safe_join([trigger, dropdown].compact)
29
+ end
30
+ end
31
+
32
+ def item_attributes
33
+ {
34
+ class: cn("relative", class_name),
35
+ "data-shadcn--navigation-menu-target": "item"
36
+ }.merge(html_options).compact
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # A link within the navigation menu (for items without dropdowns)
5
+ class NavigationMenuLinkComponent < BaseComponent
6
+ LINK_CLASSES = [
7
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2",
8
+ "text-sm font-medium transition-colors",
9
+ "hover:bg-accent hover:text-accent-foreground",
10
+ "focus:bg-accent focus:text-accent-foreground focus:outline-none",
11
+ "disabled:pointer-events-none disabled:opacity-50"
12
+ ].join(" ").freeze
13
+
14
+ ACTIVE_CLASSES = "bg-accent/50"
15
+
16
+ # @param href [String] The link destination
17
+ # @param active [Boolean] Whether this link is currently active
18
+ def initialize(href:, active: false, **options)
19
+ super(**options)
20
+ @href = href
21
+ @active = active
22
+ end
23
+
24
+ def call
25
+ content_tag(:a, content, link_attributes)
26
+ end
27
+
28
+ private
29
+
30
+ def link_attributes
31
+ {
32
+ class: cn(LINK_CLASSES, @active && ACTIVE_CLASSES, class_name),
33
+ href: @href,
34
+ "data-active": @active ? "true" : nil
35
+ }.merge(html_options).compact
36
+ end
37
+ end
38
+ end