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,272 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Resizable Panel Controller
5
+ * Handles resizable panel layouts with keyboard and mouse support
6
+ */
7
+ export default class extends Controller {
8
+ static targets = ["panel", "handle"]
9
+ static values = {
10
+ direction: { type: String, default: "horizontal" },
11
+ autoSaveId: String
12
+ }
13
+
14
+ connect() {
15
+ this.isDragging = false
16
+ this.currentHandle = null
17
+ this.startPosition = 0
18
+ this.startSizes = []
19
+
20
+ // Bind methods
21
+ this.boundResize = this.resize.bind(this)
22
+ this.boundStopResize = this.stopResize.bind(this)
23
+
24
+ // Load saved sizes if autoSaveId is set
25
+ if (this.hasAutoSaveIdValue) {
26
+ this.loadSavedSizes()
27
+ }
28
+
29
+ // Add keyboard support
30
+ this.handleTargets.forEach(handle => {
31
+ handle.addEventListener('keydown', this.handleKeydown.bind(this))
32
+ })
33
+ }
34
+
35
+ disconnect() {
36
+ document.removeEventListener('mousemove', this.boundResize)
37
+ document.removeEventListener('mouseup', this.boundStopResize)
38
+ document.removeEventListener('touchmove', this.boundResize)
39
+ document.removeEventListener('touchend', this.boundStopResize)
40
+ }
41
+
42
+ startResize(event) {
43
+ event.preventDefault()
44
+
45
+ this.isDragging = true
46
+ this.currentHandle = event.currentTarget
47
+ this.currentHandle.dataset.state = "dragging"
48
+
49
+ // Get the position based on event type
50
+ const position = event.type.includes('touch')
51
+ ? (this.isHorizontal ? event.touches[0].clientX : event.touches[0].clientY)
52
+ : (this.isHorizontal ? event.clientX : event.clientY)
53
+
54
+ this.startPosition = position
55
+
56
+ // Find adjacent panels
57
+ this.findAdjacentPanels()
58
+
59
+ // Store initial sizes
60
+ this.storePanelSizes()
61
+
62
+ // Add document listeners
63
+ document.addEventListener('mousemove', this.boundResize)
64
+ document.addEventListener('mouseup', this.boundStopResize)
65
+ document.addEventListener('touchmove', this.boundResize, { passive: false })
66
+ document.addEventListener('touchend', this.boundStopResize)
67
+
68
+ // Prevent text selection during drag
69
+ document.body.style.userSelect = 'none'
70
+ document.body.style.cursor = this.isHorizontal ? 'col-resize' : 'row-resize'
71
+ }
72
+
73
+ resize(event) {
74
+ if (!this.isDragging) return
75
+
76
+ event.preventDefault()
77
+
78
+ const position = event.type.includes('touch')
79
+ ? (this.isHorizontal ? event.touches[0].clientX : event.touches[0].clientY)
80
+ : (this.isHorizontal ? event.clientX : event.clientY)
81
+
82
+ const delta = position - this.startPosition
83
+ const containerSize = this.isHorizontal
84
+ ? this.element.offsetWidth
85
+ : this.element.offsetHeight
86
+
87
+ const deltaPercent = (delta / containerSize) * 100
88
+
89
+ if (this.prevPanel && this.nextPanel) {
90
+ const prevSize = this.prevPanelStartSize + deltaPercent
91
+ const nextSize = this.nextPanelStartSize - deltaPercent
92
+
93
+ // Get min/max constraints
94
+ const prevMin = parseFloat(this.prevPanel.dataset.minSize) || 0
95
+ const prevMax = parseFloat(this.prevPanel.dataset.maxSize) || 100
96
+ const nextMin = parseFloat(this.nextPanel.dataset.minSize) || 0
97
+ const nextMax = parseFloat(this.nextPanel.dataset.maxSize) || 100
98
+
99
+ // Apply constraints
100
+ if (prevSize >= prevMin && prevSize <= prevMax &&
101
+ nextSize >= nextMin && nextSize <= nextMax) {
102
+ this.setPanelSize(this.prevPanel, prevSize)
103
+ this.setPanelSize(this.nextPanel, nextSize)
104
+ }
105
+ }
106
+ }
107
+
108
+ stopResize() {
109
+ if (!this.isDragging) return
110
+
111
+ this.isDragging = false
112
+
113
+ if (this.currentHandle) {
114
+ this.currentHandle.dataset.state = ""
115
+ }
116
+
117
+ document.removeEventListener('mousemove', this.boundResize)
118
+ document.removeEventListener('mouseup', this.boundStopResize)
119
+ document.removeEventListener('touchmove', this.boundResize)
120
+ document.removeEventListener('touchend', this.boundStopResize)
121
+
122
+ document.body.style.userSelect = ''
123
+ document.body.style.cursor = ''
124
+
125
+ // Save sizes if autoSaveId is set
126
+ if (this.hasAutoSaveIdValue) {
127
+ this.saveSizes()
128
+ }
129
+
130
+ this.currentHandle = null
131
+ }
132
+
133
+ handleKeydown(event) {
134
+ const handle = event.currentTarget
135
+ const step = event.shiftKey ? 10 : 1
136
+
137
+ let delta = 0
138
+
139
+ if (this.isHorizontal) {
140
+ if (event.key === 'ArrowLeft') delta = -step
141
+ if (event.key === 'ArrowRight') delta = step
142
+ } else {
143
+ if (event.key === 'ArrowUp') delta = -step
144
+ if (event.key === 'ArrowDown') delta = step
145
+ }
146
+
147
+ if (delta !== 0) {
148
+ event.preventDefault()
149
+ this.currentHandle = handle
150
+ this.findAdjacentPanels()
151
+ this.storePanelSizes()
152
+
153
+ if (this.prevPanel && this.nextPanel) {
154
+ const prevSize = this.prevPanelStartSize + delta
155
+ const nextSize = this.nextPanelStartSize - delta
156
+
157
+ // Get min/max constraints
158
+ const prevMin = parseFloat(this.prevPanel.dataset.minSize) || 0
159
+ const prevMax = parseFloat(this.prevPanel.dataset.maxSize) || 100
160
+ const nextMin = parseFloat(this.nextPanel.dataset.minSize) || 0
161
+ const nextMax = parseFloat(this.nextPanel.dataset.maxSize) || 100
162
+
163
+ if (prevSize >= prevMin && prevSize <= prevMax &&
164
+ nextSize >= nextMin && nextSize <= nextMax) {
165
+ this.setPanelSize(this.prevPanel, prevSize)
166
+ this.setPanelSize(this.nextPanel, nextSize)
167
+ }
168
+ }
169
+
170
+ if (this.hasAutoSaveIdValue) {
171
+ this.saveSizes()
172
+ }
173
+ }
174
+
175
+ // Home/End keys
176
+ if (event.key === 'Home') {
177
+ event.preventDefault()
178
+ this.collapsePanel('prev')
179
+ }
180
+ if (event.key === 'End') {
181
+ event.preventDefault()
182
+ this.collapsePanel('next')
183
+ }
184
+ }
185
+
186
+ findAdjacentPanels() {
187
+ const allElements = Array.from(this.element.children)
188
+ const handleIndex = allElements.indexOf(this.currentHandle)
189
+
190
+ // Find the panel before the handle
191
+ this.prevPanel = null
192
+ for (let i = handleIndex - 1; i >= 0; i--) {
193
+ if (allElements[i].dataset.panel !== undefined) {
194
+ this.prevPanel = allElements[i]
195
+ break
196
+ }
197
+ }
198
+
199
+ // Find the panel after the handle
200
+ this.nextPanel = null
201
+ for (let i = handleIndex + 1; i < allElements.length; i++) {
202
+ if (allElements[i].dataset.panel !== undefined) {
203
+ this.nextPanel = allElements[i]
204
+ break
205
+ }
206
+ }
207
+ }
208
+
209
+ storePanelSizes() {
210
+ if (this.prevPanel) {
211
+ this.prevPanelStartSize = this.getPanelSize(this.prevPanel)
212
+ }
213
+ if (this.nextPanel) {
214
+ this.nextPanelStartSize = this.getPanelSize(this.nextPanel)
215
+ }
216
+ }
217
+
218
+ getPanelSize(panel) {
219
+ const containerSize = this.isHorizontal
220
+ ? this.element.offsetWidth
221
+ : this.element.offsetHeight
222
+ const panelSize = this.isHorizontal
223
+ ? panel.offsetWidth
224
+ : panel.offsetHeight
225
+ return (panelSize / containerSize) * 100
226
+ }
227
+
228
+ setPanelSize(panel, percent) {
229
+ panel.style.flexBasis = `${percent}%`
230
+ panel.dataset.panelSize = percent
231
+ }
232
+
233
+ collapsePanel(which) {
234
+ this.findAdjacentPanels()
235
+ this.storePanelSizes()
236
+
237
+ if (which === 'prev' && this.prevPanel && this.nextPanel) {
238
+ const prevMin = parseFloat(this.prevPanel.dataset.minSize) || 0
239
+ this.setPanelSize(this.prevPanel, prevMin)
240
+ this.setPanelSize(this.nextPanel, this.prevPanelStartSize + this.nextPanelStartSize - prevMin)
241
+ } else if (which === 'next' && this.prevPanel && this.nextPanel) {
242
+ const nextMin = parseFloat(this.nextPanel.dataset.minSize) || 0
243
+ this.setPanelSize(this.nextPanel, nextMin)
244
+ this.setPanelSize(this.prevPanel, this.prevPanelStartSize + this.nextPanelStartSize - nextMin)
245
+ }
246
+ }
247
+
248
+ saveSizes() {
249
+ const sizes = this.panelTargets.map(panel => this.getPanelSize(panel))
250
+ localStorage.setItem(`resizable-${this.autoSaveIdValue}`, JSON.stringify(sizes))
251
+ }
252
+
253
+ loadSavedSizes() {
254
+ const saved = localStorage.getItem(`resizable-${this.autoSaveIdValue}`)
255
+ if (saved) {
256
+ try {
257
+ const sizes = JSON.parse(saved)
258
+ this.panelTargets.forEach((panel, index) => {
259
+ if (sizes[index] !== undefined) {
260
+ this.setPanelSize(panel, sizes[index])
261
+ }
262
+ })
263
+ } catch (e) {
264
+ console.warn('Failed to load saved panel sizes:', e)
265
+ }
266
+ }
267
+ }
268
+
269
+ get isHorizontal() {
270
+ return this.directionValue === 'horizontal'
271
+ }
272
+ }
@@ -0,0 +1,44 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Scroll Area controller for custom scrollbars
5
+ */
6
+ export default class ScrollAreaController extends Controller {
7
+ static targets: ["viewport", "scrollbar", "thumb"];
8
+ static values: {
9
+ orientation: { type: "String"; default: "vertical" };
10
+ type: { type: "String"; default: "hover" };
11
+ };
12
+
13
+ /** Scrollable viewport target */
14
+ readonly viewportTarget: HTMLElement;
15
+ readonly hasViewportTarget: boolean;
16
+
17
+ /** Scrollbar container targets */
18
+ readonly scrollbarTargets: HTMLElement[];
19
+ readonly hasScrollbarTarget: boolean;
20
+
21
+ /** Scrollbar thumb target */
22
+ readonly thumbTarget: HTMLElement;
23
+ readonly hasThumbTarget: boolean;
24
+
25
+ /** Scroll orientation: "vertical", "horizontal", "both" */
26
+ orientationValue: "vertical" | "horizontal" | "both";
27
+ readonly hasOrientationValue: boolean;
28
+
29
+ /** Scrollbar visibility type: "hover", "scroll", "always" */
30
+ typeValue: "hover" | "scroll" | "always";
31
+ readonly hasTypeValue: boolean;
32
+
33
+ /** Handle scroll events */
34
+ handleScroll(): void;
35
+
36
+ /** Update scrollbar thumb position and size */
37
+ updateScrollbar(): void;
38
+
39
+ /** Show the scrollbar */
40
+ showScrollbar(): void;
41
+
42
+ /** Hide the scrollbar */
43
+ hideScrollbar(): void;
44
+ }
@@ -0,0 +1,74 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Scroll Area controller for custom scrollbars
5
+ */
6
+ export default class extends Controller {
7
+ static targets = ["viewport", "scrollbar", "thumb"]
8
+ static values = {
9
+ orientation: { type: String, default: "vertical" },
10
+ type: { type: String, default: "hover" }
11
+ }
12
+
13
+ connect() {
14
+ this.updateScrollbar()
15
+
16
+ if (this.hasViewportTarget) {
17
+ this.viewportTarget.addEventListener("scroll", this.handleScroll.bind(this))
18
+ }
19
+
20
+ // Show scrollbar based on type
21
+ if (this.typeValue === "always") {
22
+ this.showScrollbar()
23
+ }
24
+ }
25
+
26
+ disconnect() {
27
+ if (this.hasViewportTarget) {
28
+ this.viewportTarget.removeEventListener("scroll", this.handleScroll.bind(this))
29
+ }
30
+ }
31
+
32
+ handleScroll() {
33
+ this.updateScrollbar()
34
+ }
35
+
36
+ updateScrollbar() {
37
+ if (!this.hasViewportTarget || !this.hasThumbTarget) return
38
+
39
+ const viewport = this.viewportTarget
40
+ const thumb = this.thumbTarget
41
+
42
+ if (this.orientationValue === "vertical" || this.orientationValue === "both") {
43
+ const scrollRatio = viewport.scrollTop / (viewport.scrollHeight - viewport.clientHeight)
44
+ const thumbHeight = Math.max((viewport.clientHeight / viewport.scrollHeight) * 100, 10)
45
+ const thumbTop = scrollRatio * (100 - thumbHeight)
46
+
47
+ thumb.style.height = `${thumbHeight}%`
48
+ thumb.style.top = `${thumbTop}%`
49
+ }
50
+
51
+ if (this.orientationValue === "horizontal" || this.orientationValue === "both") {
52
+ const scrollRatio = viewport.scrollLeft / (viewport.scrollWidth - viewport.clientWidth)
53
+ const thumbWidth = Math.max((viewport.clientWidth / viewport.scrollWidth) * 100, 10)
54
+ const thumbLeft = scrollRatio * (100 - thumbWidth)
55
+
56
+ thumb.style.width = `${thumbWidth}%`
57
+ thumb.style.left = `${thumbLeft}%`
58
+ }
59
+ }
60
+
61
+ showScrollbar() {
62
+ this.scrollbarTargets.forEach(scrollbar => {
63
+ scrollbar.style.opacity = "1"
64
+ })
65
+ }
66
+
67
+ hideScrollbar() {
68
+ if (this.typeValue !== "always") {
69
+ this.scrollbarTargets.forEach(scrollbar => {
70
+ scrollbar.style.opacity = "0"
71
+ })
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,84 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Select controller for custom select dropdowns
5
+ */
6
+ export default class SelectController extends Controller {
7
+ static targets: ["trigger", "content", "input", "item", "display", "checkIcon"];
8
+ static values: {
9
+ value: "String";
10
+ };
11
+
12
+ /** Select trigger target */
13
+ readonly triggerTarget: HTMLElement;
14
+ readonly hasTriggerTarget: boolean;
15
+
16
+ /** Select content/dropdown target */
17
+ readonly contentTarget: HTMLElement;
18
+ readonly hasContentTarget: boolean;
19
+
20
+ /** Hidden input target */
21
+ readonly inputTarget: HTMLInputElement;
22
+ readonly hasInputTarget: boolean;
23
+
24
+ /** Select option item targets */
25
+ readonly itemTargets: HTMLElement[];
26
+ readonly hasItemTarget: boolean;
27
+
28
+ /** Display element for selected value */
29
+ readonly displayTarget: HTMLElement;
30
+ readonly hasDisplayTarget: boolean;
31
+
32
+ /** Check icon targets */
33
+ readonly checkIconTargets: HTMLElement[];
34
+ readonly hasCheckIconTarget: boolean;
35
+
36
+ /** Currently selected value */
37
+ valueValue: string;
38
+ readonly hasValueValue: boolean;
39
+
40
+ /** Whether the select is currently open */
41
+ isOpen: boolean;
42
+
43
+ /** Currently focused item index */
44
+ focusedIndex: number;
45
+
46
+ /** Toggle select open/closed */
47
+ toggle(event?: Event): void;
48
+
49
+ /** Open the select dropdown */
50
+ open(): void;
51
+
52
+ /** Close the select dropdown */
53
+ close(): void;
54
+
55
+ /** Select an item */
56
+ select(event: Event): void;
57
+
58
+ /** Select by value programmatically */
59
+ selectByValue(value: string, dispatch?: boolean): void;
60
+
61
+ /** Handle clicks outside the select */
62
+ handleClickOutside(event: MouseEvent): void;
63
+
64
+ /** Handle keyboard navigation */
65
+ handleKeydown(event: KeyboardEvent): void;
66
+
67
+ /** Focus next item */
68
+ focusNextItem(): void;
69
+
70
+ /** Focus previous item */
71
+ focusPreviousItem(): void;
72
+
73
+ /** Focus first item */
74
+ focusFirstItem(): void;
75
+
76
+ /** Focus last item */
77
+ focusLastItem(): void;
78
+
79
+ /** Select the currently focused item */
80
+ selectFocusedItem(): void;
81
+
82
+ /** Get enabled (non-disabled) items */
83
+ readonly enabledItems: HTMLElement[];
84
+ }
@@ -0,0 +1,222 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Select controller for custom select dropdowns
5
+ */
6
+ export default class extends Controller {
7
+ static targets = ["trigger", "content", "input", "item", "display", "checkIcon"]
8
+ static values = {
9
+ value: String
10
+ }
11
+
12
+ connect() {
13
+ this.isOpen = false
14
+ this.focusedIndex = -1
15
+ this.boundHandleClickOutside = this.handleClickOutside.bind(this)
16
+
17
+ // Set initial value display
18
+ if (this.valueValue) {
19
+ this.selectByValue(this.valueValue, false)
20
+ }
21
+ }
22
+
23
+ disconnect() {
24
+ this.close()
25
+ }
26
+
27
+ toggle(event) {
28
+ event?.preventDefault()
29
+ if (this.isOpen) {
30
+ this.close()
31
+ } else {
32
+ this.open()
33
+ }
34
+ }
35
+
36
+ open() {
37
+ if (this.isOpen) return
38
+
39
+ this.isOpen = true
40
+
41
+ // Set trigger width as CSS variable for dropdown sizing
42
+ if (this.hasTriggerTarget && this.hasContentTarget) {
43
+ const triggerWidth = this.triggerTarget.offsetWidth
44
+ this.contentTarget.style.setProperty('--radix-select-trigger-width', `${triggerWidth}px`)
45
+ }
46
+
47
+ if (this.hasContentTarget) {
48
+ this.contentTarget.hidden = false
49
+ this.contentTarget.dataset.state = "open"
50
+ }
51
+
52
+ if (this.hasTriggerTarget) {
53
+ this.triggerTarget.setAttribute("aria-expanded", "true")
54
+ }
55
+
56
+ document.addEventListener("click", this.boundHandleClickOutside)
57
+
58
+ // Focus current value or first item
59
+ this.focusedIndex = -1
60
+ const currentItem = this.itemTargets.find(item => item.dataset.value === this.valueValue)
61
+ if (currentItem) {
62
+ this.focusedIndex = this.itemTargets.indexOf(currentItem)
63
+ currentItem.focus()
64
+ } else {
65
+ this.focusNextItem()
66
+ }
67
+
68
+ this.dispatch("opened")
69
+ }
70
+
71
+ close() {
72
+ if (!this.isOpen) return
73
+
74
+ this.isOpen = false
75
+
76
+ if (this.hasContentTarget) {
77
+ this.contentTarget.dataset.state = "closed"
78
+ setTimeout(() => {
79
+ if (!this.isOpen) {
80
+ this.contentTarget.hidden = true
81
+ }
82
+ }, 150)
83
+ }
84
+
85
+ if (this.hasTriggerTarget) {
86
+ this.triggerTarget.setAttribute("aria-expanded", "false")
87
+ }
88
+
89
+ document.removeEventListener("click", this.boundHandleClickOutside)
90
+ this.focusedIndex = -1
91
+
92
+ this.dispatch("closed")
93
+ }
94
+
95
+ select(event) {
96
+ const item = event.currentTarget
97
+ if (item.dataset.disabled !== undefined) return
98
+
99
+ const value = item.dataset.value
100
+ this.selectByValue(value)
101
+ this.close()
102
+ this.triggerTarget?.focus()
103
+ }
104
+
105
+ selectByValue(value, dispatch = true) {
106
+ this.valueValue = value
107
+
108
+ // Update hidden input
109
+ if (this.hasInputTarget) {
110
+ this.inputTarget.value = value
111
+ }
112
+
113
+ // Update display
114
+ const selectedItem = this.itemTargets.find(item => item.dataset.value === value)
115
+ if (this.hasDisplayTarget && selectedItem) {
116
+ this.displayTarget.textContent = selectedItem.textContent.trim()
117
+ }
118
+
119
+ // Update aria-selected and check icons
120
+ this.itemTargets.forEach(item => {
121
+ const isSelected = item.dataset.value === value
122
+ item.setAttribute("aria-selected", isSelected.toString())
123
+
124
+ const checkIcon = item.querySelector('[data-shadcn--select-target="checkIcon"]')
125
+ if (checkIcon) {
126
+ checkIcon.style.opacity = isSelected ? "1" : "0"
127
+ }
128
+ })
129
+
130
+ if (dispatch) {
131
+ this.dispatch("change", { detail: { value } })
132
+ }
133
+ }
134
+
135
+ handleClickOutside(event) {
136
+ if (!this.element.contains(event.target)) {
137
+ this.close()
138
+ }
139
+ }
140
+
141
+ handleKeydown(event) {
142
+ if (!this.isOpen) {
143
+ if (event.key === "Enter" || event.key === " " || event.key === "ArrowDown") {
144
+ event.preventDefault()
145
+ this.open()
146
+ }
147
+ return
148
+ }
149
+
150
+ switch (event.key) {
151
+ case "Escape":
152
+ event.preventDefault()
153
+ this.close()
154
+ this.triggerTarget?.focus()
155
+ break
156
+ case "ArrowDown":
157
+ event.preventDefault()
158
+ this.focusNextItem()
159
+ break
160
+ case "ArrowUp":
161
+ event.preventDefault()
162
+ this.focusPreviousItem()
163
+ break
164
+ case "Home":
165
+ event.preventDefault()
166
+ this.focusFirstItem()
167
+ break
168
+ case "End":
169
+ event.preventDefault()
170
+ this.focusLastItem()
171
+ break
172
+ case "Enter":
173
+ case " ":
174
+ event.preventDefault()
175
+ this.selectFocusedItem()
176
+ break
177
+ }
178
+ }
179
+
180
+ focusNextItem() {
181
+ const items = this.enabledItems
182
+ if (items.length === 0) return
183
+
184
+ this.focusedIndex = (this.focusedIndex + 1) % items.length
185
+ items[this.focusedIndex].focus()
186
+ }
187
+
188
+ focusPreviousItem() {
189
+ const items = this.enabledItems
190
+ if (items.length === 0) return
191
+
192
+ this.focusedIndex = this.focusedIndex <= 0 ? items.length - 1 : this.focusedIndex - 1
193
+ items[this.focusedIndex].focus()
194
+ }
195
+
196
+ focusFirstItem() {
197
+ const items = this.enabledItems
198
+ if (items.length === 0) return
199
+
200
+ this.focusedIndex = 0
201
+ items[0].focus()
202
+ }
203
+
204
+ focusLastItem() {
205
+ const items = this.enabledItems
206
+ if (items.length === 0) return
207
+
208
+ this.focusedIndex = items.length - 1
209
+ items[this.focusedIndex].focus()
210
+ }
211
+
212
+ selectFocusedItem() {
213
+ const items = this.enabledItems
214
+ if (this.focusedIndex >= 0 && this.focusedIndex < items.length) {
215
+ items[this.focusedIndex].click()
216
+ }
217
+ }
218
+
219
+ get enabledItems() {
220
+ return this.itemTargets.filter(item => item.dataset.disabled === undefined)
221
+ }
222
+ }