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,203 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Helper to set up Stimulus controller tests
5
+ * Creates a DOM element with the controller connected
6
+ */
7
+ export function setupController(Controller, html, controllerName = 'test') {
8
+ const application = Application.start()
9
+ application.register(controllerName, Controller)
10
+
11
+ document.body.innerHTML = html
12
+
13
+ // Wait for Stimulus to connect the controller
14
+ return new Promise((resolve) => {
15
+ requestAnimationFrame(() => {
16
+ const element = document.querySelector(`[data-controller="${controllerName}"]`)
17
+ const controller = application.getControllerForElementAndIdentifier(element, controllerName)
18
+ resolve({ application, element, controller })
19
+ })
20
+ })
21
+ }
22
+
23
+ /**
24
+ * Clean up after tests
25
+ */
26
+ export function cleanupController(application) {
27
+ if (application) {
28
+ application.stop()
29
+ }
30
+ document.body.innerHTML = ''
31
+ }
32
+
33
+ /**
34
+ * Simulate a click event on an element
35
+ */
36
+ export function click(element) {
37
+ element.dispatchEvent(new MouseEvent('click', {
38
+ bubbles: true,
39
+ cancelable: true,
40
+ view: window
41
+ }))
42
+ }
43
+
44
+ /**
45
+ * Wait for a specified number of milliseconds
46
+ */
47
+ export function wait(ms) {
48
+ return new Promise(resolve => setTimeout(resolve, ms))
49
+ }
50
+
51
+ /**
52
+ * Wait for an animation frame
53
+ */
54
+ export function nextFrame() {
55
+ return new Promise(resolve => requestAnimationFrame(resolve))
56
+ }
57
+
58
+ /**
59
+ * Simulate a keyboard event on an element
60
+ * @param {Element} element - Target element
61
+ * @param {string} key - Key name (e.g., 'ArrowDown', 'Enter', 'Escape')
62
+ * @param {Object} options - Additional options (shiftKey, ctrlKey, etc.)
63
+ */
64
+ export function keydown(element, key, options = {}) {
65
+ element.dispatchEvent(new KeyboardEvent('keydown', {
66
+ key,
67
+ bubbles: true,
68
+ cancelable: true,
69
+ ...options
70
+ }))
71
+ }
72
+
73
+ /**
74
+ * Simulate a keyup event
75
+ */
76
+ export function keyup(element, key, options = {}) {
77
+ element.dispatchEvent(new KeyboardEvent('keyup', {
78
+ key,
79
+ bubbles: true,
80
+ cancelable: true,
81
+ ...options
82
+ }))
83
+ }
84
+
85
+ /**
86
+ * Wait for a portal element to appear in the DOM
87
+ * @param {string} selector - CSS selector for the portal
88
+ * @param {boolean} shouldExist - Whether portal should exist (true) or not exist (false)
89
+ * @param {number} timeout - Maximum wait time in ms
90
+ */
91
+ export async function waitForPortal(selector, shouldExist = true, timeout = 1000) {
92
+ const startTime = Date.now()
93
+
94
+ while (Date.now() - startTime < timeout) {
95
+ const element = document.querySelector(selector)
96
+ if (shouldExist && element) return element
97
+ if (!shouldExist && !element) return null
98
+ await wait(10)
99
+ }
100
+
101
+ throw new Error(`Portal ${selector} ${shouldExist ? 'did not appear' : 'did not disappear'} within ${timeout}ms`)
102
+ }
103
+
104
+ /**
105
+ * Mock window.location for URL-related tests
106
+ * Uses jsdom's internal reconfigure or modifies location properties
107
+ * @param {string} url - The URL to mock
108
+ * @returns {Function} Cleanup function to restore original location
109
+ */
110
+ export function mockLocation(url) {
111
+ // Store original href
112
+ const originalHref = window.location.href
113
+
114
+ // Use jsdom's reconfigure if available (available in newer jsdom)
115
+ if (typeof window._virtualConsole !== 'undefined' && window.location._setHref) {
116
+ window.location._setHref(url)
117
+ return () => {
118
+ if (window.location._setHref) {
119
+ window.location._setHref(originalHref)
120
+ }
121
+ }
122
+ }
123
+
124
+ // Fallback: Use history API to change URL without navigation
125
+ const urlObj = new URL(url, window.location.origin)
126
+ window.history.replaceState({}, '', urlObj.href)
127
+
128
+ return () => {
129
+ window.history.replaceState({}, '', originalHref)
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Mock history.pushState and replaceState for URL sync tests
135
+ */
136
+ export function mockHistory() {
137
+ const originalPushState = window.history.pushState
138
+ const originalReplaceState = window.history.replaceState
139
+
140
+ const calls = {
141
+ pushState: [],
142
+ replaceState: []
143
+ }
144
+
145
+ window.history.pushState = (state, title, url) => {
146
+ calls.pushState.push({ state, title, url })
147
+ }
148
+
149
+ window.history.replaceState = (state, title, url) => {
150
+ calls.replaceState.push({ state, title, url })
151
+ }
152
+
153
+ return {
154
+ calls,
155
+ restore: () => {
156
+ window.history.pushState = originalPushState
157
+ window.history.replaceState = originalReplaceState
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Get all focusable elements within a container
164
+ * @param {Element} container - Container element
165
+ */
166
+ export function getFocusableElements(container) {
167
+ return container.querySelectorAll(
168
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
169
+ )
170
+ }
171
+
172
+ /**
173
+ * Dispatch a custom event
174
+ * @param {Element} element - Target element
175
+ * @param {string} eventName - Event name
176
+ * @param {Object} detail - Event detail object
177
+ */
178
+ export function dispatchEvent(element, eventName, detail = {}) {
179
+ element.dispatchEvent(new CustomEvent(eventName, {
180
+ bubbles: true,
181
+ cancelable: true,
182
+ detail
183
+ }))
184
+ }
185
+
186
+ /**
187
+ * Wait for controller to emit a specific event
188
+ * @param {Element} element - Element to listen on
189
+ * @param {string} eventName - Event name (e.g., 'shadcn--accordion:expand')
190
+ * @param {number} timeout - Maximum wait time
191
+ */
192
+ export function waitForEvent(element, eventName, timeout = 1000) {
193
+ return new Promise((resolve, reject) => {
194
+ const timer = setTimeout(() => {
195
+ reject(new Error(`Event ${eventName} not received within ${timeout}ms`))
196
+ }, timeout)
197
+
198
+ element.addEventListener(eventName, (event) => {
199
+ clearTimeout(timer)
200
+ resolve(event)
201
+ }, { once: true })
202
+ })
203
+ }
@@ -0,0 +1 @@
1
+ //= link_tree ../stylesheets .css
@@ -0,0 +1,53 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Accordion controller for collapsible sections
5
+ * Supports single and multiple expansion modes
6
+ */
7
+ export default class AccordionController extends Controller {
8
+ static targets: ["item", "trigger", "content"];
9
+ static values: {
10
+ type: { type: "String"; default: "single" };
11
+ collapsible: { type: "Boolean"; default: false };
12
+ default: { type: "String"; default: "" };
13
+ };
14
+
15
+ /** Accordion item targets */
16
+ readonly itemTargets: HTMLElement[];
17
+ readonly hasItemTarget: boolean;
18
+
19
+ /** Accordion trigger targets */
20
+ readonly triggerTargets: HTMLElement[];
21
+ readonly hasTriggerTarget: boolean;
22
+
23
+ /** Accordion content targets */
24
+ readonly contentTargets: HTMLElement[];
25
+ readonly hasContentTarget: boolean;
26
+
27
+ /** Expansion type: "single" or "multiple" */
28
+ typeValue: "single" | "multiple";
29
+ readonly hasTypeValue: boolean;
30
+
31
+ /** Whether single items can be collapsed */
32
+ collapsibleValue: boolean;
33
+ readonly hasCollapsibleValue: boolean;
34
+
35
+ /** Default expanded items (comma-separated values for multiple) */
36
+ defaultValue: string;
37
+ readonly hasDefaultValue: boolean;
38
+
39
+ /** Toggle an accordion item open/closed */
40
+ toggle(event: Event): void;
41
+
42
+ /** Expand a specific item */
43
+ expandItem(item: HTMLElement): void;
44
+
45
+ /** Collapse a specific item */
46
+ collapseItem(item: HTMLElement): void;
47
+
48
+ /** Find an item by its value */
49
+ findItemByValue(value: string): HTMLElement | undefined;
50
+
51
+ /** Handle keyboard navigation */
52
+ handleKeydown(event: KeyboardEvent): void;
53
+ }
@@ -0,0 +1,140 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Accordion controller for collapsible sections
5
+ * Supports single and multiple expansion modes
6
+ */
7
+ export default class extends Controller {
8
+ static targets = ["item", "trigger", "content"]
9
+ static values = {
10
+ type: { type: String, default: "single" }, // "single" or "multiple"
11
+ collapsible: { type: Boolean, default: false },
12
+ default: { type: String, default: "" } // comma-separated values for multiple
13
+ }
14
+
15
+ connect() {
16
+ // Expand default items
17
+ if (this.defaultValue) {
18
+ const defaultValues = this.defaultValue.split(",").map(v => v.trim())
19
+ defaultValues.forEach(value => {
20
+ const item = this.findItemByValue(value)
21
+ if (item) {
22
+ this.expandItem(item)
23
+ }
24
+ })
25
+ }
26
+ }
27
+
28
+ toggle(event) {
29
+ const trigger = event.currentTarget
30
+ const item = trigger.closest('[data-shadcn--accordion-target="item"]')
31
+
32
+ if (!item) return
33
+
34
+ const isOpen = item.dataset.state === "open"
35
+
36
+ if (isOpen) {
37
+ if (this.collapsibleValue || this.typeValue === "multiple") {
38
+ this.collapseItem(item)
39
+ }
40
+ } else {
41
+ if (this.typeValue === "single") {
42
+ // Collapse all other items first
43
+ this.itemTargets.forEach(otherItem => {
44
+ if (otherItem !== item && otherItem.dataset.state === "open") {
45
+ this.collapseItem(otherItem)
46
+ }
47
+ })
48
+ }
49
+ this.expandItem(item)
50
+ }
51
+ }
52
+
53
+ expandItem(item) {
54
+ const trigger = item.querySelector('[data-shadcn--accordion-target="trigger"]')
55
+ const content = item.querySelector('[data-shadcn--accordion-target="content"]')
56
+
57
+ if (!trigger || !content) return
58
+
59
+ item.dataset.state = "open"
60
+ trigger.dataset.state = "open"
61
+ trigger.setAttribute("aria-expanded", "true")
62
+ content.dataset.state = "open"
63
+ content.hidden = false
64
+
65
+ // Animate height
66
+ const height = content.scrollHeight
67
+ content.style.height = "0px"
68
+ requestAnimationFrame(() => {
69
+ content.style.height = `${height}px`
70
+ // Remove fixed height after animation
71
+ setTimeout(() => {
72
+ content.style.height = ""
73
+ }, 200)
74
+ })
75
+
76
+ this.dispatch("expand", { detail: { value: item.dataset.value } })
77
+ }
78
+
79
+ collapseItem(item) {
80
+ const trigger = item.querySelector('[data-shadcn--accordion-target="trigger"]')
81
+ const content = item.querySelector('[data-shadcn--accordion-target="content"]')
82
+
83
+ if (!trigger || !content) return
84
+
85
+ // Set current height for animation
86
+ content.style.height = `${content.scrollHeight}px`
87
+
88
+ requestAnimationFrame(() => {
89
+ item.dataset.state = "closed"
90
+ trigger.dataset.state = "closed"
91
+ trigger.setAttribute("aria-expanded", "false")
92
+ content.dataset.state = "closed"
93
+ content.style.height = "0px"
94
+
95
+ setTimeout(() => {
96
+ content.hidden = true
97
+ content.style.height = ""
98
+ }, 200)
99
+ })
100
+
101
+ this.dispatch("collapse", { detail: { value: item.dataset.value } })
102
+ }
103
+
104
+ findItemByValue(value) {
105
+ return this.itemTargets.find(item => item.dataset.value === value)
106
+ }
107
+
108
+ // Keyboard navigation
109
+ handleKeydown(event) {
110
+ const triggers = this.triggerTargets
111
+ const currentIndex = triggers.findIndex(t => t === document.activeElement)
112
+
113
+ if (currentIndex === -1) return
114
+
115
+ let newIndex = currentIndex
116
+
117
+ switch (event.key) {
118
+ case "ArrowUp":
119
+ event.preventDefault()
120
+ newIndex = currentIndex === 0 ? triggers.length - 1 : currentIndex - 1
121
+ break
122
+ case "ArrowDown":
123
+ event.preventDefault()
124
+ newIndex = currentIndex === triggers.length - 1 ? 0 : currentIndex + 1
125
+ break
126
+ case "Home":
127
+ event.preventDefault()
128
+ newIndex = 0
129
+ break
130
+ case "End":
131
+ event.preventDefault()
132
+ newIndex = triggers.length - 1
133
+ break
134
+ default:
135
+ return
136
+ }
137
+
138
+ triggers[newIndex].focus()
139
+ }
140
+ }
@@ -0,0 +1,22 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Avatar controller for handling image load errors
5
+ */
6
+ export default class AvatarController extends Controller {
7
+ static targets: ["image", "fallback"];
8
+
9
+ /** Avatar image target */
10
+ readonly imageTarget: HTMLImageElement;
11
+ readonly hasImageTarget: boolean;
12
+
13
+ /** Fallback content target */
14
+ readonly fallbackTarget: HTMLElement;
15
+ readonly hasFallbackTarget: boolean;
16
+
17
+ /** Handle image load error - shows fallback */
18
+ handleError(): void;
19
+
20
+ /** Handle successful image load - shows image */
21
+ handleLoad(): void;
22
+ }
@@ -0,0 +1,26 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Avatar controller for handling image load errors
5
+ */
6
+ export default class extends Controller {
7
+ static targets = ["image", "fallback"]
8
+
9
+ handleError() {
10
+ if (this.hasImageTarget) {
11
+ this.imageTarget.hidden = true
12
+ }
13
+ if (this.hasFallbackTarget) {
14
+ this.fallbackTarget.classList.remove("hidden")
15
+ }
16
+ }
17
+
18
+ handleLoad() {
19
+ if (this.hasImageTarget) {
20
+ this.imageTarget.hidden = false
21
+ }
22
+ if (this.hasFallbackTarget) {
23
+ this.fallbackTarget.classList.add("hidden")
24
+ }
25
+ }
26
+ }