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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Aspect Ratio component for maintaining content proportions
5
+ # Matches shadcn/ui AspectRatio component
6
+ #
7
+ # @example 16:9 video ratio
8
+ # <%= render Shadcn::AspectRatioComponent.new(ratio: 16.0/9.0) do %>
9
+ # <img src="image.jpg" class="h-full w-full object-cover" />
10
+ # <% end %>
11
+ #
12
+ # @example Square ratio
13
+ # <%= render Shadcn::AspectRatioComponent.new(ratio: 1) do %>
14
+ # <img src="square.jpg" />
15
+ # <% end %>
16
+ #
17
+ class AspectRatioComponent < BaseComponent
18
+ # @param ratio [Float] Aspect ratio (width/height, e.g. 16.0/9.0 for widescreen)
19
+ def initialize(ratio: 1, **options)
20
+ super(**options)
21
+ @ratio = ratio.to_f
22
+ end
23
+
24
+ def call
25
+ content_tag(:div, wrapper_attributes) do
26
+ content_tag(:div, content, inner_attributes)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def wrapper_attributes
33
+ attrs = {
34
+ class: cn("relative w-full", class_name),
35
+ style: "padding-bottom: #{(1.0 / @ratio) * 100}%;"
36
+ }
37
+ attrs.merge!(html_options)
38
+ attrs.merge!(build_data)
39
+ attrs.compact
40
+ end
41
+
42
+ def inner_attributes
43
+ {
44
+ class: "absolute inset-0",
45
+ style: "position: absolute; top: 0; right: 0; bottom: 0; left: 0;"
46
+ }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Avatar component for user profile images
5
+ # Matches shadcn/ui Avatar component
6
+ #
7
+ # @example Basic avatar with image
8
+ # <%= render Shadcn::AvatarComponent.new(src: user.avatar_url, alt: user.name) %>
9
+ #
10
+ # @example With fallback
11
+ # <%= render Shadcn::AvatarComponent.new(src: user.avatar_url, alt: user.name, fallback: user.initials) %>
12
+ #
13
+ # @example Different sizes
14
+ # <%= render Shadcn::AvatarComponent.new(src: url, alt: name, size: :sm) %>
15
+ # <%= render Shadcn::AvatarComponent.new(src: url, alt: name, size: :lg) %>
16
+ #
17
+ # @example With slot-based fallback
18
+ # <%= render Shadcn::AvatarComponent.new(size: :sm) do |avatar| %>
19
+ # <% avatar.with_fallback { "JD" } %>
20
+ # <% end %>
21
+ #
22
+ class AvatarComponent < BaseComponent
23
+ renders_one :fallback, ->(class: nil, **options, &block) {
24
+ AvatarFallbackComponent.new(class: binding.local_variable_get(:class), **options, &block)
25
+ }
26
+ SIZES = {
27
+ sm: "h-8 w-8 text-xs",
28
+ default: "h-10 w-10 text-sm",
29
+ lg: "h-12 w-12 text-base",
30
+ xl: "h-16 w-16 text-lg"
31
+ }.freeze
32
+
33
+ BASE_CLASSES = "relative flex shrink-0 overflow-hidden rounded-full"
34
+ IMAGE_CLASSES = "aspect-square h-full w-full"
35
+ FALLBACK_CLASSES = "flex h-full w-full items-center justify-center rounded-full bg-muted"
36
+
37
+ # @param src [String, nil] Image URL
38
+ # @param alt [String] Alt text for the image
39
+ # @param fallback [String, nil] Fallback text when image fails to load
40
+ # @param size [Symbol] Avatar size (:sm, :default, :lg, :xl)
41
+ def initialize(src: nil, alt: "", fallback: nil, size: :default, **options)
42
+ super(**options)
43
+ @src = src
44
+ @alt = alt
45
+ @fallback = fallback || generate_fallback(alt)
46
+ @size = size.to_sym
47
+ end
48
+
49
+ def call
50
+ content_tag(:span, avatar_content, avatar_attributes)
51
+ end
52
+
53
+ private
54
+
55
+ def avatar_content
56
+ if @src.present?
57
+ image_with_fallback
58
+ elsif fallback?
59
+ fallback
60
+ else
61
+ fallback_element
62
+ end
63
+ end
64
+
65
+ def image_with_fallback
66
+ # Use Stimulus controller to handle image loading errors
67
+ content_tag(:span, class: "contents", data: stimulus_data(controller: "shadcn--avatar")) do
68
+ safe_join([
69
+ tag(:img,
70
+ src: @src,
71
+ alt: @alt,
72
+ class: IMAGE_CLASSES,
73
+ data: { "shadcn--avatar-target": "image", action: "error->shadcn--avatar#handleError" }
74
+ ),
75
+ content_tag(:span, @fallback, class: "#{FALLBACK_CLASSES} hidden", data: { "shadcn--avatar-target": "fallback" })
76
+ ])
77
+ end
78
+ end
79
+
80
+ def fallback_element
81
+ content_tag(:span, @fallback, class: FALLBACK_CLASSES)
82
+ end
83
+
84
+ def avatar_classes
85
+ cn(BASE_CLASSES, SIZES[@size], class_name)
86
+ end
87
+
88
+ def avatar_attributes
89
+ attrs = { class: avatar_classes }
90
+ attrs.merge!(html_options)
91
+ attrs.merge!(build_data)
92
+ attrs.compact
93
+ end
94
+
95
+ def generate_fallback(alt)
96
+ return "" if alt.blank?
97
+
98
+ # Generate initials from name
99
+ words = alt.split
100
+ if words.length >= 2
101
+ "#{words.first[0]}#{words.last[0]}".upcase
102
+ else
103
+ alt[0..1].upcase
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Avatar Fallback component for initials or placeholder
5
+ class AvatarFallbackComponent < BaseComponent
6
+ FALLBACK_CLASSES = "flex h-full w-full items-center justify-center rounded-full bg-muted"
7
+
8
+ def initialize(class: nil, **options, &block)
9
+ super(**options, &block)
10
+ @custom_class = binding.local_variable_get(:class)
11
+ end
12
+
13
+ def call
14
+ content_tag(:span, content, class: cn(FALLBACK_CLASSES, @custom_class))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Badge component for labels and status indicators
5
+ # Matches shadcn/ui Badge component
6
+ #
7
+ # @example Basic badge
8
+ # <%= render Shadcn::BadgeComponent.new { "New" } %>
9
+ #
10
+ # @example Variant badges
11
+ # <%= render Shadcn::BadgeComponent.new(variant: :secondary) { "Draft" } %>
12
+ # <%= render Shadcn::BadgeComponent.new(variant: :destructive) { "Error" } %>
13
+ # <%= render Shadcn::BadgeComponent.new(variant: :outline) { "v1.0.0" } %>
14
+ #
15
+ class BadgeComponent < BaseComponent
16
+ # Available badge variants
17
+ VARIANTS = {
18
+ default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
19
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
20
+ destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
21
+ outline: "text-foreground"
22
+ }.freeze
23
+
24
+ BASE_CLASSES = "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
25
+
26
+ # @param variant [Symbol] Badge style variant (:default, :secondary, :destructive, :outline)
27
+ def initialize(variant: :default, **options)
28
+ super(**options)
29
+ @variant = variant.to_sym
30
+ end
31
+
32
+ def call
33
+ content_tag(:span, content, badge_attributes)
34
+ end
35
+
36
+ private
37
+
38
+ def badge_classes
39
+ cn(BASE_CLASSES, VARIANTS[@variant], class_name)
40
+ end
41
+
42
+ def badge_attributes
43
+ attrs = { class: badge_classes }
44
+ attrs.merge!(html_options)
45
+ attrs.merge!(build_data)
46
+ attrs.compact
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Base class for all shadcn ViewComponents
5
+ # Provides common functionality like class merging and data attribute handling
6
+ class BaseComponent < ViewComponent::Base
7
+ # Include class name helper for cn() method
8
+ include Shadcn::Rails::Helpers::ClassNameHelper
9
+
10
+ # Common attributes shared by all components
11
+ attr_reader :class_name, :data, :html_options
12
+
13
+ # @param class_name [String, nil] Additional CSS classes
14
+ # @param data [Hash] Data attributes (will be prefixed with data-)
15
+ # @param html_options [Hash] Additional HTML attributes
16
+ def initialize(class_name: nil, data: {}, **html_options, &block)
17
+ @class_name = class_name
18
+ @data = data
19
+ @html_options = html_options
20
+ @constructor_block = block
21
+ end
22
+
23
+ # Override content to support blocks passed to new()
24
+ # This allows both syntaxes:
25
+ # render Component.new { "text" } # block to new()
26
+ # render Component.new do %>text<% end # block to render()
27
+ # Note: Only calls blocks with arity 0 (no arguments).
28
+ # Blocks expecting arguments (like slot blocks) are handled by ViewComponent.
29
+ def content
30
+ return super if super.present?
31
+ return @constructor_block.call if @constructor_block && @constructor_block.arity == 0
32
+
33
+ nil
34
+ end
35
+
36
+ private
37
+
38
+ # Merge default classes with user-provided classes
39
+ # @param default_classes [String] Default component classes
40
+ # @return [String] Merged class string
41
+ def merge_classes(default_classes)
42
+ cn(default_classes, class_name)
43
+ end
44
+
45
+ # Build data attributes hash
46
+ # Converts Ruby-style keys to HTML data attributes
47
+ # @param additional_data [Hash] Additional data attributes to merge
48
+ # @return [Hash] Merged data attributes
49
+ def build_data(additional_data = {})
50
+ merged = data.merge(additional_data)
51
+ merged.transform_keys { |key| "data-#{key.to_s.dasherize}" }
52
+ end
53
+
54
+ # Build the complete HTML attributes hash
55
+ # @param default_classes [String] Default component classes
56
+ # @param additional_data [Hash] Additional data attributes
57
+ # @return [Hash] Complete HTML attributes
58
+ def build_html_attributes(default_classes, additional_data = {})
59
+ attrs = html_options.dup
60
+ attrs[:class] = merge_classes(default_classes)
61
+ attrs.merge!(build_data(additional_data))
62
+ attrs
63
+ end
64
+
65
+ # Helper to build Stimulus controller data attributes
66
+ # @param controller [String] Stimulus controller name
67
+ # @param values [Hash] Controller values
68
+ # @param actions [Hash] Controller actions
69
+ # @return [Hash] Stimulus data attributes
70
+ def stimulus_data(controller:, values: {}, actions: {}, targets: {})
71
+ data = { controller: controller }
72
+
73
+ values.each do |key, value|
74
+ data[:"#{controller}-#{key}-value"] = value
75
+ end
76
+
77
+ actions.each do |event, action|
78
+ data[:action] = [data[:action], "#{event}->#{controller}##{action}"].compact.join(" ")
79
+ end
80
+
81
+ targets.each do |name, _|
82
+ data[:"#{controller}-target"] = name
83
+ end
84
+
85
+ data
86
+ end
87
+
88
+ # Access configuration
89
+ def config
90
+ Shadcn::Rails.configuration
91
+ end
92
+
93
+ # Add prefix to Tailwind classes if configured
94
+ def prefix_classes(classes)
95
+ return classes if config.tailwind_prefix.blank?
96
+
97
+ classes.split.map { |c| "#{config.tailwind_prefix}#{c}" }.join(" ")
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Breadcrumb component for navigation hierarchy
5
+ # Matches shadcn/ui Breadcrumb component
6
+ #
7
+ # @example Basic usage
8
+ # <%= render Shadcn::BreadcrumbComponent.new do |breadcrumb| %>
9
+ # <% breadcrumb.with_item(href: "/") { "Home" } %>
10
+ # <% breadcrumb.with_item(href: "/products") { "Products" } %>
11
+ # <% breadcrumb.with_item(current: true) { "Widget" } %>
12
+ # <% end %>
13
+ #
14
+ class BreadcrumbComponent < BaseComponent
15
+ renders_many :items, ->(href: nil, current: false, **options, &block) do
16
+ BreadcrumbItemComponent.new(
17
+ href: href,
18
+ current: current,
19
+ **options,
20
+ &block
21
+ )
22
+ end
23
+
24
+ def call
25
+ content_tag(:nav, breadcrumb_attributes) do
26
+ content_tag(:ol, class: "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5") do
27
+ safe_join(items_with_separators)
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def breadcrumb_attributes
35
+ attrs = {
36
+ "aria-label": "Breadcrumb",
37
+ class: merge_classes("")
38
+ }
39
+ attrs.merge!(html_options)
40
+ attrs.merge!(build_data)
41
+ attrs.compact
42
+ end
43
+
44
+ def items_with_separators
45
+ items.each_with_index.flat_map do |item, index|
46
+ result = [content_tag(:li, class: "inline-flex items-center gap-1.5") { item.to_s }]
47
+ result << separator unless index == items.length - 1
48
+ result
49
+ end
50
+ end
51
+
52
+ def separator
53
+ content_tag(:li, role: "presentation", "aria-hidden": "true", class: "text-muted-foreground") do
54
+ content_tag(:svg,
55
+ content_tag(:path, nil, d: "m9 18 6-6-6-6"),
56
+ xmlns: "http://www.w3.org/2000/svg",
57
+ width: "16",
58
+ height: "16",
59
+ viewBox: "0 0 24 24",
60
+ fill: "none",
61
+ stroke: "currentColor",
62
+ "stroke-width": "2",
63
+ "stroke-linecap": "round",
64
+ "stroke-linejoin": "round",
65
+ class: "h-3.5 w-3.5"
66
+ )
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Individual breadcrumb item
5
+ class BreadcrumbItemComponent < BaseComponent
6
+ LINK_CLASSES = "transition-colors hover:text-foreground"
7
+ PAGE_CLASSES = "font-normal text-foreground"
8
+
9
+ def initialize(href: nil, current: false, class_name: nil, **options)
10
+ super(class_name: class_name, **options)
11
+ @href = href
12
+ @current = current
13
+ @class_name = class_name
14
+ end
15
+
16
+ def call
17
+ if @current
18
+ content_tag(:span, item_attributes) do
19
+ content
20
+ end
21
+ else
22
+ content_tag(:a, link_attributes) do
23
+ content
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def item_attributes
31
+ attrs = {
32
+ role: "link",
33
+ class: cn(PAGE_CLASSES, @class_name),
34
+ "aria-current": "page",
35
+ "aria-disabled": "true"
36
+ }
37
+ attrs.merge!(html_options.except(:href))
38
+ attrs.compact
39
+ end
40
+
41
+ def link_attributes
42
+ attrs = {
43
+ href: @href,
44
+ class: cn(LINK_CLASSES, @class_name)
45
+ }
46
+ attrs.merge!(html_options)
47
+ attrs.compact
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Button component with multiple variants and sizes
5
+ # Matches shadcn/ui Button component
6
+ #
7
+ # @example Basic button
8
+ # <%= render Shadcn::ButtonComponent.new do %>
9
+ # Click me
10
+ # <% end %>
11
+ #
12
+ # @example Variant and size
13
+ # <%= render Shadcn::ButtonComponent.new(variant: :destructive, size: :lg) do %>
14
+ # Delete
15
+ # <% end %>
16
+ #
17
+ # @example As link
18
+ # <%= render Shadcn::ButtonComponent.new(href: "/path", variant: :link) do %>
19
+ # Go somewhere
20
+ # <% end %>
21
+ #
22
+ # @example Icon button
23
+ # <%= render Shadcn::ButtonComponent.new(size: :icon, aria: { label: "Settings" }) do %>
24
+ # <%= icon "settings" %>
25
+ # <% end %>
26
+ #
27
+ class ButtonComponent < BaseComponent
28
+ # Available button variants
29
+ VARIANTS = {
30
+ default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
31
+ destructive: "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
32
+ outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
33
+ secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
34
+ ghost: "hover:bg-accent hover:text-accent-foreground",
35
+ link: "text-primary underline-offset-4 hover:underline"
36
+ }.freeze
37
+
38
+ # Available button sizes
39
+ SIZES = {
40
+ default: "h-9 px-4 py-2",
41
+ sm: "h-8 rounded-md px-3 text-xs",
42
+ lg: "h-10 rounded-md px-8",
43
+ icon: "h-9 w-9",
44
+ icon_sm: "h-8 w-8",
45
+ icon_lg: "h-10 w-10"
46
+ }.freeze
47
+
48
+ # Base classes applied to all buttons
49
+ BASE_CLASSES = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0"
50
+
51
+ # @param variant [Symbol] Button style variant (:default, :destructive, :outline, :secondary, :ghost, :link)
52
+ # @param size [Symbol] Button size (:default, :sm, :lg, :icon, :icon_sm, :icon_lg)
53
+ # @param href [String, nil] If provided, renders as an anchor tag
54
+ # @param type [String] Button type attribute (button, submit, reset)
55
+ # @param disabled [Boolean] Whether button is disabled
56
+ # @param loading [Boolean] Whether button shows loading state
57
+ # @param class_name [String, nil] Additional CSS classes
58
+ # @param data [Hash] Data attributes
59
+ # @param html_options [Hash] Additional HTML attributes
60
+ def initialize(
61
+ variant: :default,
62
+ size: :default,
63
+ href: nil,
64
+ type: "button",
65
+ disabled: false,
66
+ loading: false,
67
+ **options
68
+ )
69
+ super(**options)
70
+ @variant = variant.to_sym
71
+ @size = size.to_sym
72
+ @href = href
73
+ @type = type
74
+ @disabled = disabled
75
+ @loading = loading
76
+ end
77
+
78
+ def call
79
+ if @href
80
+ link_tag
81
+ else
82
+ button_tag
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def button_tag
89
+ content_tag(:button, button_content, button_attributes)
90
+ end
91
+
92
+ def link_tag
93
+ content_tag(:a, button_content, link_attributes)
94
+ end
95
+
96
+ def button_content
97
+ if @loading
98
+ safe_join([loading_spinner, content])
99
+ else
100
+ content
101
+ end
102
+ end
103
+
104
+ def loading_spinner
105
+ content_tag(:span, "", class: "animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full", "aria-hidden": true)
106
+ end
107
+
108
+ def button_classes
109
+ cn(
110
+ BASE_CLASSES,
111
+ VARIANTS[@variant],
112
+ SIZES[@size],
113
+ class_name
114
+ )
115
+ end
116
+
117
+ def button_attributes
118
+ attrs = html_options.merge(
119
+ type: @type,
120
+ class: button_classes,
121
+ disabled: @disabled || @loading || nil,
122
+ "aria-disabled": (@disabled || @loading) ? "true" : nil,
123
+ "aria-busy": @loading ? "true" : nil
124
+ )
125
+ attrs.merge!(build_data)
126
+ attrs.compact
127
+ end
128
+
129
+ def link_attributes
130
+ attrs = html_options.merge(
131
+ href: @href,
132
+ class: button_classes,
133
+ role: "button",
134
+ "aria-disabled": @disabled ? "true" : nil,
135
+ tabindex: @disabled ? "-1" : nil
136
+ )
137
+ attrs.merge!(build_data)
138
+ attrs.compact
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Button Group component for grouping related buttons
5
+ # Matches shadcn/ui Button Group pattern
6
+ #
7
+ # @example Basic button group
8
+ # <%= render Shadcn::ButtonGroupComponent.new do |group| %>
9
+ # <% group.with_button(variant: :outline) { "Left" } %>
10
+ # <% group.with_button(variant: :outline) { "Center" } %>
11
+ # <% group.with_button(variant: :outline) { "Right" } %>
12
+ # <% end %>
13
+ #
14
+ # @example With different variants
15
+ # <%= render Shadcn::ButtonGroupComponent.new do |group| %>
16
+ # <% group.with_button { "Save" } %>
17
+ # <% group.with_button(variant: :outline) { "Cancel" } %>
18
+ # <% end %>
19
+ #
20
+ # @example Vertical orientation
21
+ # <%= render Shadcn::ButtonGroupComponent.new(orientation: :vertical) do |group| %>
22
+ # <% group.with_button(variant: :outline) { "Top" } %>
23
+ # <% group.with_button(variant: :outline) { "Middle" } %>
24
+ # <% group.with_button(variant: :outline) { "Bottom" } %>
25
+ # <% end %>
26
+ #
27
+ class ButtonGroupComponent < BaseComponent
28
+ ORIENTATIONS = {
29
+ horizontal: "flex-row",
30
+ vertical: "flex-col"
31
+ }.freeze
32
+
33
+ BASE_CLASSES = "inline-flex"
34
+
35
+ # Button slot - renders Button components with adjusted border radius
36
+ renders_many :buttons, lambda { |**options, &block|
37
+ # Buttons in a group need special border radius handling
38
+ options[:class_name] = cn(
39
+ "rounded-none first:rounded-l-md last:rounded-r-md",
40
+ "-ml-px first:ml-0", # Collapse borders
41
+ options[:class_name]
42
+ )
43
+ Shadcn::ButtonComponent.new(**options, &block)
44
+ }
45
+
46
+ # @param orientation [Symbol] Layout orientation (:horizontal, :vertical)
47
+ def initialize(orientation: :horizontal, **options)
48
+ super(**options)
49
+ @orientation = orientation.to_sym
50
+ end
51
+
52
+ def call
53
+ tag.div(class: group_classes, role: "group", **html_options.merge(build_data)) do
54
+ safe_join(buttons)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def group_classes
61
+ cn(
62
+ BASE_CLASSES,
63
+ ORIENTATIONS[@orientation],
64
+ @orientation == :vertical ? "first:[&>*]:rounded-t-md last:[&>*]:rounded-b-md [&>*]:rounded-none [&>*]:-mt-px [&>*]:first:mt-0" : "",
65
+ class_name
66
+ )
67
+ end
68
+ end
69
+ end