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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Empty Header component - wraps media, title, and description
5
+ class EmptyHeaderComponent < BaseComponent
6
+ BASE_CLASSES = "flex flex-col items-center gap-2"
7
+
8
+ # Media slot for icon, image, or avatar
9
+ renders_one :media, lambda { |variant: :default, **options|
10
+ EmptyMediaComponent.new(variant: variant, **options)
11
+ }
12
+
13
+ # Title slot
14
+ renders_one :title, lambda { |**options|
15
+ EmptyTitleComponent.new(**options)
16
+ }
17
+
18
+ # Description slot
19
+ renders_one :description, lambda { |**options|
20
+ EmptyDescriptionComponent.new(**options)
21
+ }
22
+
23
+ def call
24
+ content_tag(:div, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data)) do
25
+ safe_join([media, title, description, content].compact)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Empty Media component - displays icon, image, or avatar
5
+ class EmptyMediaComponent < BaseComponent
6
+ VARIANTS = {
7
+ default: "",
8
+ icon: "flex size-12 items-center justify-center rounded-full bg-muted [&>svg]:size-6 [&>svg]:text-muted-foreground"
9
+ }.freeze
10
+
11
+ # @param variant [Symbol] :default or :icon
12
+ def initialize(variant: :default, **options)
13
+ super(**options)
14
+ @variant = variant.to_sym
15
+ end
16
+
17
+ def call
18
+ content_tag(:div, content, class: merge_classes(VARIANTS[@variant]), **html_options.merge(build_data))
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Empty Title component
5
+ class EmptyTitleComponent < BaseComponent
6
+ BASE_CLASSES = "text-lg font-semibold"
7
+
8
+ def call
9
+ content_tag(:h3, content, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Field component for form field wrapper with label, input, description, and error
5
+ # Provides a consistent pattern for form fields
6
+ #
7
+ # @example Basic field
8
+ # <%= render Shadcn::FieldComponent.new do |field| %>
9
+ # <% field.with_label { "Email" } %>
10
+ # <% field.with_input(type: :email, placeholder: "you@example.com") %>
11
+ # <% end %>
12
+ #
13
+ # @example With description
14
+ # <%= render Shadcn::FieldComponent.new do |field| %>
15
+ # <% field.with_label { "Username" } %>
16
+ # <% field.with_input %>
17
+ # <% field.with_description { "This is your public display name." } %>
18
+ # <% end %>
19
+ #
20
+ # @example With error
21
+ # <%= render Shadcn::FieldComponent.new do |field| %>
22
+ # <% field.with_label { "Password" } %>
23
+ # <% field.with_input(type: :password) %>
24
+ # <% field.with_error { "Password is required." } %>
25
+ # <% end %>
26
+ #
27
+ # @example With custom content
28
+ # <%= render Shadcn::FieldComponent.new do |field| %>
29
+ # <% field.with_label { "Bio" } %>
30
+ # <% field.with_control do %>
31
+ # <%= render Shadcn::TextareaComponent.new(placeholder: "Tell us about yourself") %>
32
+ # <% end %>
33
+ # <% end %>
34
+ #
35
+ class FieldComponent < BaseComponent
36
+ BASE_CLASSES = "space-y-2"
37
+
38
+ # Label slot
39
+ renders_one :label, lambda { |required: false, **options, &block|
40
+ options[:for] ||= @input_id
41
+ options[:required] = required
42
+ Shadcn::LabelComponent.new(**options, &block)
43
+ }
44
+
45
+ # Input slot - renders an Input component
46
+ renders_one :input, lambda { |**options|
47
+ options[:id] ||= @input_id
48
+ options[:name] ||= @name
49
+ if @has_error
50
+ options[:class_name] = cn("border-destructive focus-visible:ring-destructive", options[:class_name])
51
+ end
52
+ Shadcn::InputComponent.new(**options)
53
+ }
54
+
55
+ # Control slot - for custom controls (textarea, select, etc.)
56
+ renders_one :control
57
+
58
+ # Description slot
59
+ renders_one :description, "DescriptionComponent"
60
+
61
+ # Error slot
62
+ renders_one :error, "ErrorComponent"
63
+
64
+ # @param name [String, nil] Input name attribute
65
+ # @param id [String, nil] Input ID (auto-generated if not provided)
66
+ def initialize(name: nil, id: nil, **options)
67
+ super(**options)
68
+ @name = name
69
+ @input_id = id || generate_id
70
+ @has_error = false
71
+ end
72
+
73
+ def before_render
74
+ # Track if error is present for styling
75
+ @has_error = error.present?
76
+ end
77
+
78
+ def call
79
+ tag.div(class: merge_classes(BASE_CLASSES), **html_options.merge(build_data)) do
80
+ safe_join([
81
+ label,
82
+ control || input,
83
+ description,
84
+ error
85
+ ].compact)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def generate_id
92
+ "field-#{SecureRandom.hex(4)}"
93
+ end
94
+
95
+ # Description subcomponent
96
+ class DescriptionComponent < BaseComponent
97
+ BASE_CLASSES = "text-[0.8rem] text-muted-foreground"
98
+
99
+ def call
100
+ tag.p(content, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data))
101
+ end
102
+ end
103
+
104
+ # Error subcomponent
105
+ class ErrorComponent < BaseComponent
106
+ BASE_CLASSES = "text-[0.8rem] font-medium text-destructive"
107
+
108
+ def call
109
+ tag.p(content, class: merge_classes(BASE_CLASSES), role: "alert", **html_options.merge(build_data))
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Hover Card component for displaying rich content on hover
5
+ # Matches shadcn/ui HoverCard component
6
+ #
7
+ # @example Basic usage
8
+ # <%= render Shadcn::HoverCardComponent.new do |card| %>
9
+ # <% card.with_trigger do %>
10
+ # <a href="#">@username</a>
11
+ # <% end %>
12
+ # <% card.with_card_content do %>
13
+ # <div>User profile preview</div>
14
+ # <% end %>
15
+ # <% end %>
16
+ #
17
+ class HoverCardComponent < BaseComponent
18
+ renders_one :trigger
19
+ renders_one :card_content, lambda { |**options|
20
+ HoverCardContentComponent.new(**options)
21
+ }
22
+
23
+ # @param open_delay [Integer] Delay in ms before opening
24
+ # @param close_delay [Integer] Delay in ms before closing
25
+ def initialize(open_delay: 700, close_delay: 300, **options)
26
+ super(**options)
27
+ @open_delay = open_delay
28
+ @close_delay = close_delay
29
+ end
30
+
31
+ def call
32
+ content_tag(:div, build_card_content, card_attributes)
33
+ end
34
+
35
+ private
36
+
37
+ def build_card_content
38
+ safe_join([
39
+ trigger_wrapper,
40
+ card_content
41
+ ].compact)
42
+ end
43
+
44
+ def trigger_wrapper
45
+ return unless trigger
46
+
47
+ content_tag(:div, trigger, {
48
+ "data-shadcn--hover-card-target": "trigger"
49
+ })
50
+ end
51
+
52
+ def card_attributes
53
+ attrs = {
54
+ class: cn("relative inline-block", class_name),
55
+ "data-controller": "shadcn--hover-card",
56
+ "data-shadcn--hover-card-open-delay-value": @open_delay,
57
+ "data-shadcn--hover-card-close-delay-value": @close_delay
58
+ }
59
+ attrs.merge!(html_options)
60
+ attrs.merge!(build_data)
61
+ attrs.compact
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Hover Card Content component
5
+ class HoverCardContentComponent < BaseComponent
6
+ BASE_CLASSES = "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
7
+
8
+ # @param side [Symbol] :top, :right, :bottom, or :left
9
+ # @param align [Symbol] :start, :center, or :end
10
+ def initialize(side: :bottom, align: :center, **options, &block)
11
+ super(**options, &block)
12
+ @side = side
13
+ @align = align
14
+ end
15
+
16
+ def call
17
+ content_tag(:div, content, content_attributes)
18
+ end
19
+
20
+ private
21
+
22
+ def content_attributes
23
+ attrs = {
24
+ class: merge_classes(BASE_CLASSES),
25
+ role: "tooltip",
26
+ "data-state": "closed",
27
+ "data-side": @side.to_s,
28
+ "data-align": @align.to_s,
29
+ "data-shadcn--hover-card-target": "content",
30
+ style: "display: none; position: absolute;"
31
+ }
32
+ attrs.merge!(html_options)
33
+ attrs.compact
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Input component for form fields
5
+ # Matches shadcn/ui Input component
6
+ #
7
+ # @example Basic input
8
+ # <%= render Shadcn::InputComponent.new(name: "email", type: "email", placeholder: "Email") %>
9
+ #
10
+ # @example With label
11
+ # <%= render Shadcn::LabelComponent.new(for: "email") { "Email" } %>
12
+ # <%= render Shadcn::InputComponent.new(id: "email", name: "email", type: "email") %>
13
+ #
14
+ # @example Disabled input
15
+ # <%= render Shadcn::InputComponent.new(name: "locked", disabled: true, value: "Can't change") %>
16
+ #
17
+ # @example File input
18
+ # <%= render Shadcn::InputComponent.new(name: "file", type: "file") %>
19
+ #
20
+ class InputComponent < BaseComponent
21
+ BASE_CLASSES = "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
22
+
23
+ # @param type [String] Input type (text, email, password, etc.)
24
+ # @param name [String, nil] Input name attribute
25
+ # @param id [String, nil] Input id attribute
26
+ # @param value [String, nil] Input value
27
+ # @param placeholder [String, nil] Placeholder text
28
+ # @param disabled [Boolean] Whether input is disabled
29
+ # @param required [Boolean] Whether input is required
30
+ # @param readonly [Boolean] Whether input is readonly
31
+ # @param autofocus [Boolean] Whether to autofocus
32
+ # @param autocomplete [String, nil] Autocomplete attribute
33
+ # @param pattern [String, nil] Validation pattern
34
+ # @param min [String, Integer, nil] Minimum value (for number/date inputs)
35
+ # @param max [String, Integer, nil] Maximum value (for number/date inputs)
36
+ # @param step [String, Integer, nil] Step value (for number inputs)
37
+ # @param minlength [Integer, nil] Minimum length
38
+ # @param maxlength [Integer, nil] Maximum length
39
+ def initialize(
40
+ type: "text",
41
+ name: nil,
42
+ id: nil,
43
+ value: nil,
44
+ placeholder: nil,
45
+ disabled: false,
46
+ required: false,
47
+ readonly: false,
48
+ autofocus: false,
49
+ autocomplete: nil,
50
+ pattern: nil,
51
+ min: nil,
52
+ max: nil,
53
+ step: nil,
54
+ minlength: nil,
55
+ maxlength: nil,
56
+ **options
57
+ )
58
+ super(**options)
59
+ @type = type
60
+ @name = name
61
+ @id = id
62
+ @value = value
63
+ @placeholder = placeholder
64
+ @disabled = disabled
65
+ @required = required
66
+ @readonly = readonly
67
+ @autofocus = autofocus
68
+ @autocomplete = autocomplete
69
+ @pattern = pattern
70
+ @min = min
71
+ @max = max
72
+ @step = step
73
+ @minlength = minlength
74
+ @maxlength = maxlength
75
+ end
76
+
77
+ def call
78
+ tag(:input, input_attributes)
79
+ end
80
+
81
+ private
82
+
83
+ def input_attributes
84
+ attrs = {
85
+ type: @type,
86
+ name: @name,
87
+ id: @id,
88
+ value: @value,
89
+ placeholder: @placeholder,
90
+ disabled: @disabled || nil,
91
+ required: @required || nil,
92
+ readonly: @readonly || nil,
93
+ autofocus: @autofocus || nil,
94
+ autocomplete: @autocomplete,
95
+ pattern: @pattern,
96
+ min: @min,
97
+ max: @max,
98
+ step: @step,
99
+ minlength: @minlength,
100
+ maxlength: @maxlength,
101
+ class: merge_classes(BASE_CLASSES)
102
+ }
103
+ attrs.merge!(html_options)
104
+ attrs.merge!(build_data)
105
+ attrs.compact
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Input Group component for inputs with prefix/suffix addons
5
+ # Matches shadcn/ui Input Group pattern
6
+ #
7
+ # @example With prefix icon
8
+ # <%= render Shadcn::InputGroupComponent.new do |group| %>
9
+ # <% group.with_prefix do %>
10
+ # <svg><!-- search icon --></svg>
11
+ # <% end %>
12
+ # <% group.with_input(placeholder: "Search...") %>
13
+ # <% end %>
14
+ #
15
+ # @example With suffix icon
16
+ # <%= render Shadcn::InputGroupComponent.new do |group| %>
17
+ # <% group.with_input(type: :email, placeholder: "Email") %>
18
+ # <% group.with_suffix do %>
19
+ # <svg><!-- mail icon --></svg>
20
+ # <% end %>
21
+ # <% end %>
22
+ #
23
+ # @example With prefix text
24
+ # <%= render Shadcn::InputGroupComponent.new do |group| %>
25
+ # <% group.with_prefix { "https://" } %>
26
+ # <% group.with_input(placeholder: "example.com") %>
27
+ # <% end %>
28
+ #
29
+ # @example With both prefix and suffix
30
+ # <%= render Shadcn::InputGroupComponent.new do |group| %>
31
+ # <% group.with_prefix { "$" } %>
32
+ # <% group.with_input(type: :number, placeholder: "0.00") %>
33
+ # <% group.with_suffix { "USD" } %>
34
+ # <% end %>
35
+ #
36
+ class InputGroupComponent < BaseComponent
37
+ BASE_CLASSES = "flex items-center rounded-md border border-input ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
38
+
39
+ # Prefix addon slot
40
+ renders_one :prefix, "AddonComponent"
41
+
42
+ # Input slot
43
+ renders_one :input, lambda { |**options|
44
+ # Remove border, ring, rounded corners and shadow from input since the group handles it
45
+ options[:class_name] = cn(
46
+ "border-0 rounded-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:ring-0 focus:outline-none",
47
+ options[:class_name]
48
+ )
49
+ Shadcn::InputComponent.new(**options)
50
+ }
51
+
52
+ # Suffix addon slot
53
+ renders_one :suffix, "AddonComponent"
54
+
55
+ def call
56
+ tag.div(class: merge_classes(BASE_CLASSES), **html_options.merge(build_data)) do
57
+ safe_join([prefix, input, suffix].compact)
58
+ end
59
+ end
60
+
61
+ # Addon subcomponent for prefix/suffix
62
+ class AddonComponent < BaseComponent
63
+ BASE_CLASSES = "flex items-center justify-center px-3 text-sm text-muted-foreground"
64
+
65
+ def call
66
+ tag.span(content, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data))
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Input OTP component for one-time password entry
5
+ # Matches shadcn/ui Input OTP component
6
+ #
7
+ # @example Basic 6-digit OTP
8
+ # <%= render Shadcn::InputOtpComponent.new(length: 6, name: "otp") %>
9
+ #
10
+ # @example With groups (3 + 3)
11
+ # <%= render Shadcn::InputOtpComponent.new(length: 6, name: "otp") do |otp| %>
12
+ # <% otp.with_group(slots: 3) %>
13
+ # <% otp.with_separator %>
14
+ # <% otp.with_group(slots: 3) %>
15
+ # <% end %>
16
+ #
17
+ # @example 4-digit PIN
18
+ # <%= render Shadcn::InputOtpComponent.new(length: 4, name: "pin", pattern: "^[0-9]*$") %>
19
+ #
20
+ # @example Disabled
21
+ # <%= render Shadcn::InputOtpComponent.new(length: 6, name: "otp", disabled: true) %>
22
+ #
23
+ class InputOtpComponent < BaseComponent
24
+ BASE_CLASSES = "flex items-center gap-2"
25
+ SLOT_CLASSES = "relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md"
26
+ SLOT_ACTIVE_CLASSES = "z-10 ring-1 ring-ring"
27
+ CARET_CLASSES = "pointer-events-none absolute inset-0 flex items-center justify-center"
28
+ CARET_BLINK_CLASSES = "animate-caret-blink h-4 w-px bg-foreground duration-1000"
29
+
30
+ # Groups for visual grouping of slots
31
+ renders_many :groups, "GroupComponent"
32
+
33
+ # Separators between groups
34
+ renders_many :separators, "SeparatorComponent"
35
+
36
+ # @param length [Integer] Number of OTP digits
37
+ # @param name [String] Input name for form submission
38
+ # @param pattern [String] Regex pattern for validation (defaults to alphanumeric)
39
+ # @param disabled [Boolean] Whether the input is disabled
40
+ # @param required [Boolean] Whether the input is required
41
+ # @param autocomplete [String] Autocomplete attribute (defaults to "one-time-code")
42
+ def initialize(length: 6, name: nil, pattern: nil, disabled: false, required: false, autocomplete: "one-time-code", **options)
43
+ super(**options)
44
+ @length = length
45
+ @name = name
46
+ @pattern = pattern
47
+ @disabled = disabled
48
+ @required = required
49
+ @autocomplete = autocomplete
50
+ end
51
+
52
+ def call
53
+ tag.div(**container_attributes) do
54
+ safe_join([
55
+ hidden_input,
56
+ groups.any? ? render_with_groups : render_default_slots
57
+ ])
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def container_attributes
64
+ {
65
+ class: merge_classes(BASE_CLASSES),
66
+ data: {
67
+ controller: "shadcn--input-otp",
68
+ "shadcn--input-otp-length-value": @length,
69
+ "shadcn--input-otp-pattern-value": @pattern,
70
+ "shadcn--input-otp-disabled-value": @disabled
71
+ }
72
+ }.merge(html_options).merge(build_data).compact
73
+ end
74
+
75
+ def hidden_input
76
+ tag.input(
77
+ type: "hidden",
78
+ name: @name,
79
+ data: { "shadcn--input-otp-target": "hiddenInput" }
80
+ )
81
+ end
82
+
83
+ def render_with_groups
84
+ parts = []
85
+ slot_index = 0
86
+
87
+ groups.each_with_index do |group, group_index|
88
+ # Render the group with its slots
89
+ group_slots = group.slots.times.map do |_|
90
+ slot = render_slot(slot_index)
91
+ slot_index += 1
92
+ slot
93
+ end
94
+
95
+ parts << tag.div(class: "flex items-center") { safe_join(group_slots) }
96
+
97
+ # Add separator after group if there's another group
98
+ if group_index < groups.size - 1
99
+ separator = separators[group_index]
100
+ parts << (separator || default_separator)
101
+ end
102
+ end
103
+
104
+ safe_join(parts)
105
+ end
106
+
107
+ def default_separator
108
+ tag.div(class: "flex items-center justify-center px-2", role: "separator") do
109
+ tag.span(class: "text-muted-foreground") { "-" }
110
+ end
111
+ end
112
+
113
+ def render_default_slots
114
+ # Render all slots in one group
115
+ slots = @length.times.map do |index|
116
+ render_slot(index)
117
+ end
118
+
119
+ tag.div(class: "flex items-center") { safe_join(slots) }
120
+ end
121
+
122
+ def render_slot(index)
123
+ tag.div(
124
+ class: SLOT_CLASSES,
125
+ data: {
126
+ "shadcn--input-otp-target": "slot",
127
+ index: index,
128
+ action: "click->shadcn--input-otp#focusSlot"
129
+ }
130
+ ) do
131
+ safe_join([
132
+ tag.input(
133
+ type: "text",
134
+ maxlength: 1,
135
+ inputmode: "numeric",
136
+ autocomplete: index == 0 ? @autocomplete : "off",
137
+ disabled: @disabled || nil,
138
+ required: @required && index == 0 || nil,
139
+ class: "absolute inset-0 w-full h-full text-center bg-transparent outline-none border-0 focus:ring-0",
140
+ data: {
141
+ "shadcn--input-otp-target": "input",
142
+ index: index,
143
+ action: "input->shadcn--input-otp#handleInput keydown->shadcn--input-otp#handleKeydown focus->shadcn--input-otp#handleFocus blur->shadcn--input-otp#handleBlur paste->shadcn--input-otp#handlePaste"
144
+ }
145
+ ),
146
+ tag.div(class: CARET_CLASSES, data: { "shadcn--input-otp-target": "caret" }) do
147
+ tag.div(class: CARET_BLINK_CLASSES)
148
+ end
149
+ ])
150
+ end
151
+ end
152
+
153
+ # Group subcomponent
154
+ class GroupComponent < BaseComponent
155
+ BASE_CLASSES = "flex items-center"
156
+
157
+ # @param slots [Integer] Number of slots in this group
158
+ def initialize(slots: 3, **options)
159
+ super(**options)
160
+ @slots = slots
161
+ end
162
+
163
+ attr_reader :slots
164
+
165
+ def call
166
+ tag.div(class: merge_classes(BASE_CLASSES), **html_options.merge(build_data)) do
167
+ content
168
+ end
169
+ end
170
+ end
171
+
172
+ # Separator subcomponent
173
+ class SeparatorComponent < BaseComponent
174
+ BASE_CLASSES = "flex items-center justify-center px-2"
175
+
176
+ def call
177
+ tag.div(class: merge_classes(BASE_CLASSES), role: "separator", **html_options.merge(build_data)) do
178
+ content.presence || tag.span(class: "text-muted-foreground") { "-" }
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Item Actions component - container for action buttons
5
+ class ItemActionsComponent < BaseComponent
6
+ BASE_CLASSES = "shrink-0 flex items-center gap-2"
7
+
8
+ def call
9
+ content_tag(:div, content, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data))
10
+ end
11
+ end
12
+ end