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,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Radio Group component for selecting one option from a set
5
+ # Uses native <input type="radio"> elements styled with CSS
6
+ # Works without JavaScript for progressive enhancement
7
+ #
8
+ # @example Data-driven usage (Tier 1)
9
+ # <%= render Shadcn::RadioGroupComponent.new(
10
+ # name: "plan",
11
+ # value: "pro",
12
+ # items: [
13
+ # { value: "free", label: "Free" },
14
+ # { value: "pro", label: "Pro" },
15
+ # { value: "enterprise", label: "Enterprise", disabled: true }
16
+ # ]
17
+ # ) %>
18
+ #
19
+ # @example Simple DSL with labels (Tier 2)
20
+ # <%= render Shadcn::RadioGroupComponent.new(name: "plan") do |group| %>
21
+ # <%= group.with_item(value: "free", label: "Free") %>
22
+ # <%= group.with_item(value: "pro", label: "Pro") %>
23
+ # <% end %>
24
+ #
25
+ # @example With separate labels (backward compatible)
26
+ # <%= render Shadcn::RadioGroupComponent.new(name: "plan") do |group| %>
27
+ # <div class="flex items-center space-x-2">
28
+ # <%= group.with_item(value: "free", id: "free") %>
29
+ # <%= render Shadcn::LabelComponent.new(for: "free") { "Free" } %>
30
+ # </div>
31
+ # <% end %>
32
+ #
33
+ class RadioGroupComponent < BaseComponent
34
+ BASE_CLASSES = "grid gap-3"
35
+
36
+ renders_many :items, ->(value:, id: nil, label: nil, disabled: false, **options, &block) do
37
+ RadioGroupItemComponent.new(
38
+ value: value,
39
+ id: id || generate_item_id(value),
40
+ label: label,
41
+ disabled: @disabled || disabled,
42
+ group_name: @name,
43
+ selected: @value.to_s == value.to_s,
44
+ **options,
45
+ &block
46
+ )
47
+ end
48
+
49
+ # @param name [String] Input name attribute for form submission
50
+ # @param value [String, nil] Currently selected value
51
+ # @param items [Array<Hash>] Array of item hashes with :value, :label, :disabled keys
52
+ # @param disabled [Boolean] Whether the entire group is disabled
53
+ # @param required [Boolean] Whether a selection is required
54
+ # @param orientation [Symbol] :vertical or :horizontal layout
55
+ def initialize(
56
+ name:,
57
+ value: nil,
58
+ items: [],
59
+ disabled: false,
60
+ required: false,
61
+ orientation: :vertical,
62
+ **options
63
+ )
64
+ super(**options)
65
+ @name = name
66
+ @value = value
67
+ @items_data = items
68
+ @disabled = disabled
69
+ @required = required
70
+ @orientation = orientation
71
+ end
72
+
73
+ def call
74
+ content_tag(:div, group_attributes) do
75
+ safe_join([render_data_items, render_slot_items].compact)
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ # Render items from the items: data array
82
+ def render_data_items
83
+ return nil if @items_data.empty?
84
+
85
+ safe_join(@items_data.map do |item|
86
+ RadioGroupItemComponent.new(
87
+ value: item[:value],
88
+ id: generate_item_id(item[:value]),
89
+ label: item[:label],
90
+ disabled: @disabled || item[:disabled],
91
+ group_name: @name,
92
+ selected: @value.to_s == item[:value].to_s
93
+ ).render_in(view_context)
94
+ end)
95
+ end
96
+
97
+ # Render items from with_item slot calls
98
+ def render_slot_items
99
+ return nil if items.empty?
100
+
101
+ safe_join(items.map(&:to_s))
102
+ end
103
+
104
+ def generate_item_id(value)
105
+ "#{@name}-#{value}".parameterize
106
+ end
107
+
108
+ def group_attributes
109
+ attrs = {
110
+ role: "radiogroup",
111
+ class: cn(BASE_CLASSES, orientation_classes, class_name),
112
+ "aria-required": @required ? "true" : nil,
113
+ "aria-disabled": @disabled ? "true" : nil
114
+ }
115
+ attrs.merge!(html_options)
116
+ attrs.merge!(build_data)
117
+ attrs.compact
118
+ end
119
+
120
+ def orientation_classes
121
+ case @orientation
122
+ when :horizontal
123
+ "flex flex-row gap-4"
124
+ else
125
+ "grid gap-3"
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Individual radio button item using native <input type="radio">
5
+ # Styled with CSS to match shadcn/ui design
6
+ # Works without JavaScript
7
+ #
8
+ # @example With label parameter (Tier 2 API)
9
+ # <%= group.with_item(value: "free", label: "Free") %>
10
+ #
11
+ # @example With block content (backward compatible)
12
+ # <%= group.with_item(value: "free") { "Free" } %>
13
+ #
14
+ # @example Without label (for external labels)
15
+ # <%= group.with_item(value: "free", id: "plan-free") %>
16
+ #
17
+ class RadioGroupItemComponent < BaseComponent
18
+ # CSS classes for the native radio input styled as a custom circle
19
+ ITEM_CLASSES = [
20
+ # Reset native appearance
21
+ "appearance-none",
22
+ # Size and shape
23
+ "aspect-square h-4 w-4 shrink-0 rounded-full",
24
+ # Border and colors
25
+ "border border-primary",
26
+ # Ring focus style
27
+ "focus:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1",
28
+ # Disabled state
29
+ "disabled:cursor-not-allowed disabled:opacity-50",
30
+ # Custom checked indicator using CSS
31
+ "relative",
32
+ # Checked state - show inner circle
33
+ "checked:bg-primary",
34
+ # Transition for smooth state changes
35
+ "transition-colors"
36
+ ].join(" ")
37
+
38
+ LABEL_CLASSES = "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
39
+
40
+ # @param value [String] The value for this radio option
41
+ # @param id [String, nil] HTML id attribute
42
+ # @param label [String, nil] Label text (alternative to block content)
43
+ # @param disabled [Boolean] Whether this option is disabled
44
+ # @param group_name [String, nil] The name attribute from parent group
45
+ # @param selected [Boolean] Whether this option is selected
46
+ def initialize(
47
+ value:,
48
+ id: nil,
49
+ label: nil,
50
+ disabled: false,
51
+ group_name: nil,
52
+ selected: false,
53
+ **options,
54
+ &block
55
+ )
56
+ super(**options, &block)
57
+ @value = value
58
+ @id = id || "radio-#{value}"
59
+ @label = label
60
+ @disabled = disabled
61
+ @group_name = group_name
62
+ @selected = selected
63
+ end
64
+
65
+ def call
66
+ label_text = @label || content.presence
67
+
68
+ if label_text.present?
69
+ # Render with integrated label
70
+ content_tag(:label, label_wrapper_attributes) do
71
+ safe_join([
72
+ radio_input,
73
+ content_tag(:span, label_text, class: LABEL_CLASSES)
74
+ ])
75
+ end
76
+ else
77
+ # Render just the radio input (for use with external labels)
78
+ radio_input
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def radio_input
85
+ tag(:input, input_attributes)
86
+ end
87
+
88
+ def input_attributes
89
+ attrs = {
90
+ type: "radio",
91
+ name: @group_name,
92
+ value: @value,
93
+ id: @id,
94
+ class: cn(ITEM_CLASSES, "peer", class_name),
95
+ disabled: @disabled || nil,
96
+ checked: @selected || nil
97
+ }
98
+ attrs.merge!(html_options.except(:class))
99
+ attrs.compact
100
+ end
101
+
102
+ def label_wrapper_attributes
103
+ {
104
+ class: "flex items-center space-x-2 cursor-pointer",
105
+ for: @id
106
+ }
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Draggable handle between resizable panels
5
+ class ResizableHandleComponent < BaseComponent
6
+ HORIZONTAL_CLASSES = "w-px bg-border cursor-col-resize hover:bg-primary/50 transition-colors"
7
+ VERTICAL_CLASSES = "h-px bg-border cursor-row-resize hover:bg-primary/50 transition-colors"
8
+
9
+ # @param with_handle [Boolean] Whether to show a visual handle indicator
10
+ # @param direction [Symbol] Direction passed from parent group
11
+ def initialize(with_handle: false, direction: :horizontal, **options)
12
+ super(**options)
13
+ @with_handle = with_handle
14
+ @direction = direction
15
+ end
16
+
17
+ def call
18
+ content_tag(:div, handle_content, handle_attributes)
19
+ end
20
+
21
+ private
22
+
23
+ def handle_content
24
+ return nil unless @with_handle
25
+
26
+ content_tag(:div, grip_icon, handle_indicator_attributes)
27
+ end
28
+
29
+ def handle_attributes
30
+ {
31
+ class: class_names,
32
+ "data-panel-resize-handle": "",
33
+ "data-shadcn--resizable-target": "handle",
34
+ "data-action": "mousedown->shadcn--resizable#startResize touchstart->shadcn--resizable#startResize",
35
+ role: "separator",
36
+ tabindex: "0",
37
+ "aria-orientation": @direction == :horizontal ? "vertical" : "horizontal"
38
+ }.merge(html_options).compact
39
+ end
40
+
41
+ def handle_indicator_attributes
42
+ {
43
+ class: cn(
44
+ "z-10 flex items-center justify-center rounded-sm border bg-border",
45
+ @direction == :horizontal ? "h-4 w-3" : "w-4 h-3"
46
+ )
47
+ }
48
+ end
49
+
50
+ def grip_icon
51
+ if @direction == :horizontal
52
+ # Vertical grip dots for horizontal resize
53
+ content_tag(:svg, grip_dots_vertical, xmlns: "http://www.w3.org/2000/svg", width: "10", height: "16", viewBox: "0 0 10 16", fill: "currentColor", class: "h-2.5 w-2.5")
54
+ else
55
+ # Horizontal grip dots for vertical resize
56
+ content_tag(:svg, grip_dots_horizontal, xmlns: "http://www.w3.org/2000/svg", width: "16", height: "10", viewBox: "0 0 16 10", fill: "currentColor", class: "h-2.5 w-2.5")
57
+ end
58
+ end
59
+
60
+ def grip_dots_vertical
61
+ # GripVertical icon - 6 dots in 2 columns
62
+ safe_join([
63
+ content_tag(:circle, nil, cx: "3", cy: "2", r: "1"),
64
+ content_tag(:circle, nil, cx: "7", cy: "2", r: "1"),
65
+ content_tag(:circle, nil, cx: "3", cy: "8", r: "1"),
66
+ content_tag(:circle, nil, cx: "7", cy: "8", r: "1"),
67
+ content_tag(:circle, nil, cx: "3", cy: "14", r: "1"),
68
+ content_tag(:circle, nil, cx: "7", cy: "14", r: "1")
69
+ ])
70
+ end
71
+
72
+ def grip_dots_horizontal
73
+ # GripHorizontal icon - 6 dots in 2 rows
74
+ safe_join([
75
+ content_tag(:circle, nil, cx: "2", cy: "3", r: "1"),
76
+ content_tag(:circle, nil, cx: "8", cy: "3", r: "1"),
77
+ content_tag(:circle, nil, cx: "14", cy: "3", r: "1"),
78
+ content_tag(:circle, nil, cx: "2", cy: "7", r: "1"),
79
+ content_tag(:circle, nil, cx: "8", cy: "7", r: "1"),
80
+ content_tag(:circle, nil, cx: "14", cy: "7", r: "1")
81
+ ])
82
+ end
83
+
84
+ def class_names
85
+ base = @direction == :horizontal ? HORIZONTAL_CLASSES : VERTICAL_CLASSES
86
+ cn(
87
+ "relative flex items-center justify-center",
88
+ base,
89
+ @with_handle && (@direction == :horizontal ? "w-2" : "h-2"),
90
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1",
91
+ "after:absolute",
92
+ @direction == :horizontal ? "after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2" : "after:inset-x-0 after:top-1/2 after:h-1 after:-translate-y-1/2",
93
+ "[&[data-state=dragging]]:bg-primary",
94
+ @class_name
95
+ )
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Individual panel within a ResizablePanelGroup
5
+ class ResizablePanelComponent < BaseComponent
6
+ BASE_CLASSES = "relative"
7
+
8
+ # @param default_size [Integer] Initial size percentage (0-100)
9
+ # @param min_size [Integer] Minimum size percentage
10
+ # @param max_size [Integer] Maximum size percentage
11
+ # @param direction [Symbol] Direction passed from parent group
12
+ def initialize(default_size: nil, min_size: nil, max_size: nil, direction: :horizontal, **options)
13
+ super(**options)
14
+ @default_size = default_size
15
+ @min_size = min_size
16
+ @max_size = max_size
17
+ @direction = direction
18
+ end
19
+
20
+ def call
21
+ content_tag(:div, content, panel_attributes)
22
+ end
23
+
24
+ private
25
+
26
+ def panel_attributes
27
+ {
28
+ class: class_names,
29
+ style: panel_style,
30
+ "data-panel": "",
31
+ "data-panel-size": @default_size,
32
+ "data-shadcn--resizable-target": "panel",
33
+ "data-min-size": @min_size,
34
+ "data-max-size": @max_size
35
+ }.merge(html_options).compact
36
+ end
37
+
38
+ def panel_style
39
+ return unless @default_size
40
+
41
+ if @direction == :horizontal
42
+ "flex-basis: #{@default_size}%; flex-grow: 0; flex-shrink: 0;"
43
+ else
44
+ "flex-basis: #{@default_size}%; flex-grow: 0; flex-shrink: 0;"
45
+ end
46
+ end
47
+
48
+ def class_names
49
+ cn(
50
+ BASE_CLASSES,
51
+ "overflow-hidden",
52
+ @class_name
53
+ )
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # ResizablePanelGroup component for creating resizable panel layouts
5
+ # Matches shadcn/ui Resizable component
6
+ # Uses Stimulus for interactivity
7
+ #
8
+ # @example Horizontal layout
9
+ # <%= render Shadcn::ResizablePanelGroupComponent.new(direction: :horizontal) do |group| %>
10
+ # <% group.with_panel(default_size: 50) do %>
11
+ # <div class="flex h-full items-center justify-center p-6">
12
+ # <span class="font-semibold">One</span>
13
+ # </div>
14
+ # <% end %>
15
+ # <% group.with_handle %>
16
+ # <% group.with_panel(default_size: 50) do %>
17
+ # <div class="flex h-full items-center justify-center p-6">
18
+ # <span class="font-semibold">Two</span>
19
+ # </div>
20
+ # <% end %>
21
+ # <% end %>
22
+ #
23
+ # @example Vertical layout
24
+ # <%= render Shadcn::ResizablePanelGroupComponent.new(direction: :vertical) do |group| %>
25
+ # <% group.with_panel(default_size: 25) do %>
26
+ # Header
27
+ # <% end %>
28
+ # <% group.with_handle %>
29
+ # <% group.with_panel(default_size: 75) do %>
30
+ # Content
31
+ # <% end %>
32
+ # <% end %>
33
+ #
34
+ class ResizablePanelGroupComponent < BaseComponent
35
+ renders_many :panels, lambda { |default_size: nil, min_size: nil, max_size: nil, **options|
36
+ ResizablePanelComponent.new(
37
+ default_size: default_size,
38
+ min_size: min_size,
39
+ max_size: max_size,
40
+ direction: @direction,
41
+ **options
42
+ )
43
+ }
44
+
45
+ renders_many :handles, lambda { |with_handle: false, **options|
46
+ ResizableHandleComponent.new(
47
+ with_handle: with_handle,
48
+ direction: @direction,
49
+ **options
50
+ )
51
+ }
52
+
53
+ DIRECTIONS = {
54
+ horizontal: "flex h-full",
55
+ vertical: "flex flex-col"
56
+ }.freeze
57
+
58
+ # @param direction [Symbol] Direction of the panels (:horizontal, :vertical)
59
+ # @param auto_save_id [String] ID for persisting panel sizes to localStorage
60
+ def initialize(direction: :horizontal, auto_save_id: nil, **options)
61
+ super(**options)
62
+ @direction = direction.to_sym
63
+ @auto_save_id = auto_save_id
64
+ end
65
+
66
+ def call
67
+ content_tag(:div, group_content, group_attributes)
68
+ end
69
+
70
+ private
71
+
72
+ def group_content
73
+ content
74
+ end
75
+
76
+ def group_attributes
77
+ {
78
+ class: class_names,
79
+ "data-controller": "shadcn--resizable",
80
+ "data-shadcn--resizable-direction-value": @direction.to_s,
81
+ "data-shadcn--resizable-auto-save-id-value": @auto_save_id,
82
+ "data-panel-group": "",
83
+ "data-panel-group-direction": @direction.to_s
84
+ }.merge(html_options).compact
85
+ end
86
+
87
+ def class_names
88
+ cn(
89
+ DIRECTIONS[@direction],
90
+ @class_name
91
+ )
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Scroll Area component for custom scrollbars
5
+ # Matches shadcn/ui ScrollArea component
6
+ # Uses Stimulus for interactivity
7
+ #
8
+ # @example Basic scroll area
9
+ # <%= render Shadcn::ScrollAreaComponent.new(class_name: "h-72 w-48") do %>
10
+ # <div class="p-4">
11
+ # Long content here...
12
+ # </div>
13
+ # <% end %>
14
+ #
15
+ # @example Horizontal scroll
16
+ # <%= render Shadcn::ScrollAreaComponent.new(orientation: :horizontal, class_name: "w-96 whitespace-nowrap") do %>
17
+ # <div class="flex space-x-4 p-4">
18
+ # <!-- items -->
19
+ # </div>
20
+ # <% end %>
21
+ #
22
+ class ScrollAreaComponent < BaseComponent
23
+ BASE_CLASSES = "relative overflow-hidden"
24
+ VIEWPORT_CLASSES = "h-full w-full rounded-[inherit]"
25
+ SCROLLBAR_CLASSES = "flex touch-none select-none transition-colors"
26
+ THUMB_CLASSES = "relative flex-1 rounded-full bg-border"
27
+
28
+ # @param orientation [Symbol] Scroll orientation (:vertical, :horizontal, :both)
29
+ # @param type [Symbol] Scrollbar type (:auto, :always, :scroll, :hover)
30
+ def initialize(orientation: :vertical, type: :hover, **options)
31
+ super(**options)
32
+ @orientation = orientation.to_sym
33
+ @type = type.to_sym
34
+ end
35
+
36
+ def call
37
+ content_tag(:div, scroll_structure, scroll_attributes)
38
+ end
39
+
40
+ private
41
+
42
+ def scroll_structure
43
+ safe_join([
44
+ viewport,
45
+ scrollbars,
46
+ corner
47
+ ])
48
+ end
49
+
50
+ def viewport
51
+ content_tag(:div, content, {
52
+ class: cn(VIEWPORT_CLASSES, @orientation == :horizontal ? "overflow-x-auto" : "overflow-y-auto"),
53
+ style: "overflow: hidden scroll;",
54
+ "data-shadcn--scroll-area-target": "viewport"
55
+ })
56
+ end
57
+
58
+ def scrollbars
59
+ case @orientation
60
+ when :both
61
+ safe_join([vertical_scrollbar, horizontal_scrollbar])
62
+ when :horizontal
63
+ horizontal_scrollbar
64
+ else
65
+ vertical_scrollbar
66
+ end
67
+ end
68
+
69
+ def vertical_scrollbar
70
+ content_tag(:div, scrollbar_thumb, {
71
+ class: cn(SCROLLBAR_CLASSES, "h-full w-2.5 border-l border-l-transparent p-[1px]"),
72
+ "data-orientation": "vertical",
73
+ "data-shadcn--scroll-area-target": "scrollbar"
74
+ })
75
+ end
76
+
77
+ def horizontal_scrollbar
78
+ content_tag(:div, scrollbar_thumb, {
79
+ class: cn(SCROLLBAR_CLASSES, "h-2.5 flex-col border-t border-t-transparent p-[1px]"),
80
+ "data-orientation": "horizontal",
81
+ "data-shadcn--scroll-area-target": "scrollbar"
82
+ })
83
+ end
84
+
85
+ def scrollbar_thumb
86
+ content_tag(:div, "", {
87
+ class: THUMB_CLASSES,
88
+ "data-shadcn--scroll-area-target": "thumb"
89
+ })
90
+ end
91
+
92
+ def corner
93
+ return unless @orientation == :both
94
+
95
+ content_tag(:div, "", class: "absolute right-0 bottom-0 h-2.5 w-2.5 bg-transparent")
96
+ end
97
+
98
+ def scroll_attributes
99
+ attrs = {
100
+ class: merge_classes(BASE_CLASSES),
101
+ "data-controller": "shadcn--scroll-area",
102
+ "data-shadcn--scroll-area-orientation-value": @orientation.to_s,
103
+ "data-shadcn--scroll-area-type-value": @type.to_s
104
+ }
105
+ attrs.merge!(html_options)
106
+ attrs.merge!(build_data)
107
+ attrs.compact
108
+ end
109
+ end
110
+ end