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,364 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Slider Controller
5
+ *
6
+ * Handles slider value selection with drag and keyboard support
7
+ *
8
+ * Targets:
9
+ * - track: The slider track
10
+ * - range: The filled range portion
11
+ * - thumb: The draggable thumb
12
+ * - input: Hidden input for form submission
13
+ * - output: Optional element to display the current value (auto-synced)
14
+ *
15
+ * Values:
16
+ * - min: Minimum value
17
+ * - max: Maximum value
18
+ * - step: Step increment
19
+ * - value: Current value
20
+ * - name: Input name
21
+ * - disabled: Whether slider is disabled
22
+ * - outputFormat: Format string for output (use {value} for value, {percent} for percentage)
23
+ *
24
+ * Data attributes for native <input type="range">:
25
+ * - data-output-target: ID of element to display value (one-way: slider → output)
26
+ * - data-output-format: Format string with {value} and {percent} placeholders
27
+ * - data-input-target: ID of input element for two-way binding (slider ↔ input)
28
+ */
29
+ export default class extends Controller {
30
+ static targets = ["track", "range", "thumb", "input", "output"]
31
+ static values = {
32
+ min: { type: Number, default: 0 },
33
+ max: { type: Number, default: 100 },
34
+ step: { type: Number, default: 1 },
35
+ value: { type: Number, default: 0 },
36
+ name: String,
37
+ disabled: { type: Boolean, default: false },
38
+ outputFormat: { type: String, default: "{value}" }
39
+ }
40
+
41
+ connect() {
42
+ this.isDragging = false
43
+ this.updateVisuals()
44
+ this.setupTwoWayBindings()
45
+ }
46
+
47
+ disconnect() {
48
+ this.teardownTwoWayBindings()
49
+ }
50
+
51
+ /**
52
+ * Set up two-way bindings for native range inputs with data-input-target
53
+ */
54
+ setupTwoWayBindings() {
55
+ this.inputBindings = []
56
+
57
+ // Check if the controller element itself is a range input with data-input-target
58
+ // (This is the case when data-controller is on the input element directly)
59
+ if (this.element.matches &&
60
+ this.element.matches('input[type="range"][data-input-target]')) {
61
+ this.setupBindingForInput(this.element)
62
+ return
63
+ }
64
+
65
+ // Otherwise, find all native range inputs with data-input-target attribute within the element
66
+ const rangeInputs = this.element.querySelectorAll('input[type="range"][data-input-target]')
67
+ rangeInputs.forEach(rangeInput => {
68
+ this.setupBindingForInput(rangeInput)
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Set up two-way binding for a single range input
74
+ * @param {HTMLInputElement} rangeInput - The range input element
75
+ */
76
+ setupBindingForInput(rangeInput) {
77
+ const inputTargetId = rangeInput.dataset.inputTarget
78
+ const linkedInput = document.getElementById(inputTargetId)
79
+
80
+ if (linkedInput) {
81
+ // Create bound handler for this specific pair
82
+ const handler = this.handleLinkedInputChange.bind(this, rangeInput)
83
+
84
+ // Store binding info for cleanup
85
+ this.inputBindings.push({
86
+ rangeInput,
87
+ linkedInput,
88
+ handler
89
+ })
90
+
91
+ // Listen for changes on the linked input
92
+ linkedInput.addEventListener('input', handler)
93
+ linkedInput.addEventListener('change', handler)
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Clean up event listeners when disconnecting
99
+ */
100
+ teardownTwoWayBindings() {
101
+ if (this.inputBindings) {
102
+ this.inputBindings.forEach(({ linkedInput, handler }) => {
103
+ linkedInput.removeEventListener('input', handler)
104
+ linkedInput.removeEventListener('change', handler)
105
+ })
106
+ this.inputBindings = []
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Handle changes from a linked input element (input → slider sync)
112
+ * @param {HTMLInputElement} rangeInput - The range input to update
113
+ * @param {Event} event - The input/change event from the linked input
114
+ */
115
+ handleLinkedInputChange(rangeInput, event) {
116
+ const linkedInput = event.target
117
+ let value = parseFloat(linkedInput.value)
118
+
119
+ // Validate and clamp the value
120
+ const min = parseFloat(rangeInput.min) || 0
121
+ const max = parseFloat(rangeInput.max) || 100
122
+ const step = parseFloat(rangeInput.step) || 1
123
+
124
+ // Handle invalid input
125
+ if (isNaN(value)) {
126
+ value = min
127
+ }
128
+
129
+ // Clamp to min/max
130
+ value = Math.max(min, Math.min(max, value))
131
+
132
+ // Snap to step
133
+ const steps = Math.round((value - min) / step)
134
+ value = min + steps * step
135
+
136
+ // Update range input
137
+ rangeInput.value = value
138
+
139
+ // Update CSS custom property for fill
140
+ const percentage = ((value - min) / (max - min)) * 100
141
+ rangeInput.style.setProperty("--slider-fill", `${percentage}%`)
142
+
143
+ // Update the linked input if value was clamped/snapped
144
+ if (parseFloat(linkedInput.value) !== value) {
145
+ linkedInput.value = value
146
+ }
147
+
148
+ // Also update output if present
149
+ const outputTargetId = rangeInput.dataset.outputTarget
150
+ if (outputTargetId) {
151
+ const outputElement = document.getElementById(outputTargetId)
152
+ if (outputElement) {
153
+ const format = rangeInput.dataset.outputFormat || "{value}"
154
+ const formattedValue = format
155
+ .replace("{value}", value)
156
+ .replace("{percent}", Math.round(percentage))
157
+ outputElement.textContent = formattedValue
158
+ }
159
+ }
160
+
161
+ // Dispatch change event
162
+ this.dispatch("change", {
163
+ detail: { value: value, percentage: percentage }
164
+ })
165
+ }
166
+
167
+ startDrag(event) {
168
+ if (this.disabledValue) return
169
+
170
+ event.preventDefault()
171
+ this.isDragging = true
172
+
173
+ // Handle both mouse and touch events
174
+ const moveEvent = event.type === "touchstart" ? "touchmove" : "mousemove"
175
+ const endEvent = event.type === "touchstart" ? "touchend" : "mouseup"
176
+
177
+ this.handleDrag(event)
178
+
179
+ this.boundHandleDrag = this.handleDrag.bind(this)
180
+ this.boundStopDrag = this.stopDrag.bind(this)
181
+
182
+ document.addEventListener(moveEvent, this.boundHandleDrag)
183
+ document.addEventListener(endEvent, this.boundStopDrag)
184
+ }
185
+
186
+ handleDrag(event) {
187
+ if (!this.isDragging && event.type !== "mousedown" && event.type !== "touchstart") return
188
+
189
+ const track = this.trackTarget
190
+ const rect = track.getBoundingClientRect()
191
+
192
+ // Get clientX from either mouse or touch event
193
+ const clientX = event.type.includes("touch")
194
+ ? event.touches[0].clientX
195
+ : event.clientX
196
+
197
+ const percentage = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
198
+ const rawValue = this.minValue + percentage * (this.maxValue - this.minValue)
199
+ const steppedValue = this.snapToStep(rawValue)
200
+
201
+ this.valueValue = steppedValue
202
+ this.updateVisuals()
203
+ this.dispatchChange()
204
+ }
205
+
206
+ stopDrag() {
207
+ this.isDragging = false
208
+ document.removeEventListener("mousemove", this.boundHandleDrag)
209
+ document.removeEventListener("mouseup", this.boundStopDrag)
210
+ document.removeEventListener("touchmove", this.boundHandleDrag)
211
+ document.removeEventListener("touchend", this.boundStopDrag)
212
+ }
213
+
214
+ handleKeydown(event) {
215
+ if (this.disabledValue) return
216
+
217
+ let newValue = this.valueValue
218
+ const bigStep = (this.maxValue - this.minValue) / 10
219
+
220
+ switch (event.key) {
221
+ case "ArrowRight":
222
+ case "ArrowUp":
223
+ event.preventDefault()
224
+ newValue = Math.min(this.maxValue, this.valueValue + this.stepValue)
225
+ break
226
+ case "ArrowLeft":
227
+ case "ArrowDown":
228
+ event.preventDefault()
229
+ newValue = Math.max(this.minValue, this.valueValue - this.stepValue)
230
+ break
231
+ case "PageUp":
232
+ event.preventDefault()
233
+ newValue = Math.min(this.maxValue, this.valueValue + bigStep)
234
+ break
235
+ case "PageDown":
236
+ event.preventDefault()
237
+ newValue = Math.max(this.minValue, this.valueValue - bigStep)
238
+ break
239
+ case "Home":
240
+ event.preventDefault()
241
+ newValue = this.minValue
242
+ break
243
+ case "End":
244
+ event.preventDefault()
245
+ newValue = this.maxValue
246
+ break
247
+ default:
248
+ return
249
+ }
250
+
251
+ this.valueValue = this.snapToStep(newValue)
252
+ this.updateVisuals()
253
+ this.dispatchChange()
254
+ }
255
+
256
+ snapToStep(value) {
257
+ const steps = Math.round((value - this.minValue) / this.stepValue)
258
+ return Math.max(this.minValue, Math.min(this.maxValue, this.minValue + steps * this.stepValue))
259
+ }
260
+
261
+ updateVisuals() {
262
+ const percentage = this.percentage
263
+
264
+ if (this.hasRangeTarget) {
265
+ this.rangeTarget.style.width = `${percentage}%`
266
+ }
267
+
268
+ if (this.hasThumbTarget) {
269
+ this.thumbTarget.style.left = `calc(${percentage}% - 8px)`
270
+ }
271
+
272
+ // Update ARIA attributes
273
+ this.element.setAttribute("aria-valuenow", this.valueValue)
274
+
275
+ // Update hidden input
276
+ if (this.hasInputTarget) {
277
+ this.inputTarget.value = this.valueValue
278
+ }
279
+
280
+ // Update output element if present (for syncing value labels)
281
+ if (this.hasOutputTarget) {
282
+ this.updateOutput()
283
+ }
284
+ }
285
+
286
+ updateOutput() {
287
+ const formattedValue = this.outputFormatValue
288
+ .replace("{value}", this.valueValue)
289
+ .replace("{percent}", Math.round(this.percentage))
290
+
291
+ this.outputTarget.textContent = formattedValue
292
+ }
293
+
294
+ dispatchChange() {
295
+ this.dispatch("change", {
296
+ detail: { value: this.valueValue, name: this.nameValue }
297
+ })
298
+
299
+ // Dispatch native input event for form compatibility
300
+ if (this.hasInputTarget) {
301
+ this.inputTarget.dispatchEvent(new Event("input", { bubbles: true }))
302
+ }
303
+ }
304
+
305
+ get percentage() {
306
+ if (this.maxValue === this.minValue) return 0
307
+ return ((this.valueValue - this.minValue) / (this.maxValue - this.minValue)) * 100
308
+ }
309
+
310
+ valueValueChanged() {
311
+ this.updateVisuals()
312
+ }
313
+
314
+ /**
315
+ * Update style for native input range element
316
+ * Called on input event from native <input type="range">
317
+ * Updates CSS custom property for fill and syncs output element
318
+ */
319
+ updateStyle(event) {
320
+ const input = event.target
321
+ const value = parseFloat(input.value)
322
+ const min = parseFloat(input.min) || 0
323
+ const max = parseFloat(input.max) || 100
324
+
325
+ // Calculate percentage and update CSS custom property
326
+ const percentage = ((value - min) / (max - min)) * 100
327
+ input.style.setProperty("--slider-fill", `${percentage}%`)
328
+
329
+ // Update value for output sync
330
+ this.valueValue = value
331
+
332
+ // Update output if present (Stimulus target)
333
+ if (this.hasOutputTarget) {
334
+ this.updateOutput()
335
+ }
336
+
337
+ // Also check for data-output-target attribute (ID-based targeting)
338
+ const outputTargetId = input.dataset.outputTarget
339
+ if (outputTargetId) {
340
+ const outputElement = document.getElementById(outputTargetId)
341
+ if (outputElement) {
342
+ const format = input.dataset.outputFormat || "{value}"
343
+ const formattedValue = format
344
+ .replace("{value}", value)
345
+ .replace("{percent}", Math.round(percentage))
346
+ outputElement.textContent = formattedValue
347
+ }
348
+ }
349
+
350
+ // Sync to linked input element for two-way binding (slider → input)
351
+ const inputTargetId = input.dataset.inputTarget
352
+ if (inputTargetId) {
353
+ const linkedInput = document.getElementById(inputTargetId)
354
+ if (linkedInput) {
355
+ linkedInput.value = value
356
+ }
357
+ }
358
+
359
+ // Dispatch change event
360
+ this.dispatch("change", {
361
+ detail: { value: value, percentage: percentage }
362
+ })
363
+ }
364
+ }
@@ -0,0 +1,46 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Switch Controller
5
+ * Handles toggle switch with hidden input sync for form submission
6
+ */
7
+ export default class SwitchController extends Controller {
8
+ static targets: ["button", "thumb", "input"];
9
+ static values: {
10
+ checked: { type: "Boolean"; default: false };
11
+ };
12
+
13
+ /** Switch button target */
14
+ readonly buttonTarget: HTMLButtonElement;
15
+ readonly hasButtonTarget: boolean;
16
+
17
+ /** Switch thumb target */
18
+ readonly thumbTarget: HTMLElement;
19
+ readonly hasThumbTarget: boolean;
20
+
21
+ /** Hidden input target */
22
+ readonly inputTarget: HTMLInputElement;
23
+ readonly hasInputTarget: boolean;
24
+
25
+ /** Whether the switch is checked */
26
+ checkedValue: boolean;
27
+ readonly hasCheckedValue: boolean;
28
+
29
+ /** Toggle the switch */
30
+ toggle(): void;
31
+
32
+ /** Handle keyboard events (Space, Enter) */
33
+ handleKeydown(event: KeyboardEvent): void;
34
+
35
+ /** Update visual state */
36
+ updateVisuals(): void;
37
+
38
+ /** Sync hidden input value */
39
+ syncInput(): void;
40
+
41
+ /** Dispatch change event */
42
+ dispatchChange(): void;
43
+
44
+ /** Called when checkedValue changes */
45
+ checkedValueChanged(): void;
46
+ }
@@ -0,0 +1,78 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Switch Controller
5
+ *
6
+ * Handles toggle switch with hidden input sync for form submission
7
+ *
8
+ * Targets:
9
+ * - button: The visual switch button element
10
+ * - thumb: The sliding thumb element
11
+ * - input: Hidden checkbox input for form submission
12
+ *
13
+ * Values:
14
+ * - checked: Boolean indicating current state
15
+ */
16
+ export default class extends Controller {
17
+ static targets = ["button", "thumb", "input"]
18
+ static values = {
19
+ checked: { type: Boolean, default: false }
20
+ }
21
+
22
+ connect() {
23
+ this.updateVisuals()
24
+ }
25
+
26
+ toggle() {
27
+ if (this.hasButtonTarget && this.buttonTarget.disabled) return
28
+
29
+ this.checkedValue = !this.checkedValue
30
+ this.updateVisuals()
31
+ this.syncInput()
32
+ this.dispatchChange()
33
+ }
34
+
35
+ handleKeydown(event) {
36
+ if (event.key === " " || event.key === "Enter") {
37
+ event.preventDefault()
38
+ this.toggle()
39
+ }
40
+ }
41
+
42
+ updateVisuals() {
43
+ const state = this.checkedValue ? "checked" : "unchecked"
44
+
45
+ // Update button state
46
+ if (this.hasButtonTarget) {
47
+ this.buttonTarget.dataset.state = state
48
+ this.buttonTarget.setAttribute("aria-checked", this.checkedValue.toString())
49
+ }
50
+
51
+ // Update thumb position
52
+ if (this.hasThumbTarget) {
53
+ this.thumbTarget.dataset.state = state
54
+ }
55
+
56
+ // Update wrapper element state
57
+ this.element.dataset.state = state
58
+ }
59
+
60
+ syncInput() {
61
+ if (this.hasInputTarget) {
62
+ this.inputTarget.checked = this.checkedValue
63
+ // Dispatch native change event for form compatibility
64
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
65
+ }
66
+ }
67
+
68
+ dispatchChange() {
69
+ this.dispatch("change", {
70
+ detail: { checked: this.checkedValue }
71
+ })
72
+ }
73
+
74
+ checkedValueChanged() {
75
+ this.updateVisuals()
76
+ this.syncInput()
77
+ }
78
+ }
@@ -0,0 +1,51 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Tabs controller for tabbed interfaces
5
+ * Handles tab selection, keyboard navigation, content switching, and URL sync
6
+ */
7
+ export default class TabsController extends Controller {
8
+ static targets: ["list", "trigger", "content"];
9
+ static values: {
10
+ defaultValue: "String";
11
+ urlParam: "String";
12
+ };
13
+
14
+ /** Tab list container target */
15
+ readonly listTarget: HTMLElement;
16
+ readonly hasListTarget: boolean;
17
+
18
+ /** Tab trigger targets */
19
+ readonly triggerTargets: HTMLElement[];
20
+ readonly hasTriggerTarget: boolean;
21
+
22
+ /** Tab content panel targets */
23
+ readonly contentTargets: HTMLElement[];
24
+ readonly hasContentTarget: boolean;
25
+
26
+ /** Default tab value to select */
27
+ defaultValueValue: string;
28
+ readonly hasDefaultValueValue: boolean;
29
+
30
+ /** URL parameter name for syncing tab state */
31
+ urlParamValue: string;
32
+ readonly hasUrlParamValue: boolean;
33
+
34
+ /** Handle browser back/forward navigation */
35
+ handlePopState(): void;
36
+
37
+ /** Get current value from URL */
38
+ getValueFromUrl(): string | null;
39
+
40
+ /** Update URL with current tab value */
41
+ updateUrl(value: string): void;
42
+
43
+ /** Select a tab via click event */
44
+ selectTab(event: Event): void;
45
+
46
+ /** Select a tab by its value */
47
+ selectTabByValue(value: string, updateUrl?: boolean): void;
48
+
49
+ /** Handle keyboard navigation (Arrow keys, Home, End) */
50
+ handleKeydown(event: KeyboardEvent): void;
51
+ }
@@ -0,0 +1,126 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ /**
4
+ * Tabs controller for tabbed interfaces
5
+ * Handles tab selection, keyboard navigation, content switching, and URL sync
6
+ */
7
+ export default class extends Controller {
8
+ static targets = ["list", "trigger", "content"]
9
+ static values = {
10
+ defaultValue: String,
11
+ urlParam: String // Query parameter name for URL sync (e.g., "tab")
12
+ }
13
+
14
+ connect() {
15
+ // Determine initial tab value
16
+ let initialValue = this.getValueFromUrl() || this.defaultValueValue || this.triggerTargets[0]?.dataset.value
17
+
18
+ if (initialValue) {
19
+ // Validate that the value exists in our triggers
20
+ const validValues = this.triggerTargets.map(t => t.dataset.value)
21
+ if (!validValues.includes(initialValue)) {
22
+ initialValue = this.defaultValueValue || this.triggerTargets[0]?.dataset.value
23
+ }
24
+ this.selectTabByValue(initialValue, false) // Don't update URL on initial load
25
+ }
26
+
27
+ // Listen for browser back/forward navigation
28
+ if (this.hasUrlParamValue) {
29
+ window.addEventListener("popstate", this.handlePopState.bind(this))
30
+ }
31
+ }
32
+
33
+ disconnect() {
34
+ if (this.hasUrlParamValue) {
35
+ window.removeEventListener("popstate", this.handlePopState.bind(this))
36
+ }
37
+ }
38
+
39
+ handlePopState() {
40
+ const value = this.getValueFromUrl()
41
+ if (value) {
42
+ this.selectTabByValue(value, false)
43
+ }
44
+ }
45
+
46
+ getValueFromUrl() {
47
+ if (!this.hasUrlParamValue) return null
48
+
49
+ const url = new URL(window.location.href)
50
+ return url.searchParams.get(this.urlParamValue)
51
+ }
52
+
53
+ updateUrl(value) {
54
+ if (!this.hasUrlParamValue) return
55
+
56
+ const url = new URL(window.location.href)
57
+ url.searchParams.set(this.urlParamValue, value)
58
+ window.history.replaceState({}, "", url.toString())
59
+ }
60
+
61
+ selectTab(event) {
62
+ const trigger = event.currentTarget
63
+ const value = trigger.dataset.value
64
+ this.selectTabByValue(value, true)
65
+ }
66
+
67
+ selectTabByValue(value, updateUrl = true) {
68
+ // Update triggers
69
+ this.triggerTargets.forEach(trigger => {
70
+ const isSelected = trigger.dataset.value === value
71
+ trigger.dataset.state = isSelected ? "active" : "inactive"
72
+ trigger.setAttribute("aria-selected", isSelected.toString())
73
+ trigger.tabIndex = isSelected ? 0 : -1
74
+ })
75
+
76
+ // Update content panels
77
+ this.contentTargets.forEach(content => {
78
+ const isSelected = content.dataset.value === value
79
+ content.dataset.state = isSelected ? "active" : "inactive"
80
+ content.hidden = !isSelected
81
+ })
82
+
83
+ // Update URL if enabled
84
+ if (updateUrl) {
85
+ this.updateUrl(value)
86
+ }
87
+
88
+ this.dispatch("change", { detail: { value } })
89
+ }
90
+
91
+ // Keyboard navigation
92
+ handleKeydown(event) {
93
+ const triggers = this.triggerTargets.filter(t => !t.disabled)
94
+ const currentIndex = triggers.findIndex(t => t === document.activeElement)
95
+
96
+ if (currentIndex === -1) return
97
+
98
+ let newIndex = currentIndex
99
+
100
+ switch (event.key) {
101
+ case "ArrowLeft":
102
+ case "ArrowUp":
103
+ event.preventDefault()
104
+ newIndex = currentIndex === 0 ? triggers.length - 1 : currentIndex - 1
105
+ break
106
+ case "ArrowRight":
107
+ case "ArrowDown":
108
+ event.preventDefault()
109
+ newIndex = currentIndex === triggers.length - 1 ? 0 : currentIndex + 1
110
+ break
111
+ case "Home":
112
+ event.preventDefault()
113
+ newIndex = 0
114
+ break
115
+ case "End":
116
+ event.preventDefault()
117
+ newIndex = triggers.length - 1
118
+ break
119
+ default:
120
+ return
121
+ }
122
+
123
+ triggers[newIndex].focus()
124
+ triggers[newIndex].click()
125
+ }
126
+ }
@@ -0,0 +1,37 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ /**
4
+ * Toast controller for notification toasts
5
+ */
6
+ export default class ToastController extends Controller {
7
+ static values: {
8
+ duration: { type: "Number"; default: 5000 };
9
+ open: { type: "Boolean"; default: true };
10
+ };
11
+
12
+ /** Auto-dismiss duration in milliseconds (0 to disable) */
13
+ durationValue: number;
14
+ readonly hasDurationValue: boolean;
15
+
16
+ /** Whether the toast is currently visible */
17
+ openValue: boolean;
18
+ readonly hasOpenValue: boolean;
19
+
20
+ /** Dismiss timeout handle */
21
+ dismissTimeout: ReturnType<typeof setTimeout> | null;
22
+
23
+ /** Close the toast */
24
+ close(): void;
25
+
26
+ /** Start the auto-dismiss timer */
27
+ startDismissTimer(): void;
28
+
29
+ /** Clear the dismiss timer */
30
+ clearDismissTimer(): void;
31
+
32
+ /** Pause the dismiss timer (on hover) */
33
+ pause(): void;
34
+
35
+ /** Resume the dismiss timer (on hover end) */
36
+ resume(): void;
37
+ }