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,680 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ import ResizableController from "../../app/assets/javascripts/shadcn/controllers/resizable_controller.js"
3
+ import { setupController, cleanupController, click, nextFrame, wait } from '../helpers/stimulus-test-helper.js'
4
+
5
+ describe("ResizableController", () => {
6
+ let application
7
+ let element
8
+ let controller
9
+
10
+ afterEach(() => {
11
+ cleanupController(application)
12
+ // Clean up localStorage
13
+ localStorage.clear()
14
+ })
15
+
16
+ describe("basic rendering and initialization", () => {
17
+ const basicHTML = `
18
+ <div data-controller="shadcn--resizable"
19
+ data-shadcn--resizable-direction-value="horizontal"
20
+ style="display: flex; width: 500px; height: 300px;">
21
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 1</div>
22
+ <div data-shadcn--resizable-target="handle"
23
+ role="separator"
24
+ tabindex="0"
25
+ data-action="mousedown->shadcn--resizable#startResize touchstart->shadcn--resizable#startResize"
26
+ style="width: 4px; cursor: col-resize;"></div>
27
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 2</div>
28
+ </div>
29
+ `
30
+
31
+ beforeEach(async () => {
32
+ const setup = await setupController(ResizableController, basicHTML, 'shadcn--resizable')
33
+ application = setup.application
34
+ element = setup.element
35
+ controller = setup.controller
36
+ })
37
+
38
+ test("initializes with horizontal direction", () => {
39
+ expect(controller.directionValue).toBe("horizontal")
40
+ })
41
+
42
+ test("initializes isDragging to false", () => {
43
+ expect(controller.isDragging).toBe(false)
44
+ })
45
+
46
+ test("isHorizontal returns true for horizontal direction", () => {
47
+ expect(controller.isHorizontal).toBe(true)
48
+ })
49
+
50
+ test("has panel targets", () => {
51
+ expect(controller.panelTargets.length).toBe(2)
52
+ })
53
+
54
+ test("has handle target", () => {
55
+ expect(controller.handleTargets.length).toBe(1)
56
+ })
57
+ })
58
+
59
+ describe("vertical direction", () => {
60
+ const verticalHTML = `
61
+ <div data-controller="shadcn--resizable"
62
+ data-shadcn--resizable-direction-value="vertical"
63
+ style="display: flex; flex-direction: column; width: 500px; height: 300px;">
64
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 1</div>
65
+ <div data-shadcn--resizable-target="handle"
66
+ role="separator"
67
+ tabindex="0"
68
+ style="height: 4px; cursor: row-resize;"></div>
69
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 2</div>
70
+ </div>
71
+ `
72
+
73
+ beforeEach(async () => {
74
+ const setup = await setupController(ResizableController, verticalHTML, 'shadcn--resizable')
75
+ application = setup.application
76
+ element = setup.element
77
+ controller = setup.controller
78
+ })
79
+
80
+ test("initializes with vertical direction", () => {
81
+ expect(controller.directionValue).toBe("vertical")
82
+ })
83
+
84
+ test("isHorizontal returns false for vertical direction", () => {
85
+ expect(controller.isHorizontal).toBe(false)
86
+ })
87
+ })
88
+
89
+ describe("startResize", () => {
90
+ const startResizeHTML = `
91
+ <div data-controller="shadcn--resizable"
92
+ data-shadcn--resizable-direction-value="horizontal"
93
+ style="display: flex; width: 500px; height: 300px;">
94
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 1</div>
95
+ <div data-shadcn--resizable-target="handle"
96
+ role="separator"
97
+ tabindex="0"
98
+ data-action="mousedown->shadcn--resizable#startResize"
99
+ style="width: 4px;"></div>
100
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 2</div>
101
+ </div>
102
+ `
103
+
104
+ beforeEach(async () => {
105
+ const setup = await setupController(ResizableController, startResizeHTML, 'shadcn--resizable')
106
+ application = setup.application
107
+ element = setup.element
108
+ controller = setup.controller
109
+ })
110
+
111
+ test("sets isDragging to true", async () => {
112
+ const handle = controller.handleTargets[0]
113
+ controller.startResize({
114
+ currentTarget: handle,
115
+ type: 'mousedown',
116
+ clientX: 250,
117
+ clientY: 150,
118
+ preventDefault: jest.fn()
119
+ })
120
+ await nextFrame()
121
+
122
+ expect(controller.isDragging).toBe(true)
123
+ })
124
+
125
+ test("sets currentHandle", async () => {
126
+ const handle = controller.handleTargets[0]
127
+ controller.startResize({
128
+ currentTarget: handle,
129
+ type: 'mousedown',
130
+ clientX: 250,
131
+ clientY: 150,
132
+ preventDefault: jest.fn()
133
+ })
134
+ await nextFrame()
135
+
136
+ expect(controller.currentHandle).toBe(handle)
137
+ })
138
+
139
+ test("sets handle data-state to dragging", async () => {
140
+ const handle = controller.handleTargets[0]
141
+ controller.startResize({
142
+ currentTarget: handle,
143
+ type: 'mousedown',
144
+ clientX: 250,
145
+ clientY: 150,
146
+ preventDefault: jest.fn()
147
+ })
148
+ await nextFrame()
149
+
150
+ expect(handle.dataset.state).toBe("dragging")
151
+ })
152
+
153
+ test("stores start position for horizontal", async () => {
154
+ const handle = controller.handleTargets[0]
155
+ controller.startResize({
156
+ currentTarget: handle,
157
+ type: 'mousedown',
158
+ clientX: 250,
159
+ clientY: 150,
160
+ preventDefault: jest.fn()
161
+ })
162
+ await nextFrame()
163
+
164
+ expect(controller.startPosition).toBe(250)
165
+ })
166
+
167
+ test("prevents default", async () => {
168
+ const handle = controller.handleTargets[0]
169
+ const preventDefault = jest.fn()
170
+ controller.startResize({
171
+ currentTarget: handle,
172
+ type: 'mousedown',
173
+ clientX: 250,
174
+ clientY: 150,
175
+ preventDefault
176
+ })
177
+
178
+ expect(preventDefault).toHaveBeenCalled()
179
+ })
180
+ })
181
+
182
+ describe("stopResize", () => {
183
+ const stopResizeHTML = `
184
+ <div data-controller="shadcn--resizable"
185
+ data-shadcn--resizable-direction-value="horizontal"
186
+ style="display: flex; width: 500px; height: 300px;">
187
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 1</div>
188
+ <div data-shadcn--resizable-target="handle"
189
+ role="separator"
190
+ tabindex="0"
191
+ style="width: 4px;"></div>
192
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 2</div>
193
+ </div>
194
+ `
195
+
196
+ beforeEach(async () => {
197
+ const setup = await setupController(ResizableController, stopResizeHTML, 'shadcn--resizable')
198
+ application = setup.application
199
+ element = setup.element
200
+ controller = setup.controller
201
+ })
202
+
203
+ test("sets isDragging to false", async () => {
204
+ const handle = controller.handleTargets[0]
205
+ controller.startResize({
206
+ currentTarget: handle,
207
+ type: 'mousedown',
208
+ clientX: 250,
209
+ clientY: 150,
210
+ preventDefault: jest.fn()
211
+ })
212
+ await nextFrame()
213
+
214
+ controller.stopResize()
215
+ await nextFrame()
216
+
217
+ expect(controller.isDragging).toBe(false)
218
+ })
219
+
220
+ test("clears handle data-state", async () => {
221
+ const handle = controller.handleTargets[0]
222
+ controller.startResize({
223
+ currentTarget: handle,
224
+ type: 'mousedown',
225
+ clientX: 250,
226
+ clientY: 150,
227
+ preventDefault: jest.fn()
228
+ })
229
+ await nextFrame()
230
+
231
+ controller.stopResize()
232
+ await nextFrame()
233
+
234
+ expect(handle.dataset.state).toBe("")
235
+ })
236
+
237
+ test("clears currentHandle", async () => {
238
+ const handle = controller.handleTargets[0]
239
+ controller.startResize({
240
+ currentTarget: handle,
241
+ type: 'mousedown',
242
+ clientX: 250,
243
+ clientY: 150,
244
+ preventDefault: jest.fn()
245
+ })
246
+ await nextFrame()
247
+
248
+ controller.stopResize()
249
+ await nextFrame()
250
+
251
+ expect(controller.currentHandle).toBeNull()
252
+ })
253
+
254
+ test("does nothing if not dragging", async () => {
255
+ // Should not throw
256
+ expect(() => {
257
+ controller.stopResize()
258
+ }).not.toThrow()
259
+ })
260
+ })
261
+
262
+ describe("keyboard navigation", () => {
263
+ const keyboardHTML = `
264
+ <div data-controller="shadcn--resizable"
265
+ data-shadcn--resizable-direction-value="horizontal"
266
+ style="display: flex; width: 500px; height: 300px;">
267
+ <div data-shadcn--resizable-target="panel" data-panel
268
+ data-min-size="10" data-max-size="90"
269
+ style="flex-basis: 50%;">Panel 1</div>
270
+ <div data-shadcn--resizable-target="handle"
271
+ role="separator"
272
+ tabindex="0"
273
+ style="width: 4px;"></div>
274
+ <div data-shadcn--resizable-target="panel" data-panel
275
+ data-min-size="10" data-max-size="90"
276
+ style="flex-basis: 50%;">Panel 2</div>
277
+ </div>
278
+ `
279
+
280
+ beforeEach(async () => {
281
+ const setup = await setupController(ResizableController, keyboardHTML, 'shadcn--resizable')
282
+ application = setup.application
283
+ element = setup.element
284
+ controller = setup.controller
285
+ })
286
+
287
+ test("ArrowRight calls findAdjacentPanels and storePanelSizes", async () => {
288
+ const handle = controller.handleTargets[0]
289
+ const findAdjacentSpy = jest.spyOn(controller, 'findAdjacentPanels')
290
+ const storeSizesSpy = jest.spyOn(controller, 'storePanelSizes')
291
+
292
+ controller.handleKeydown.call(controller, {
293
+ currentTarget: handle,
294
+ key: "ArrowRight",
295
+ shiftKey: false,
296
+ preventDefault: jest.fn()
297
+ })
298
+ await nextFrame()
299
+
300
+ expect(findAdjacentSpy).toHaveBeenCalled()
301
+ expect(storeSizesSpy).toHaveBeenCalled()
302
+ })
303
+
304
+ test("ArrowLeft calls findAdjacentPanels and storePanelSizes", async () => {
305
+ const handle = controller.handleTargets[0]
306
+ const findAdjacentSpy = jest.spyOn(controller, 'findAdjacentPanels')
307
+ const storeSizesSpy = jest.spyOn(controller, 'storePanelSizes')
308
+
309
+ controller.handleKeydown.call(controller, {
310
+ currentTarget: handle,
311
+ key: "ArrowLeft",
312
+ shiftKey: false,
313
+ preventDefault: jest.fn()
314
+ })
315
+ await nextFrame()
316
+
317
+ expect(findAdjacentSpy).toHaveBeenCalled()
318
+ expect(storeSizesSpy).toHaveBeenCalled()
319
+ })
320
+
321
+ test("Shift key modifies step size calculation", async () => {
322
+ const handle = controller.handleTargets[0]
323
+
324
+ // Without shift
325
+ controller.handleKeydown.call(controller, {
326
+ currentTarget: handle,
327
+ key: "ArrowRight",
328
+ shiftKey: false,
329
+ preventDefault: jest.fn()
330
+ })
331
+
332
+ // With shift - step is 10 instead of 1
333
+ controller.handleKeydown.call(controller, {
334
+ currentTarget: handle,
335
+ key: "ArrowRight",
336
+ shiftKey: true,
337
+ preventDefault: jest.fn()
338
+ })
339
+ await nextFrame()
340
+
341
+ // Just verify it doesn't throw
342
+ expect(true).toBe(true)
343
+ })
344
+
345
+ test("prevents default on arrow keys", async () => {
346
+ const handle = controller.handleTargets[0]
347
+ const preventDefault = jest.fn()
348
+
349
+ controller.handleKeydown.call(controller, {
350
+ currentTarget: handle,
351
+ key: "ArrowRight",
352
+ shiftKey: false,
353
+ preventDefault
354
+ })
355
+
356
+ expect(preventDefault).toHaveBeenCalled()
357
+ })
358
+ })
359
+
360
+ describe("panel size constraints", () => {
361
+ const constraintsHTML = `
362
+ <div data-controller="shadcn--resizable"
363
+ data-shadcn--resizable-direction-value="horizontal"
364
+ style="display: flex; width: 500px; height: 300px;">
365
+ <div data-shadcn--resizable-target="panel" data-panel
366
+ data-min-size="20" data-max-size="80"
367
+ style="flex-basis: 50%;">Panel 1</div>
368
+ <div data-shadcn--resizable-target="handle"
369
+ role="separator"
370
+ tabindex="0"
371
+ style="width: 4px;"></div>
372
+ <div data-shadcn--resizable-target="panel" data-panel
373
+ data-min-size="20" data-max-size="80"
374
+ style="flex-basis: 50%;">Panel 2</div>
375
+ </div>
376
+ `
377
+
378
+ beforeEach(async () => {
379
+ const setup = await setupController(ResizableController, constraintsHTML, 'shadcn--resizable')
380
+ application = setup.application
381
+ element = setup.element
382
+ controller = setup.controller
383
+ })
384
+
385
+ test("respects min-size constraint", async () => {
386
+ const handle = controller.handleTargets[0]
387
+ const prevPanel = controller.panelTargets[0]
388
+
389
+ controller.setPanelSize(prevPanel, 50)
390
+ controller.setPanelSize(controller.panelTargets[1], 50)
391
+
392
+ // Try to resize below min
393
+ for (let i = 0; i < 50; i++) {
394
+ controller.handleKeydown.call(controller, {
395
+ currentTarget: handle,
396
+ key: "ArrowLeft",
397
+ shiftKey: false,
398
+ preventDefault: jest.fn()
399
+ })
400
+ }
401
+ await nextFrame()
402
+
403
+ expect(parseFloat(prevPanel.dataset.panelSize)).toBeGreaterThanOrEqual(20)
404
+ })
405
+
406
+ test("respects max-size constraint", async () => {
407
+ const handle = controller.handleTargets[0]
408
+ const prevPanel = controller.panelTargets[0]
409
+
410
+ controller.setPanelSize(prevPanel, 50)
411
+ controller.setPanelSize(controller.panelTargets[1], 50)
412
+
413
+ // Try to resize above max
414
+ for (let i = 0; i < 50; i++) {
415
+ controller.handleKeydown.call(controller, {
416
+ currentTarget: handle,
417
+ key: "ArrowRight",
418
+ shiftKey: false,
419
+ preventDefault: jest.fn()
420
+ })
421
+ }
422
+ await nextFrame()
423
+
424
+ expect(parseFloat(prevPanel.dataset.panelSize)).toBeLessThanOrEqual(80)
425
+ })
426
+ })
427
+
428
+ describe("collapse functionality", () => {
429
+ const collapseHTML = `
430
+ <div data-controller="shadcn--resizable"
431
+ data-shadcn--resizable-direction-value="horizontal"
432
+ style="display: flex; width: 500px; height: 300px;">
433
+ <div data-shadcn--resizable-target="panel" data-panel
434
+ data-min-size="0" data-max-size="100"
435
+ style="flex-basis: 50%;">Panel 1</div>
436
+ <div data-shadcn--resizable-target="handle"
437
+ role="separator"
438
+ tabindex="0"
439
+ style="width: 4px;"></div>
440
+ <div data-shadcn--resizable-target="panel" data-panel
441
+ data-min-size="0" data-max-size="100"
442
+ style="flex-basis: 50%;">Panel 2</div>
443
+ </div>
444
+ `
445
+
446
+ beforeEach(async () => {
447
+ const setup = await setupController(ResizableController, collapseHTML, 'shadcn--resizable')
448
+ application = setup.application
449
+ element = setup.element
450
+ controller = setup.controller
451
+ })
452
+
453
+ test("Home calls collapsePanel with prev", async () => {
454
+ const handle = controller.handleTargets[0]
455
+ const collapseSpy = jest.spyOn(controller, 'collapsePanel')
456
+
457
+ controller.handleKeydown.call(controller, {
458
+ currentTarget: handle,
459
+ key: "Home",
460
+ shiftKey: false,
461
+ preventDefault: jest.fn()
462
+ })
463
+ await nextFrame()
464
+
465
+ expect(collapseSpy).toHaveBeenCalledWith('prev')
466
+ })
467
+
468
+ test("End calls collapsePanel with next", async () => {
469
+ const handle = controller.handleTargets[0]
470
+ const collapseSpy = jest.spyOn(controller, 'collapsePanel')
471
+
472
+ controller.handleKeydown.call(controller, {
473
+ currentTarget: handle,
474
+ key: "End",
475
+ shiftKey: false,
476
+ preventDefault: jest.fn()
477
+ })
478
+ await nextFrame()
479
+
480
+ expect(collapseSpy).toHaveBeenCalledWith('next')
481
+ })
482
+
483
+ test("Home prevents default", async () => {
484
+ const handle = controller.handleTargets[0]
485
+ const preventDefault = jest.fn()
486
+
487
+ controller.handleKeydown.call(controller, {
488
+ currentTarget: handle,
489
+ key: "Home",
490
+ shiftKey: false,
491
+ preventDefault
492
+ })
493
+
494
+ expect(preventDefault).toHaveBeenCalled()
495
+ })
496
+
497
+ test("End prevents default", async () => {
498
+ const handle = controller.handleTargets[0]
499
+ const preventDefault = jest.fn()
500
+
501
+ controller.handleKeydown.call(controller, {
502
+ currentTarget: handle,
503
+ key: "End",
504
+ shiftKey: false,
505
+ preventDefault
506
+ })
507
+
508
+ expect(preventDefault).toHaveBeenCalled()
509
+ })
510
+ })
511
+
512
+ describe("auto-save functionality", () => {
513
+ const autoSaveHTML = `
514
+ <div data-controller="shadcn--resizable"
515
+ data-shadcn--resizable-direction-value="horizontal"
516
+ data-shadcn--resizable-auto-save-id-value="test-layout"
517
+ style="display: flex; width: 500px; height: 300px;">
518
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 1</div>
519
+ <div data-shadcn--resizable-target="handle"
520
+ role="separator"
521
+ tabindex="0"
522
+ style="width: 4px;"></div>
523
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 2</div>
524
+ </div>
525
+ `
526
+
527
+ beforeEach(async () => {
528
+ const setup = await setupController(ResizableController, autoSaveHTML, 'shadcn--resizable')
529
+ application = setup.application
530
+ element = setup.element
531
+ controller = setup.controller
532
+ })
533
+
534
+ test("has autoSaveId value", () => {
535
+ expect(controller.autoSaveIdValue).toBe("test-layout")
536
+ })
537
+
538
+ test("saves sizes to localStorage", async () => {
539
+ controller.setPanelSize(controller.panelTargets[0], 60)
540
+ controller.setPanelSize(controller.panelTargets[1], 40)
541
+ controller.saveSizes()
542
+
543
+ const saved = localStorage.getItem("resizable-test-layout")
544
+ expect(saved).not.toBeNull()
545
+
546
+ const sizes = JSON.parse(saved)
547
+ expect(sizes).toHaveLength(2)
548
+ })
549
+
550
+ test("loads sizes from localStorage", async () => {
551
+ localStorage.setItem("resizable-test-layout", JSON.stringify([70, 30]))
552
+
553
+ controller.loadSavedSizes()
554
+ await nextFrame()
555
+
556
+ expect(parseFloat(controller.panelTargets[0].dataset.panelSize)).toBe(70)
557
+ expect(parseFloat(controller.panelTargets[1].dataset.panelSize)).toBe(30)
558
+ })
559
+ })
560
+
561
+ describe("setPanelSize and getPanelSize", () => {
562
+ const sizeHTML = `
563
+ <div data-controller="shadcn--resizable"
564
+ data-shadcn--resizable-direction-value="horizontal"
565
+ style="display: flex; width: 500px; height: 300px;">
566
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 1</div>
567
+ <div data-shadcn--resizable-target="handle"
568
+ role="separator"
569
+ tabindex="0"
570
+ style="width: 4px;"></div>
571
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 2</div>
572
+ </div>
573
+ `
574
+
575
+ beforeEach(async () => {
576
+ const setup = await setupController(ResizableController, sizeHTML, 'shadcn--resizable')
577
+ application = setup.application
578
+ element = setup.element
579
+ controller = setup.controller
580
+ })
581
+
582
+ test("setPanelSize sets flex-basis", async () => {
583
+ const panel = controller.panelTargets[0]
584
+ controller.setPanelSize(panel, 60)
585
+
586
+ expect(panel.style.flexBasis).toBe("60%")
587
+ })
588
+
589
+ test("setPanelSize sets data-panel-size", async () => {
590
+ const panel = controller.panelTargets[0]
591
+ controller.setPanelSize(panel, 60)
592
+
593
+ expect(panel.dataset.panelSize).toBe("60")
594
+ })
595
+ })
596
+
597
+ describe("findAdjacentPanels", () => {
598
+ const adjacentHTML = `
599
+ <div data-controller="shadcn--resizable"
600
+ data-shadcn--resizable-direction-value="horizontal"
601
+ style="display: flex; width: 500px; height: 300px;">
602
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 33%;">Panel 1</div>
603
+ <div data-shadcn--resizable-target="handle"
604
+ role="separator"
605
+ tabindex="0"
606
+ style="width: 4px;"></div>
607
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 33%;">Panel 2</div>
608
+ <div data-shadcn--resizable-target="handle"
609
+ role="separator"
610
+ tabindex="0"
611
+ style="width: 4px;"></div>
612
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 33%;">Panel 3</div>
613
+ </div>
614
+ `
615
+
616
+ beforeEach(async () => {
617
+ const setup = await setupController(ResizableController, adjacentHTML, 'shadcn--resizable')
618
+ application = setup.application
619
+ element = setup.element
620
+ controller = setup.controller
621
+ })
622
+
623
+ test("finds correct adjacent panels for first handle", async () => {
624
+ controller.currentHandle = controller.handleTargets[0]
625
+ controller.findAdjacentPanels()
626
+
627
+ expect(controller.prevPanel).toBe(controller.panelTargets[0])
628
+ expect(controller.nextPanel).toBe(controller.panelTargets[1])
629
+ })
630
+
631
+ test("finds correct adjacent panels for second handle", async () => {
632
+ controller.currentHandle = controller.handleTargets[1]
633
+ controller.findAdjacentPanels()
634
+
635
+ expect(controller.prevPanel).toBe(controller.panelTargets[1])
636
+ expect(controller.nextPanel).toBe(controller.panelTargets[2])
637
+ })
638
+ })
639
+
640
+ describe("disconnect cleanup", () => {
641
+ const disconnectHTML = `
642
+ <div data-controller="shadcn--resizable"
643
+ data-shadcn--resizable-direction-value="horizontal"
644
+ style="display: flex; width: 500px; height: 300px;">
645
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 1</div>
646
+ <div data-shadcn--resizable-target="handle"
647
+ role="separator"
648
+ tabindex="0"
649
+ style="width: 4px;"></div>
650
+ <div data-shadcn--resizable-target="panel" data-panel style="flex-basis: 50%;">Panel 2</div>
651
+ </div>
652
+ `
653
+
654
+ beforeEach(async () => {
655
+ const setup = await setupController(ResizableController, disconnectHTML, 'shadcn--resizable')
656
+ application = setup.application
657
+ element = setup.element
658
+ controller = setup.controller
659
+ })
660
+
661
+ test("cleans up event listeners on disconnect", async () => {
662
+ const handle = controller.handleTargets[0]
663
+
664
+ // Start resize
665
+ controller.startResize({
666
+ currentTarget: handle,
667
+ type: 'mousedown',
668
+ clientX: 250,
669
+ clientY: 150,
670
+ preventDefault: jest.fn()
671
+ })
672
+ await nextFrame()
673
+
674
+ // Disconnect
675
+ expect(() => {
676
+ controller.disconnect()
677
+ }).not.toThrow()
678
+ })
679
+ })
680
+ })