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,263 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Carousel controller for sliding content
5
+ * Handles navigation, autoplay, keyboard navigation, and touch/swipe
6
+ */
7
+ export default class extends Controller {
8
+ static targets = ["viewport", "content", "item", "prevButton", "nextButton"]
9
+ static values = {
10
+ orientation: { type: String, default: "horizontal" },
11
+ loop: { type: Boolean, default: false },
12
+ autoplay: { type: Boolean, default: false },
13
+ autoplayInterval: { type: Number, default: 4000 },
14
+ align: { type: String, default: "start" },
15
+ selectedIndex: { type: Number, default: 0 }
16
+ }
17
+
18
+ connect() {
19
+ this.boundHandleKeydown = this.handleKeydown.bind(this)
20
+ this.element.addEventListener("keydown", this.boundHandleKeydown)
21
+
22
+ // Touch/swipe support
23
+ this.touchStartX = 0
24
+ this.touchStartY = 0
25
+ this.boundHandleTouchStart = this.handleTouchStart.bind(this)
26
+ this.boundHandleTouchEnd = this.handleTouchEnd.bind(this)
27
+
28
+ if (this.hasViewportTarget) {
29
+ this.viewportTarget.addEventListener("touchstart", this.boundHandleTouchStart, { passive: true })
30
+ this.viewportTarget.addEventListener("touchend", this.boundHandleTouchEnd, { passive: true })
31
+ }
32
+
33
+ // Set initial state
34
+ this.updateButtonStates()
35
+ this.scrollToIndex(this.selectedIndexValue, false)
36
+
37
+ // Start autoplay if enabled
38
+ if (this.autoplayValue) {
39
+ this.startAutoplay()
40
+ }
41
+ }
42
+
43
+ disconnect() {
44
+ this.element.removeEventListener("keydown", this.boundHandleKeydown)
45
+
46
+ if (this.hasViewportTarget) {
47
+ this.viewportTarget.removeEventListener("touchstart", this.boundHandleTouchStart)
48
+ this.viewportTarget.removeEventListener("touchend", this.boundHandleTouchEnd)
49
+ }
50
+
51
+ this.stopAutoplay()
52
+ }
53
+
54
+ previous() {
55
+ const newIndex = this.selectedIndexValue - 1
56
+
57
+ if (newIndex < 0) {
58
+ if (this.loopValue) {
59
+ this.selectedIndexValue = this.itemTargets.length - 1
60
+ }
61
+ } else {
62
+ this.selectedIndexValue = newIndex
63
+ }
64
+
65
+ this.scrollToIndex(this.selectedIndexValue)
66
+ this.dispatch("select", { detail: { index: this.selectedIndexValue } })
67
+ }
68
+
69
+ next() {
70
+ const newIndex = this.selectedIndexValue + 1
71
+ const maxIndex = this.itemTargets.length - 1
72
+
73
+ if (newIndex > maxIndex) {
74
+ if (this.loopValue) {
75
+ this.selectedIndexValue = 0
76
+ }
77
+ } else {
78
+ this.selectedIndexValue = newIndex
79
+ }
80
+
81
+ this.scrollToIndex(this.selectedIndexValue)
82
+ this.dispatch("select", { detail: { index: this.selectedIndexValue } })
83
+ }
84
+
85
+ goToSlide(event) {
86
+ const index = parseInt(event.currentTarget.dataset.index, 10)
87
+ if (!isNaN(index) && index >= 0 && index < this.itemTargets.length) {
88
+ this.selectedIndexValue = index
89
+ this.scrollToIndex(index)
90
+ this.dispatch("select", { detail: { index } })
91
+ }
92
+ }
93
+
94
+ scrollToIndex(index, animate = true) {
95
+ if (!this.hasContentTarget || !this.itemTargets.length) return
96
+
97
+ const item = this.itemTargets[index]
98
+ if (!item) return
99
+
100
+ const isHorizontal = this.orientationValue === "horizontal"
101
+
102
+ // Calculate scroll position
103
+ let scrollPosition
104
+ if (isHorizontal) {
105
+ scrollPosition = item.offsetLeft - this.getAlignOffset(item, "width")
106
+ } else {
107
+ scrollPosition = item.offsetTop - this.getAlignOffset(item, "height")
108
+ }
109
+
110
+ // Apply scroll
111
+ if (animate) {
112
+ this.contentTarget.style.transition = "transform 0.3s ease-out"
113
+ } else {
114
+ this.contentTarget.style.transition = "none"
115
+ }
116
+
117
+ if (isHorizontal) {
118
+ this.contentTarget.style.transform = `translateX(-${scrollPosition}px)`
119
+ } else {
120
+ this.contentTarget.style.transform = `translateY(-${scrollPosition}px)`
121
+ }
122
+
123
+ // Update ARIA attributes
124
+ this.itemTargets.forEach((target, i) => {
125
+ target.setAttribute("aria-hidden", i !== index)
126
+ target.inert = i !== index
127
+ })
128
+
129
+ this.updateButtonStates()
130
+ }
131
+
132
+ getAlignOffset(item, dimension) {
133
+ if (this.alignValue === "center") {
134
+ const viewportSize = dimension === "width"
135
+ ? this.viewportTarget.offsetWidth
136
+ : this.viewportTarget.offsetHeight
137
+ const itemSize = dimension === "width"
138
+ ? item.offsetWidth
139
+ : item.offsetHeight
140
+ return (viewportSize - itemSize) / 2
141
+ } else if (this.alignValue === "end") {
142
+ const viewportSize = dimension === "width"
143
+ ? this.viewportTarget.offsetWidth
144
+ : this.viewportTarget.offsetHeight
145
+ const itemSize = dimension === "width"
146
+ ? item.offsetWidth
147
+ : item.offsetHeight
148
+ return viewportSize - itemSize
149
+ }
150
+ return 0 // start alignment
151
+ }
152
+
153
+ updateButtonStates() {
154
+ const atStart = this.selectedIndexValue === 0
155
+ const atEnd = this.selectedIndexValue === this.itemTargets.length - 1
156
+
157
+ if (this.hasPrevButtonTarget) {
158
+ this.prevButtonTarget.disabled = !this.loopValue && atStart
159
+ }
160
+
161
+ if (this.hasNextButtonTarget) {
162
+ this.nextButtonTarget.disabled = !this.loopValue && atEnd
163
+ }
164
+ }
165
+
166
+ handleKeydown(event) {
167
+ const isHorizontal = this.orientationValue === "horizontal"
168
+
169
+ if (isHorizontal) {
170
+ if (event.key === "ArrowLeft") {
171
+ event.preventDefault()
172
+ this.previous()
173
+ } else if (event.key === "ArrowRight") {
174
+ event.preventDefault()
175
+ this.next()
176
+ }
177
+ } else {
178
+ if (event.key === "ArrowUp") {
179
+ event.preventDefault()
180
+ this.previous()
181
+ } else if (event.key === "ArrowDown") {
182
+ event.preventDefault()
183
+ this.next()
184
+ }
185
+ }
186
+ }
187
+
188
+ handleTouchStart(event) {
189
+ this.touchStartX = event.touches[0].clientX
190
+ this.touchStartY = event.touches[0].clientY
191
+
192
+ // Pause autoplay on interaction
193
+ if (this.autoplayValue) {
194
+ this.stopAutoplay()
195
+ }
196
+ }
197
+
198
+ handleTouchEnd(event) {
199
+ const touchEndX = event.changedTouches[0].clientX
200
+ const touchEndY = event.changedTouches[0].clientY
201
+
202
+ const deltaX = touchEndX - this.touchStartX
203
+ const deltaY = touchEndY - this.touchStartY
204
+
205
+ const isHorizontal = this.orientationValue === "horizontal"
206
+ const threshold = 50
207
+
208
+ if (isHorizontal) {
209
+ if (Math.abs(deltaX) > threshold && Math.abs(deltaX) > Math.abs(deltaY)) {
210
+ if (deltaX > 0) {
211
+ this.previous()
212
+ } else {
213
+ this.next()
214
+ }
215
+ }
216
+ } else {
217
+ if (Math.abs(deltaY) > threshold && Math.abs(deltaY) > Math.abs(deltaX)) {
218
+ if (deltaY > 0) {
219
+ this.previous()
220
+ } else {
221
+ this.next()
222
+ }
223
+ }
224
+ }
225
+
226
+ // Resume autoplay after interaction
227
+ if (this.autoplayValue) {
228
+ this.startAutoplay()
229
+ }
230
+ }
231
+
232
+ startAutoplay() {
233
+ this.stopAutoplay()
234
+ this.autoplayTimer = setInterval(() => {
235
+ this.next()
236
+ }, this.autoplayIntervalValue)
237
+ }
238
+
239
+ stopAutoplay() {
240
+ if (this.autoplayTimer) {
241
+ clearInterval(this.autoplayTimer)
242
+ this.autoplayTimer = null
243
+ }
244
+ }
245
+
246
+ // Pause autoplay when mouse enters
247
+ mouseEnter() {
248
+ if (this.autoplayValue) {
249
+ this.stopAutoplay()
250
+ }
251
+ }
252
+
253
+ // Resume autoplay when mouse leaves
254
+ mouseLeave() {
255
+ if (this.autoplayValue) {
256
+ this.startAutoplay()
257
+ }
258
+ }
259
+
260
+ selectedIndexValueChanged() {
261
+ this.scrollToIndex(this.selectedIndexValue)
262
+ }
263
+ }
@@ -0,0 +1,31 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Checkbox controller for custom checkboxes
5
+ */
6
+ export default class CheckboxController extends Controller {
7
+ static values: {
8
+ checked: { type: "Boolean"; default: false };
9
+ name: "String";
10
+ };
11
+
12
+ /** Whether the checkbox is checked */
13
+ checkedValue: boolean;
14
+ readonly hasCheckedValue: boolean;
15
+
16
+ /** Input name for form submission */
17
+ nameValue: string;
18
+ readonly hasNameValue: boolean;
19
+
20
+ /** Toggle the checkbox state */
21
+ toggle(): void;
22
+
23
+ /** Update the visual state */
24
+ updateState(): void;
25
+
26
+ /** Update hidden input value */
27
+ updateHiddenInput(): void;
28
+
29
+ /** Called when checkedValue changes */
30
+ checkedValueChanged(): void;
31
+ }
@@ -0,0 +1,48 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Checkbox controller for custom checkboxes
5
+ */
6
+ export default class extends Controller {
7
+ static values = {
8
+ checked: { type: Boolean, default: false },
9
+ name: String
10
+ }
11
+
12
+ connect() {
13
+ this.updateState()
14
+ }
15
+
16
+ toggle() {
17
+ this.checkedValue = !this.checkedValue
18
+ this.updateState()
19
+ this.updateHiddenInput()
20
+ this.dispatch("change", { detail: { checked: this.checkedValue } })
21
+ }
22
+
23
+ updateState() {
24
+ const state = this.checkedValue ? "checked" : "unchecked"
25
+ this.element.dataset.state = state
26
+ this.element.setAttribute("aria-checked", this.checkedValue.toString())
27
+
28
+ // Update checkmark visibility
29
+ const checkIcon = this.element.querySelector("svg")
30
+ if (checkIcon) {
31
+ checkIcon.style.opacity = this.checkedValue ? "1" : "0"
32
+ }
33
+ }
34
+
35
+ updateHiddenInput() {
36
+ if (!this.nameValue) return
37
+
38
+ // Find or create hidden input
39
+ let input = this.element.parentElement.querySelector(`input[name="${this.nameValue}"]`)
40
+ if (input) {
41
+ input.value = this.checkedValue ? "1" : "0"
42
+ }
43
+ }
44
+
45
+ checkedValueChanged() {
46
+ this.updateState()
47
+ }
48
+ }
@@ -0,0 +1,43 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Collapsible controller for expandable content
5
+ */
6
+ export default class CollapsibleController extends Controller {
7
+ static targets: ["trigger", "content"];
8
+ static values: {
9
+ open: { type: "Boolean"; default: false };
10
+ disabled: { type: "Boolean"; default: false };
11
+ };
12
+
13
+ /** Trigger element target */
14
+ readonly triggerTarget: HTMLElement;
15
+ readonly hasTriggerTarget: boolean;
16
+
17
+ /** Content element target */
18
+ readonly contentTarget: HTMLElement;
19
+ readonly hasContentTarget: boolean;
20
+
21
+ /** Whether the collapsible is open */
22
+ openValue: boolean;
23
+ readonly hasOpenValue: boolean;
24
+
25
+ /** Whether the collapsible is disabled */
26
+ disabledValue: boolean;
27
+ readonly hasDisabledValue: boolean;
28
+
29
+ /** Toggle open/closed state */
30
+ toggle(): void;
31
+
32
+ /** Open the collapsible */
33
+ open(): void;
34
+
35
+ /** Close the collapsible */
36
+ close(): void;
37
+
38
+ /** Update the visual state */
39
+ updateState(): void;
40
+
41
+ /** Called when openValue changes */
42
+ openValueChanged(): void;
43
+ }
@@ -0,0 +1,73 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Collapsible controller for expandable content
5
+ */
6
+ export default class extends Controller {
7
+ static targets = ["trigger", "content"]
8
+ static values = {
9
+ open: { type: Boolean, default: false },
10
+ disabled: { type: Boolean, default: false }
11
+ }
12
+
13
+ connect() {
14
+ this.updateState()
15
+ }
16
+
17
+ toggle() {
18
+ if (this.disabledValue) return
19
+
20
+ this.openValue = !this.openValue
21
+ this.updateState()
22
+ }
23
+
24
+ open() {
25
+ if (this.disabledValue) return
26
+
27
+ this.openValue = true
28
+ this.updateState()
29
+ }
30
+
31
+ close() {
32
+ this.openValue = false
33
+ this.updateState()
34
+ }
35
+
36
+ updateState() {
37
+ const state = this.openValue ? "open" : "closed"
38
+ this.element.dataset.state = state
39
+
40
+ if (this.hasContentTarget) {
41
+ this.contentTarget.dataset.state = state
42
+
43
+ if (this.openValue) {
44
+ this.contentTarget.hidden = false
45
+ // Animate open
46
+ const height = this.contentTarget.scrollHeight
47
+ this.contentTarget.style.height = "0px"
48
+ requestAnimationFrame(() => {
49
+ this.contentTarget.style.height = `${height}px`
50
+ setTimeout(() => {
51
+ this.contentTarget.style.height = ""
52
+ }, 200)
53
+ })
54
+ } else {
55
+ // Animate close
56
+ this.contentTarget.style.height = `${this.contentTarget.scrollHeight}px`
57
+ requestAnimationFrame(() => {
58
+ this.contentTarget.style.height = "0px"
59
+ setTimeout(() => {
60
+ this.contentTarget.hidden = true
61
+ this.contentTarget.style.height = ""
62
+ }, 200)
63
+ })
64
+ }
65
+ }
66
+
67
+ this.dispatch(this.openValue ? "opened" : "closed")
68
+ }
69
+
70
+ openValueChanged() {
71
+ this.updateState()
72
+ }
73
+ }
@@ -0,0 +1,234 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Combobox controller for searchable select dropdown
5
+ * Handles open/close, filtering, keyboard navigation, and item selection
6
+ */
7
+ export default class extends Controller {
8
+ static targets = ["trigger", "content", "input", "list", "item", "empty", "displayValue", "hiddenInput"]
9
+ static values = {
10
+ open: { type: Boolean, default: false },
11
+ value: { type: String, default: "" },
12
+ selectedIndex: { type: Number, default: -1 }
13
+ }
14
+
15
+ connect() {
16
+ this.boundHandleKeydown = this.handleKeydown.bind(this)
17
+ }
18
+
19
+ disconnect() {
20
+ document.removeEventListener("keydown", this.boundHandleKeydown)
21
+ }
22
+
23
+ toggle() {
24
+ if (this.openValue) {
25
+ this.close()
26
+ } else {
27
+ this.open()
28
+ }
29
+ }
30
+
31
+ open() {
32
+ if (this.openValue) return
33
+
34
+ this.openValue = true
35
+ this.contentTarget.hidden = false
36
+ this.contentTarget.dataset.state = "open"
37
+ this.triggerTarget.setAttribute("aria-expanded", "true")
38
+
39
+ // Focus the input
40
+ requestAnimationFrame(() => {
41
+ if (this.hasInputTarget) {
42
+ this.inputTarget.focus()
43
+ }
44
+ })
45
+
46
+ // Add keyboard listener
47
+ document.addEventListener("keydown", this.boundHandleKeydown)
48
+
49
+ // Reset selection index
50
+ this.selectedIndexValue = -1
51
+ this.updateSelection()
52
+ }
53
+
54
+ close() {
55
+ if (!this.openValue) return
56
+
57
+ this.openValue = false
58
+ this.contentTarget.dataset.state = "closed"
59
+ this.triggerTarget.setAttribute("aria-expanded", "false")
60
+
61
+ // Hide after animation completes, then reset filter state
62
+ const hideAndReset = () => {
63
+ this.contentTarget.hidden = true
64
+ // Reset search and filter state after hiding to avoid flash
65
+ if (this.hasInputTarget) {
66
+ this.inputTarget.value = ""
67
+ }
68
+ // Reset all items to visible for next open
69
+ this.itemTargets.forEach((item) => {
70
+ item.style.display = ""
71
+ })
72
+ // Hide empty state
73
+ if (this.hasEmptyTarget) {
74
+ this.emptyTarget.hidden = true
75
+ }
76
+ }
77
+
78
+ // Listen for animation end, with fallback timeout
79
+ const onAnimationEnd = () => {
80
+ this.contentTarget.removeEventListener("animationend", onAnimationEnd)
81
+ hideAndReset()
82
+ }
83
+ this.contentTarget.addEventListener("animationend", onAnimationEnd)
84
+
85
+ // Fallback in case animation doesn't fire (e.g., no animation defined)
86
+ setTimeout(() => {
87
+ this.contentTarget.removeEventListener("animationend", onAnimationEnd)
88
+ if (!this.contentTarget.hidden) {
89
+ hideAndReset()
90
+ }
91
+ }, 200)
92
+
93
+ // Remove keyboard listener
94
+ document.removeEventListener("keydown", this.boundHandleKeydown)
95
+ }
96
+
97
+ /**
98
+ * Filter items based on input value
99
+ */
100
+ filter() {
101
+ const query = this.hasInputTarget ? this.inputTarget.value.toLowerCase().trim() : ""
102
+ let visibleCount = 0
103
+
104
+ this.itemTargets.forEach((item) => {
105
+ const label = item.dataset.label?.toLowerCase() || item.textContent.toLowerCase()
106
+ const value = item.dataset.value?.toLowerCase() || ""
107
+ const matches = query === "" || label.includes(query) || value.includes(query)
108
+ // Use style.display instead of hidden attribute to avoid Tailwind flex override
109
+ item.style.display = matches ? "" : "none"
110
+ if (matches) visibleCount++
111
+ })
112
+
113
+ // Show/hide empty state - only show when there's a query AND no results
114
+ if (this.hasEmptyTarget) {
115
+ const shouldHide = query === "" || visibleCount > 0
116
+ this.emptyTarget.hidden = shouldHide
117
+ }
118
+
119
+ // Reset selection
120
+ this.selectedIndexValue = -1
121
+ this.updateSelection()
122
+ }
123
+
124
+ /**
125
+ * Select an item
126
+ */
127
+ select(event) {
128
+ const item = event.currentTarget
129
+ const value = item.dataset.value
130
+ const label = item.dataset.label
131
+
132
+ // Update value
133
+ this.valueValue = value
134
+
135
+ // Update hidden input for form submission
136
+ if (this.hasHiddenInputTarget) {
137
+ this.hiddenInputTarget.value = value
138
+ }
139
+
140
+ // Update display value
141
+ if (this.hasDisplayValueTarget) {
142
+ this.displayValueTarget.textContent = label
143
+ this.displayValueTarget.classList.remove("text-muted-foreground")
144
+ }
145
+
146
+ // Update selected state on items
147
+ this.itemTargets.forEach((i) => {
148
+ const isSelected = i.dataset.value === value
149
+ i.dataset.selected = isSelected
150
+ // Update check icon opacity
151
+ const checkIcon = i.querySelector("svg")
152
+ if (checkIcon) {
153
+ if (isSelected) {
154
+ checkIcon.classList.remove("opacity-0")
155
+ checkIcon.classList.add("opacity-100")
156
+ } else {
157
+ checkIcon.classList.remove("opacity-100")
158
+ checkIcon.classList.add("opacity-0")
159
+ }
160
+ }
161
+ })
162
+
163
+ // Dispatch change event
164
+ this.dispatch("change", { detail: { value, label } })
165
+
166
+ // Close the dropdown
167
+ this.close()
168
+ }
169
+
170
+ /**
171
+ * Handle keyboard navigation
172
+ */
173
+ handleKeydown(event) {
174
+ const visibleItems = this.getVisibleItems()
175
+
176
+ switch (event.key) {
177
+ case "ArrowDown":
178
+ event.preventDefault()
179
+ this.selectedIndexValue = Math.min(this.selectedIndexValue + 1, visibleItems.length - 1)
180
+ this.updateSelection()
181
+ break
182
+ case "ArrowUp":
183
+ event.preventDefault()
184
+ this.selectedIndexValue = Math.max(this.selectedIndexValue - 1, 0)
185
+ this.updateSelection()
186
+ break
187
+ case "Enter":
188
+ event.preventDefault()
189
+ if (this.selectedIndexValue >= 0 && visibleItems[this.selectedIndexValue]) {
190
+ // Simulate click on the selected item
191
+ visibleItems[this.selectedIndexValue].click()
192
+ }
193
+ break
194
+ case "Escape":
195
+ event.preventDefault()
196
+ this.close()
197
+ break
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Update visual selection state
203
+ */
204
+ updateSelection() {
205
+ const visibleItems = this.getVisibleItems()
206
+
207
+ visibleItems.forEach((item, index) => {
208
+ if (index === this.selectedIndexValue) {
209
+ item.classList.add("bg-accent", "text-accent-foreground")
210
+ item.scrollIntoView({ block: "nearest" })
211
+ } else {
212
+ item.classList.remove("bg-accent", "text-accent-foreground")
213
+ }
214
+ })
215
+ }
216
+
217
+ /**
218
+ * Get all visible items
219
+ */
220
+ getVisibleItems() {
221
+ return this.itemTargets.filter((item) => item.style.display !== "none")
222
+ }
223
+
224
+ /**
225
+ * Handle click outside to close
226
+ */
227
+ handleClickOutside(event) {
228
+ if (!this.openValue) return
229
+
230
+ if (!this.element.contains(event.target)) {
231
+ this.close()
232
+ }
233
+ }
234
+ }