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,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Context Menu component
5
+ # Matches shadcn/ui ContextMenu component
6
+ # Displays a menu when the user right-clicks on an element
7
+ #
8
+ # @example Basic context menu
9
+ # <%= render Shadcn::ContextMenuComponent.new do |menu| %>
10
+ # <% menu.with_trigger do %>
11
+ # <div class="border border-dashed p-8 text-center">
12
+ # Right click here
13
+ # </div>
14
+ # <% end %>
15
+ # <% menu.with_menu do |content| %>
16
+ # <% content.with_item(href: "#") { "Back" } %>
17
+ # <% content.with_item(href: "#", disabled: true) { "Forward" } %>
18
+ # <% content.with_item(href: "#") { "Reload" } %>
19
+ # <% content.with_separator %>
20
+ # <% content.with_item(href: "#") { "Save As..." } %>
21
+ # <% content.with_item(href: "#") { "Print" } %>
22
+ # <% end %>
23
+ # <% end %>
24
+ #
25
+ class ContextMenuComponent < BaseComponent
26
+ renders_one :trigger
27
+ renders_one :menu, lambda { |**options|
28
+ ContextMenuContentComponent.new(**options)
29
+ }
30
+
31
+ def call
32
+ content_tag(:div, context_menu_content, context_menu_attributes)
33
+ end
34
+
35
+ private
36
+
37
+ def context_menu_content
38
+ safe_join([
39
+ trigger_wrapper,
40
+ menu
41
+ ].compact)
42
+ end
43
+
44
+ def trigger_wrapper
45
+ return unless trigger
46
+
47
+ content_tag(:div, trigger, {
48
+ "data-shadcn--context-menu-target": "trigger",
49
+ "data-action": "contextmenu->shadcn--context-menu#show:prevent"
50
+ })
51
+ end
52
+
53
+ def context_menu_attributes
54
+ attrs = {
55
+ class: cn("relative inline-block", class_name),
56
+ "data-controller": "shadcn--context-menu",
57
+ "data-action": "keydown.escape->shadcn--context-menu#close"
58
+ }
59
+ attrs.merge!(html_options)
60
+ attrs.merge!(build_data)
61
+ attrs.compact
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Context Menu Content component
5
+ class ContextMenuContentComponent < BaseComponent
6
+ BASE_CLASSES = "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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"
7
+
8
+ renders_many :items, lambda { |**options, &block|
9
+ ContextMenuItemComponent.new(**options, &block)
10
+ }
11
+ renders_many :labels, lambda { |**options, &block|
12
+ ContextMenuLabelComponent.new(**options, &block)
13
+ }
14
+ renders_many :separators, lambda { |**options|
15
+ ContextMenuSeparatorComponent.new(**options)
16
+ }
17
+
18
+ def call
19
+ content_tag(:div, menu_content, menu_attributes)
20
+ end
21
+
22
+ private
23
+
24
+ def menu_content
25
+ if items.any? || labels.any? || separators.any?
26
+ safe_join([labels, items, separators, content].flatten.compact)
27
+ else
28
+ content
29
+ end
30
+ end
31
+
32
+ def menu_attributes
33
+ {
34
+ class: merge_classes(BASE_CLASSES),
35
+ role: "menu",
36
+ "aria-orientation": "vertical",
37
+ "data-shadcn--context-menu-target": "content",
38
+ "data-state": "closed",
39
+ style: "position: fixed;",
40
+ hidden: true
41
+ }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Context Menu Item component
5
+ class ContextMenuItemComponent < BaseComponent
6
+ BASE_CLASSES = "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0"
7
+
8
+ VARIANTS = {
9
+ default: "",
10
+ destructive: "text-destructive focus:bg-destructive focus:text-destructive-foreground"
11
+ }.freeze
12
+
13
+ renders_one :shortcut, lambda { |**options|
14
+ ContextMenuShortcutComponent.new(**options)
15
+ }
16
+
17
+ # @param href [String, nil] Link URL
18
+ # @param variant [Symbol] Item variant (:default, :destructive)
19
+ # @param disabled [Boolean] Whether item is disabled
20
+ # @param inset [Boolean] Whether to add left padding for icons
21
+ def initialize(href: nil, variant: :default, disabled: false, inset: false, **options, &block)
22
+ super(**options, &block)
23
+ @href = href
24
+ @variant = variant.to_sym
25
+ @disabled = disabled
26
+ @inset = inset
27
+ end
28
+
29
+ def call
30
+ tag_name = @href ? :a : :div
31
+ content_tag(tag_name, item_content, item_attributes)
32
+ end
33
+
34
+ private
35
+
36
+ def item_content
37
+ safe_join([content, shortcut].compact)
38
+ end
39
+
40
+ def item_classes
41
+ cn(
42
+ BASE_CLASSES,
43
+ VARIANTS[@variant],
44
+ @inset ? "pl-8" : "",
45
+ class_name
46
+ )
47
+ end
48
+
49
+ def item_attributes
50
+ attrs = {
51
+ class: item_classes,
52
+ role: "menuitem",
53
+ tabindex: @disabled ? nil : "-1",
54
+ href: @href,
55
+ "data-disabled": @disabled ? "" : nil,
56
+ "data-action": "click->shadcn--context-menu#selectItem"
57
+ }
58
+ attrs.merge!(html_options)
59
+ attrs.merge!(build_data)
60
+ attrs.compact
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Context Menu Label component
5
+ class ContextMenuLabelComponent < BaseComponent
6
+ BASE_CLASSES = "px-2 py-1.5 text-sm font-semibold"
7
+
8
+ # @param inset [Boolean] Whether to add left padding
9
+ def initialize(inset: false, **options, &block)
10
+ super(**options, &block)
11
+ @inset = inset
12
+ end
13
+
14
+ def call
15
+ content_tag(:div, content, class: cn(BASE_CLASSES, @inset ? "pl-8" : "", class_name))
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Context Menu Separator component
5
+ class ContextMenuSeparatorComponent < BaseComponent
6
+ BASE_CLASSES = "-mx-1 my-1 h-px bg-muted"
7
+
8
+ def call
9
+ content_tag(:div, "", class: merge_classes(BASE_CLASSES), role: "separator")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Context Menu Shortcut component
5
+ class ContextMenuShortcutComponent < BaseComponent
6
+ BASE_CLASSES = "ml-auto text-xs tracking-widest opacity-60"
7
+
8
+ def call
9
+ content_tag(:span, content, class: merge_classes(BASE_CLASSES))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,368 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Date Picker component - a button that opens a calendar popover
5
+ # Composition of Popover and Calendar components
6
+ # Matches shadcn/ui Date Picker component
7
+ #
8
+ # @example Basic date picker
9
+ # <%= render Shadcn::DatePickerComponent.new %>
10
+ #
11
+ # @example With selected date
12
+ # <%= render Shadcn::DatePickerComponent.new(selected: Date.today) %>
13
+ #
14
+ # @example With name for form submission
15
+ # <%= render Shadcn::DatePickerComponent.new(name: "event[date]") %>
16
+ #
17
+ # @example With date constraints
18
+ # <%= render Shadcn::DatePickerComponent.new(
19
+ # min_date: Date.today,
20
+ # max_date: Date.today + 30.days
21
+ # ) %>
22
+ #
23
+ class DatePickerComponent < BaseComponent
24
+ TRIGGER_CLASSES = "inline-flex items-center justify-start whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2 w-[240px] pl-3 text-left"
25
+ PLACEHOLDER_CLASSES = "text-muted-foreground"
26
+ CONTENT_CLASSES = "p-0 w-auto"
27
+
28
+ MONTHS = %w[January February March April May June July August September October November December].freeze
29
+
30
+ # @param selected [Date, nil] Currently selected date
31
+ # @param month [Date, nil] Month to display (defaults to current month)
32
+ # @param min_date [Date, nil] Minimum selectable date
33
+ # @param max_date [Date, nil] Maximum selectable date
34
+ # @param name [String, nil] Form field name for hidden input
35
+ # @param disabled_dates [Array<Date>] Specific dates that cannot be selected
36
+ # @param disabled_days_of_week [Array<Integer>] Days of week to disable (0=Sun, 6=Sat)
37
+ # @param show_outside_days [Boolean] Whether to show days outside current month
38
+ # @param week_starts_on [Integer] First day of week (0=Sunday, 1=Monday, etc.)
39
+ # @param placeholder [String] Placeholder text when no date selected
40
+ # @param format [Symbol] Date format (:short, :medium, :long)
41
+ # @param disabled [Boolean] Whether the date picker is disabled
42
+ def initialize(
43
+ selected: nil,
44
+ month: nil,
45
+ min_date: nil,
46
+ max_date: nil,
47
+ name: nil,
48
+ disabled_dates: [],
49
+ disabled_days_of_week: [],
50
+ show_outside_days: true,
51
+ week_starts_on: 0,
52
+ placeholder: "Pick a date",
53
+ format: :medium,
54
+ disabled: false,
55
+ **options
56
+ )
57
+ super(**options)
58
+ @selected = selected
59
+ @month = month || selected || Date.today
60
+ @min_date = min_date
61
+ @max_date = max_date
62
+ @name = name
63
+ @disabled_dates = disabled_dates
64
+ @disabled_days_of_week = disabled_days_of_week
65
+ @show_outside_days = show_outside_days
66
+ @week_starts_on = week_starts_on
67
+ @placeholder = placeholder
68
+ @format = format
69
+ @disabled = disabled
70
+ end
71
+
72
+ def call
73
+ content_tag(:div, picker_content, picker_attributes)
74
+ end
75
+
76
+ private
77
+
78
+ def picker_content
79
+ safe_join([
80
+ hidden_input,
81
+ trigger_button,
82
+ popover_content
83
+ ].compact)
84
+ end
85
+
86
+ def hidden_input
87
+ return unless @name
88
+
89
+ tag.input(
90
+ type: "hidden",
91
+ name: @name,
92
+ value: @selected&.iso8601,
93
+ data: { "shadcn--date-picker-target": "hiddenInput" }
94
+ )
95
+ end
96
+
97
+ def trigger_button
98
+ content_tag(:button,
99
+ trigger_content,
100
+ type: "button",
101
+ class: cn(TRIGGER_CLASSES, ("cursor-not-allowed opacity-50" if @disabled)),
102
+ disabled: @disabled || nil,
103
+ "aria-haspopup": "dialog",
104
+ "aria-expanded": "false",
105
+ data: {
106
+ "shadcn--date-picker-target": "trigger",
107
+ action: "click->shadcn--date-picker#toggle"
108
+ }
109
+ )
110
+ end
111
+
112
+ def trigger_content
113
+ safe_join([
114
+ calendar_icon,
115
+ date_display
116
+ ])
117
+ end
118
+
119
+ def calendar_icon
120
+ content_tag(:svg,
121
+ xmlns: "http://www.w3.org/2000/svg",
122
+ width: "16",
123
+ height: "16",
124
+ viewBox: "0 0 24 24",
125
+ fill: "none",
126
+ stroke: "currentColor",
127
+ "stroke-width": "2",
128
+ "stroke-linecap": "round",
129
+ "stroke-linejoin": "round",
130
+ class: "mr-2 h-4 w-4"
131
+ ) do
132
+ safe_join([
133
+ tag.path(d: "M8 2v4"),
134
+ tag.path(d: "M16 2v4"),
135
+ tag.rect(width: "18", height: "18", x: "3", y: "4", rx: "2"),
136
+ tag.path(d: "M3 10h18")
137
+ ])
138
+ end
139
+ end
140
+
141
+ def date_display
142
+ if @selected
143
+ content_tag(:span, format_date(@selected), data: { "shadcn--date-picker-target": "displayValue" })
144
+ else
145
+ content_tag(:span, @placeholder, class: PLACEHOLDER_CLASSES, data: { "shadcn--date-picker-target": "displayValue" })
146
+ end
147
+ end
148
+
149
+ def format_date(date)
150
+ case @format
151
+ when :short
152
+ date.strftime("%m/%d/%Y")
153
+ when :medium
154
+ date.strftime("%B %-d, %Y") # e.g., "November 26, 2024"
155
+ when :long
156
+ date.strftime("%A, %B %-d, %Y") # e.g., "Tuesday, November 26, 2024"
157
+ when :iso
158
+ date.iso8601
159
+ else
160
+ date.strftime("%B %-d, %Y")
161
+ end
162
+ end
163
+
164
+ def popover_content
165
+ content_tag(:div,
166
+ calendar,
167
+ class: cn("absolute z-50 mt-1 rounded-md border bg-popover shadow-md", CONTENT_CLASSES),
168
+ "data-shadcn--date-picker-target": "content",
169
+ style: "display: none;"
170
+ )
171
+ end
172
+
173
+ def calendar
174
+ content_tag(:div, calendar_content, calendar_attributes)
175
+ end
176
+
177
+ def calendar_content
178
+ safe_join([
179
+ calendar_header,
180
+ weekday_header,
181
+ days_grid
182
+ ])
183
+ end
184
+
185
+ def calendar_header
186
+ content_tag(:div, class: "flex items-center justify-between p-3 pb-0") do
187
+ safe_join([
188
+ prev_month_button,
189
+ month_year_label,
190
+ next_month_button
191
+ ])
192
+ end
193
+ end
194
+
195
+ def prev_month_button
196
+ content_tag(:button,
197
+ chevron_left_icon,
198
+ type: "button",
199
+ class: "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",
200
+ "aria-label": "Previous month",
201
+ data: { action: "click->shadcn--date-picker#previousMonth" }
202
+ )
203
+ end
204
+
205
+ def next_month_button
206
+ content_tag(:button,
207
+ chevron_right_icon,
208
+ type: "button",
209
+ class: "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",
210
+ "aria-label": "Next month",
211
+ data: { action: "click->shadcn--date-picker#nextMonth" }
212
+ )
213
+ end
214
+
215
+ def month_year_label
216
+ content_tag(:div,
217
+ "#{MONTHS[@month.month - 1]} #{@month.year}",
218
+ class: "text-sm font-medium",
219
+ data: { "shadcn--date-picker-target": "monthYear" }
220
+ )
221
+ end
222
+
223
+ def weekday_header
224
+ content_tag(:div, class: "grid grid-cols-7 gap-1 p-3 pb-0") do
225
+ safe_join(CalendarComponent::WEEKDAYS.map do |day|
226
+ content_tag(:div, day, class: "text-center text-xs font-medium text-muted-foreground")
227
+ end)
228
+ end
229
+ end
230
+
231
+ def days_grid
232
+ content_tag(:div, class: "grid grid-cols-7 gap-1 p-3", data: { "shadcn--date-picker-target": "grid" }) do
233
+ safe_join(calendar_days.map { |day| render_day(day) })
234
+ end
235
+ end
236
+
237
+ def calendar_days
238
+ first_day = @month.beginning_of_month
239
+ last_day = @month.end_of_month
240
+
241
+ start_date = first_day.beginning_of_week(:sunday)
242
+ end_date = last_day.end_of_week(:sunday)
243
+
244
+ (start_date..end_date).to_a
245
+ end
246
+
247
+ def render_day(date)
248
+ is_outside = date.month != @month.month
249
+ is_selected = @selected && date == @selected
250
+ is_today = date == Date.today
251
+ is_disabled = date_disabled?(date)
252
+
253
+ return empty_day if is_outside && !@show_outside_days
254
+
255
+ 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"]
256
+ classes << "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground" if is_selected
257
+ classes << "bg-accent text-accent-foreground" if is_today && !is_selected
258
+ classes << "text-muted-foreground opacity-50" if is_outside
259
+ classes << "text-muted-foreground opacity-50 pointer-events-none" if is_disabled
260
+
261
+ content_tag(:button,
262
+ date.day.to_s,
263
+ type: "button",
264
+ class: cn(*classes),
265
+ tabindex: is_disabled ? "-1" : "0",
266
+ "aria-selected": is_selected || nil,
267
+ "aria-disabled": is_disabled || nil,
268
+ disabled: is_disabled || nil,
269
+ data: {
270
+ date: date.iso8601,
271
+ "shadcn--date-picker-target": "day",
272
+ action: is_disabled ? nil : "click->shadcn--date-picker#selectDay"
273
+ }.compact
274
+ )
275
+ end
276
+
277
+ def empty_day
278
+ content_tag(:div, "", class: "h-8 w-8")
279
+ end
280
+
281
+ def date_disabled?(date)
282
+ return true if @min_date && date < @min_date
283
+ return true if @max_date && date > @max_date
284
+ return true if @disabled_dates.include?(date)
285
+
286
+ false
287
+ end
288
+
289
+ def chevron_left_icon
290
+ content_tag(:svg,
291
+ xmlns: "http://www.w3.org/2000/svg",
292
+ width: "16",
293
+ height: "16",
294
+ viewBox: "0 0 24 24",
295
+ fill: "none",
296
+ stroke: "currentColor",
297
+ "stroke-width": "2",
298
+ "stroke-linecap": "round",
299
+ "stroke-linejoin": "round"
300
+ ) do
301
+ tag.path(d: "m15 18-6-6 6-6")
302
+ end
303
+ end
304
+
305
+ def chevron_right_icon
306
+ content_tag(:svg,
307
+ xmlns: "http://www.w3.org/2000/svg",
308
+ width: "16",
309
+ height: "16",
310
+ viewBox: "0 0 24 24",
311
+ fill: "none",
312
+ stroke: "currentColor",
313
+ "stroke-width": "2",
314
+ "stroke-linecap": "round",
315
+ "stroke-linejoin": "round"
316
+ ) do
317
+ tag.path(d: "m9 18 6-6-6-6")
318
+ end
319
+ end
320
+
321
+ def calendar_attributes
322
+ {
323
+ class: "p-0",
324
+ role: "grid",
325
+ "aria-label": "Calendar",
326
+ data: {
327
+ "shadcn--date-picker-month-value": @month.iso8601,
328
+ "shadcn--date-picker-selected-value": @selected&.iso8601,
329
+ "shadcn--date-picker-format-value": @format.to_s,
330
+ "shadcn--date-picker-placeholder-value": @placeholder
331
+ }
332
+ }
333
+ end
334
+
335
+ def picker_attributes
336
+ {
337
+ class: cn("relative inline-block", class_name),
338
+ data: stimulus_data
339
+ }.merge(html_options).merge(build_data)
340
+ end
341
+
342
+ def stimulus_data
343
+ data = {
344
+ controller: "shadcn--date-picker",
345
+ "shadcn--date-picker-open-value": "false",
346
+ "shadcn--date-picker-month-value": @month.iso8601,
347
+ "shadcn--date-picker-selected-value": @selected&.iso8601,
348
+ "shadcn--date-picker-format-value": @format.to_s,
349
+ "shadcn--date-picker-placeholder-value": @placeholder,
350
+ "shadcn--date-picker-show-outside-days-value": @show_outside_days.to_s,
351
+ "shadcn--date-picker-week-starts-on-value": @week_starts_on.to_s,
352
+ action: "keydown.escape->shadcn--date-picker#close click@window->shadcn--date-picker#closeOnClickOutside"
353
+ }
354
+
355
+ # Add optional values only if present
356
+ data["shadcn--date-picker-min-date-value"] = @min_date.iso8601 if @min_date
357
+ data["shadcn--date-picker-max-date-value"] = @max_date.iso8601 if @max_date
358
+ data["shadcn--date-picker-disabled-dates-value"] = format_disabled_dates if @disabled_dates.any?
359
+ data["shadcn--date-picker-disabled-days-of-week-value"] = @disabled_days_of_week.join(",") if @disabled_days_of_week.any?
360
+
361
+ data
362
+ end
363
+
364
+ def format_disabled_dates
365
+ @disabled_dates.map(&:iso8601).join(",")
366
+ end
367
+ end
368
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dialog component for modal windows
5
+ # Matches shadcn/ui Dialog component
6
+ # Uses Stimulus for interactivity
7
+ #
8
+ # @example Basic dialog
9
+ # <%= render Shadcn::DialogComponent.new do |dialog| %>
10
+ # <% dialog.with_trigger do %>
11
+ # <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Dialog" } %>
12
+ # <% end %>
13
+ # <% dialog.with_content do |content| %>
14
+ # <% content.with_header do %>
15
+ # <% content.with_title { "Dialog Title" } %>
16
+ # <% content.with_description { "Dialog description here." } %>
17
+ # <% end %>
18
+ # <p>Dialog body content</p>
19
+ # <% content.with_footer do %>
20
+ # <%= render Shadcn::ButtonComponent.new { "Save" } %>
21
+ # <% end %>
22
+ # <% end %>
23
+ # <% end %>
24
+ #
25
+ class DialogComponent < BaseComponent
26
+ renders_one :trigger
27
+ renders_one :body, lambda { |**options|
28
+ DialogContentComponent.new(**options)
29
+ }
30
+
31
+ # @param id [String] Unique identifier for the dialog (used for Turbo Stream targeting)
32
+ # @param open [Boolean] Whether dialog starts open
33
+ # @param modal [Boolean] Whether dialog is modal (traps focus, blocks interaction)
34
+ def initialize(id: nil, open: false, modal: true, **options)
35
+ super(**options)
36
+ @id = id
37
+ @open = open
38
+ @modal = modal
39
+ end
40
+
41
+ def call
42
+ content_tag(:div, dialog_content, dialog_attributes)
43
+ end
44
+
45
+ private
46
+
47
+ def dialog_content
48
+ safe_join([
49
+ trigger_wrapper,
50
+ body
51
+ ].compact)
52
+ end
53
+
54
+ def trigger_wrapper
55
+ return unless trigger
56
+
57
+ content_tag(:div, trigger, {
58
+ "data-shadcn--dialog-target": "trigger",
59
+ "data-action": "click->shadcn--dialog#open"
60
+ })
61
+ end
62
+
63
+ def dialog_attributes
64
+ attrs = {
65
+ id: @id,
66
+ class: class_name,
67
+ "data-controller": "shadcn--dialog",
68
+ "data-shadcn--dialog-open-value": @open.to_s,
69
+ "data-shadcn--dialog-modal-value": @modal.to_s,
70
+ "data-dialog-id": @id
71
+ }
72
+ attrs.merge!(html_options)
73
+ attrs.merge!(build_data)
74
+ attrs.compact
75
+ end
76
+ end
77
+ end