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,592 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Calendar controller for date picker
5
+ * Handles month navigation, date selection, and rendering
6
+ *
7
+ * API inspired by React DayPicker (https://daypicker.dev/)
8
+ *
9
+ * Selection modes:
10
+ * - single: Select one date at a time (default)
11
+ * - multiple: Select multiple individual dates
12
+ * - range: Select a date range (start to end)
13
+ *
14
+ * Disabled dates:
15
+ * - minDate/maxDate: Disable dates outside a range
16
+ * - disabledDates: Comma-separated list of YYYY-MM-DD dates
17
+ * - disabledDaysOfWeek: Comma-separated list of day numbers (0=Sun, 6=Sat)
18
+ */
19
+ export default class extends Controller {
20
+ static targets = ["grid", "monthYear", "monthSelect", "yearSelect", "day", "hiddenInput"]
21
+ static values = {
22
+ month: String,
23
+ selected: String,
24
+ mode: { type: String, default: "single" }, // single, multiple, range
25
+ minDate: String,
26
+ maxDate: String,
27
+ disabledDates: String, // comma-separated YYYY-MM-DD
28
+ disabledDaysOfWeek: String, // comma-separated 0-6
29
+ required: { type: Boolean, default: false },
30
+ weekStartsOn: { type: Number, default: 0 }, // 0 = Sunday, 1 = Monday, etc.
31
+ showOutsideDays: { type: Boolean, default: true } // Show days from prev/next month
32
+ }
33
+
34
+ static MONTHS = [
35
+ "January", "February", "March", "April", "May", "June",
36
+ "July", "August", "September", "October", "November", "December"
37
+ ]
38
+ static WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
39
+
40
+ connect() {
41
+ this.currentMonth = this.monthValue ? this.parseLocalDate(this.monthValue) : new Date()
42
+ this.initializeSelection()
43
+ this.focusedDate = null
44
+ this.boundHandleKeydown = this.handleKeydown.bind(this)
45
+ }
46
+
47
+ disconnect() {
48
+ document.removeEventListener("keydown", this.boundHandleKeydown)
49
+ }
50
+
51
+ /**
52
+ * Initialize selected date(s) based on mode
53
+ */
54
+ initializeSelection() {
55
+ if (!this.selectedValue) {
56
+ this.selectedDate = this.modeValue === "multiple" ? [] : null
57
+ this.rangeStart = null
58
+ this.rangeEnd = null
59
+ return
60
+ }
61
+
62
+ switch (this.modeValue) {
63
+ case "multiple":
64
+ this.selectedDate = this.selectedValue.split(",").map(d => this.parseLocalDate(d.trim())).filter(Boolean)
65
+ break
66
+ case "range":
67
+ const [start, end] = this.selectedValue.split(",").map(d => this.parseLocalDate(d.trim()))
68
+ this.rangeStart = start || null
69
+ this.rangeEnd = end || null
70
+ this.selectedDate = null
71
+ break
72
+ default:
73
+ this.selectedDate = this.parseLocalDate(this.selectedValue)
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Parse a date string (YYYY-MM-DD) as local date, not UTC
79
+ * This prevents timezone issues where "2024-11-26" becomes Nov 25 in western timezones
80
+ */
81
+ parseLocalDate(dateStr) {
82
+ if (!dateStr) return null
83
+ const [year, month, day] = dateStr.split('-').map(Number)
84
+ return new Date(year, month - 1, day)
85
+ }
86
+
87
+ /**
88
+ * Format a date as YYYY-MM-DD using local date components
89
+ */
90
+ formatDateString(date) {
91
+ if (!date) return ''
92
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
93
+ }
94
+
95
+ /**
96
+ * Check if a date is disabled
97
+ */
98
+ isDateDisabled(date) {
99
+ const dateStr = this.formatDateString(date)
100
+
101
+ // Check min/max date
102
+ if (this.minDateValue) {
103
+ const minDate = this.parseLocalDate(this.minDateValue)
104
+ if (date < minDate) return true
105
+ }
106
+ if (this.maxDateValue) {
107
+ const maxDate = this.parseLocalDate(this.maxDateValue)
108
+ if (date > maxDate) return true
109
+ }
110
+
111
+ // Check disabled dates list
112
+ if (this.disabledDatesValue) {
113
+ const disabledDates = this.disabledDatesValue.split(",").map(d => d.trim())
114
+ if (disabledDates.includes(dateStr)) return true
115
+ }
116
+
117
+ // Check disabled days of week
118
+ if (this.disabledDaysOfWeekValue) {
119
+ const disabledDays = this.disabledDaysOfWeekValue.split(",").map(d => parseInt(d.trim(), 10))
120
+ if (disabledDays.includes(date.getDay())) return true
121
+ }
122
+
123
+ return false
124
+ }
125
+
126
+ /**
127
+ * Check if a date is selected
128
+ */
129
+ isDateSelected(date) {
130
+ if (this.modeValue === "multiple" && Array.isArray(this.selectedDate)) {
131
+ return this.selectedDate.some(d => d.toDateString() === date.toDateString())
132
+ }
133
+ if (this.modeValue === "range") {
134
+ if (this.rangeStart && date.toDateString() === this.rangeStart.toDateString()) return true
135
+ if (this.rangeEnd && date.toDateString() === this.rangeEnd.toDateString()) return true
136
+ return false
137
+ }
138
+ return this.selectedDate && date.toDateString() === this.selectedDate.toDateString()
139
+ }
140
+
141
+ /**
142
+ * Check if a date is in range (for range mode)
143
+ */
144
+ isDateInRange(date) {
145
+ if (this.modeValue !== "range" || !this.rangeStart || !this.rangeEnd) return false
146
+ return date > this.rangeStart && date < this.rangeEnd
147
+ }
148
+
149
+ /**
150
+ * Check if date is the start of a range
151
+ */
152
+ isRangeStart(date) {
153
+ if (this.modeValue !== "range" || !this.rangeStart) return false
154
+ return date.toDateString() === this.rangeStart.toDateString()
155
+ }
156
+
157
+ /**
158
+ * Check if date is the end of a range
159
+ */
160
+ isRangeEnd(date) {
161
+ if (this.modeValue !== "range" || !this.rangeEnd) return false
162
+ return date.toDateString() === this.rangeEnd.toDateString()
163
+ }
164
+
165
+ previousMonth() {
166
+ this.currentMonth.setMonth(this.currentMonth.getMonth() - 1)
167
+ this.render()
168
+ }
169
+
170
+ nextMonth() {
171
+ this.currentMonth.setMonth(this.currentMonth.getMonth() + 1)
172
+ this.render()
173
+ }
174
+
175
+ selectMonth(event) {
176
+ const month = parseInt(event.target.value, 10)
177
+ this.currentMonth.setMonth(month)
178
+ this.render()
179
+ }
180
+
181
+ selectYear(event) {
182
+ const year = parseInt(event.target.value, 10)
183
+ this.currentMonth.setFullYear(year)
184
+ this.render()
185
+ }
186
+
187
+ selectDay(event) {
188
+ const dateStr = event.currentTarget.dataset.date
189
+ if (!dateStr) return
190
+
191
+ const date = this.parseLocalDate(dateStr)
192
+
193
+ // Check if disabled
194
+ if (this.isDateDisabled(date)) return
195
+
196
+ switch (this.modeValue) {
197
+ case "multiple":
198
+ this.handleMultipleSelection(date, dateStr)
199
+ break
200
+ case "range":
201
+ this.handleRangeSelection(date, dateStr)
202
+ break
203
+ default:
204
+ this.handleSingleSelection(date, dateStr)
205
+ }
206
+
207
+ // Re-render to update selection styling
208
+ this.render()
209
+ }
210
+
211
+ handleSingleSelection(date, dateStr) {
212
+ // If required is true, don't allow deselection
213
+ if (this.requiredValue && this.selectedDate && date.toDateString() === this.selectedDate.toDateString()) {
214
+ return
215
+ }
216
+
217
+ // Toggle selection if already selected
218
+ if (this.selectedDate && date.toDateString() === this.selectedDate.toDateString()) {
219
+ this.selectedDate = null
220
+ this.selectedValue = ""
221
+ if (this.hasHiddenInputTarget) {
222
+ this.hiddenInputTarget.value = ""
223
+ }
224
+ this.dispatchSelectEvent(null, "")
225
+ return
226
+ }
227
+
228
+ this.selectedDate = date
229
+ this.selectedValue = dateStr
230
+
231
+ if (this.hasHiddenInputTarget) {
232
+ this.hiddenInputTarget.value = dateStr
233
+ }
234
+
235
+ this.dispatchSelectEvent(date, dateStr)
236
+ }
237
+
238
+ handleMultipleSelection(date, dateStr) {
239
+ const index = this.selectedDate.findIndex(d => d.toDateString() === date.toDateString())
240
+
241
+ if (index >= 0) {
242
+ // Deselect if required allows it
243
+ if (!this.requiredValue || this.selectedDate.length > 1) {
244
+ this.selectedDate.splice(index, 1)
245
+ }
246
+ } else {
247
+ this.selectedDate.push(date)
248
+ }
249
+
250
+ const dateStrings = this.selectedDate.map(d => this.formatDateString(d))
251
+ this.selectedValue = dateStrings.join(",")
252
+
253
+ if (this.hasHiddenInputTarget) {
254
+ this.hiddenInputTarget.value = this.selectedValue
255
+ }
256
+
257
+ this.dispatch("select", {
258
+ detail: {
259
+ dates: this.selectedDate,
260
+ dateStrings: dateStrings
261
+ }
262
+ })
263
+ }
264
+
265
+ handleRangeSelection(date, dateStr) {
266
+ // If no start, set start
267
+ if (!this.rangeStart) {
268
+ this.rangeStart = date
269
+ this.rangeEnd = null
270
+ this.selectedValue = dateStr
271
+ }
272
+ // If start exists but no end, set end (ensure start < end)
273
+ else if (!this.rangeEnd) {
274
+ if (date < this.rangeStart) {
275
+ this.rangeEnd = this.rangeStart
276
+ this.rangeStart = date
277
+ } else if (date.toDateString() === this.rangeStart.toDateString()) {
278
+ // Clicking same date resets
279
+ this.rangeStart = null
280
+ this.selectedValue = ""
281
+ } else {
282
+ this.rangeEnd = date
283
+ }
284
+ this.selectedValue = this.rangeStart
285
+ ? `${this.formatDateString(this.rangeStart)}${this.rangeEnd ? `,${this.formatDateString(this.rangeEnd)}` : ""}`
286
+ : ""
287
+ }
288
+ // If both exist, start new selection
289
+ else {
290
+ this.rangeStart = date
291
+ this.rangeEnd = null
292
+ this.selectedValue = dateStr
293
+ }
294
+
295
+ if (this.hasHiddenInputTarget) {
296
+ this.hiddenInputTarget.value = this.selectedValue
297
+ }
298
+
299
+ this.dispatch("select", {
300
+ detail: {
301
+ rangeStart: this.rangeStart,
302
+ rangeEnd: this.rangeEnd,
303
+ dateString: this.selectedValue
304
+ }
305
+ })
306
+ }
307
+
308
+ dispatchSelectEvent(date, dateStr) {
309
+ this.dispatch("select", {
310
+ detail: {
311
+ date: date,
312
+ dateString: dateStr
313
+ }
314
+ })
315
+ }
316
+
317
+ /**
318
+ * Handle keyboard navigation
319
+ */
320
+ handleKeydown(event) {
321
+ if (!this.focusedDate) {
322
+ this.focusedDate = this.getInitialFocusDate()
323
+ }
324
+
325
+ let newDate = new Date(this.focusedDate)
326
+ let handled = false
327
+
328
+ switch (event.key) {
329
+ case "ArrowLeft":
330
+ newDate.setDate(newDate.getDate() - 1)
331
+ handled = true
332
+ break
333
+ case "ArrowRight":
334
+ newDate.setDate(newDate.getDate() + 1)
335
+ handled = true
336
+ break
337
+ case "ArrowUp":
338
+ newDate.setDate(newDate.getDate() - 7)
339
+ handled = true
340
+ break
341
+ case "ArrowDown":
342
+ newDate.setDate(newDate.getDate() + 7)
343
+ handled = true
344
+ break
345
+ case "PageUp":
346
+ if (event.shiftKey) {
347
+ newDate.setFullYear(newDate.getFullYear() - 1)
348
+ } else {
349
+ newDate.setMonth(newDate.getMonth() - 1)
350
+ }
351
+ handled = true
352
+ break
353
+ case "PageDown":
354
+ if (event.shiftKey) {
355
+ newDate.setFullYear(newDate.getFullYear() + 1)
356
+ } else {
357
+ newDate.setMonth(newDate.getMonth() + 1)
358
+ }
359
+ handled = true
360
+ break
361
+ case "Home":
362
+ newDate.setDate(1)
363
+ handled = true
364
+ break
365
+ case "End":
366
+ newDate = new Date(newDate.getFullYear(), newDate.getMonth() + 1, 0)
367
+ handled = true
368
+ break
369
+ case "Enter":
370
+ case " ":
371
+ if (!this.isDateDisabled(this.focusedDate)) {
372
+ this.selectDay({
373
+ currentTarget: {
374
+ dataset: { date: this.formatDateString(this.focusedDate) }
375
+ }
376
+ })
377
+ }
378
+ handled = true
379
+ break
380
+ }
381
+
382
+ if (handled) {
383
+ event.preventDefault()
384
+
385
+ // Skip disabled dates when navigating
386
+ while (this.isDateDisabled(newDate)) {
387
+ const direction = event.key.includes("Left") || event.key.includes("Up") ? -1 : 1
388
+ newDate.setDate(newDate.getDate() + direction)
389
+ }
390
+
391
+ this.focusedDate = newDate
392
+
393
+ // Update current month if focused date is in different month
394
+ if (newDate.getMonth() !== this.currentMonth.getMonth() ||
395
+ newDate.getFullYear() !== this.currentMonth.getFullYear()) {
396
+ this.currentMonth = new Date(newDate.getFullYear(), newDate.getMonth(), 1)
397
+ }
398
+
399
+ this.render()
400
+ this.focusDay(newDate)
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Focus a specific day button
406
+ */
407
+ focusDay(date) {
408
+ const dateStr = this.formatDateString(date)
409
+ const dayButton = this.element.querySelector(`[data-date="${dateStr}"]`)
410
+ if (dayButton) {
411
+ dayButton.focus()
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Enable keyboard navigation when calendar gets focus
417
+ */
418
+ enableKeyboard() {
419
+ document.addEventListener("keydown", this.boundHandleKeydown)
420
+ if (!this.focusedDate) {
421
+ this.focusedDate = this.getInitialFocusDate()
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Get an initial focus date based on selection mode
427
+ */
428
+ getInitialFocusDate() {
429
+ switch (this.modeValue) {
430
+ case "multiple":
431
+ // For multiple mode, use first selected date or today
432
+ return (Array.isArray(this.selectedDate) && this.selectedDate.length > 0)
433
+ ? this.selectedDate[0]
434
+ : new Date()
435
+ case "range":
436
+ // For range mode, use range start or today
437
+ return this.rangeStart || new Date()
438
+ default:
439
+ // For single mode, use selected date or today
440
+ return this.selectedDate || new Date()
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Disable keyboard navigation
446
+ */
447
+ disableKeyboard() {
448
+ document.removeEventListener("keydown", this.boundHandleKeydown)
449
+ }
450
+
451
+ render() {
452
+ // Update month/year label (for backwards compatibility)
453
+ if (this.hasMonthYearTarget) {
454
+ const monthName = this.constructor.MONTHS[this.currentMonth.getMonth()]
455
+ const year = this.currentMonth.getFullYear()
456
+ this.monthYearTarget.textContent = `${monthName} ${year}`
457
+ }
458
+
459
+ // Update month select
460
+ if (this.hasMonthSelectTarget) {
461
+ this.monthSelectTarget.value = this.currentMonth.getMonth()
462
+ }
463
+
464
+ // Update year select
465
+ if (this.hasYearSelectTarget) {
466
+ this.yearSelectTarget.value = this.currentMonth.getFullYear()
467
+ }
468
+
469
+ // Render days grid
470
+ if (this.hasGridTarget) {
471
+ this.gridTarget.innerHTML = this.renderDays()
472
+ }
473
+ }
474
+
475
+ renderDays() {
476
+ const year = this.currentMonth.getFullYear()
477
+ const month = this.currentMonth.getMonth()
478
+
479
+ // Get first and last day of month
480
+ const firstDay = new Date(year, month, 1)
481
+ const lastDay = new Date(year, month + 1, 0)
482
+
483
+ // Get start date based on weekStartsOn
484
+ const startDate = new Date(firstDay)
485
+ const dayOffset = (firstDay.getDay() - this.weekStartsOnValue + 7) % 7
486
+ startDate.setDate(firstDay.getDate() - dayOffset)
487
+
488
+ // Get end date (complete the last week)
489
+ const endDate = new Date(lastDay)
490
+ const endDayOffset = (6 - lastDay.getDay() + this.weekStartsOnValue) % 7
491
+ endDate.setDate(lastDay.getDate() + endDayOffset)
492
+
493
+ const today = new Date()
494
+ today.setHours(0, 0, 0, 0)
495
+
496
+ let html = ""
497
+ const currentDate = new Date(startDate)
498
+
499
+ while (currentDate <= endDate) {
500
+ const isOutside = currentDate.getMonth() !== month
501
+ const isToday = currentDate.getTime() === today.getTime()
502
+ const isSelected = this.isDateSelected(currentDate)
503
+ const isInRange = this.isDateInRange(currentDate)
504
+ const isRangeStart = this.isRangeStart(currentDate)
505
+ const isRangeEnd = this.isRangeEnd(currentDate)
506
+ const isDisabled = this.isDateDisabled(currentDate)
507
+ const isFocused = this.focusedDate && currentDate.toDateString() === this.focusedDate.toDateString()
508
+
509
+ const dateStr = this.formatDateString(currentDate)
510
+
511
+ // Skip outside days if showOutsideDays is false
512
+ if (isOutside && !this.showOutsideDaysValue) {
513
+ html += '<div class="h-8 w-8"></div>'
514
+ currentDate.setDate(currentDate.getDate() + 1)
515
+ continue
516
+ }
517
+
518
+ let classes = "h-8 w-8 text-center text-sm p-0 relative flex items-center justify-center focus:outline-none focus:ring-1 focus:ring-ring"
519
+
520
+ // Range styling
521
+ if (isInRange) {
522
+ classes += " bg-accent/50"
523
+ }
524
+ if (isRangeStart) {
525
+ classes += " rounded-l-md"
526
+ }
527
+ if (isRangeEnd) {
528
+ classes += " rounded-r-md"
529
+ }
530
+ if (!isRangeStart && !isRangeEnd && !isInRange) {
531
+ classes += " rounded-md"
532
+ }
533
+
534
+ // Selection and state styling
535
+ if (isDisabled) {
536
+ classes += " text-muted-foreground opacity-50 cursor-not-allowed"
537
+ } else if (isSelected) {
538
+ classes += " bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground cursor-pointer"
539
+ } else if (isToday && !isInRange) {
540
+ classes += " bg-accent text-accent-foreground cursor-pointer hover:bg-accent hover:text-accent-foreground"
541
+ } else if (!isInRange) {
542
+ classes += " cursor-pointer hover:bg-accent hover:text-accent-foreground"
543
+ } else {
544
+ classes += " cursor-pointer hover:bg-accent hover:text-accent-foreground"
545
+ }
546
+
547
+ if (isOutside && !isDisabled) {
548
+ classes += " text-muted-foreground opacity-50"
549
+ }
550
+
551
+ const ariaAttrs = []
552
+ if (isSelected) ariaAttrs.push('aria-selected="true"')
553
+ if (isDisabled) {
554
+ ariaAttrs.push('aria-disabled="true"')
555
+ ariaAttrs.push('disabled')
556
+ }
557
+ if (isFocused) ariaAttrs.push('tabindex="0"')
558
+ else ariaAttrs.push('tabindex="-1"')
559
+
560
+ // Only add click action for non-disabled days
561
+ const dataAction = isDisabled
562
+ ? 'data-action="focus->shadcn--calendar#enableKeyboard blur->shadcn--calendar#disableKeyboard"'
563
+ : 'data-action="click->shadcn--calendar#selectDay focus->shadcn--calendar#enableKeyboard blur->shadcn--calendar#disableKeyboard"'
564
+
565
+ html += `<button type="button" class="${classes}" data-date="${dateStr}" data-shadcn--calendar-target="day" ${dataAction} ${ariaAttrs.join(" ")}>${currentDate.getDate()}</button>`
566
+
567
+ currentDate.setDate(currentDate.getDate() + 1)
568
+ }
569
+
570
+ return html
571
+ }
572
+
573
+ /**
574
+ * Go to today's date
575
+ */
576
+ goToToday() {
577
+ const today = new Date()
578
+ this.currentMonth = new Date(today.getFullYear(), today.getMonth(), 1)
579
+ this.focusedDate = today
580
+ this.render()
581
+ }
582
+
583
+ monthValueChanged() {
584
+ if (this.monthValue) {
585
+ this.currentMonth = this.parseLocalDate(this.monthValue)
586
+ }
587
+ }
588
+
589
+ selectedValueChanged() {
590
+ this.initializeSelection()
591
+ }
592
+ }