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,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Calendar component - date picker grid
5
+ # Matches shadcn/ui Calendar component
6
+ #
7
+ # @example Basic calendar
8
+ # <%= render Shadcn::CalendarComponent.new %>
9
+ #
10
+ # @example With selected date
11
+ # <%= render Shadcn::CalendarComponent.new(selected: Date.today) %>
12
+ #
13
+ # @example With name for form submission
14
+ # <%= render Shadcn::CalendarComponent.new(name: "event[date]") %>
15
+ #
16
+ class CalendarComponent < BaseComponent
17
+ CONTAINER_CLASSES = "p-3 rounded-md border bg-background"
18
+ HEADER_CLASSES = "flex items-center justify-between mb-4"
19
+ MONTH_YEAR_CLASSES = "text-sm font-medium"
20
+ NAV_BUTTON_CLASSES = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-7 w-7"
21
+ WEEKDAY_CLASSES = "text-center text-xs font-medium text-muted-foreground"
22
+ DAY_CLASSES = "h-8 w-8 text-center text-sm p-0 relative flex items-center justify-center rounded-md cursor-pointer hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-1 focus:ring-ring"
23
+ DAY_SELECTED_CLASSES = "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground"
24
+ DAY_TODAY_CLASSES = "bg-accent text-accent-foreground"
25
+ DAY_OUTSIDE_CLASSES = "text-muted-foreground opacity-50"
26
+ DAY_DISABLED_CLASSES = "text-muted-foreground opacity-50 pointer-events-none"
27
+
28
+ WEEKDAYS = %w[Su Mo Tu We Th Fr Sa].freeze
29
+ # Mapping for Rails beginning_of_week symbols
30
+ WEEK_START_SYMBOLS = {
31
+ 0 => :sunday,
32
+ 1 => :monday,
33
+ 2 => :tuesday,
34
+ 3 => :wednesday,
35
+ 4 => :thursday,
36
+ 5 => :friday,
37
+ 6 => :saturday
38
+ }.freeze
39
+ MONTHS = %w[January February March April May June July August September October November December].freeze
40
+
41
+ MODES = %i[single multiple range].freeze
42
+
43
+ # @param selected [Date, Array<Date>, nil] Currently selected date(s)
44
+ # @param month [Date, nil] Month to display (defaults to current month)
45
+ # @param min_date [Date, nil] Minimum selectable date
46
+ # @param max_date [Date, nil] Maximum selectable date
47
+ # @param name [String, nil] Form field name for hidden input
48
+ # @param disabled_dates [Array<Date>] Specific dates that cannot be selected
49
+ # @param disabled_days_of_week [Array<Integer>] Days of week to disable (0=Sun, 6=Sat)
50
+ # @param show_outside_days [Boolean] Whether to show days outside current month
51
+ # @param mode [Symbol] Selection mode: :single, :multiple, or :range
52
+ # @param required [Boolean] Whether a selection is required (prevents deselection)
53
+ # @param week_starts_on [Integer] First day of week (0=Sunday, 1=Monday, etc.)
54
+ def initialize(
55
+ selected: nil,
56
+ month: nil,
57
+ min_date: nil,
58
+ max_date: nil,
59
+ name: nil,
60
+ disabled_dates: [],
61
+ disabled_days_of_week: [],
62
+ show_outside_days: true,
63
+ mode: :single,
64
+ required: false,
65
+ week_starts_on: 0,
66
+ **options
67
+ )
68
+ super(**options)
69
+ @selected = selected
70
+ @month = month || (selected.is_a?(Array) ? selected.first : selected) || Date.today
71
+ @min_date = min_date
72
+ @max_date = max_date
73
+ @name = name
74
+ @disabled_dates = disabled_dates
75
+ @disabled_days_of_week = disabled_days_of_week
76
+ @show_outside_days = show_outside_days
77
+ @mode = mode.to_sym
78
+ @required = required
79
+ @week_starts_on = week_starts_on
80
+ end
81
+
82
+ def call
83
+ content_tag(:div, calendar_content, **calendar_attributes)
84
+ end
85
+
86
+ private
87
+
88
+ def calendar_content
89
+ safe_join([
90
+ hidden_input,
91
+ header,
92
+ weekday_header,
93
+ days_grid
94
+ ].compact)
95
+ end
96
+
97
+ def hidden_input
98
+ return unless @name
99
+
100
+ tag.input(
101
+ type: "hidden",
102
+ name: @name,
103
+ value: @selected&.iso8601,
104
+ data: { "shadcn--calendar-target": "hiddenInput" }
105
+ )
106
+ end
107
+
108
+ def header
109
+ content_tag(:div, class: HEADER_CLASSES) do
110
+ safe_join([
111
+ prev_button,
112
+ month_year_selectors,
113
+ next_button
114
+ ])
115
+ end
116
+ end
117
+
118
+ def prev_button
119
+ content_tag(:button,
120
+ chevron_left_icon,
121
+ type: "button",
122
+ class: NAV_BUTTON_CLASSES,
123
+ "aria-label": "Previous month",
124
+ data: { action: "click->shadcn--calendar#previousMonth" }
125
+ )
126
+ end
127
+
128
+ def next_button
129
+ content_tag(:button,
130
+ chevron_right_icon,
131
+ type: "button",
132
+ class: NAV_BUTTON_CLASSES,
133
+ "aria-label": "Next month",
134
+ data: { action: "click->shadcn--calendar#nextMonth" }
135
+ )
136
+ end
137
+
138
+ def month_year_selectors
139
+ content_tag(:div, class: "flex items-center gap-1") do
140
+ safe_join([
141
+ month_select,
142
+ year_select
143
+ ])
144
+ end
145
+ end
146
+
147
+ def month_select
148
+ content_tag(:select,
149
+ class: cn(
150
+ "appearance-none bg-transparent text-sm font-medium cursor-pointer",
151
+ "hover:bg-accent hover:text-accent-foreground rounded px-2 py-1",
152
+ "focus:outline-none focus:ring-1 focus:ring-ring"
153
+ ),
154
+ data: {
155
+ "shadcn--calendar-target": "monthSelect",
156
+ action: "change->shadcn--calendar#selectMonth"
157
+ }
158
+ ) do
159
+ safe_join(MONTHS.each_with_index.map { |month, index|
160
+ content_tag(:option, month, value: index, selected: index == @month.month - 1)
161
+ })
162
+ end
163
+ end
164
+
165
+ def year_select
166
+ # Generate year range: current year -10 to +10
167
+ current_year = @month.year
168
+ year_range = (current_year - 10)..(current_year + 10)
169
+
170
+ content_tag(:select,
171
+ class: cn(
172
+ "appearance-none bg-transparent text-sm font-medium cursor-pointer",
173
+ "hover:bg-accent hover:text-accent-foreground rounded px-2 py-1",
174
+ "focus:outline-none focus:ring-1 focus:ring-ring"
175
+ ),
176
+ data: {
177
+ "shadcn--calendar-target": "yearSelect",
178
+ action: "change->shadcn--calendar#selectYear"
179
+ }
180
+ ) do
181
+ safe_join(year_range.map { |year|
182
+ content_tag(:option, year, value: year, selected: year == current_year)
183
+ })
184
+ end
185
+ end
186
+
187
+ def weekday_header
188
+ content_tag(:div, class: "grid grid-cols-7 gap-1 mb-2") do
189
+ safe_join(rotated_weekdays.map { |day| content_tag(:div, day, class: WEEKDAY_CLASSES) })
190
+ end
191
+ end
192
+
193
+ def rotated_weekdays
194
+ WEEKDAYS.rotate(@week_starts_on)
195
+ end
196
+
197
+ def days_grid
198
+ content_tag(:div, class: "grid grid-cols-7 gap-1", data: { "shadcn--calendar-target": "grid" }) do
199
+ safe_join(calendar_days.map { |day| render_day(day) })
200
+ end
201
+ end
202
+
203
+ def calendar_days
204
+ first_day = @month.beginning_of_month
205
+ last_day = @month.end_of_month
206
+
207
+ # Get the week start symbol from the mapping (defaults to :sunday)
208
+ week_start_symbol = WEEK_START_SYMBOLS[@week_starts_on] || :sunday
209
+
210
+ # Get the starting day based on week_starts_on
211
+ start_date = first_day.beginning_of_week(week_start_symbol)
212
+ # Get the ending day based on week_starts_on
213
+ end_date = last_day.end_of_week(week_start_symbol)
214
+
215
+ (start_date..end_date).to_a
216
+ end
217
+
218
+ def render_day(date)
219
+ is_outside = date.month != @month.month
220
+ is_selected = @selected && date == @selected
221
+ is_today = date == Date.today
222
+ is_disabled = date_disabled?(date)
223
+
224
+ return empty_day if is_outside && !@show_outside_days
225
+
226
+ classes = [DAY_CLASSES]
227
+ classes << DAY_SELECTED_CLASSES if is_selected
228
+ classes << DAY_TODAY_CLASSES if is_today && !is_selected
229
+ classes << DAY_OUTSIDE_CLASSES if is_outside
230
+ classes << DAY_DISABLED_CLASSES if is_disabled
231
+
232
+ content_tag(:button,
233
+ date.day.to_s,
234
+ type: "button",
235
+ class: cn(*classes),
236
+ tabindex: is_disabled ? "-1" : "0",
237
+ "aria-selected": is_selected || nil,
238
+ "aria-disabled": is_disabled || nil,
239
+ disabled: is_disabled || nil,
240
+ data: {
241
+ date: date.iso8601,
242
+ "shadcn--calendar-target": "day",
243
+ action: is_disabled ? nil : "click->shadcn--calendar#selectDay"
244
+ }.compact
245
+ )
246
+ end
247
+
248
+ def empty_day
249
+ content_tag(:div, "", class: "h-8 w-8")
250
+ end
251
+
252
+ def date_disabled?(date)
253
+ return true if @min_date && date < @min_date
254
+ return true if @max_date && date > @max_date
255
+ return true if @disabled_dates.include?(date)
256
+ return true if @disabled_days_of_week.include?(date.wday)
257
+
258
+ false
259
+ end
260
+
261
+ def chevron_left_icon
262
+ content_tag(:svg,
263
+ xmlns: "http://www.w3.org/2000/svg",
264
+ width: "16",
265
+ height: "16",
266
+ viewBox: "0 0 24 24",
267
+ fill: "none",
268
+ stroke: "currentColor",
269
+ "stroke-width": "2",
270
+ "stroke-linecap": "round",
271
+ "stroke-linejoin": "round"
272
+ ) do
273
+ tag.path(d: "m15 18-6-6 6-6")
274
+ end
275
+ end
276
+
277
+ def chevron_right_icon
278
+ content_tag(:svg,
279
+ xmlns: "http://www.w3.org/2000/svg",
280
+ width: "16",
281
+ height: "16",
282
+ viewBox: "0 0 24 24",
283
+ fill: "none",
284
+ stroke: "currentColor",
285
+ "stroke-width": "2",
286
+ "stroke-linecap": "round",
287
+ "stroke-linejoin": "round"
288
+ ) do
289
+ tag.path(d: "m9 18 6-6-6-6")
290
+ end
291
+ end
292
+
293
+ def calendar_attributes
294
+ {
295
+ class: cn(CONTAINER_CLASSES, class_name),
296
+ role: "grid",
297
+ "aria-label": "Calendar",
298
+ data: stimulus_data
299
+ }.merge(html_options).merge(build_data)
300
+ end
301
+
302
+ def stimulus_data
303
+ data = {
304
+ controller: "shadcn--calendar",
305
+ "shadcn--calendar-month-value": @month.iso8601,
306
+ "shadcn--calendar-selected-value": format_selected_value,
307
+ "shadcn--calendar-mode-value": @mode.to_s,
308
+ "shadcn--calendar-required-value": @required.to_s,
309
+ "shadcn--calendar-week-starts-on-value": @week_starts_on.to_s,
310
+ "shadcn--calendar-show-outside-days-value": @show_outside_days.to_s
311
+ }
312
+
313
+ # Add optional values only if present
314
+ data["shadcn--calendar-min-date-value"] = @min_date.iso8601 if @min_date
315
+ data["shadcn--calendar-max-date-value"] = @max_date.iso8601 if @max_date
316
+ data["shadcn--calendar-disabled-dates-value"] = format_disabled_dates if @disabled_dates.any?
317
+ data["shadcn--calendar-disabled-days-of-week-value"] = @disabled_days_of_week.join(",") if @disabled_days_of_week.any?
318
+
319
+ data
320
+ end
321
+
322
+ def format_selected_value
323
+ return nil unless @selected
324
+
325
+ case @selected
326
+ when Array
327
+ @selected.map(&:iso8601).join(",")
328
+ else
329
+ @selected.iso8601
330
+ end
331
+ end
332
+
333
+ def format_disabled_dates
334
+ @disabled_dates.map(&:iso8601).join(",")
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Card Action component
5
+ class CardActionComponent < BaseComponent
6
+ def call
7
+ content_tag(:div, content, **html_options)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Card component with header, content, and footer slots
5
+ # Matches shadcn/ui Card component
6
+ #
7
+ # @example Basic card
8
+ # <%= render Shadcn::CardComponent.new do |card| %>
9
+ # <% card.with_header do %>
10
+ # <% card.with_title { "Card Title" } %>
11
+ # <% card.with_description { "Card description" } %>
12
+ # <% end %>
13
+ # <% card.with_content do %>
14
+ # Card content goes here
15
+ # <% end %>
16
+ # <% card.with_footer do %>
17
+ # <button>Action</button>
18
+ # <% end %>
19
+ # <% end %>
20
+ #
21
+ class CardComponent < BaseComponent
22
+ # Card header slot
23
+ renders_one :header, lambda { |**options, &block|
24
+ CardHeaderComponent.new(**options, &block)
25
+ }
26
+
27
+ # Card title slot (can be used inside or outside header)
28
+ renders_one :title, lambda { |**options, &block|
29
+ CardTitleComponent.new(**options, &block)
30
+ }
31
+
32
+ # Card description slot
33
+ renders_one :description, lambda { |**options, &block|
34
+ CardDescriptionComponent.new(**options, &block)
35
+ }
36
+
37
+ # Card content slot
38
+ # Note: Named content_slot because 'content' is a reserved ViewComponent method
39
+ renders_one :content_slot, lambda { |**options, &block|
40
+ CardContentComponent.new(**options, &block)
41
+ }
42
+
43
+ # Alias for more intuitive API: card.with_content instead of card.with_content_slot
44
+ alias_method :with_content, :with_content_slot
45
+
46
+ # Card footer slot
47
+ renders_one :footer, lambda { |**options, &block|
48
+ CardFooterComponent.new(**options, &block)
49
+ }
50
+
51
+ BASE_CLASSES = "rounded-xl border bg-card text-card-foreground shadow"
52
+
53
+ def call
54
+ content_tag(:div, card_content, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data))
55
+ end
56
+
57
+ private
58
+
59
+ def card_content
60
+ safe_join([header, title, description, content_slot, content, footer].compact)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Card Content component
5
+ class CardContentComponent < BaseComponent
6
+ BASE_CLASSES = "p-6"
7
+
8
+ # @param standalone [Boolean] Whether the content is standalone (no header above)
9
+ def initialize(standalone: false, **options, &block)
10
+ super(**options, &block)
11
+ @standalone = standalone
12
+ end
13
+
14
+ def call
15
+ classes = @standalone ? BASE_CLASSES : "#{BASE_CLASSES} pt-0"
16
+ content_tag(:div, content, class: merge_classes(classes), **html_options)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Card Description component
5
+ class CardDescriptionComponent < BaseComponent
6
+ BASE_CLASSES = "text-sm text-muted-foreground"
7
+
8
+ def call
9
+ content_tag(:p, content, class: merge_classes(BASE_CLASSES), **html_options)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Card Footer component
5
+ class CardFooterComponent < BaseComponent
6
+ BASE_CLASSES = "flex items-center p-6 pt-0"
7
+
8
+ def call
9
+ content_tag(:div, content, class: merge_classes(BASE_CLASSES), **html_options)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Card Header component
5
+ class CardHeaderComponent < BaseComponent
6
+ BASE_CLASSES = "flex flex-col space-y-1.5 p-6"
7
+
8
+ renders_one :title, lambda { |**options|
9
+ CardTitleComponent.new(**options)
10
+ }
11
+
12
+ renders_one :description, lambda { |**options|
13
+ CardDescriptionComponent.new(**options)
14
+ }
15
+
16
+ renders_one :action, "CardActionComponent"
17
+
18
+ def call
19
+ content_tag(:div, class: merge_classes(BASE_CLASSES), **html_options) do
20
+ safe_join([title, description, action, content].compact)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Card Title component
5
+ class CardTitleComponent < BaseComponent
6
+ BASE_CLASSES = "font-semibold leading-none tracking-tight"
7
+
8
+ # @param tag [Symbol] HTML tag to use (default: :h3)
9
+ def initialize(tag: :h3, **options, &block)
10
+ super(**options, &block)
11
+ @tag = tag
12
+ end
13
+
14
+ def call
15
+ content_tag(@tag, content, class: merge_classes(BASE_CLASSES), **html_options)
16
+ end
17
+ end
18
+ end