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,636 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import DatePickerController from "../../app/assets/javascripts/shadcn/controllers/date_picker_controller.js"
3
+
4
+ describe("DatePickerController", () => {
5
+ let application
6
+ let element
7
+ let controller
8
+
9
+ const datePickerHTML = `
10
+ <div data-controller="date-picker"
11
+ data-date-picker-open-value="false"
12
+ data-date-picker-month-value="2024-11-01"
13
+ data-date-picker-selected-value=""
14
+ data-date-picker-format-value="medium"
15
+ data-date-picker-placeholder-value="Pick a date">
16
+ <button data-date-picker-target="trigger" type="button">
17
+ <span data-date-picker-target="displayValue" class="text-muted-foreground">Pick a date</span>
18
+ </button>
19
+ <div data-date-picker-target="content" style="display: none;">
20
+ <div data-date-picker-target="monthYear"></div>
21
+ <div data-date-picker-target="grid"></div>
22
+ </div>
23
+ <input type="hidden" data-date-picker-target="hiddenInput">
24
+ </div>
25
+ `
26
+
27
+ beforeEach(async () => {
28
+ application = Application.start()
29
+ application.register("date-picker", DatePickerController)
30
+ document.body.innerHTML = datePickerHTML
31
+
32
+ await new Promise(resolve => requestAnimationFrame(resolve))
33
+
34
+ element = document.querySelector('[data-controller="date-picker"]')
35
+ controller = application.getControllerForElementAndIdentifier(element, "date-picker")
36
+ })
37
+
38
+ afterEach(() => {
39
+ if (application) {
40
+ application.stop()
41
+ }
42
+ document.body.innerHTML = ""
43
+ })
44
+
45
+ describe("parseLocalDate", () => {
46
+ test("parses date string as local date, not UTC", () => {
47
+ const date = controller.parseLocalDate("2024-11-26")
48
+
49
+ expect(date.getFullYear()).toBe(2024)
50
+ expect(date.getMonth()).toBe(10) // November is month 10 (0-indexed)
51
+ expect(date.getDate()).toBe(26)
52
+ })
53
+
54
+ test("returns null for empty string", () => {
55
+ expect(controller.parseLocalDate("")).toBeNull()
56
+ })
57
+
58
+ test("returns null for null input", () => {
59
+ expect(controller.parseLocalDate(null)).toBeNull()
60
+ })
61
+ })
62
+
63
+ describe("formatDateString", () => {
64
+ test("formats date as YYYY-MM-DD", () => {
65
+ const date = new Date(2024, 10, 26)
66
+ expect(controller.formatDateString(date)).toBe("2024-11-26")
67
+ })
68
+
69
+ test("pads single digit months and days", () => {
70
+ const date = new Date(2024, 0, 5)
71
+ expect(controller.formatDateString(date)).toBe("2024-01-05")
72
+ })
73
+
74
+ test("returns empty string for null", () => {
75
+ expect(controller.formatDateString(null)).toBe("")
76
+ })
77
+ })
78
+
79
+ describe("formatDate", () => {
80
+ test("formats with medium style by default", () => {
81
+ const date = new Date(2024, 10, 26)
82
+ const formatted = controller.formatDate(date)
83
+ // "November 26, 2024"
84
+ expect(formatted).toContain("November")
85
+ expect(formatted).toContain("26")
86
+ expect(formatted).toContain("2024")
87
+ })
88
+
89
+ test("formats with short style", () => {
90
+ controller.formatValue = "short"
91
+ const date = new Date(2024, 10, 26)
92
+ const formatted = controller.formatDate(date)
93
+ // "11/26/2024"
94
+ expect(formatted).toBe("11/26/2024")
95
+ })
96
+
97
+ test("formats with long style", () => {
98
+ controller.formatValue = "long"
99
+ const date = new Date(2024, 10, 26)
100
+ const formatted = controller.formatDate(date)
101
+ // "Tuesday, November 26, 2024"
102
+ expect(formatted).toContain("Tuesday")
103
+ expect(formatted).toContain("November")
104
+ expect(formatted).toContain("26")
105
+ expect(formatted).toContain("2024")
106
+ })
107
+
108
+ test("formats with iso style", () => {
109
+ controller.formatValue = "iso"
110
+ const date = new Date(2024, 10, 26)
111
+ const formatted = controller.formatDate(date)
112
+ expect(formatted).toBe("2024-11-26")
113
+ })
114
+ })
115
+
116
+ describe("connect", () => {
117
+ test("initializes currentMonth from monthValue", () => {
118
+ expect(controller.currentMonth.getFullYear()).toBe(2024)
119
+ expect(controller.currentMonth.getMonth()).toBe(10) // November
120
+ })
121
+
122
+ test("initializes selectedDate as null when no selectedValue", () => {
123
+ expect(controller.selectedDate).toBeNull()
124
+ })
125
+ })
126
+
127
+ describe("toggle", () => {
128
+ test("opens when closed", () => {
129
+ expect(controller.openValue).toBe(false)
130
+ controller.toggle()
131
+ expect(controller.openValue).toBe(true)
132
+ })
133
+
134
+ test("closes when open", () => {
135
+ controller.openValue = true
136
+ controller.toggle()
137
+ expect(controller.openValue).toBe(false)
138
+ })
139
+ })
140
+
141
+ describe("open", () => {
142
+ test("sets openValue to true", () => {
143
+ controller.open()
144
+ expect(controller.openValue).toBe(true)
145
+ })
146
+ })
147
+
148
+ describe("close", () => {
149
+ test("sets openValue to false", () => {
150
+ controller.openValue = true
151
+ controller.close()
152
+ expect(controller.openValue).toBe(false)
153
+ })
154
+ })
155
+
156
+ describe("openValueChanged", () => {
157
+ test("shows content when open", () => {
158
+ controller.openValue = true
159
+ controller.openValueChanged()
160
+
161
+ const content = element.querySelector('[data-date-picker-target="content"]')
162
+ expect(content.style.display).toBe("block")
163
+ })
164
+
165
+ test("hides content when closed", () => {
166
+ controller.openValue = false
167
+ controller.openValueChanged()
168
+
169
+ const content = element.querySelector('[data-date-picker-target="content"]')
170
+ expect(content.style.display).toBe("none")
171
+ })
172
+
173
+ test("sets aria-expanded on trigger", () => {
174
+ controller.openValue = true
175
+ controller.openValueChanged()
176
+
177
+ const trigger = element.querySelector('[data-date-picker-target="trigger"]')
178
+ expect(trigger.getAttribute("aria-expanded")).toBe("true")
179
+
180
+ controller.openValue = false
181
+ controller.openValueChanged()
182
+ expect(trigger.getAttribute("aria-expanded")).toBe("false")
183
+ })
184
+ })
185
+
186
+ describe("previousMonth", () => {
187
+ test("moves to the previous month", () => {
188
+ controller.previousMonth()
189
+ expect(controller.currentMonth.getMonth()).toBe(9) // October
190
+ })
191
+ })
192
+
193
+ describe("nextMonth", () => {
194
+ test("moves to the next month", () => {
195
+ controller.nextMonth()
196
+ expect(controller.currentMonth.getMonth()).toBe(11) // December
197
+ })
198
+ })
199
+
200
+ describe("selectDay", () => {
201
+ test("selects the clicked date", () => {
202
+ const mockEvent = {
203
+ currentTarget: {
204
+ dataset: { date: "2024-11-15" }
205
+ }
206
+ }
207
+
208
+ controller.selectDay(mockEvent)
209
+
210
+ expect(controller.selectedDate).not.toBeNull()
211
+ expect(controller.selectedDate.getDate()).toBe(15)
212
+ expect(controller.selectedDate.getMonth()).toBe(10)
213
+ expect(controller.selectedDate.getFullYear()).toBe(2024)
214
+ })
215
+
216
+ test("updates the hidden input value", () => {
217
+ const mockEvent = {
218
+ currentTarget: {
219
+ dataset: { date: "2024-11-20" }
220
+ }
221
+ }
222
+
223
+ controller.selectDay(mockEvent)
224
+
225
+ const hiddenInput = element.querySelector('[data-date-picker-target="hiddenInput"]')
226
+ expect(hiddenInput.value).toBe("2024-11-20")
227
+ })
228
+
229
+ test("updates the display value", () => {
230
+ const mockEvent = {
231
+ currentTarget: {
232
+ dataset: { date: "2024-11-15" }
233
+ }
234
+ }
235
+
236
+ controller.selectDay(mockEvent)
237
+
238
+ const displayValue = element.querySelector('[data-date-picker-target="displayValue"]')
239
+ expect(displayValue.textContent).toContain("November")
240
+ expect(displayValue.textContent).toContain("15")
241
+ expect(displayValue.classList.contains("text-muted-foreground")).toBe(false)
242
+ })
243
+
244
+ test("closes the popover after selection", () => {
245
+ controller.openValue = true
246
+
247
+ const mockEvent = {
248
+ currentTarget: {
249
+ dataset: { date: "2024-11-15" }
250
+ }
251
+ }
252
+
253
+ controller.selectDay(mockEvent)
254
+
255
+ expect(controller.openValue).toBe(false)
256
+ })
257
+
258
+ test("dispatches select event with date details", () => {
259
+ let eventDetail = null
260
+ element.addEventListener("date-picker:select", (e) => {
261
+ eventDetail = e.detail
262
+ })
263
+
264
+ const mockEvent = {
265
+ currentTarget: {
266
+ dataset: { date: "2024-11-10" }
267
+ }
268
+ }
269
+
270
+ controller.selectDay(mockEvent)
271
+
272
+ expect(eventDetail).not.toBeNull()
273
+ expect(eventDetail.dateString).toBe("2024-11-10")
274
+ expect(eventDetail.date.getDate()).toBe(10)
275
+ })
276
+
277
+ test("does nothing if no date in event", () => {
278
+ const mockEvent = {
279
+ currentTarget: {
280
+ dataset: {}
281
+ }
282
+ }
283
+
284
+ controller.selectDay(mockEvent)
285
+
286
+ expect(controller.selectedDate).toBeNull()
287
+ })
288
+ })
289
+
290
+ describe("render", () => {
291
+ test("updates month/year display", () => {
292
+ controller.render()
293
+
294
+ const monthYearDisplay = element.querySelector('[data-date-picker-target="monthYear"]')
295
+ expect(monthYearDisplay.textContent).toBe("November 2024")
296
+ })
297
+
298
+ test("renders day buttons in grid", () => {
299
+ controller.render()
300
+
301
+ const grid = element.querySelector('[data-date-picker-target="grid"]')
302
+ const dayButtons = grid.querySelectorAll('button[data-date]')
303
+ expect(dayButtons.length).toBeGreaterThanOrEqual(28)
304
+ expect(dayButtons.length).toBeLessThanOrEqual(42)
305
+ })
306
+ })
307
+
308
+ describe("closeOnClickOutside", () => {
309
+ test("closes when clicking outside the component", () => {
310
+ controller.openValue = true
311
+
312
+ // Create an outside element
313
+ const outsideElement = document.createElement("div")
314
+ document.body.appendChild(outsideElement)
315
+
316
+ const event = {
317
+ target: outsideElement
318
+ }
319
+
320
+ controller.closeOnClickOutside(event)
321
+
322
+ expect(controller.openValue).toBe(false)
323
+ })
324
+
325
+ test("does not close when clicking inside the component", () => {
326
+ controller.openValue = true
327
+
328
+ const event = {
329
+ target: element.querySelector('[data-date-picker-target="content"]')
330
+ }
331
+
332
+ controller.closeOnClickOutside(event)
333
+
334
+ expect(controller.openValue).toBe(true)
335
+ })
336
+
337
+ test("does nothing when already closed", () => {
338
+ controller.openValue = false
339
+
340
+ const outsideElement = document.createElement("div")
341
+ document.body.appendChild(outsideElement)
342
+
343
+ const event = {
344
+ target: outsideElement
345
+ }
346
+
347
+ // Should not throw or change anything
348
+ controller.closeOnClickOutside(event)
349
+
350
+ expect(controller.openValue).toBe(false)
351
+ })
352
+ })
353
+
354
+ describe("timezone handling", () => {
355
+ test("selecting a date preserves the correct day regardless of timezone", () => {
356
+ const mockEvent = {
357
+ currentTarget: {
358
+ dataset: { date: "2024-11-15" }
359
+ }
360
+ }
361
+
362
+ controller.selectDay(mockEvent)
363
+
364
+ expect(controller.selectedDate.getDate()).toBe(15)
365
+ expect(controller.selectedDate.getMonth()).toBe(10)
366
+ expect(controller.selectedValue).toBe("2024-11-15")
367
+ })
368
+
369
+ test("initializing with a selected value preserves the correct day", async () => {
370
+ application.stop()
371
+ document.body.innerHTML = ""
372
+
373
+ document.body.innerHTML = `
374
+ <div data-controller="date-picker"
375
+ data-date-picker-month-value="2024-11-01"
376
+ data-date-picker-selected-value="2024-11-26">
377
+ <div data-date-picker-target="grid"></div>
378
+ </div>
379
+ `
380
+
381
+ application = Application.start()
382
+ application.register("date-picker", DatePickerController)
383
+
384
+ await new Promise(resolve => requestAnimationFrame(resolve))
385
+
386
+ const newElement = document.querySelector('[data-controller="date-picker"]')
387
+ const newController = application.getControllerForElementAndIdentifier(newElement, "date-picker")
388
+
389
+ expect(newController.selectedDate.getDate()).toBe(26)
390
+ expect(newController.selectedDate.getMonth()).toBe(10)
391
+ })
392
+
393
+ test("parseLocalDate avoids UTC timezone shift for DST dates", () => {
394
+ const dstDates = [
395
+ "2024-03-10", // DST start (US)
396
+ "2024-11-03", // DST end (US)
397
+ ]
398
+
399
+ dstDates.forEach(dateStr => {
400
+ const [year, month, day] = dateStr.split('-').map(Number)
401
+ const parsed = controller.parseLocalDate(dateStr)
402
+
403
+ expect(parsed.getFullYear()).toBe(year)
404
+ expect(parsed.getMonth()).toBe(month - 1)
405
+ expect(parsed.getDate()).toBe(day)
406
+ })
407
+ })
408
+ })
409
+
410
+ describe("monthValueChanged", () => {
411
+ test("updates currentMonth when value changes", () => {
412
+ controller.monthValue = "2024-06-01"
413
+ controller.monthValueChanged()
414
+
415
+ expect(controller.currentMonth.getMonth()).toBe(5)
416
+ expect(controller.currentMonth.getFullYear()).toBe(2024)
417
+ })
418
+ })
419
+
420
+ describe("selectedValueChanged", () => {
421
+ test("updates selectedDate when value changes", () => {
422
+ controller.selectedValue = "2024-07-20"
423
+ controller.selectedValueChanged()
424
+
425
+ expect(controller.selectedDate.getDate()).toBe(20)
426
+ expect(controller.selectedDate.getMonth()).toBe(6)
427
+ })
428
+ })
429
+
430
+ describe("MONTHS constant", () => {
431
+ test("contains all 12 months in order", () => {
432
+ expect(DatePickerController.MONTHS).toEqual([
433
+ "January", "February", "March", "April", "May", "June",
434
+ "July", "August", "September", "October", "November", "December"
435
+ ])
436
+ })
437
+ })
438
+
439
+ describe("isDateDisabled", () => {
440
+ test("returns false for dates within valid range", () => {
441
+ controller.minDateValue = "2024-11-01"
442
+ controller.maxDateValue = "2024-11-30"
443
+
444
+ const date = new Date(2024, 10, 15)
445
+ expect(controller.isDateDisabled(date)).toBe(false)
446
+ })
447
+
448
+ test("returns true for dates before minDate", () => {
449
+ controller.minDateValue = "2024-11-10"
450
+
451
+ const date = new Date(2024, 10, 5)
452
+ expect(controller.isDateDisabled(date)).toBe(true)
453
+ })
454
+
455
+ test("returns true for dates after maxDate", () => {
456
+ controller.maxDateValue = "2024-11-20"
457
+
458
+ const date = new Date(2024, 10, 25)
459
+ expect(controller.isDateDisabled(date)).toBe(true)
460
+ })
461
+
462
+ test("returns true for dates in disabledDates list", () => {
463
+ controller.disabledDatesValue = "2024-11-15,2024-11-16,2024-11-17"
464
+
465
+ expect(controller.isDateDisabled(new Date(2024, 10, 15))).toBe(true)
466
+ expect(controller.isDateDisabled(new Date(2024, 10, 18))).toBe(false)
467
+ })
468
+
469
+ test("returns true for disabled days of week", () => {
470
+ controller.disabledDaysOfWeekValue = "0,6" // Sunday and Saturday
471
+
472
+ // November 16, 2024 is a Saturday
473
+ expect(controller.isDateDisabled(new Date(2024, 10, 16))).toBe(true)
474
+ // November 17, 2024 is a Sunday
475
+ expect(controller.isDateDisabled(new Date(2024, 10, 17))).toBe(true)
476
+ // November 18, 2024 is a Monday
477
+ expect(controller.isDateDisabled(new Date(2024, 10, 18))).toBe(false)
478
+ })
479
+ })
480
+
481
+ describe("disabled dates in DatePicker", () => {
482
+ test("clicking a disabled date does not select it", () => {
483
+ controller.disabledDaysOfWeekValue = "0,6" // Weekends
484
+
485
+ // Try to select a Saturday
486
+ controller.selectDay({ currentTarget: { dataset: { date: "2024-11-16" } } })
487
+
488
+ expect(controller.selectedDate).toBeNull()
489
+ })
490
+
491
+ test("disabled dates have correct CSS after render", () => {
492
+ controller.disabledDaysOfWeekValue = "0,6" // Weekends
493
+ controller.render()
494
+
495
+ const grid = element.querySelector('[data-date-picker-target="grid"]')
496
+ const saturdayButton = grid.querySelector('[data-date="2024-11-16"]')
497
+
498
+ expect(saturdayButton.classList.contains("cursor-not-allowed")).toBe(true)
499
+ expect(saturdayButton.hasAttribute("disabled")).toBe(true)
500
+ expect(saturdayButton.getAttribute("aria-disabled")).toBe("true")
501
+ })
502
+
503
+ test("disabled dates persist after selecting a date", () => {
504
+ controller.disabledDaysOfWeekValue = "0,6" // Weekends
505
+ controller.render()
506
+
507
+ // Select a weekday
508
+ controller.selectDay({ currentTarget: { dataset: { date: "2024-11-18" } } })
509
+
510
+ // Weekends should still be disabled
511
+ const grid = element.querySelector('[data-date-picker-target="grid"]')
512
+ const saturdayButton = grid.querySelector('[data-date="2024-11-16"]')
513
+
514
+ expect(saturdayButton.hasAttribute("disabled")).toBe(true)
515
+ expect(saturdayButton.classList.contains("cursor-not-allowed")).toBe(true)
516
+ })
517
+ })
518
+
519
+ describe("showOutsideDays in DatePicker", () => {
520
+ test("renders empty placeholders when showOutsideDays is false", async () => {
521
+ application.stop()
522
+ document.body.innerHTML = ""
523
+
524
+ document.body.innerHTML = `
525
+ <div data-controller="date-picker"
526
+ data-date-picker-month-value="2024-11-01"
527
+ data-date-picker-show-outside-days-value="false">
528
+ <div data-date-picker-target="grid"></div>
529
+ </div>
530
+ `
531
+
532
+ application = Application.start()
533
+ application.register("date-picker", DatePickerController)
534
+
535
+ await new Promise(resolve => requestAnimationFrame(resolve))
536
+
537
+ element = document.querySelector('[data-controller="date-picker"]')
538
+ controller = application.getControllerForElementAndIdentifier(element, "date-picker")
539
+
540
+ controller.render()
541
+
542
+ const grid = element.querySelector('[data-date-picker-target="grid"]')
543
+
544
+ // First button should be November 1 (October days replaced with empty divs)
545
+ const firstButton = grid.querySelector('button[data-date]')
546
+ expect(firstButton.dataset.date).toBe("2024-11-01")
547
+
548
+ // Empty divs should exist for October days
549
+ const emptyDivs = grid.querySelectorAll('div.h-8.w-8:not([data-date])')
550
+ expect(emptyDivs.length).toBeGreaterThan(0)
551
+ })
552
+
553
+ test("showOutsideDays persists after month navigation", async () => {
554
+ application.stop()
555
+ document.body.innerHTML = ""
556
+
557
+ document.body.innerHTML = `
558
+ <div data-controller="date-picker"
559
+ data-date-picker-month-value="2024-11-01"
560
+ data-date-picker-show-outside-days-value="false">
561
+ <div data-date-picker-target="grid"></div>
562
+ </div>
563
+ `
564
+
565
+ application = Application.start()
566
+ application.register("date-picker", DatePickerController)
567
+
568
+ await new Promise(resolve => requestAnimationFrame(resolve))
569
+
570
+ element = document.querySelector('[data-controller="date-picker"]')
571
+ controller = application.getControllerForElementAndIdentifier(element, "date-picker")
572
+
573
+ // Navigate to December
574
+ controller.nextMonth()
575
+
576
+ // First button should be December 1
577
+ let grid = element.querySelector('[data-date-picker-target="grid"]')
578
+ let firstButton = grid.querySelector('button[data-date]')
579
+ expect(firstButton.dataset.date).toBe("2024-12-01")
580
+
581
+ // Navigate back
582
+ controller.previousMonth()
583
+
584
+ // First button should still be November 1
585
+ grid = element.querySelector('[data-date-picker-target="grid"]')
586
+ firstButton = grid.querySelector('button[data-date]')
587
+ expect(firstButton.dataset.date).toBe("2024-11-01")
588
+ })
589
+ })
590
+
591
+ describe("month navigation with disabled dates", () => {
592
+ test("disabled days of week persist across month navigation", () => {
593
+ controller.disabledDaysOfWeekValue = "0,6" // Weekends
594
+ controller.render()
595
+
596
+ // Navigate to December
597
+ controller.nextMonth()
598
+
599
+ // December 7 is a Saturday
600
+ const grid = element.querySelector('[data-date-picker-target="grid"]')
601
+ const decSat = grid.querySelector('[data-date="2024-12-07"]')
602
+ expect(decSat.hasAttribute("disabled")).toBe(true)
603
+
604
+ // Navigate back to November
605
+ controller.previousMonth()
606
+
607
+ // November 16 is still Saturday and should be disabled
608
+ const novSat = element.querySelector('[data-date="2024-11-16"]')
609
+ expect(novSat.hasAttribute("disabled")).toBe(true)
610
+ })
611
+
612
+ test("minDate/maxDate constraints persist across navigation", () => {
613
+ controller.minDateValue = "2024-11-10"
614
+ controller.maxDateValue = "2024-12-20"
615
+ controller.render()
616
+
617
+ // November 5 should be disabled (before minDate)
618
+ let date5 = element.querySelector('[data-date="2024-11-05"]')
619
+ expect(date5.hasAttribute("disabled")).toBe(true)
620
+
621
+ // Navigate to December
622
+ controller.nextMonth()
623
+
624
+ // December 25 should be disabled (after maxDate)
625
+ const date25 = element.querySelector('[data-date="2024-12-25"]')
626
+ expect(date25.hasAttribute("disabled")).toBe(true)
627
+
628
+ // Navigate back to November
629
+ controller.previousMonth()
630
+
631
+ // November 5 should still be disabled
632
+ date5 = element.querySelector('[data-date="2024-11-05"]')
633
+ expect(date5.hasAttribute("disabled")).toBe(true)
634
+ })
635
+ })
636
+ })