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
data/README.md ADDED
@@ -0,0 +1,1483 @@
1
+ # shadcn-rails
2
+
3
+ Beautiful, accessible UI components for Rails built with ViewComponents, Stimulus, and Tailwind CSS. A Ruby port of [shadcn/ui](https://ui.shadcn.com).
4
+
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-red.svg)](https://www.ruby-lang.org/)
6
+ [![Rails](https://img.shields.io/badge/rails-%3E%3D%207.0-red.svg)](https://rubyonrails.org/)
7
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
9
+ ## Features
10
+
11
+ - **Beautiful by default** - Carefully crafted components that look great out of the box
12
+ - **Accessible** - Built with accessibility in mind, following WAI-ARIA patterns
13
+ - **Customizable** - Use CSS variables to customize the look and feel
14
+ - **Dark mode** - Built-in dark mode support with multiple strategies
15
+ - **ViewComponents** - Leverages Rails' ViewComponent library for composable, testable components
16
+ - **Stimulus** - Interactive components powered by Stimulus controllers
17
+ - **Rails-first** - Designed specifically for Ruby on Rails applications
18
+ - **47 Components** - Comprehensive library covering all common UI patterns
19
+
20
+ ## Table of Contents
21
+
22
+ - [Installation](#installation)
23
+ - [Quick Start](#quick-start)
24
+ - [Components](#components)
25
+ - [Buttons & Actions](#buttons--actions)
26
+ - [Form Inputs](#form-inputs)
27
+ - [Data Display](#data-display)
28
+ - [Feedback](#feedback)
29
+ - [Overlays](#overlays)
30
+ - [Navigation](#navigation)
31
+ - [Layout](#layout)
32
+ - [Theming](#theming)
33
+ - [Dark Mode](#dark-mode)
34
+ - [Configuration](#configuration)
35
+ - [Stimulus Controllers](#stimulus-controllers)
36
+ - [Testing](#testing)
37
+ - [Development](#development)
38
+ - [Security Considerations](#security-considerations)
39
+ - [Contributing](#contributing)
40
+
41
+ ## Installation
42
+
43
+ Add this line to your application's Gemfile:
44
+
45
+ ```ruby
46
+ gem "shadcn-rails"
47
+ ```
48
+
49
+ Then execute:
50
+
51
+ ```bash
52
+ bundle install
53
+ rails generate shadcn:install
54
+ ```
55
+
56
+ This will:
57
+ 1. Create a configuration initializer at `config/initializers/shadcn.rb`
58
+ 2. Add the required CSS imports to your application
59
+ 3. Configure your Stimulus controllers
60
+
61
+ ### Stylesheets
62
+
63
+ shadcn-rails includes two CSS files:
64
+
65
+ | File | Purpose |
66
+ |------|---------|
67
+ | `shadcn/base.css` | CSS variables for theming (colors, border radius), animations, and focus styles |
68
+ | `shadcn/components.css` | Component-specific styles for interactive elements (`data-state` attributes, custom inputs) |
69
+
70
+ **For Tailwind CSS** (application.tailwind.css):
71
+
72
+ ```css
73
+ @import "shadcn/base";
74
+ @import "shadcn/components";
75
+
76
+ @tailwind base;
77
+ @tailwind components;
78
+ @tailwind utilities;
79
+ ```
80
+
81
+ **For Sprockets** (application.css):
82
+
83
+ ```css
84
+ /*
85
+ *= require shadcn/base
86
+ *= require shadcn/components
87
+ *= require_self
88
+ */
89
+ ```
90
+
91
+ The `components.css` file includes essential styles for:
92
+ - **Switch** - `data-state` based checked/unchecked styling
93
+ - **Slider** - Custom range input with fill indicator
94
+ - **Checkbox/Radio** - Native inputs with custom styling
95
+ - **Accordion/Collapsible** - Content animations
96
+ - **Dialog/Sheet/Popover** - Open/close animations and overlays
97
+ - **Tabs** - Active/inactive state styling
98
+
99
+ ### Requirements
100
+
101
+ - Ruby >= 3.1
102
+ - Rails >= 7.0
103
+ - Tailwind CSS >= 3.0
104
+ - Stimulus >= 3.0
105
+ - ViewComponent >= 3.0
106
+
107
+ ### Tailwind CSS Configuration
108
+
109
+ Ensure your `tailwind.config.js` includes the shadcn-rails color configuration:
110
+
111
+ ```javascript
112
+ module.exports = {
113
+ darkMode: 'class',
114
+ theme: {
115
+ extend: {
116
+ colors: {
117
+ border: "hsl(var(--border))",
118
+ input: "hsl(var(--input))",
119
+ ring: "hsl(var(--ring))",
120
+ background: "hsl(var(--background))",
121
+ foreground: "hsl(var(--foreground))",
122
+ primary: {
123
+ DEFAULT: "hsl(var(--primary))",
124
+ foreground: "hsl(var(--primary-foreground))",
125
+ },
126
+ secondary: {
127
+ DEFAULT: "hsl(var(--secondary))",
128
+ foreground: "hsl(var(--secondary-foreground))",
129
+ },
130
+ destructive: {
131
+ DEFAULT: "hsl(var(--destructive))",
132
+ foreground: "hsl(var(--destructive-foreground))",
133
+ },
134
+ muted: {
135
+ DEFAULT: "hsl(var(--muted))",
136
+ foreground: "hsl(var(--muted-foreground))",
137
+ },
138
+ accent: {
139
+ DEFAULT: "hsl(var(--accent))",
140
+ foreground: "hsl(var(--accent-foreground))",
141
+ },
142
+ popover: {
143
+ DEFAULT: "hsl(var(--popover))",
144
+ foreground: "hsl(var(--popover-foreground))",
145
+ },
146
+ card: {
147
+ DEFAULT: "hsl(var(--card))",
148
+ foreground: "hsl(var(--card-foreground))",
149
+ },
150
+ },
151
+ borderRadius: {
152
+ lg: "var(--radius)",
153
+ md: "calc(var(--radius) - 2px)",
154
+ sm: "calc(var(--radius) - 4px)",
155
+ },
156
+ },
157
+ },
158
+ }
159
+ ```
160
+
161
+ ## Quick Start
162
+
163
+ ```erb
164
+ <%# Simple button %>
165
+ <%= render Shadcn::ButtonComponent.new { "Click me" } %>
166
+
167
+ <%# Button with variant %>
168
+ <%= render Shadcn::ButtonComponent.new(variant: :destructive) { "Delete" } %>
169
+
170
+ <%# Card with slots %>
171
+ <%= render Shadcn::CardComponent.new do |card| %>
172
+ <% card.with_header do |header| %>
173
+ <% header.with_title { "Welcome" } %>
174
+ <% header.with_description { "Get started with shadcn-rails" } %>
175
+ <% end %>
176
+ <% card.with_content_slot do %>
177
+ <p>Your content here</p>
178
+ <% end %>
179
+ <% end %>
180
+ ```
181
+
182
+ ## Components
183
+
184
+ ### Buttons & Actions
185
+
186
+ #### Button
187
+
188
+ Displays a button or a component that looks like a button.
189
+
190
+ ```erb
191
+ <%# Variants %>
192
+ <%= render Shadcn::ButtonComponent.new(variant: :default) { "Default" } %>
193
+ <%= render Shadcn::ButtonComponent.new(variant: :secondary) { "Secondary" } %>
194
+ <%= render Shadcn::ButtonComponent.new(variant: :destructive) { "Destructive" } %>
195
+ <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Outline" } %>
196
+ <%= render Shadcn::ButtonComponent.new(variant: :ghost) { "Ghost" } %>
197
+ <%= render Shadcn::ButtonComponent.new(variant: :link) { "Link" } %>
198
+
199
+ <%# Sizes %>
200
+ <%= render Shadcn::ButtonComponent.new(size: :sm) { "Small" } %>
201
+ <%= render Shadcn::ButtonComponent.new(size: :default) { "Default" } %>
202
+ <%= render Shadcn::ButtonComponent.new(size: :lg) { "Large" } %>
203
+ <%= render Shadcn::ButtonComponent.new(size: :icon) { "+" } %>
204
+
205
+ <%# States %>
206
+ <%= render Shadcn::ButtonComponent.new(disabled: true) { "Disabled" } %>
207
+
208
+ <%# As link %>
209
+ <%= render Shadcn::ButtonComponent.new(href: "/path", variant: :outline) { "Link Button" } %>
210
+ ```
211
+
212
+ **Props:**
213
+ | Prop | Type | Default | Description |
214
+ |------|------|---------|-------------|
215
+ | `variant` | Symbol | `:default` | `:default`, `:secondary`, `:destructive`, `:outline`, `:ghost`, `:link` |
216
+ | `size` | Symbol | `:default` | `:default`, `:sm`, `:lg`, `:icon` |
217
+ | `disabled` | Boolean | `false` | Disables the button |
218
+ | `href` | String | `nil` | Renders as a link when provided |
219
+ | `type` | String | `"button"` | Button type attribute |
220
+
221
+ #### Toggle
222
+
223
+ A two-state button that can be either on or off.
224
+
225
+ ```erb
226
+ <%= render Shadcn::ToggleComponent.new do %>
227
+ <svg><!-- icon --></svg>
228
+ <% end %>
229
+
230
+ <%= render Shadcn::ToggleComponent.new(variant: :outline, pressed: true) do %>
231
+ Bold
232
+ <% end %>
233
+ ```
234
+
235
+ #### Toggle Group
236
+
237
+ A set of two-state buttons that can be toggled on or off.
238
+
239
+ ```erb
240
+ <%# Single selection %>
241
+ <%= render Shadcn::ToggleGroupComponent.new(type: :single) do |group| %>
242
+ <% group.with_item(value: "bold") { "B" } %>
243
+ <% group.with_item(value: "italic") { "I" } %>
244
+ <% group.with_item(value: "underline") { "U" } %>
245
+ <% end %>
246
+
247
+ <%# Multiple selection %>
248
+ <%= render Shadcn::ToggleGroupComponent.new(type: :multiple, variant: :outline) do |group| %>
249
+ <% group.with_item(value: "left") { "Left" } %>
250
+ <% group.with_item(value: "center") { "Center" } %>
251
+ <% group.with_item(value: "right") { "Right" } %>
252
+ <% end %>
253
+ ```
254
+
255
+ ### Form Inputs
256
+
257
+ #### Input
258
+
259
+ Displays a form input field.
260
+
261
+ ```erb
262
+ <%= render Shadcn::InputComponent.new(
263
+ type: :email,
264
+ placeholder: "you@example.com",
265
+ id: "email",
266
+ name: "user[email]"
267
+ ) %>
268
+
269
+ <%# Disabled %>
270
+ <%= render Shadcn::InputComponent.new(
271
+ type: :text,
272
+ placeholder: "Disabled",
273
+ disabled: true
274
+ ) %>
275
+
276
+ <%# With validation error %>
277
+ <%= render Shadcn::InputComponent.new(
278
+ type: :text,
279
+ class_name: "border-destructive"
280
+ ) %>
281
+ ```
282
+
283
+ **Props:**
284
+ | Prop | Type | Default | Description |
285
+ |------|------|---------|-------------|
286
+ | `type` | Symbol/String | `:text` | Input type (`:text`, `:email`, `:password`, `:number`, `:search`, `:file`, etc.) |
287
+ | `placeholder` | String | `nil` | Placeholder text |
288
+ | `disabled` | Boolean | `false` | Disables the input |
289
+ | `required` | Boolean | `false` | Makes input required |
290
+
291
+ #### Textarea
292
+
293
+ Displays a multi-line text input.
294
+
295
+ ```erb
296
+ <%= render Shadcn::TextareaComponent.new(
297
+ placeholder: "Type your message here...",
298
+ rows: 4,
299
+ name: "message"
300
+ ) %>
301
+ ```
302
+
303
+ #### Label
304
+
305
+ Renders an accessible label associated with controls.
306
+
307
+ ```erb
308
+ <%= render Shadcn::LabelComponent.new(for: "email") { "Email Address" } %>
309
+ <%= render Shadcn::InputComponent.new(id: "email", type: :email) %>
310
+ ```
311
+
312
+ #### Checkbox
313
+
314
+ A control that allows toggling between checked and not checked.
315
+
316
+ ```erb
317
+ <%= render Shadcn::CheckboxComponent.new(
318
+ id: "terms",
319
+ label: "Accept terms and conditions"
320
+ ) %>
321
+
322
+ <%# Checked by default %>
323
+ <%= render Shadcn::CheckboxComponent.new(
324
+ id: "newsletter",
325
+ label: "Subscribe to newsletter",
326
+ checked: true
327
+ ) %>
328
+
329
+ <%# Disabled %>
330
+ <%= render Shadcn::CheckboxComponent.new(
331
+ id: "disabled",
332
+ label: "Disabled option",
333
+ disabled: true
334
+ ) %>
335
+ ```
336
+
337
+ #### Switch
338
+
339
+ A control that allows toggling between a checked and not checked state.
340
+
341
+ ```erb
342
+ <%= render Shadcn::SwitchComponent.new(
343
+ id: "airplane",
344
+ label: "Airplane Mode"
345
+ ) %>
346
+
347
+ <%= render Shadcn::SwitchComponent.new(
348
+ id: "notifications",
349
+ label: "Enable notifications",
350
+ checked: true
351
+ ) %>
352
+ ```
353
+
354
+ #### Radio Group
355
+
356
+ A set of checkable buttons where only one can be checked at a time.
357
+
358
+ ```erb
359
+ <%= render Shadcn::RadioGroupComponent.new(name: "plan", default_value: "comfortable") do |group| %>
360
+ <% group.with_item(value: "default", label: "Default") %>
361
+ <% group.with_item(value: "comfortable", label: "Comfortable") %>
362
+ <% group.with_item(value: "compact", label: "Compact") %>
363
+ <% end %>
364
+ ```
365
+
366
+ #### Select
367
+
368
+ Displays a list of options for the user to pick from.
369
+
370
+ ```erb
371
+ <%= render Shadcn::SelectComponent.new(placeholder: "Select a fruit") do |select| %>
372
+ <% select.with_group(label: "Fruits") do |group| %>
373
+ <% group.with_item(value: "apple") { "Apple" } %>
374
+ <% group.with_item(value: "banana") { "Banana" } %>
375
+ <% group.with_item(value: "orange") { "Orange" } %>
376
+ <% end %>
377
+ <% end %>
378
+ ```
379
+
380
+ #### Slider
381
+
382
+ An input where the user selects a value from within a given range.
383
+
384
+ ```erb
385
+ <%= render Shadcn::SliderComponent.new(value: 50, max: 100) %>
386
+ <%= render Shadcn::SliderComponent.new(value: 25, min: 0, max: 100, step: 5) %>
387
+ ```
388
+
389
+ ### Data Display
390
+
391
+ #### Badge
392
+
393
+ Displays a badge or label.
394
+
395
+ ```erb
396
+ <%= render Shadcn::BadgeComponent.new(variant: :default) { "Default" } %>
397
+ <%= render Shadcn::BadgeComponent.new(variant: :secondary) { "Secondary" } %>
398
+ <%= render Shadcn::BadgeComponent.new(variant: :destructive) { "Error" } %>
399
+ <%= render Shadcn::BadgeComponent.new(variant: :outline) { "Outline" } %>
400
+ ```
401
+
402
+ #### Avatar
403
+
404
+ An image element with a fallback for representing the user.
405
+
406
+ ```erb
407
+ <%# With image %>
408
+ <%= render Shadcn::AvatarComponent.new(
409
+ src: "https://example.com/avatar.jpg",
410
+ alt: "John Doe",
411
+ fallback: "JD"
412
+ ) %>
413
+
414
+ <%# Without image (shows fallback) %>
415
+ <%= render Shadcn::AvatarComponent.new(
416
+ alt: "Jane Smith",
417
+ fallback: "JS"
418
+ ) %>
419
+
420
+ <%# Sizes %>
421
+ <%= render Shadcn::AvatarComponent.new(size: :sm, fallback: "SM") %>
422
+ <%= render Shadcn::AvatarComponent.new(size: :default, fallback: "MD") %>
423
+ <%= render Shadcn::AvatarComponent.new(size: :lg, fallback: "LG") %>
424
+ ```
425
+
426
+ #### Card
427
+
428
+ Displays a card with header, content, and footer.
429
+
430
+ ```erb
431
+ <%= render Shadcn::CardComponent.new do |card| %>
432
+ <% card.with_header do |header| %>
433
+ <% header.with_title { "Card Title" } %>
434
+ <% header.with_description { "Card description goes here" } %>
435
+ <% end %>
436
+ <% card.with_content_slot do %>
437
+ <p>This is the main content of the card.</p>
438
+ <% end %>
439
+ <% card.with_footer do %>
440
+ <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Cancel" } %>
441
+ <%= render Shadcn::ButtonComponent.new { "Save" } %>
442
+ <% end %>
443
+ <% end %>
444
+
445
+ <%# Simple card (content only) %>
446
+ <%= render Shadcn::CardComponent.new do |card| %>
447
+ <% card.with_content_slot(standalone: true) do %>
448
+ <p>A simple card with just content.</p>
449
+ <% end %>
450
+ <% end %>
451
+ ```
452
+
453
+ #### Table
454
+
455
+ A responsive table component.
456
+
457
+ ```erb
458
+ <%= render Shadcn::TableComponent.new do |table| %>
459
+ <% table.with_header do |header| %>
460
+ <% header.with_row do |row| %>
461
+ <% row.with_head { "Name" } %>
462
+ <% row.with_head { "Status" } %>
463
+ <% row.with_head(class_name: "text-right") { "Amount" } %>
464
+ <% end %>
465
+ <% end %>
466
+ <% table.with_body do |body| %>
467
+ <% body.with_row do |row| %>
468
+ <% row.with_cell { "John Doe" } %>
469
+ <% row.with_cell { "Active" } %>
470
+ <% row.with_cell(class_name: "text-right") { "$250.00" } %>
471
+ <% end %>
472
+ <% end %>
473
+ <% end %>
474
+ ```
475
+
476
+ #### Progress
477
+
478
+ Displays an indicator showing the completion progress of a task.
479
+
480
+ ```erb
481
+ <%= render Shadcn::ProgressComponent.new(value: 33) %>
482
+ <%= render Shadcn::ProgressComponent.new(value: 66) %>
483
+ <%= render Shadcn::ProgressComponent.new(value: 100) %>
484
+ ```
485
+
486
+ #### Skeleton
487
+
488
+ Use to show a placeholder while content is loading.
489
+
490
+ ```erb
491
+ <div class="flex items-center space-x-4">
492
+ <%= render Shadcn::SkeletonComponent.new(class_name: "h-12 w-12 rounded-full") %>
493
+ <div class="space-y-2">
494
+ <%= render Shadcn::SkeletonComponent.new(class_name: "h-4 w-[250px]") %>
495
+ <%= render Shadcn::SkeletonComponent.new(class_name: "h-4 w-[200px]") %>
496
+ </div>
497
+ </div>
498
+ ```
499
+
500
+ #### Aspect Ratio
501
+
502
+ Displays content within a desired ratio.
503
+
504
+ ```erb
505
+ <%= render Shadcn::AspectRatioComponent.new(ratio: "16/9") do %>
506
+ <img src="image.jpg" class="object-cover w-full h-full" />
507
+ <% end %>
508
+
509
+ <%# Common ratios: "1/1", "4/3", "16/9", "21/9" %>
510
+ ```
511
+
512
+ ### Feedback
513
+
514
+ #### Alert
515
+
516
+ Displays a callout for user attention.
517
+
518
+ ```erb
519
+ <%# Default alert %>
520
+ <%= render Shadcn::AlertComponent.new do |alert| %>
521
+ <% alert.with_title { "Heads up!" } %>
522
+ <% alert.with_description { "You can add components using the CLI." } %>
523
+ <% end %>
524
+
525
+ <%# Destructive alert %>
526
+ <%= render Shadcn::AlertComponent.new(variant: :destructive) do |alert| %>
527
+ <% alert.with_title { "Error" } %>
528
+ <% alert.with_description { "Your session has expired." } %>
529
+ <% end %>
530
+ ```
531
+
532
+ #### Tooltip
533
+
534
+ A popup that displays information when hovering.
535
+
536
+ ```erb
537
+ <%= render Shadcn::TooltipComponent.new(content: "Add to library", side: :top) do |tooltip| %>
538
+ <% tooltip.with_trigger do %>
539
+ <%= render Shadcn::ButtonComponent.new(variant: :outline, size: :icon) { "+" } %>
540
+ <% end %>
541
+ <% end %>
542
+ ```
543
+
544
+ **Props:**
545
+ | Prop | Type | Default | Description |
546
+ |------|------|---------|-------------|
547
+ | `content` | String | required | Tooltip text |
548
+ | `side` | Symbol | `:top` | `:top`, `:bottom`, `:left`, `:right` |
549
+ | `delay` | Integer | `200` | Delay in milliseconds before showing |
550
+
551
+ ### Overlays
552
+
553
+ #### Dialog
554
+
555
+ A modal dialog window.
556
+
557
+ ```erb
558
+ <%= render Shadcn::DialogComponent.new do |dialog| %>
559
+ <% dialog.with_trigger do %>
560
+ <%= render Shadcn::ButtonComponent.new { "Open Dialog" } %>
561
+ <% end %>
562
+ <% dialog.with_body do |body| %>
563
+ <% body.with_header do |header| %>
564
+ <% header.with_title { "Edit Profile" } %>
565
+ <% header.with_description { "Make changes to your profile here." } %>
566
+ <% end %>
567
+ <div class="py-4">
568
+ <%# Form content %>
569
+ </div>
570
+ <% body.with_footer do %>
571
+ <%= render Shadcn::ButtonComponent.new { "Save changes" } %>
572
+ <% end %>
573
+ <% end %>
574
+ <% end %>
575
+ ```
576
+
577
+ ##### Dialog with ID (for Turbo Stream targeting)
578
+
579
+ ```erb
580
+ <%= render Shadcn::DialogComponent.new(id: "edit-profile-dialog") do |dialog| %>
581
+ <%# ... %>
582
+ <% end %>
583
+ ```
584
+
585
+ ##### Closing Dialog Programmatically
586
+
587
+ Use Stimulus actions to close dialogs from buttons:
588
+
589
+ ```erb
590
+ <%# Cancel button closes immediately %>
591
+ <%= render Shadcn::ButtonComponent.new(
592
+ variant: :outline,
593
+ type: "button",
594
+ data: { action: "click->shadcn--dialog#close" }
595
+ ) { "Cancel" } %>
596
+
597
+ <%# Or close from any element %>
598
+ <button data-action="click->shadcn--dialog#close">Close</button>
599
+ ```
600
+
601
+ ##### Forms Inside Dialogs
602
+
603
+ For forms that should close the dialog only on successful submission:
604
+
605
+ ```erb
606
+ <%= render Shadcn::DialogComponent.new do |dialog| %>
607
+ <% dialog.with_trigger do %>
608
+ <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Edit Profile" } %>
609
+ <% end %>
610
+ <% dialog.with_body do |body| %>
611
+ <% body.with_header do |header| %>
612
+ <% header.with_title { "Edit Profile" } %>
613
+ <% header.with_description { "Make changes to your profile here." } %>
614
+ <% end %>
615
+ <%= form_with model: @user, data: { remote: "true" } do |f| %>
616
+ <div class="space-y-4">
617
+ <%= render Shadcn::LabelComponent.new(for: "name") { "Name" } %>
618
+ <%= render Shadcn::InputComponent.new(id: "name", name: "user[name]", value: @user.name) %>
619
+ </div>
620
+ <div class="flex justify-end gap-3 mt-4">
621
+ <%= render Shadcn::ButtonComponent.new(
622
+ variant: :outline,
623
+ type: "button",
624
+ data: { action: "click->shadcn--dialog#close" }
625
+ ) { "Cancel" } %>
626
+ <%= render Shadcn::ButtonComponent.new(type: "submit") { "Save Changes" } %>
627
+ </div>
628
+ <% end %>
629
+ <% end %>
630
+ <% end %>
631
+ ```
632
+
633
+ **Props:**
634
+ | Prop | Type | Default | Description |
635
+ |------|------|---------|-------------|
636
+ | `id` | String | `nil` | Unique identifier for Turbo Stream targeting |
637
+ | `open` | Boolean | `false` | Whether dialog starts open |
638
+ | `modal` | Boolean | `true` | Whether dialog traps focus and blocks interaction |
639
+
640
+ #### Alert Dialog
641
+
642
+ A modal dialog for destructive or important actions.
643
+
644
+ ```erb
645
+ <%= render Shadcn::AlertDialogComponent.new do |dialog| %>
646
+ <% dialog.with_trigger do %>
647
+ <%= render Shadcn::ButtonComponent.new(variant: :destructive) { "Delete Account" } %>
648
+ <% end %>
649
+ <% dialog.with_body do |body| %>
650
+ <% body.with_header do |header| %>
651
+ <% header.with_title { "Are you absolutely sure?" } %>
652
+ <% header.with_description { "This action cannot be undone." } %>
653
+ <% end %>
654
+ <% body.with_footer do %>
655
+ <%= render Shadcn::ButtonComponent.new(variant: :outline, data: { action: "click->shadcn--alert-dialog#close" }) { "Cancel" } %>
656
+ <%= render Shadcn::ButtonComponent.new(variant: :destructive) { "Delete" } %>
657
+ <% end %>
658
+ <% end %>
659
+ <% end %>
660
+ ```
661
+
662
+ #### Sheet
663
+
664
+ Extends the Dialog component to display content that complements the main content.
665
+
666
+ ```erb
667
+ <%= render Shadcn::SheetComponent.new(side: :right) do |sheet| %>
668
+ <% sheet.with_trigger do %>
669
+ <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Sheet" } %>
670
+ <% end %>
671
+ <% sheet.with_body do |body| %>
672
+ <% body.with_header do |header| %>
673
+ <% header.with_title { "Settings" } %>
674
+ <% header.with_description { "Configure your preferences." } %>
675
+ <% end %>
676
+ <div class="py-4">
677
+ <%# Sheet content %>
678
+ </div>
679
+ <% end %>
680
+ <% end %>
681
+ ```
682
+
683
+ **Props:**
684
+ | Prop | Type | Default | Description |
685
+ |------|------|---------|-------------|
686
+ | `side` | Symbol | `:right` | `:top`, `:right`, `:bottom`, `:left` |
687
+
688
+ #### Drawer
689
+
690
+ A drawer component for mobile interfaces.
691
+
692
+ ```erb
693
+ <%= render Shadcn::DrawerComponent.new do |drawer| %>
694
+ <% drawer.with_trigger do %>
695
+ <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Drawer" } %>
696
+ <% end %>
697
+ <% drawer.with_body do |body| %>
698
+ <% body.with_header do |header| %>
699
+ <% header.with_title { "Drawer Title" } %>
700
+ <% header.with_description { "Drawer description" } %>
701
+ <% end %>
702
+ <div class="p-4">
703
+ <%# Drawer content %>
704
+ </div>
705
+ <% body.with_footer do %>
706
+ <%= render Shadcn::ButtonComponent.new(class_name: "w-full") { "Submit" } %>
707
+ <% end %>
708
+ <% end %>
709
+ <% end %>
710
+ ```
711
+
712
+ #### Popover
713
+
714
+ Displays rich content in a portal, triggered by a button.
715
+
716
+ ```erb
717
+ <%= render Shadcn::PopoverComponent.new do |popover| %>
718
+ <% popover.with_trigger do %>
719
+ <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Popover" } %>
720
+ <% end %>
721
+ <% popover.with_content do %>
722
+ <div class="grid gap-4">
723
+ <h4 class="font-medium">Dimensions</h4>
724
+ <p class="text-sm text-muted-foreground">Set the dimensions for the layer.</p>
725
+ </div>
726
+ <% end %>
727
+ <% end %>
728
+ ```
729
+
730
+ #### Hover Card
731
+
732
+ For sighted users to preview content available behind a link.
733
+
734
+ ```erb
735
+ <%= render Shadcn::HoverCardComponent.new do |card| %>
736
+ <% card.with_trigger do %>
737
+ <a href="#" class="underline">@shadcn</a>
738
+ <% end %>
739
+ <% card.with_card_content do %>
740
+ <div class="flex space-x-4">
741
+ <%= render Shadcn::AvatarComponent.new(src: "avatar.jpg", fallback: "SC") %>
742
+ <div>
743
+ <h4 class="text-sm font-semibold">@shadcn</h4>
744
+ <p class="text-sm">Creator of shadcn/ui</p>
745
+ </div>
746
+ </div>
747
+ <% end %>
748
+ <% end %>
749
+ ```
750
+
751
+ #### Dropdown Menu
752
+
753
+ Displays a menu of actions or functions triggered by a button.
754
+
755
+ ```erb
756
+ <%= render Shadcn::DropdownMenuComponent.new do |menu| %>
757
+ <% menu.with_trigger do %>
758
+ <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Menu" } %>
759
+ <% end %>
760
+ <% menu.with_content do |content| %>
761
+ <% content.with_label { "My Account" } %>
762
+ <% content.with_separator %>
763
+ <% content.with_item { "Profile" } %>
764
+ <% content.with_item { "Settings" } %>
765
+ <% content.with_separator %>
766
+ <% content.with_item { "Log out" } %>
767
+ <% end %>
768
+ <% end %>
769
+ ```
770
+
771
+ ### Navigation
772
+
773
+ #### Tabs
774
+
775
+ A set of layered sections of content that are displayed one at a time.
776
+
777
+ ```erb
778
+ <%= render Shadcn::TabsComponent.new(default_value: "account") do |tabs| %>
779
+ <% tabs.with_list do |list| %>
780
+ <% list.with_trigger(value: "account") { "Account" } %>
781
+ <% list.with_trigger(value: "password") { "Password" } %>
782
+ <% list.with_trigger(value: "settings", disabled: true) { "Settings" } %>
783
+ <% end %>
784
+ <% tabs.with_panel(value: "account") do %>
785
+ <p>Account settings here.</p>
786
+ <% end %>
787
+ <% tabs.with_panel(value: "password") do %>
788
+ <p>Password settings here.</p>
789
+ <% end %>
790
+ <% end %>
791
+ ```
792
+
793
+ ##### URL Synchronization
794
+
795
+ Sync the active tab state with the URL query parameter for shareable links and browser history support:
796
+
797
+ ```erb
798
+ <%# Tab state syncs to URL: /settings?tab=billing %>
799
+ <%= render Shadcn::TabsComponent.new(default_value: "general", url_param: "tab") do |tabs| %>
800
+ <% tabs.with_list do |list| %>
801
+ <% list.with_trigger(value: "general") { "General" } %>
802
+ <% list.with_trigger(value: "billing") { "Billing" } %>
803
+ <% list.with_trigger(value: "security") { "Security" } %>
804
+ <% end %>
805
+ <% tabs.with_panel(value: "general") do %>
806
+ <p>General settings</p>
807
+ <% end %>
808
+ <% tabs.with_panel(value: "billing") do %>
809
+ <p>Billing settings</p>
810
+ <% end %>
811
+ <% tabs.with_panel(value: "security") do %>
812
+ <p>Security settings</p>
813
+ <% end %>
814
+ <% end %>
815
+ ```
816
+
817
+ When `url_param` is set:
818
+ - The URL updates when tabs are clicked (e.g., `?tab=billing`)
819
+ - Direct navigation to URLs with the parameter selects the correct tab
820
+ - Browser back/forward navigation works as expected
821
+
822
+ **Props:**
823
+ | Prop | Type | Default | Description |
824
+ |------|------|---------|-------------|
825
+ | `default_value` | String | `nil` | Initially active tab value |
826
+ | `url_param` | String | `nil` | URL query parameter name for state sync |
827
+
828
+ #### Accordion
829
+
830
+ A vertically stacked set of interactive headings that reveal sections of content.
831
+
832
+ ```erb
833
+ <%= render Shadcn::AccordionComponent.new(type: :single, collapsible: true) do |accordion| %>
834
+ <% accordion.with_item(value: "item-1") do |item| %>
835
+ <% item.with_trigger { "Is it accessible?" } %>
836
+ <% item.with_body { "Yes. It adheres to WAI-ARIA design patterns." } %>
837
+ <% end %>
838
+ <% accordion.with_item(value: "item-2") do |item| %>
839
+ <% item.with_trigger { "Is it styled?" } %>
840
+ <% item.with_body { "Yes. It comes with default styles." } %>
841
+ <% end %>
842
+ <% end %>
843
+ ```
844
+
845
+ **Props:**
846
+ | Prop | Type | Default | Description |
847
+ |------|------|---------|-------------|
848
+ | `type` | Symbol | `:single` | `:single` (one open), `:multiple` (many open) |
849
+ | `collapsible` | Boolean | `false` | Allow closing all items |
850
+ | `default_value` | String | `nil` | Initially open item(s) |
851
+
852
+ #### Breadcrumb
853
+
854
+ Displays the path to the current resource using a hierarchy of links.
855
+
856
+ ```erb
857
+ <%= render Shadcn::BreadcrumbComponent.new do |breadcrumb| %>
858
+ <% breadcrumb.with_item(href: "/") { "Home" } %>
859
+ <% breadcrumb.with_item(href: "/products") { "Products" } %>
860
+ <% breadcrumb.with_item(current: true) { "Widget" } %>
861
+ <% end %>
862
+ ```
863
+
864
+ #### Pagination
865
+
866
+ Pagination with page navigation, next and previous links. Supports three usage patterns:
867
+
868
+ **1. Auto-generated from Kaminari collection:**
869
+
870
+ ```erb
871
+ <%# Works with Kaminari paginated collections %>
872
+ <%= render Shadcn::PaginationComponent.new(collection: @posts) %>
873
+ ```
874
+
875
+ **2. Auto-generated from will_paginate collection:**
876
+
877
+ ```erb
878
+ <%# Works with will_paginate collections %>
879
+ <%= render Shadcn::PaginationComponent.new(collection: @users) %>
880
+ ```
881
+
882
+ **3. Auto-generated from Pagy object:**
883
+
884
+ ```erb
885
+ <%# Works with Pagy pagination objects %>
886
+ <%= render Shadcn::PaginationComponent.new(pagy: @pagy) %>
887
+ ```
888
+
889
+ **4. Custom URL builder:**
890
+
891
+ ```erb
892
+ <%# Use a custom URL builder for complex routes %>
893
+ <%= render Shadcn::PaginationComponent.new(
894
+ collection: @posts,
895
+ url_builder: ->(page) { posts_path(page: page, sort: params[:sort]) }
896
+ ) %>
897
+ ```
898
+
899
+ **5. Full slot-based control:**
900
+
901
+ ```erb
902
+ <%= render Shadcn::PaginationComponent.new do |pagination| %>
903
+ <% pagination.with_pagination_content do |content| %>
904
+ <% content.with_previous(href: "?page=1") %>
905
+ <% content.with_item(href: "?page=1") { "1" } %>
906
+ <% content.with_item(href: "?page=2", active: true) { "2" } %>
907
+ <% content.with_item(href: "?page=3") { "3" } %>
908
+ <% content.with_ellipse %>
909
+ <% content.with_item(href: "?page=10") { "10" } %>
910
+ <% content.with_next_page(href: "?page=3") %>
911
+ <% end %>
912
+ <% end %>
913
+ ```
914
+
915
+ **6. Using the `shadcn_paginate` helper:**
916
+
917
+ ```erb
918
+ <%# Simple one-liner that auto-detects your pagination gem %>
919
+ <%= shadcn_paginate @posts %>
920
+
921
+ <%# With Pagy %>
922
+ <%= shadcn_paginate @pagy %>
923
+
924
+ <%# Custom URL builder and window size %>
925
+ <%= shadcn_paginate @posts,
926
+ url_builder: ->(page) { posts_path(page: page) },
927
+ window: 3 %>
928
+ ```
929
+
930
+ | Prop | Type | Default | Description |
931
+ |------|------|---------|-------------|
932
+ | `collection` | Object | `nil` | Kaminari or will_paginate collection |
933
+ | `pagy` | Object | `nil` | Pagy pagination object |
934
+ | `url_builder` | Proc | `"?page=N"` | Lambda to generate page URLs |
935
+ | `window` | Integer | `2` | Pages to show around current page |
936
+
937
+ **Supported Pagination Gems:**
938
+
939
+ | Gem | Usage |
940
+ |-----|-------|
941
+ | [Kaminari](https://github.com/kaminari/kaminari) | `collection: @posts.page(1).per(10)` |
942
+ | [will_paginate](https://github.com/mislav/will_paginate) | `collection: @posts.paginate(page: 1)` |
943
+ | [Pagy](https://github.com/ddnexus/pagy) | `pagy: @pagy` (from `pagy(@posts)`) |
944
+
945
+ #### Collapsible
946
+
947
+ An interactive component which expands/collapses a panel.
948
+
949
+ ```erb
950
+ <%= render Shadcn::CollapsibleComponent.new do |collapsible| %>
951
+ <% collapsible.with_trigger do %>
952
+ <div class="flex items-center justify-between px-4 py-2 border rounded-md">
953
+ <span>Click to expand</span>
954
+ <svg><!-- chevron icon --></svg>
955
+ </div>
956
+ <% end %>
957
+ <% collapsible.with_content do %>
958
+ <div class="mt-2 p-4 border rounded-md">
959
+ Collapsible content here
960
+ </div>
961
+ <% end %>
962
+ <% end %>
963
+ ```
964
+
965
+ ### Layout
966
+
967
+ #### Separator
968
+
969
+ Visually or semantically separates content.
970
+
971
+ ```erb
972
+ <%# Horizontal separator %>
973
+ <%= render Shadcn::SeparatorComponent.new %>
974
+
975
+ <%# Vertical separator %>
976
+ <div class="flex h-5 items-center space-x-4">
977
+ <span>Blog</span>
978
+ <%= render Shadcn::SeparatorComponent.new(orientation: :vertical) %>
979
+ <span>Docs</span>
980
+ </div>
981
+ ```
982
+
983
+ #### Scroll Area
984
+
985
+ Augments native scroll functionality for custom, cross-browser styling.
986
+
987
+ ```erb
988
+ <%= render Shadcn::ScrollAreaComponent.new(class_name: "h-[200px] w-[350px] rounded-md border p-4") do %>
989
+ <div class="space-y-4">
990
+ <% 20.times do |i| %>
991
+ <div>Item <%= i + 1 %></div>
992
+ <% end %>
993
+ </div>
994
+ <% end %>
995
+ ```
996
+
997
+ ## Theming
998
+
999
+ ### Available Themes
1000
+
1001
+ shadcn-rails includes 5 built-in color themes:
1002
+
1003
+ | Theme | Description |
1004
+ |-------|-------------|
1005
+ | `neutral` | Clean grayscale palette (default) |
1006
+ | `slate` | Cool blue-gray tones |
1007
+ | `stone` | Warm brown-gray tones |
1008
+ | `gray` | Standard gray palette |
1009
+ | `zinc` | Cool gray with slight purple tint |
1010
+
1011
+ ### Switching Themes
1012
+
1013
+ ```ruby
1014
+ # config/initializers/shadcn.rb
1015
+ Shadcn::Rails.configure do |config|
1016
+ config.base_color = "slate" # neutral, slate, stone, gray, zinc
1017
+ end
1018
+ ```
1019
+
1020
+ Or use the generator:
1021
+
1022
+ ```bash
1023
+ rails generate shadcn:theme slate
1024
+ ```
1025
+
1026
+ ### CSS Variables
1027
+
1028
+ shadcn-rails uses CSS variables for theming, matching the shadcn/ui approach:
1029
+
1030
+ ```css
1031
+ :root {
1032
+ --background: 0 0% 100%;
1033
+ --foreground: 0 0% 3.9%;
1034
+ --card: 0 0% 100%;
1035
+ --card-foreground: 0 0% 3.9%;
1036
+ --popover: 0 0% 100%;
1037
+ --popover-foreground: 0 0% 3.9%;
1038
+ --primary: 0 0% 9%;
1039
+ --primary-foreground: 0 0% 98%;
1040
+ --secondary: 0 0% 96.1%;
1041
+ --secondary-foreground: 0 0% 9%;
1042
+ --muted: 0 0% 96.1%;
1043
+ --muted-foreground: 0 0% 45.1%;
1044
+ --accent: 0 0% 96.1%;
1045
+ --accent-foreground: 0 0% 9%;
1046
+ --destructive: 0 84.2% 60.2%;
1047
+ --destructive-foreground: 0 0% 98%;
1048
+ --border: 0 0% 89.8%;
1049
+ --input: 0 0% 89.8%;
1050
+ --ring: 0 0% 3.9%;
1051
+ --radius: 0.5rem;
1052
+ }
1053
+
1054
+ .dark {
1055
+ --background: 0 0% 3.9%;
1056
+ --foreground: 0 0% 98%;
1057
+ /* ... dark mode overrides */
1058
+ }
1059
+ ```
1060
+
1061
+ ### Custom Themes
1062
+
1063
+ Register custom themes in your initializer:
1064
+
1065
+ ```ruby
1066
+ Shadcn::Rails.configure do |config|
1067
+ config.register_theme(:brand, {
1068
+ primary: "220 90% 56%",
1069
+ primary_foreground: "0 0% 100%",
1070
+ # ... other variables
1071
+ })
1072
+ end
1073
+ ```
1074
+
1075
+ ## Dark Mode
1076
+
1077
+ ### Class Strategy (Recommended)
1078
+
1079
+ Add the `dark` class to your `<html>` element:
1080
+
1081
+ ```html
1082
+ <html class="dark">
1083
+ ```
1084
+
1085
+ Toggle with JavaScript:
1086
+
1087
+ ```javascript
1088
+ // Toggle dark mode
1089
+ document.documentElement.classList.toggle('dark')
1090
+
1091
+ // Check system preference
1092
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
1093
+ document.documentElement.classList.add('dark')
1094
+ }
1095
+ ```
1096
+
1097
+ ### Media Strategy
1098
+
1099
+ Use the system preference automatically:
1100
+
1101
+ ```ruby
1102
+ Shadcn::Rails.configure do |config|
1103
+ config.dark_mode = :media
1104
+ end
1105
+ ```
1106
+
1107
+ ## Configuration
1108
+
1109
+ Full configuration options:
1110
+
1111
+ ```ruby
1112
+ # config/initializers/shadcn.rb
1113
+ Shadcn::Rails.configure do |config|
1114
+ # Base color theme: neutral, stone, zinc, gray, slate
1115
+ config.base_color = "neutral"
1116
+
1117
+ # Use CSS variables for theming
1118
+ config.css_variables = true
1119
+
1120
+ # Dark mode strategy: :class, :media, or :selector
1121
+ config.dark_mode = :class
1122
+
1123
+ # Default border radius
1124
+ config.radius = "0.5rem"
1125
+
1126
+ # Tailwind class prefix (if using one)
1127
+ config.tailwind_prefix = ""
1128
+
1129
+ # Icon library: :lucide (default), :heroicons
1130
+ config.icon_library = :lucide
1131
+ end
1132
+ ```
1133
+
1134
+ ## Stimulus Controllers
1135
+
1136
+ Interactive components require Stimulus controllers. Setup depends on your JavaScript bundler.
1137
+
1138
+ ### Importmap-rails (Rails Default)
1139
+
1140
+ Add to your `config/importmap.rb`:
1141
+
1142
+ ```ruby
1143
+ pin "shadcn-rails", to: "shadcn/index.js"
1144
+ ```
1145
+
1146
+ Then in `app/javascript/controllers/index.js`:
1147
+
1148
+ ```javascript
1149
+ import { Application } from "@hotwired/stimulus"
1150
+ import { registerShadcnControllers } from "shadcn-rails"
1151
+
1152
+ const application = Application.start()
1153
+ registerShadcnControllers(application)
1154
+ ```
1155
+
1156
+ ### esbuild
1157
+
1158
+ Install the npm package:
1159
+
1160
+ ```bash
1161
+ npm install shadcn-rails
1162
+ # or
1163
+ yarn add shadcn-rails
1164
+ ```
1165
+
1166
+ Then in `app/javascript/controllers/index.js`:
1167
+
1168
+ ```javascript
1169
+ import { Application } from "@hotwired/stimulus"
1170
+ import { registerShadcnControllers } from "shadcn-rails"
1171
+
1172
+ const application = Application.start()
1173
+ registerShadcnControllers(application)
1174
+ ```
1175
+
1176
+ ### Webpack
1177
+
1178
+ Install the npm package:
1179
+
1180
+ ```bash
1181
+ npm install shadcn-rails
1182
+ # or
1183
+ yarn add shadcn-rails
1184
+ ```
1185
+
1186
+ In `app/javascript/controllers/index.js`:
1187
+
1188
+ ```javascript
1189
+ import { Application } from "@hotwired/stimulus"
1190
+ import { registerShadcnControllers } from "shadcn-rails"
1191
+
1192
+ const application = Application.start()
1193
+ registerShadcnControllers(application)
1194
+ ```
1195
+
1196
+ ### Vite (vite-ruby)
1197
+
1198
+ Install the npm package:
1199
+
1200
+ ```bash
1201
+ npm install shadcn-rails
1202
+ # or
1203
+ yarn add shadcn-rails
1204
+ ```
1205
+
1206
+ In your entrypoint (e.g., `app/frontend/entrypoints/application.js`):
1207
+
1208
+ ```javascript
1209
+ import { Application } from "@hotwired/stimulus"
1210
+ import { registerShadcnControllers } from "shadcn-rails"
1211
+
1212
+ const application = Application.start()
1213
+ registerShadcnControllers(application)
1214
+ ```
1215
+
1216
+ ### Registering Individual Controllers
1217
+
1218
+ If you prefer to only load specific controllers (tree-shaking):
1219
+
1220
+ ```javascript
1221
+ import { Application } from "@hotwired/stimulus"
1222
+ import DialogController from "shadcn-rails/controllers/dialog_controller"
1223
+ import TabsController from "shadcn-rails/controllers/tabs_controller"
1224
+
1225
+ const application = Application.start()
1226
+ application.register("shadcn--dialog", DialogController)
1227
+ application.register("shadcn--tabs", TabsController)
1228
+ ```
1229
+
1230
+ ### Available Controllers
1231
+
1232
+ | Controller | Components |
1233
+ |------------|------------|
1234
+ | `shadcn--accordion` | Accordion |
1235
+ | `shadcn--alert-dialog` | AlertDialog |
1236
+ | `shadcn--avatar` | Avatar |
1237
+ | `shadcn--checkbox` | Checkbox |
1238
+ | `shadcn--collapsible` | Collapsible |
1239
+ | `shadcn--context-menu` | ContextMenu |
1240
+ | `shadcn--dialog` | Dialog |
1241
+ | `shadcn--drawer` | Drawer |
1242
+ | `shadcn--dropdown-menu` | DropdownMenu |
1243
+ | `shadcn--hover-card` | HoverCard |
1244
+ | `shadcn--input-otp` | InputOtp |
1245
+ | `shadcn--menubar` | Menubar |
1246
+ | `shadcn--navigation-menu` | NavigationMenu |
1247
+ | `shadcn--popover` | Popover |
1248
+ | `shadcn--radio-group` | RadioGroup |
1249
+ | `shadcn--resizable` | Resizable |
1250
+ | `shadcn--scroll-area` | ScrollArea |
1251
+ | `shadcn--select` | Select |
1252
+ | `shadcn--sheet` | Sheet |
1253
+ | `shadcn--slider` | Slider |
1254
+ | `shadcn--switch` | Switch |
1255
+ | `shadcn--tabs` | Tabs |
1256
+ | `shadcn--toast` | Toast |
1257
+ | `shadcn--toggle` | Toggle |
1258
+ | `shadcn--toggle-group` | ToggleGroup |
1259
+ | `shadcn--tooltip` | Tooltip |
1260
+
1261
+ ### TypeScript Support
1262
+
1263
+ shadcn-rails includes comprehensive TypeScript type definitions (`.d.ts` files) for all 20 Stimulus controllers. Types are provided without requiring TypeScript compilation - your JavaScript remains the source of truth.
1264
+
1265
+ **Using Types in TypeScript Projects:**
1266
+
1267
+ ```typescript
1268
+ import { Application } from "@hotwired/stimulus"
1269
+ import { registerShadcnControllers } from "shadcn-rails"
1270
+
1271
+ // Full IDE autocomplete and type checking
1272
+ const application = Application.start()
1273
+ registerShadcnControllers(application)
1274
+ ```
1275
+
1276
+ **Importing Individual Controllers with Types:**
1277
+
1278
+ ```typescript
1279
+ import DialogController from "shadcn-rails/controllers/dialog_controller"
1280
+ import TabsController from "shadcn-rails/controllers/tabs_controller"
1281
+
1282
+ // Full type information available
1283
+ const dialog = new DialogController()
1284
+ dialog.open() // ✓ TypeScript knows this method exists
1285
+ dialog.openValue // ✓ Type: boolean
1286
+ ```
1287
+
1288
+ **Available Type Definitions:**
1289
+
1290
+ Each controller includes typed definitions for:
1291
+ - Static `targets` and `values` declarations
1292
+ - Target accessors (`*Target`, `*Targets`, `has*Target`)
1293
+ - Value accessors (`*Value`, `has*Value`)
1294
+ - All public methods
1295
+ - Custom properties and getters
1296
+
1297
+ **Example Type Definition (DialogController):**
1298
+
1299
+ ```typescript
1300
+ import { Controller } from "@hotwired/stimulus"
1301
+
1302
+ export default class DialogController extends Controller {
1303
+ static targets: ["trigger", "template", "overlay", "content"]
1304
+ static values: {
1305
+ open: { type: "Boolean"; default: false }
1306
+ modal: { type: "Boolean"; default: true }
1307
+ }
1308
+
1309
+ // Target accessors
1310
+ readonly triggerTarget: HTMLElement
1311
+ readonly hasTemplateTarget: boolean
1312
+
1313
+ // Value accessors
1314
+ openValue: boolean
1315
+ modalValue: boolean
1316
+
1317
+ // Methods
1318
+ open(): void
1319
+ close(): void
1320
+ toggle(): void
1321
+ }
1322
+ ```
1323
+
1324
+ **For Importmap Users:**
1325
+
1326
+ If using TypeScript with importmaps, add a type declaration file at `types/shadcn-rails.d.ts`:
1327
+
1328
+ ```typescript
1329
+ declare module "shadcn-rails" {
1330
+ import { Application } from "@hotwired/stimulus"
1331
+ export function registerShadcnControllers(application: Application): void
1332
+ export const controllers: Record<string, typeof import("@hotwired/stimulus").Controller>
1333
+ }
1334
+ ```
1335
+
1336
+ ## Helper Methods
1337
+
1338
+ shadcn-rails provides helper methods for your views:
1339
+
1340
+ ```erb
1341
+ <%# Class name merging (like cn() in shadcn/ui) %>
1342
+ <div class="<%= Shadcn::Rails.cn("base-class", conditional && "conditional-class", @class_name) %>">
1343
+ ```
1344
+
1345
+ ## Testing
1346
+
1347
+ Run the full test suite:
1348
+
1349
+ ```bash
1350
+ bundle exec rake test
1351
+ ```
1352
+
1353
+ Run component tests only:
1354
+
1355
+ ```bash
1356
+ bundle exec rake test_components
1357
+ ```
1358
+
1359
+ ## Development
1360
+
1361
+ After checking out the repo:
1362
+
1363
+ ```bash
1364
+ bundle install
1365
+ cd test/dummy
1366
+ rails server
1367
+ ```
1368
+
1369
+ Visit `http://localhost:3000` to see the demo app with:
1370
+ - `/docs` - Full documentation with examples
1371
+ - `/showcase` - Full component showcase
1372
+ - `/themes` - Theme preview and comparison
1373
+ - `/lookbook` - Component previews with Lookbook
1374
+
1375
+ ### Deploying the Documentation Site
1376
+
1377
+ The documentation site in `test/dummy/` can be deployed as a standalone Rails application. Recommended platforms:
1378
+
1379
+ **Render (Free tier available)**
1380
+ ```bash
1381
+ # In test/dummy/ directory
1382
+ render.yaml # Already configured for deployment
1383
+ ```
1384
+
1385
+ **Railway**
1386
+ ```bash
1387
+ cd test/dummy
1388
+ railway init
1389
+ railway up
1390
+ ```
1391
+
1392
+ **Fly.io**
1393
+ ```bash
1394
+ cd test/dummy
1395
+ fly launch
1396
+ fly deploy
1397
+ ```
1398
+
1399
+ **Heroku**
1400
+ ```bash
1401
+ cd test/dummy
1402
+ heroku create your-shadcn-rails-docs
1403
+ git subtree push --prefix test/dummy heroku main
1404
+ ```
1405
+
1406
+ ## Security Considerations
1407
+
1408
+ shadcn-rails follows Rails security best practices. Here are important security guidelines:
1409
+
1410
+ ### CSRF Protection
1411
+
1412
+ Always use Rails form helpers to automatically include CSRF tokens:
1413
+
1414
+ ```erb
1415
+ <%= form_with url: "/submit" do |f| %>
1416
+ <%= render Shadcn::InputComponent.new(name: "email") %>
1417
+ <%= render Shadcn::ButtonComponent.new(type: "submit") { "Submit" } %>
1418
+ <% end %>
1419
+ ```
1420
+
1421
+ ### XSS Prevention
1422
+
1423
+ ViewComponent auto-escapes all content by default. Never call `html_safe` on user-provided content:
1424
+
1425
+ ```erb
1426
+ <%# SAFE - auto-escaped %>
1427
+ <%= render Shadcn::BadgeComponent.new { user.name } %>
1428
+
1429
+ <%# DANGEROUS - never do this with user input! %>
1430
+ <%= render Shadcn::BadgeComponent.new { user.name.html_safe } %>
1431
+ ```
1432
+
1433
+ ### Input Validation
1434
+
1435
+ Form components (Input, Textarea, Select) pass through values without validation. Always implement:
1436
+
1437
+ - Server-side input validation
1438
+ - Strong parameters in controllers
1439
+ - Model validations
1440
+
1441
+ ```ruby
1442
+ # In your controller
1443
+ def user_params
1444
+ params.require(:user).permit(:name, :email)
1445
+ end
1446
+
1447
+ # In your model
1448
+ validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
1449
+ ```
1450
+
1451
+ ### Content Security Policy
1452
+
1453
+ If using CSP headers, ensure your policy allows inline styles for theming:
1454
+
1455
+ ```ruby
1456
+ # config/initializers/content_security_policy.rb
1457
+ Rails.application.configure do
1458
+ config.content_security_policy do |policy|
1459
+ policy.style_src :self, :unsafe_inline # Required for CSS variables
1460
+ end
1461
+ end
1462
+ ```
1463
+
1464
+ ## Contributing
1465
+
1466
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/shadcn-rails.
1467
+
1468
+ 1. Fork the repository
1469
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
1470
+ 3. Commit your changes (`git commit -am 'Add amazing feature'`)
1471
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
1472
+ 5. Open a Pull Request
1473
+
1474
+ ## License
1475
+
1476
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1477
+
1478
+ ## Credits
1479
+
1480
+ - [shadcn/ui](https://ui.shadcn.com) - The original React component library
1481
+ - [ViewComponent](https://viewcomponent.org) - Ruby component framework
1482
+ - [Stimulus](https://stimulus.hotwired.dev) - JavaScript framework
1483
+ - [Tailwind CSS](https://tailwindcss.com) - Utility-first CSS framework