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,282 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Date Picker controller
5
+ * Handles opening/closing the calendar popover and date selection
6
+ *
7
+ * API inspired by React DayPicker (https://daypicker.dev/)
8
+ *
9
+ * Disabled dates:
10
+ * - minDate/maxDate: Disable dates outside a range
11
+ * - disabledDates: Comma-separated list of YYYY-MM-DD dates
12
+ * - disabledDaysOfWeek: Comma-separated list of day numbers (0=Sun, 6=Sat)
13
+ */
14
+ export default class extends Controller {
15
+ static targets = ["trigger", "content", "grid", "monthYear", "day", "displayValue", "hiddenInput"]
16
+ static values = {
17
+ open: { type: Boolean, default: false },
18
+ month: String,
19
+ selected: String,
20
+ format: { type: String, default: "medium" },
21
+ placeholder: { type: String, default: "Pick a date" },
22
+ minDate: String,
23
+ maxDate: String,
24
+ disabledDates: String, // comma-separated YYYY-MM-DD
25
+ disabledDaysOfWeek: String, // comma-separated 0-6
26
+ showOutsideDays: { type: Boolean, default: true },
27
+ weekStartsOn: { type: Number, default: 0 } // 0 = Sunday, 1 = Monday, etc.
28
+ }
29
+
30
+ static MONTHS = [
31
+ "January", "February", "March", "April", "May", "June",
32
+ "July", "August", "September", "October", "November", "December"
33
+ ]
34
+
35
+ connect() {
36
+ this.currentMonth = this.monthValue ? this.parseLocalDate(this.monthValue) : new Date()
37
+ this.selectedDate = this.selectedValue ? this.parseLocalDate(this.selectedValue) : null
38
+ }
39
+
40
+ /**
41
+ * Parse a date string (YYYY-MM-DD) as local date, not UTC
42
+ * This prevents timezone issues where "2024-11-26" becomes Nov 25 in western timezones
43
+ */
44
+ parseLocalDate(dateStr) {
45
+ if (!dateStr) return null
46
+ const [year, month, day] = dateStr.split('-').map(Number)
47
+ return new Date(year, month - 1, day)
48
+ }
49
+
50
+ /**
51
+ * Format a date as YYYY-MM-DD using local date components
52
+ */
53
+ formatDateString(date) {
54
+ if (!date) return ''
55
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
56
+ }
57
+
58
+ /**
59
+ * Check if a date is disabled
60
+ */
61
+ isDateDisabled(date) {
62
+ const dateStr = this.formatDateString(date)
63
+
64
+ // Check min/max date
65
+ if (this.minDateValue) {
66
+ const minDate = this.parseLocalDate(this.minDateValue)
67
+ if (date < minDate) return true
68
+ }
69
+ if (this.maxDateValue) {
70
+ const maxDate = this.parseLocalDate(this.maxDateValue)
71
+ if (date > maxDate) return true
72
+ }
73
+
74
+ // Check disabled dates list
75
+ if (this.disabledDatesValue) {
76
+ const disabledDates = this.disabledDatesValue.split(",").map(d => d.trim())
77
+ if (disabledDates.includes(dateStr)) return true
78
+ }
79
+
80
+ // Check disabled days of week
81
+ if (this.disabledDaysOfWeekValue) {
82
+ const disabledDays = this.disabledDaysOfWeekValue.split(",").map(d => parseInt(d.trim(), 10))
83
+ if (disabledDays.includes(date.getDay())) return true
84
+ }
85
+
86
+ return false
87
+ }
88
+
89
+ toggle() {
90
+ this.openValue = !this.openValue
91
+ }
92
+
93
+ open() {
94
+ this.openValue = true
95
+ }
96
+
97
+ close() {
98
+ this.openValue = false
99
+ }
100
+
101
+ openValueChanged() {
102
+ if (this.hasContentTarget) {
103
+ this.contentTarget.style.display = this.openValue ? "block" : "none"
104
+ }
105
+ if (this.hasTriggerTarget) {
106
+ this.triggerTarget.setAttribute("aria-expanded", this.openValue.toString())
107
+ }
108
+ }
109
+
110
+ closeOnClickOutside(event) {
111
+ if (!this.openValue) return
112
+ if (this.element.contains(event.target)) return
113
+
114
+ this.close()
115
+ }
116
+
117
+ previousMonth() {
118
+ this.currentMonth.setMonth(this.currentMonth.getMonth() - 1)
119
+ this.render()
120
+ }
121
+
122
+ nextMonth() {
123
+ this.currentMonth.setMonth(this.currentMonth.getMonth() + 1)
124
+ this.render()
125
+ }
126
+
127
+ selectDay(event) {
128
+ const dateStr = event.currentTarget.dataset.date
129
+ if (!dateStr) return
130
+
131
+ const date = this.parseLocalDate(dateStr)
132
+
133
+ // Check if disabled
134
+ if (this.isDateDisabled(date)) return
135
+
136
+ this.selectedDate = date
137
+ this.selectedValue = dateStr
138
+
139
+ // Update hidden input
140
+ if (this.hasHiddenInputTarget) {
141
+ this.hiddenInputTarget.value = dateStr
142
+ }
143
+
144
+ // Update display value
145
+ if (this.hasDisplayValueTarget) {
146
+ this.displayValueTarget.textContent = this.formatDate(this.selectedDate)
147
+ this.displayValueTarget.classList.remove("text-muted-foreground")
148
+ }
149
+
150
+ // Re-render calendar to update selection styling
151
+ this.render()
152
+
153
+ // Close the popover
154
+ this.close()
155
+
156
+ // Dispatch custom event
157
+ this.dispatch("select", {
158
+ detail: {
159
+ date: this.selectedDate,
160
+ dateString: dateStr
161
+ }
162
+ })
163
+ }
164
+
165
+ formatDate(date) {
166
+ switch (this.formatValue) {
167
+ case "short":
168
+ return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()}`
169
+ case "long":
170
+ return date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })
171
+ case "iso":
172
+ // Use local date components to avoid timezone issues
173
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
174
+ case "medium":
175
+ default:
176
+ return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })
177
+ }
178
+ }
179
+
180
+ render() {
181
+ // Update month/year label
182
+ if (this.hasMonthYearTarget) {
183
+ const monthName = this.constructor.MONTHS[this.currentMonth.getMonth()]
184
+ const year = this.currentMonth.getFullYear()
185
+ this.monthYearTarget.textContent = `${monthName} ${year}`
186
+ }
187
+
188
+ // Render days grid
189
+ if (this.hasGridTarget) {
190
+ this.gridTarget.innerHTML = this.renderDays()
191
+ }
192
+ }
193
+
194
+ renderDays() {
195
+ const year = this.currentMonth.getFullYear()
196
+ const month = this.currentMonth.getMonth()
197
+
198
+ // Get first and last day of month
199
+ const firstDay = new Date(year, month, 1)
200
+ const lastDay = new Date(year, month + 1, 0)
201
+
202
+ // Get start date based on weekStartsOn
203
+ const startDate = new Date(firstDay)
204
+ const dayOffset = (firstDay.getDay() - this.weekStartsOnValue + 7) % 7
205
+ startDate.setDate(firstDay.getDate() - dayOffset)
206
+
207
+ // Get end date (complete the last week)
208
+ const endDate = new Date(lastDay)
209
+ const endDayOffset = (6 - lastDay.getDay() + this.weekStartsOnValue) % 7
210
+ endDate.setDate(lastDay.getDate() + endDayOffset)
211
+
212
+ const today = new Date()
213
+ today.setHours(0, 0, 0, 0)
214
+
215
+ let html = ""
216
+ const currentDate = new Date(startDate)
217
+
218
+ while (currentDate <= endDate) {
219
+ const isOutside = currentDate.getMonth() !== month
220
+ const isToday = currentDate.getTime() === today.getTime()
221
+ const isSelected = this.selectedDate &&
222
+ currentDate.toDateString() === this.selectedDate.toDateString()
223
+ const isDisabled = this.isDateDisabled(currentDate)
224
+
225
+ // Use local date components to avoid timezone issues with toISOString()
226
+ const dateStr = this.formatDateString(currentDate)
227
+
228
+ // Skip outside days if showOutsideDays is false
229
+ if (isOutside && !this.showOutsideDaysValue) {
230
+ html += '<div class="h-8 w-8"></div>'
231
+ currentDate.setDate(currentDate.getDate() + 1)
232
+ continue
233
+ }
234
+
235
+ let classes = "h-8 w-8 text-center text-sm p-0 relative flex items-center justify-center rounded-md focus:outline-none focus:ring-1 focus:ring-ring"
236
+
237
+ if (isDisabled) {
238
+ classes += " text-muted-foreground opacity-50 cursor-not-allowed"
239
+ } else if (isSelected) {
240
+ classes += " bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground cursor-pointer"
241
+ } else if (isToday) {
242
+ classes += " bg-accent text-accent-foreground cursor-pointer hover:bg-accent hover:text-accent-foreground"
243
+ } else {
244
+ classes += " cursor-pointer hover:bg-accent hover:text-accent-foreground"
245
+ }
246
+
247
+ if (isOutside && !isDisabled) {
248
+ classes += " text-muted-foreground opacity-50"
249
+ }
250
+
251
+ const ariaAttrs = []
252
+ if (isSelected) ariaAttrs.push('aria-selected="true"')
253
+ if (isDisabled) {
254
+ ariaAttrs.push('aria-disabled="true"')
255
+ ariaAttrs.push('disabled')
256
+ }
257
+
258
+ // Only add click action for non-disabled days
259
+ const dataAction = isDisabled
260
+ ? ''
261
+ : 'data-action="click->shadcn--date-picker#selectDay"'
262
+
263
+ html += `<button type="button" class="${classes}" data-date="${dateStr}" data-shadcn--date-picker-target="day" ${dataAction} ${ariaAttrs.join(" ")}>${currentDate.getDate()}</button>`
264
+
265
+ currentDate.setDate(currentDate.getDate() + 1)
266
+ }
267
+
268
+ return html
269
+ }
270
+
271
+ monthValueChanged() {
272
+ if (this.monthValue) {
273
+ this.currentMonth = this.parseLocalDate(this.monthValue)
274
+ }
275
+ }
276
+
277
+ selectedValueChanged() {
278
+ if (this.selectedValue) {
279
+ this.selectedDate = this.parseLocalDate(this.selectedValue)
280
+ }
281
+ }
282
+ }
@@ -0,0 +1,67 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Dialog controller for modal dialogs
5
+ * Handles opening, closing, focus trapping, and keyboard navigation
6
+ */
7
+ export default class DialogController extends Controller {
8
+ static targets: ["trigger", "template", "overlay", "content"];
9
+ static values: {
10
+ open: { type: "Boolean"; default: false };
11
+ modal: { type: "Boolean"; default: true };
12
+ };
13
+
14
+ /** Dialog trigger target */
15
+ readonly triggerTarget: HTMLElement;
16
+ readonly hasTriggerTarget: boolean;
17
+
18
+ /** Template containing dialog content */
19
+ readonly templateTarget: HTMLTemplateElement;
20
+ readonly hasTemplateTarget: boolean;
21
+
22
+ /** Dialog overlay target */
23
+ readonly overlayTarget: HTMLElement;
24
+ readonly hasOverlayTarget: boolean;
25
+
26
+ /** Dialog content target */
27
+ readonly contentTarget: HTMLElement;
28
+ readonly hasContentTarget: boolean;
29
+
30
+ /** Whether the dialog is open */
31
+ openValue: boolean;
32
+ readonly hasOpenValue: boolean;
33
+
34
+ /** Whether the dialog is modal (traps focus, prevents body scroll) */
35
+ modalValue: boolean;
36
+ readonly hasModalValue: boolean;
37
+
38
+ /** Portal element (created dynamically) */
39
+ portal: HTMLDivElement | null;
40
+
41
+ /** Previously focused element */
42
+ previousActiveElement: Element | null;
43
+
44
+ /** Open the dialog */
45
+ open(): void;
46
+
47
+ /** Close the dialog */
48
+ close(): void;
49
+
50
+ /** Toggle dialog open/closed */
51
+ toggle(): void;
52
+
53
+ /** Handle keydown events (Escape to close, Tab for focus trapping) */
54
+ handleKeydown(event: KeyboardEvent): void;
55
+
56
+ /** Handle clicks outside the dialog content */
57
+ handleClickOutside(event: MouseEvent): void;
58
+
59
+ /** Focus the first focusable element in the dialog */
60
+ focusFirstElement(): void;
61
+
62
+ /** Trap focus within the dialog */
63
+ trapFocus(event: KeyboardEvent): void;
64
+
65
+ /** Called when openValue changes */
66
+ openValueChanged(): void;
67
+ }
@@ -0,0 +1,187 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Dialog controller for modal dialogs
5
+ * Handles opening, closing, focus trapping, and keyboard navigation
6
+ */
7
+ export default class extends Controller {
8
+ static targets = ["trigger", "template", "overlay", "content"]
9
+ static values = {
10
+ open: { type: Boolean, default: false },
11
+ modal: { type: Boolean, default: true }
12
+ }
13
+
14
+ connect() {
15
+ this.portal = null
16
+ this.previousActiveElement = null
17
+ this.boundHandleKeydown = this.handleKeydown.bind(this)
18
+ this.boundHandleClickOutside = this.handleClickOutside.bind(this)
19
+
20
+ if (this.openValue) {
21
+ this.open()
22
+ }
23
+ }
24
+
25
+ disconnect() {
26
+ this.close()
27
+ if (this.portal) {
28
+ this.portal.remove()
29
+ }
30
+ }
31
+
32
+ open() {
33
+ if (this.openValue) return
34
+
35
+ this.previousActiveElement = document.activeElement
36
+ this.openValue = true
37
+
38
+ // Move template content to body
39
+ if (this.hasTemplateTarget && !this.portal) {
40
+ this.portal = document.createElement("div")
41
+ this.portal.className = "shadcn-dialog-portal"
42
+ this.portal.innerHTML = this.templateTarget.innerHTML
43
+ document.body.appendChild(this.portal)
44
+
45
+ // Re-query targets from portal
46
+ this.portalOverlay = this.portal.querySelector('[data-shadcn--dialog-target="overlay"]')
47
+ this.portalContent = this.portal.querySelector('[data-shadcn--dialog-target="content"]')
48
+
49
+ // Wire up close actions on portal elements (since they're outside controller scope)
50
+ this.portal.querySelectorAll('[data-action*="shadcn--dialog#close"]').forEach(el => {
51
+ el.addEventListener("click", (e) => {
52
+ e.preventDefault()
53
+ this.close()
54
+ })
55
+ })
56
+
57
+ // Also handle overlay click
58
+ if (this.portalOverlay) {
59
+ this.portalOverlay.addEventListener("click", () => this.close())
60
+ }
61
+ }
62
+
63
+ // Show overlay and content
64
+ requestAnimationFrame(() => {
65
+ if (this.portalOverlay) {
66
+ this.portalOverlay.dataset.state = "open"
67
+ this.portalOverlay.removeAttribute("hidden")
68
+ }
69
+ if (this.portalContent) {
70
+ this.portalContent.dataset.state = "open"
71
+ this.portalContent.removeAttribute("hidden")
72
+ }
73
+
74
+ // Setup event listeners
75
+ document.addEventListener("keydown", this.boundHandleKeydown)
76
+
77
+ // Focus first focusable element
78
+ this.focusFirstElement()
79
+
80
+ // Prevent body scroll
81
+ if (this.modalValue) {
82
+ document.body.style.overflow = "hidden"
83
+ }
84
+ })
85
+
86
+ this.dispatch("opened")
87
+ }
88
+
89
+ close() {
90
+ if (!this.openValue) return
91
+
92
+ this.openValue = false
93
+
94
+ if (this.portalOverlay) {
95
+ this.portalOverlay.dataset.state = "closed"
96
+ }
97
+ if (this.portalContent) {
98
+ this.portalContent.dataset.state = "closed"
99
+ }
100
+
101
+ // Remove event listeners
102
+ document.removeEventListener("keydown", this.boundHandleKeydown)
103
+
104
+ // Restore body scroll
105
+ document.body.style.overflow = ""
106
+
107
+ // Return focus
108
+ if (this.previousActiveElement) {
109
+ this.previousActiveElement.focus()
110
+ }
111
+
112
+ // Remove portal after animation
113
+ setTimeout(() => {
114
+ if (this.portal) {
115
+ this.portal.remove()
116
+ this.portal = null
117
+ }
118
+ }, 200)
119
+
120
+ this.dispatch("closed")
121
+ }
122
+
123
+ toggle() {
124
+ if (this.openValue) {
125
+ this.close()
126
+ } else {
127
+ this.open()
128
+ }
129
+ }
130
+
131
+ handleKeydown(event) {
132
+ if (event.key === "Escape") {
133
+ this.close()
134
+ } else if (event.key === "Tab" && this.modalValue) {
135
+ this.trapFocus(event)
136
+ }
137
+ }
138
+
139
+ handleClickOutside(event) {
140
+ if (this.portalContent && !this.portalContent.contains(event.target)) {
141
+ this.close()
142
+ }
143
+ }
144
+
145
+ focusFirstElement() {
146
+ if (!this.portalContent) return
147
+
148
+ const focusable = this.portalContent.querySelectorAll(
149
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
150
+ )
151
+ if (focusable.length > 0) {
152
+ focusable[0].focus()
153
+ } else {
154
+ this.portalContent.focus()
155
+ }
156
+ }
157
+
158
+ trapFocus(event) {
159
+ if (!this.portalContent) return
160
+
161
+ const focusable = this.portalContent.querySelectorAll(
162
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
163
+ )
164
+ const firstFocusable = focusable[0]
165
+ const lastFocusable = focusable[focusable.length - 1]
166
+
167
+ if (event.shiftKey) {
168
+ if (document.activeElement === firstFocusable) {
169
+ lastFocusable.focus()
170
+ event.preventDefault()
171
+ }
172
+ } else {
173
+ if (document.activeElement === lastFocusable) {
174
+ firstFocusable.focus()
175
+ event.preventDefault()
176
+ }
177
+ }
178
+ }
179
+
180
+ openValueChanged() {
181
+ if (this.openValue) {
182
+ this.open()
183
+ } else {
184
+ this.close()
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,58 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Drawer Controller
5
+ * Handles opening/closing drawer panels
6
+ */
7
+ export default class DrawerController extends Controller {
8
+ static targets: ["trigger", "template", "overlay", "content"];
9
+ static values: {
10
+ open: { type: "Boolean"; default: false };
11
+ direction: { type: "String"; default: "bottom" };
12
+ };
13
+
14
+ /** Drawer trigger target */
15
+ readonly triggerTarget: HTMLElement;
16
+ readonly hasTriggerTarget: boolean;
17
+
18
+ /** Template containing drawer content */
19
+ readonly templateTarget: HTMLTemplateElement;
20
+ readonly hasTemplateTarget: boolean;
21
+
22
+ /** Drawer overlay target */
23
+ readonly overlayTarget: HTMLElement;
24
+ readonly hasOverlayTarget: boolean;
25
+
26
+ /** Drawer content target */
27
+ readonly contentTarget: HTMLElement;
28
+ readonly hasContentTarget: boolean;
29
+
30
+ /** Whether the drawer is open */
31
+ openValue: boolean;
32
+ readonly hasOpenValue: boolean;
33
+
34
+ /** Direction the drawer slides from: "top", "right", "bottom", "left" */
35
+ directionValue: "top" | "right" | "bottom" | "left";
36
+ readonly hasDirectionValue: boolean;
37
+
38
+ /** Portal element (created dynamically) */
39
+ portal: HTMLDivElement | null;
40
+
41
+ /** Open the drawer */
42
+ open(): void;
43
+
44
+ /** Close the drawer */
45
+ close(): void;
46
+
47
+ /** Toggle drawer open/closed */
48
+ toggle(): void;
49
+
50
+ /** Handle keydown events (Escape to close) */
51
+ handleKeydown(event: KeyboardEvent): void;
52
+
53
+ /** Remove the portal from the DOM */
54
+ removePortal(): void;
55
+
56
+ /** Called when openValue changes */
57
+ openValueChanged(): void;
58
+ }
@@ -0,0 +1,112 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Drawer Controller
5
+ * Handles opening/closing drawer panels with swipe support
6
+ */
7
+ export default class extends Controller {
8
+ static targets = ["trigger", "template", "overlay", "content"]
9
+ static values = {
10
+ open: { type: Boolean, default: false },
11
+ direction: { type: String, default: "bottom" }
12
+ }
13
+
14
+ connect() {
15
+ this.portal = null
16
+ this.boundHandleKeydown = this.handleKeydown.bind(this)
17
+
18
+ if (this.openValue) {
19
+ this.open()
20
+ }
21
+ }
22
+
23
+ disconnect() {
24
+ this.removePortal()
25
+ document.removeEventListener("keydown", this.boundHandleKeydown)
26
+ }
27
+
28
+ open() {
29
+ if (!this.hasTemplateTarget) return
30
+
31
+ // Create portal at body level
32
+ this.portal = document.createElement("div")
33
+ this.portal.innerHTML = this.templateTarget.innerHTML
34
+ document.body.appendChild(this.portal)
35
+
36
+ // Get references to portal elements
37
+ const overlay = this.portal.querySelector("[data-shadcn--drawer-target='overlay']")
38
+ const content = this.portal.querySelector("[data-shadcn--drawer-target='content']")
39
+
40
+ // Add click handler to overlay
41
+ if (overlay) {
42
+ overlay.addEventListener("click", () => this.close())
43
+ }
44
+
45
+ // Set open state
46
+ requestAnimationFrame(() => {
47
+ if (overlay) overlay.setAttribute("data-state", "open")
48
+ if (content) {
49
+ content.setAttribute("data-state", "open")
50
+ content.focus()
51
+ }
52
+ })
53
+
54
+ // Prevent body scroll
55
+ document.body.style.overflow = "hidden"
56
+ document.addEventListener("keydown", this.boundHandleKeydown)
57
+
58
+ this.openValue = true
59
+ this.dispatch("open")
60
+ }
61
+
62
+ close() {
63
+ if (!this.portal) return
64
+
65
+ const overlay = this.portal.querySelector("[data-shadcn--drawer-target='overlay']")
66
+ const content = this.portal.querySelector("[data-shadcn--drawer-target='content']")
67
+
68
+ // Set closing state
69
+ if (overlay) overlay.setAttribute("data-state", "closed")
70
+ if (content) content.setAttribute("data-state", "closed")
71
+
72
+ // Wait for animation then remove portal
73
+ setTimeout(() => {
74
+ this.removePortal()
75
+ }, 200)
76
+
77
+ document.body.style.overflow = ""
78
+ document.removeEventListener("keydown", this.boundHandleKeydown)
79
+
80
+ this.openValue = false
81
+ this.dispatch("close")
82
+ }
83
+
84
+ toggle() {
85
+ if (this.openValue) {
86
+ this.close()
87
+ } else {
88
+ this.open()
89
+ }
90
+ }
91
+
92
+ handleKeydown(event) {
93
+ if (event.key === "Escape") {
94
+ this.close()
95
+ }
96
+ }
97
+
98
+ removePortal() {
99
+ if (this.portal) {
100
+ this.portal.remove()
101
+ this.portal = null
102
+ }
103
+ }
104
+
105
+ openValueChanged() {
106
+ if (this.openValue && !this.portal) {
107
+ this.open()
108
+ } else if (!this.openValue && this.portal) {
109
+ this.close()
110
+ }
111
+ }
112
+ }