shadcn_view_component 0.3.3

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 (507) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +255 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +604 -0
  5. data/app/assets/stylesheets/shadcn-themes.css +1075 -0
  6. data/app/assets/stylesheets/shadcn.css +853 -0
  7. data/app/components/shadcn/accordion/component.rb +44 -0
  8. data/app/components/shadcn/accordion/content/component.rb +44 -0
  9. data/app/components/shadcn/accordion/item/component.rb +41 -0
  10. data/app/components/shadcn/accordion/preview.rb +16 -0
  11. data/app/components/shadcn/accordion/previews/default.html.erb +10 -0
  12. data/app/components/shadcn/accordion/previews/multiple.html.erb +13 -0
  13. data/app/components/shadcn/accordion/trigger/component.rb +52 -0
  14. data/app/components/shadcn/alert/component.rb +52 -0
  15. data/app/components/shadcn/alert/preview.rb +11 -0
  16. data/app/components/shadcn/alert/previews/default.html.erb +8 -0
  17. data/app/components/shadcn/alert.rb +17 -0
  18. data/app/components/shadcn/alert_dialog/action/component.rb +35 -0
  19. data/app/components/shadcn/alert_dialog/cancel/component.rb +16 -0
  20. data/app/components/shadcn/alert_dialog/component.rb +20 -0
  21. data/app/components/shadcn/alert_dialog/content/component.rb +55 -0
  22. data/app/components/shadcn/alert_dialog/header/component.rb +33 -0
  23. data/app/components/shadcn/alert_dialog/preview.rb +16 -0
  24. data/app/components/shadcn/alert_dialog/previews/default.html.erb +13 -0
  25. data/app/components/shadcn/alert_dialog/previews/small_with_media.html.erb +38 -0
  26. data/app/components/shadcn/alert_dialog.rb +32 -0
  27. data/app/components/shadcn/application_view_component.rb +225 -0
  28. data/app/components/shadcn/application_view_component_preview.rb +8 -0
  29. data/app/components/shadcn/aspect_ratio/component.rb +34 -0
  30. data/app/components/shadcn/aspect_ratio/preview.rb +16 -0
  31. data/app/components/shadcn/aspect_ratio/previews/default.html.erb +5 -0
  32. data/app/components/shadcn/aspect_ratio/previews/ratios.html.erb +12 -0
  33. data/app/components/shadcn/attachment/action/component.rb +19 -0
  34. data/app/components/shadcn/attachment/component.rb +76 -0
  35. data/app/components/shadcn/attachment/media/component.rb +56 -0
  36. data/app/components/shadcn/attachment/preview.rb +11 -0
  37. data/app/components/shadcn/attachment/previews/default.html.erb +80 -0
  38. data/app/components/shadcn/attachment/trigger/component.rb +31 -0
  39. data/app/components/shadcn/attachment.rb +42 -0
  40. data/app/components/shadcn/avatar/component.rb +43 -0
  41. data/app/components/shadcn/avatar/fallback/component.rb +23 -0
  42. data/app/components/shadcn/avatar/image/component.rb +20 -0
  43. data/app/components/shadcn/avatar/preview.rb +11 -0
  44. data/app/components/shadcn/avatar/previews/default.html.erb +13 -0
  45. data/app/components/shadcn/avatar.rb +32 -0
  46. data/app/components/shadcn/badge/component.rb +57 -0
  47. data/app/components/shadcn/badge/preview.rb +11 -0
  48. data/app/components/shadcn/badge/previews/default.html.erb +5 -0
  49. data/app/components/shadcn/breadcrumb/component.rb +15 -0
  50. data/app/components/shadcn/breadcrumb/ellipsis/component.rb +28 -0
  51. data/app/components/shadcn/breadcrumb/page/component.rb +25 -0
  52. data/app/components/shadcn/breadcrumb/preview.rb +16 -0
  53. data/app/components/shadcn/breadcrumb/previews/custom_separator.html.erb +17 -0
  54. data/app/components/shadcn/breadcrumb/previews/default.html.erb +15 -0
  55. data/app/components/shadcn/breadcrumb/separator/component.rb +31 -0
  56. data/app/components/shadcn/breadcrumb.rb +18 -0
  57. data/app/components/shadcn/bubble/component.rb +90 -0
  58. data/app/components/shadcn/bubble/preview.rb +16 -0
  59. data/app/components/shadcn/bubble/previews/alignment.html.erb +33 -0
  60. data/app/components/shadcn/bubble/previews/default.html.erb +22 -0
  61. data/app/components/shadcn/bubble/reactions/component.rb +54 -0
  62. data/app/components/shadcn/bubble.rb +24 -0
  63. data/app/components/shadcn/button/component.rb +85 -0
  64. data/app/components/shadcn/button/preview.rb +31 -0
  65. data/app/components/shadcn/button/previews/default.html.erb +8 -0
  66. data/app/components/shadcn/button/previews/sizes.html.erb +5 -0
  67. data/app/components/shadcn/button/previews/variants.html.erb +5 -0
  68. data/app/components/shadcn/button_group/component.rb +53 -0
  69. data/app/components/shadcn/button_group/preview.rb +15 -0
  70. data/app/components/shadcn/button_group/previews/default.html.erb +18 -0
  71. data/app/components/shadcn/button_group/previews/vertical.html.erb +5 -0
  72. data/app/components/shadcn/button_group/separator/component.rb +30 -0
  73. data/app/components/shadcn/button_group/text/component.rb +20 -0
  74. data/app/components/shadcn/calendar/component.rb +558 -0
  75. data/app/components/shadcn/calendar/month.rb +74 -0
  76. data/app/components/shadcn/calendar/preview.rb +64 -0
  77. data/app/components/shadcn/calendar/previews/booked.html.erb +18 -0
  78. data/app/components/shadcn/calendar/previews/custom_cell_size.html.erb +11 -0
  79. data/app/components/shadcn/calendar/previews/date_and_time.html.erb +31 -0
  80. data/app/components/shadcn/calendar/previews/default.html.erb +8 -0
  81. data/app/components/shadcn/calendar/previews/dropdown_caption.html.erb +11 -0
  82. data/app/components/shadcn/calendar/previews/in_a_form.html.erb +8 -0
  83. data/app/components/shadcn/calendar/previews/presets.html.erb +20 -0
  84. data/app/components/shadcn/calendar/previews/range.html.erb +10 -0
  85. data/app/components/shadcn/calendar/previews/week_numbers.html.erb +8 -0
  86. data/app/components/shadcn/card/component.rb +24 -0
  87. data/app/components/shadcn/card/header/component.rb +26 -0
  88. data/app/components/shadcn/card/preview.rb +16 -0
  89. data/app/components/shadcn/card/previews/default.html.erb +18 -0
  90. data/app/components/shadcn/card/previews/image.html.erb +37 -0
  91. data/app/components/shadcn/card.rb +21 -0
  92. data/app/components/shadcn/carousel/component.rb +61 -0
  93. data/app/components/shadcn/carousel/content/component.rb +86 -0
  94. data/app/components/shadcn/carousel/item/component.rb +42 -0
  95. data/app/components/shadcn/carousel/next/component.rb +59 -0
  96. data/app/components/shadcn/carousel/preview.rb +22 -0
  97. data/app/components/shadcn/carousel/previews/default.html.erb +22 -0
  98. data/app/components/shadcn/carousel/previews/sizes.html.erb +42 -0
  99. data/app/components/shadcn/carousel/previews/vertical.html.erb +27 -0
  100. data/app/components/shadcn/carousel/previous/component.rb +59 -0
  101. data/app/components/shadcn/chart/area/component.rb +43 -0
  102. data/app/components/shadcn/chart/bar/component.rb +75 -0
  103. data/app/components/shadcn/chart/cartesian/component.rb +152 -0
  104. data/app/components/shadcn/chart/component.rb +124 -0
  105. data/app/components/shadcn/chart/focusable.rb +38 -0
  106. data/app/components/shadcn/chart/legend/component.rb +55 -0
  107. data/app/components/shadcn/chart/line/component.rb +50 -0
  108. data/app/components/shadcn/chart/pie/component.rb +169 -0
  109. data/app/components/shadcn/chart/plot.rb +147 -0
  110. data/app/components/shadcn/chart/preview.rb +45 -0
  111. data/app/components/shadcn/chart/previews/area.html.erb +31 -0
  112. data/app/components/shadcn/chart/previews/bar.html.erb +32 -0
  113. data/app/components/shadcn/chart/previews/bar_stacked.html.erb +30 -0
  114. data/app/components/shadcn/chart/previews/default.html.erb +30 -0
  115. data/app/components/shadcn/chart/previews/donut.html.erb +16 -0
  116. data/app/components/shadcn/chart/previews/line.html.erb +30 -0
  117. data/app/components/shadcn/chart/previews/percentages.html.erb +17 -0
  118. data/app/components/shadcn/chart/table/component.rb +75 -0
  119. data/app/components/shadcn/chart/tooltip/component.rb +111 -0
  120. data/app/components/shadcn/checkbox/component.rb +109 -0
  121. data/app/components/shadcn/checkbox/preview.rb +16 -0
  122. data/app/components/shadcn/checkbox/previews/default.html.erb +14 -0
  123. data/app/components/shadcn/checkbox/previews/states.html.erb +23 -0
  124. data/app/components/shadcn/collapsible/component.rb +35 -0
  125. data/app/components/shadcn/collapsible/content/component.rb +20 -0
  126. data/app/components/shadcn/collapsible/preview.rb +11 -0
  127. data/app/components/shadcn/collapsible/previews/default.html.erb +9 -0
  128. data/app/components/shadcn/collapsible/trigger/component.rb +23 -0
  129. data/app/components/shadcn/combobox/chip/component.rb +60 -0
  130. data/app/components/shadcn/combobox/chip_input/component.rb +43 -0
  131. data/app/components/shadcn/combobox/chips/component.rb +54 -0
  132. data/app/components/shadcn/combobox/clear/component.rb +32 -0
  133. data/app/components/shadcn/combobox/component.rb +113 -0
  134. data/app/components/shadcn/combobox/content/component.rb +59 -0
  135. data/app/components/shadcn/combobox/empty/component.rb +23 -0
  136. data/app/components/shadcn/combobox/input/component.rb +85 -0
  137. data/app/components/shadcn/combobox/item/component.rb +69 -0
  138. data/app/components/shadcn/combobox/list/component.rb +27 -0
  139. data/app/components/shadcn/combobox/preview.rb +31 -0
  140. data/app/components/shadcn/combobox/previews/default.html.erb +20 -0
  141. data/app/components/shadcn/combobox/previews/grouped.html.erb +25 -0
  142. data/app/components/shadcn/combobox/previews/multiple.html.erb +28 -0
  143. data/app/components/shadcn/combobox/previews/with_clear.html.erb +16 -0
  144. data/app/components/shadcn/combobox/trigger/component.rb +54 -0
  145. data/app/components/shadcn/combobox.rb +16 -0
  146. data/app/components/shadcn/command/component.rb +67 -0
  147. data/app/components/shadcn/command/dialog/component.rb +72 -0
  148. data/app/components/shadcn/command/empty/component.rb +29 -0
  149. data/app/components/shadcn/command/group/component.rb +63 -0
  150. data/app/components/shadcn/command/input/component.rb +66 -0
  151. data/app/components/shadcn/command/item/component.rb +62 -0
  152. data/app/components/shadcn/command/list/component.rb +36 -0
  153. data/app/components/shadcn/command/preview.rb +26 -0
  154. data/app/components/shadcn/command/previews/default.html.erb +41 -0
  155. data/app/components/shadcn/command/previews/dialog.html.erb +33 -0
  156. data/app/components/shadcn/command/previews/ranking.html.erb +16 -0
  157. data/app/components/shadcn/command/separator/component.rb +33 -0
  158. data/app/components/shadcn/command.rb +14 -0
  159. data/app/components/shadcn/concerns/modal_root.rb +43 -0
  160. data/app/components/shadcn/concerns/submits_value.rb +81 -0
  161. data/app/components/shadcn/context_menu/component.rb +45 -0
  162. data/app/components/shadcn/context_menu/content/component.rb +40 -0
  163. data/app/components/shadcn/context_menu/preview.rb +16 -0
  164. data/app/components/shadcn/context_menu/previews/default.html.erb +34 -0
  165. data/app/components/shadcn/context_menu/previews/items.html.erb +35 -0
  166. data/app/components/shadcn/context_menu/sub/component.rb +47 -0
  167. data/app/components/shadcn/context_menu/sub_content/component.rb +31 -0
  168. data/app/components/shadcn/context_menu/trigger/component.rb +50 -0
  169. data/app/components/shadcn/context_menu.rb +43 -0
  170. data/app/components/shadcn/dialog/close/component.rb +20 -0
  171. data/app/components/shadcn/dialog/component.rb +20 -0
  172. data/app/components/shadcn/dialog/content/component.rb +78 -0
  173. data/app/components/shadcn/dialog/header/component.rb +23 -0
  174. data/app/components/shadcn/dialog/overlay/component.rb +28 -0
  175. data/app/components/shadcn/dialog/preview.rb +28 -0
  176. data/app/components/shadcn/dialog/previews/default.html.erb +17 -0
  177. data/app/components/shadcn/dialog/previews/inside_stacking_context.html.erb +25 -0
  178. data/app/components/shadcn/dialog/previews/no_close_button.html.erb +15 -0
  179. data/app/components/shadcn/dialog/previews/scrollable_content.html.erb +29 -0
  180. data/app/components/shadcn/dialog/trigger/component.rb +24 -0
  181. data/app/components/shadcn/dialog.rb +17 -0
  182. data/app/components/shadcn/drawer/component.rb +33 -0
  183. data/app/components/shadcn/drawer/content/component.rb +95 -0
  184. data/app/components/shadcn/drawer/header/component.rb +34 -0
  185. data/app/components/shadcn/drawer/preview.rb +29 -0
  186. data/app/components/shadcn/drawer/previews/default.html.erb +21 -0
  187. data/app/components/shadcn/drawer/previews/scrollable.html.erb +20 -0
  188. data/app/components/shadcn/drawer/previews/scrollable_side.html.erb +21 -0
  189. data/app/components/shadcn/drawer/previews/sides.html.erb +17 -0
  190. data/app/components/shadcn/drawer.rb +27 -0
  191. data/app/components/shadcn/dropdown_menu/checkbox_item/component.rb +61 -0
  192. data/app/components/shadcn/dropdown_menu/component.rb +48 -0
  193. data/app/components/shadcn/dropdown_menu/content/component.rb +49 -0
  194. data/app/components/shadcn/dropdown_menu/group/component.rb +16 -0
  195. data/app/components/shadcn/dropdown_menu/item/component.rb +50 -0
  196. data/app/components/shadcn/dropdown_menu/preview.rb +16 -0
  197. data/app/components/shadcn/dropdown_menu/previews/checkboxes_and_radio.html.erb +34 -0
  198. data/app/components/shadcn/dropdown_menu/previews/default.html.erb +23 -0
  199. data/app/components/shadcn/dropdown_menu/radio_group/component.rb +16 -0
  200. data/app/components/shadcn/dropdown_menu/radio_item/component.rb +63 -0
  201. data/app/components/shadcn/dropdown_menu/sub/component.rb +44 -0
  202. data/app/components/shadcn/dropdown_menu/sub_content/component.rb +45 -0
  203. data/app/components/shadcn/dropdown_menu/sub_trigger/component.rb +58 -0
  204. data/app/components/shadcn/dropdown_menu/trigger/component.rb +25 -0
  205. data/app/components/shadcn/dropdown_menu.rb +18 -0
  206. data/app/components/shadcn/empty/component.rb +17 -0
  207. data/app/components/shadcn/empty/media/component.rb +48 -0
  208. data/app/components/shadcn/empty/preview.rb +16 -0
  209. data/app/components/shadcn/empty/previews/default.html.erb +15 -0
  210. data/app/components/shadcn/empty/previews/media.html.erb +32 -0
  211. data/app/components/shadcn/empty.rb +26 -0
  212. data/app/components/shadcn/field/component.rb +61 -0
  213. data/app/components/shadcn/field/error/component.rb +49 -0
  214. data/app/components/shadcn/field/legend/component.rb +30 -0
  215. data/app/components/shadcn/field/preview.rb +17 -0
  216. data/app/components/shadcn/field/previews/default.html.erb +29 -0
  217. data/app/components/shadcn/field/previews/form_builder.html.erb +38 -0
  218. data/app/components/shadcn/field/separator/component.rb +35 -0
  219. data/app/components/shadcn/field.rb +44 -0
  220. data/app/components/shadcn/hover_card/component.rb +51 -0
  221. data/app/components/shadcn/hover_card/content/component.rb +45 -0
  222. data/app/components/shadcn/hover_card/preview.rb +16 -0
  223. data/app/components/shadcn/hover_card/previews/default.html.erb +36 -0
  224. data/app/components/shadcn/hover_card/previews/sides.html.erb +15 -0
  225. data/app/components/shadcn/hover_card/trigger/component.rb +33 -0
  226. data/app/components/shadcn/icon/component.rb +76 -0
  227. data/app/components/shadcn/icon.rb +43 -0
  228. data/app/components/shadcn/input/component.rb +26 -0
  229. data/app/components/shadcn/input/preview.rb +16 -0
  230. data/app/components/shadcn/input/previews/default.html.erb +5 -0
  231. data/app/components/shadcn/input/previews/states.html.erb +20 -0
  232. data/app/components/shadcn/input_group/addon/component.rb +65 -0
  233. data/app/components/shadcn/input_group/button/component.rb +40 -0
  234. data/app/components/shadcn/input_group/component.rb +43 -0
  235. data/app/components/shadcn/input_group/input/component.rb +25 -0
  236. data/app/components/shadcn/input_group/preview.rb +17 -0
  237. data/app/components/shadcn/input_group/previews/addons.html.erb +53 -0
  238. data/app/components/shadcn/input_group/previews/default.html.erb +25 -0
  239. data/app/components/shadcn/input_group/text/component.rb +20 -0
  240. data/app/components/shadcn/input_group/textarea/component.rb +21 -0
  241. data/app/components/shadcn/input_otp/component.rb +95 -0
  242. data/app/components/shadcn/input_otp/preview.rb +17 -0
  243. data/app/components/shadcn/input_otp/previews/default.html.erb +19 -0
  244. data/app/components/shadcn/input_otp/previews/variations.html.erb +33 -0
  245. data/app/components/shadcn/input_otp/separator/component.rb +21 -0
  246. data/app/components/shadcn/input_otp/slot/component.rb +60 -0
  247. data/app/components/shadcn/input_otp.rb +11 -0
  248. data/app/components/shadcn/item/component.rb +56 -0
  249. data/app/components/shadcn/item/group/component.rb +53 -0
  250. data/app/components/shadcn/item/media/component.rb +53 -0
  251. data/app/components/shadcn/item/preview.rb +20 -0
  252. data/app/components/shadcn/item/previews/default.html.erb +32 -0
  253. data/app/components/shadcn/item/previews/link.html.erb +39 -0
  254. data/app/components/shadcn/item/previews/outline.html.erb +8 -0
  255. data/app/components/shadcn/item/separator/component.rb +24 -0
  256. data/app/components/shadcn/item.rb +34 -0
  257. data/app/components/shadcn/kbd/component.rb +23 -0
  258. data/app/components/shadcn/kbd/group/component.rb +17 -0
  259. data/app/components/shadcn/kbd/preview.rb +11 -0
  260. data/app/components/shadcn/kbd/previews/default.html.erb +4 -0
  261. data/app/components/shadcn/label/component.rb +20 -0
  262. data/app/components/shadcn/label/preview.rb +11 -0
  263. data/app/components/shadcn/label/previews/default.html.erb +4 -0
  264. data/app/components/shadcn/marker/component.rb +46 -0
  265. data/app/components/shadcn/marker/icon/component.rb +23 -0
  266. data/app/components/shadcn/marker/preview.rb +17 -0
  267. data/app/components/shadcn/marker/previews/default.html.erb +18 -0
  268. data/app/components/shadcn/marker/previews/status.html.erb +32 -0
  269. data/app/components/shadcn/marker.rb +17 -0
  270. data/app/components/shadcn/menubar/component.rb +36 -0
  271. data/app/components/shadcn/menubar/content/component.rb +49 -0
  272. data/app/components/shadcn/menubar/menu/component.rb +39 -0
  273. data/app/components/shadcn/menubar/preview.rb +16 -0
  274. data/app/components/shadcn/menubar/previews/default.html.erb +52 -0
  275. data/app/components/shadcn/menubar/previews/items.html.erb +38 -0
  276. data/app/components/shadcn/menubar/sub/component.rb +43 -0
  277. data/app/components/shadcn/menubar/sub_content/component.rb +31 -0
  278. data/app/components/shadcn/menubar/sub_trigger/component.rb +31 -0
  279. data/app/components/shadcn/menubar/trigger/component.rb +41 -0
  280. data/app/components/shadcn/menubar.rb +47 -0
  281. data/app/components/shadcn/message/component.rb +33 -0
  282. data/app/components/shadcn/message/preview.rb +16 -0
  283. data/app/components/shadcn/message/previews/actions.html.erb +51 -0
  284. data/app/components/shadcn/message/previews/default.html.erb +35 -0
  285. data/app/components/shadcn/message.rb +35 -0
  286. data/app/components/shadcn/message_scroller/button/component.rb +78 -0
  287. data/app/components/shadcn/message_scroller/component.rb +59 -0
  288. data/app/components/shadcn/message_scroller/content/component.rb +50 -0
  289. data/app/components/shadcn/message_scroller/item/component.rb +45 -0
  290. data/app/components/shadcn/message_scroller/preview.rb +11 -0
  291. data/app/components/shadcn/message_scroller/previews/default.html.erb +72 -0
  292. data/app/components/shadcn/message_scroller/viewport/component.rb +41 -0
  293. data/app/components/shadcn/mode_switcher/component.rb +67 -0
  294. data/app/components/shadcn/mode_switcher/preview.rb +11 -0
  295. data/app/components/shadcn/mode_switcher/previews/default.html.erb +5 -0
  296. data/app/components/shadcn/mode_toggle/component.rb +75 -0
  297. data/app/components/shadcn/mode_toggle/preview.rb +11 -0
  298. data/app/components/shadcn/mode_toggle/previews/default.html.erb +20 -0
  299. data/app/components/shadcn/native_select/component.rb +59 -0
  300. data/app/components/shadcn/native_select/preview.rb +16 -0
  301. data/app/components/shadcn/native_select/previews/default.html.erb +9 -0
  302. data/app/components/shadcn/native_select/previews/groups.html.erb +26 -0
  303. data/app/components/shadcn/native_select.rb +16 -0
  304. data/app/components/shadcn/navigation_menu/component.rb +51 -0
  305. data/app/components/shadcn/navigation_menu/content/component.rb +58 -0
  306. data/app/components/shadcn/navigation_menu/preview.rb +11 -0
  307. data/app/components/shadcn/navigation_menu/previews/default.html.erb +49 -0
  308. data/app/components/shadcn/navigation_menu/trigger/component.rb +59 -0
  309. data/app/components/shadcn/navigation_menu.rb +28 -0
  310. data/app/components/shadcn/pagination/component.rb +19 -0
  311. data/app/components/shadcn/pagination/ellipsis/component.rb +28 -0
  312. data/app/components/shadcn/pagination/link/component.rb +37 -0
  313. data/app/components/shadcn/pagination/next/component.rb +31 -0
  314. data/app/components/shadcn/pagination/preview.rb +16 -0
  315. data/app/components/shadcn/pagination/previews/default.html.erb +18 -0
  316. data/app/components/shadcn/pagination/previews/simple.html.erb +39 -0
  317. data/app/components/shadcn/pagination/previous/component.rb +31 -0
  318. data/app/components/shadcn/pagination.rb +14 -0
  319. data/app/components/shadcn/parts.rb +38 -0
  320. data/app/components/shadcn/popover/anchor/component.rb +17 -0
  321. data/app/components/shadcn/popover/component.rb +42 -0
  322. data/app/components/shadcn/popover/content/component.rb +40 -0
  323. data/app/components/shadcn/popover/preview.rb +28 -0
  324. data/app/components/shadcn/popover/previews/align.html.erb +13 -0
  325. data/app/components/shadcn/popover/previews/default.html.erb +9 -0
  326. data/app/components/shadcn/popover/previews/inside_containing_block.html.erb +41 -0
  327. data/app/components/shadcn/popover/previews/inside_stacking_context.html.erb +30 -0
  328. data/app/components/shadcn/popover/trigger/component.rb +24 -0
  329. data/app/components/shadcn/popover.rb +16 -0
  330. data/app/components/shadcn/progress/component.rb +62 -0
  331. data/app/components/shadcn/progress/preview.rb +11 -0
  332. data/app/components/shadcn/progress/previews/default.html.erb +5 -0
  333. data/app/components/shadcn/radio_group/component.rb +51 -0
  334. data/app/components/shadcn/radio_group/item/component.rb +74 -0
  335. data/app/components/shadcn/radio_group/preview.rb +16 -0
  336. data/app/components/shadcn/radio_group/previews/default.html.erb +15 -0
  337. data/app/components/shadcn/radio_group/previews/states.html.erb +41 -0
  338. data/app/components/shadcn/resizable/component.rb +59 -0
  339. data/app/components/shadcn/resizable/handle/component.rb +70 -0
  340. data/app/components/shadcn/resizable/panel/component.rb +53 -0
  341. data/app/components/shadcn/resizable/preview.rb +23 -0
  342. data/app/components/shadcn/resizable/previews/default.html.erb +25 -0
  343. data/app/components/shadcn/resizable/previews/vertical.html.erb +19 -0
  344. data/app/components/shadcn/resizable/previews/with_handle.html.erb +17 -0
  345. data/app/components/shadcn/scroll_area/component.rb +84 -0
  346. data/app/components/shadcn/scroll_area/preview.rb +11 -0
  347. data/app/components/shadcn/scroll_area/previews/default.html.erb +36 -0
  348. data/app/components/shadcn/scroll_area/scrollbar/component.rb +72 -0
  349. data/app/components/shadcn/scroll_area/viewport/component.rb +70 -0
  350. data/app/components/shadcn/scroll_area.rb +14 -0
  351. data/app/components/shadcn/select/component.rb +67 -0
  352. data/app/components/shadcn/select/content/component.rb +157 -0
  353. data/app/components/shadcn/select/empty/component.rb +38 -0
  354. data/app/components/shadcn/select/group/component.rb +16 -0
  355. data/app/components/shadcn/select/item/component.rb +71 -0
  356. data/app/components/shadcn/select/list/component.rb +41 -0
  357. data/app/components/shadcn/select/preview.rb +24 -0
  358. data/app/components/shadcn/select/previews/default.html.erb +17 -0
  359. data/app/components/shadcn/select/previews/scrollable.html.erb +19 -0
  360. data/app/components/shadcn/select/previews/searchable.html.erb +18 -0
  361. data/app/components/shadcn/select/previews/states.html.erb +25 -0
  362. data/app/components/shadcn/select/search/component.rb +54 -0
  363. data/app/components/shadcn/select/trigger/component.rb +78 -0
  364. data/app/components/shadcn/select/value/component.rb +28 -0
  365. data/app/components/shadcn/select.rb +15 -0
  366. data/app/components/shadcn/separator/component.rb +41 -0
  367. data/app/components/shadcn/separator/preview.rb +11 -0
  368. data/app/components/shadcn/separator/previews/default.html.erb +9 -0
  369. data/app/components/shadcn/sheet/component.rb +16 -0
  370. data/app/components/shadcn/sheet/content/component.rb +98 -0
  371. data/app/components/shadcn/sheet/header/component.rb +23 -0
  372. data/app/components/shadcn/sheet/preview.rb +16 -0
  373. data/app/components/shadcn/sheet/previews/default.html.erb +16 -0
  374. data/app/components/shadcn/sheet/previews/no_close_button.html.erb +14 -0
  375. data/app/components/shadcn/sheet.rb +22 -0
  376. data/app/components/shadcn/sidebar/component.rb +232 -0
  377. data/app/components/shadcn/sidebar/input/component.rb +25 -0
  378. data/app/components/shadcn/sidebar/menu_action/component.rb +58 -0
  379. data/app/components/shadcn/sidebar/menu_button/component.rb +100 -0
  380. data/app/components/shadcn/sidebar/menu_skeleton/component.rb +51 -0
  381. data/app/components/shadcn/sidebar/menu_sub_button/component.rb +60 -0
  382. data/app/components/shadcn/sidebar/preview.rb +22 -0
  383. data/app/components/shadcn/sidebar/previews/default.html.erb +189 -0
  384. data/app/components/shadcn/sidebar/previews/floating.html.erb +39 -0
  385. data/app/components/shadcn/sidebar/previews/inset.html.erb +48 -0
  386. data/app/components/shadcn/sidebar/provider/component.rb +53 -0
  387. data/app/components/shadcn/sidebar/rail/component.rb +43 -0
  388. data/app/components/shadcn/sidebar/separator/component.rb +23 -0
  389. data/app/components/shadcn/sidebar/trigger/component.rb +39 -0
  390. data/app/components/shadcn/sidebar.rb +98 -0
  391. data/app/components/shadcn/skeleton/component.rb +14 -0
  392. data/app/components/shadcn/skeleton/preview.rb +11 -0
  393. data/app/components/shadcn/skeleton/previews/default.html.erb +7 -0
  394. data/app/components/shadcn/slider/component.rb +145 -0
  395. data/app/components/shadcn/slider/preview.rb +16 -0
  396. data/app/components/shadcn/slider/previews/default.html.erb +17 -0
  397. data/app/components/shadcn/slider/previews/vertical.html.erb +7 -0
  398. data/app/components/shadcn/slider/range/component.rb +51 -0
  399. data/app/components/shadcn/slider/thumb/component.rb +58 -0
  400. data/app/components/shadcn/slider/track/component.rb +32 -0
  401. data/app/components/shadcn/spinner/component.rb +21 -0
  402. data/app/components/shadcn/spinner/preview.rb +11 -0
  403. data/app/components/shadcn/spinner/previews/default.html.erb +7 -0
  404. data/app/components/shadcn/switch/component.rb +104 -0
  405. data/app/components/shadcn/switch/preview.rb +11 -0
  406. data/app/components/shadcn/switch/previews/default.html.erb +24 -0
  407. data/app/components/shadcn/table/component.rb +26 -0
  408. data/app/components/shadcn/table/preview.rb +16 -0
  409. data/app/components/shadcn/table/previews/default.html.erb +19 -0
  410. data/app/components/shadcn/table/previews/footer.html.erb +25 -0
  411. data/app/components/shadcn/table.rb +33 -0
  412. data/app/components/shadcn/tabs/component.rb +40 -0
  413. data/app/components/shadcn/tabs/content/component.rb +34 -0
  414. data/app/components/shadcn/tabs/list/component.rb +51 -0
  415. data/app/components/shadcn/tabs/preview.rb +16 -0
  416. data/app/components/shadcn/tabs/previews/default.html.erb +21 -0
  417. data/app/components/shadcn/tabs/previews/line.html.erb +23 -0
  418. data/app/components/shadcn/tabs/trigger/component.rb +66 -0
  419. data/app/components/shadcn/textarea/component.rb +24 -0
  420. data/app/components/shadcn/textarea/preview.rb +16 -0
  421. data/app/components/shadcn/textarea/previews/default.html.erb +3 -0
  422. data/app/components/shadcn/textarea/previews/states.html.erb +18 -0
  423. data/app/components/shadcn/theme_selector/component.rb +88 -0
  424. data/app/components/shadcn/theme_selector/preview.rb +17 -0
  425. data/app/components/shadcn/theme_selector/previews/all_themes.html.erb +20 -0
  426. data/app/components/shadcn/theme_selector/previews/default.html.erb +18 -0
  427. data/app/components/shadcn/toaster/component.rb +139 -0
  428. data/app/components/shadcn/toaster/preview.rb +16 -0
  429. data/app/components/shadcn/toaster/previews/default.html.erb +19 -0
  430. data/app/components/shadcn/toaster/previews/from_the_server.html.erb +15 -0
  431. data/app/components/shadcn/toaster/toast/component.rb +134 -0
  432. data/app/components/shadcn/toggle/component.rb +81 -0
  433. data/app/components/shadcn/toggle/preview.rb +11 -0
  434. data/app/components/shadcn/toggle/previews/default.html.erb +6 -0
  435. data/app/components/shadcn/toggle_group/component.rb +62 -0
  436. data/app/components/shadcn/toggle_group/item/component.rb +59 -0
  437. data/app/components/shadcn/toggle_group/preview.rb +16 -0
  438. data/app/components/shadcn/toggle_group/previews/default.html.erb +12 -0
  439. data/app/components/shadcn/toggle_group/previews/sizes.html.erb +37 -0
  440. data/app/components/shadcn/tooltip/component.rb +38 -0
  441. data/app/components/shadcn/tooltip/content/component.rb +62 -0
  442. data/app/components/shadcn/tooltip/preview.rb +21 -0
  443. data/app/components/shadcn/tooltip/previews/default.html.erb +4 -0
  444. data/app/components/shadcn/tooltip/previews/sides.html.erb +19 -0
  445. data/app/components/shadcn/tooltip/previews/with_keyboard_shortcut.html.erb +11 -0
  446. data/app/components/shadcn/tooltip/provider/component.rb +28 -0
  447. data/app/components/shadcn/tooltip/trigger/component.rb +25 -0
  448. data/app/helpers/shadcn_view_component/form_helper.rb +25 -0
  449. data/app/helpers/shadcn_view_component/theme_helper.rb +61 -0
  450. data/app/javascript/shadcn/animation.js +180 -0
  451. data/app/javascript/shadcn/command_score.js +110 -0
  452. data/app/javascript/shadcn/controllers/accordion_controller.js +127 -0
  453. data/app/javascript/shadcn/controllers/avatar_controller.js +32 -0
  454. data/app/javascript/shadcn/controllers/calendar_controller.js +633 -0
  455. data/app/javascript/shadcn/controllers/carousel_controller.js +139 -0
  456. data/app/javascript/shadcn/controllers/chart_controller.js +110 -0
  457. data/app/javascript/shadcn/controllers/checkbox_controller.js +61 -0
  458. data/app/javascript/shadcn/controllers/collapsible_controller.js +55 -0
  459. data/app/javascript/shadcn/controllers/combobox_controller.js +360 -0
  460. data/app/javascript/shadcn/controllers/command_controller.js +199 -0
  461. data/app/javascript/shadcn/controllers/dialog_controller.js +154 -0
  462. data/app/javascript/shadcn/controllers/drawer_controller.js +265 -0
  463. data/app/javascript/shadcn/controllers/dropdown_menu_controller.js +259 -0
  464. data/app/javascript/shadcn/controllers/hover_card_controller.js +85 -0
  465. data/app/javascript/shadcn/controllers/input_group_controller.js +17 -0
  466. data/app/javascript/shadcn/controllers/input_otp_controller.js +80 -0
  467. data/app/javascript/shadcn/controllers/menubar_controller.js +151 -0
  468. data/app/javascript/shadcn/controllers/message_scroller_controller.js +456 -0
  469. data/app/javascript/shadcn/controllers/navigation_menu_controller.js +211 -0
  470. data/app/javascript/shadcn/controllers/popover_controller.js +66 -0
  471. data/app/javascript/shadcn/controllers/radio_group_controller.js +69 -0
  472. data/app/javascript/shadcn/controllers/resizable_controller.js +167 -0
  473. data/app/javascript/shadcn/controllers/scroll_area_controller.js +253 -0
  474. data/app/javascript/shadcn/controllers/select_controller.js +320 -0
  475. data/app/javascript/shadcn/controllers/sidebar_controller.js +273 -0
  476. data/app/javascript/shadcn/controllers/slider_controller.js +230 -0
  477. data/app/javascript/shadcn/controllers/switch_controller.js +39 -0
  478. data/app/javascript/shadcn/controllers/tabs_controller.js +81 -0
  479. data/app/javascript/shadcn/controllers/theme_controller.js +67 -0
  480. data/app/javascript/shadcn/controllers/toaster_controller.js +281 -0
  481. data/app/javascript/shadcn/controllers/toggle_controller.js +24 -0
  482. data/app/javascript/shadcn/controllers/toggle_group_controller.js +73 -0
  483. data/app/javascript/shadcn/controllers/tooltip_controller.js +58 -0
  484. data/app/javascript/shadcn/direction.js +41 -0
  485. data/app/javascript/shadcn/dismiss.js +73 -0
  486. data/app/javascript/shadcn/floating.js +207 -0
  487. data/app/javascript/shadcn/focus.js +86 -0
  488. data/app/javascript/shadcn/id.js +14 -0
  489. data/app/javascript/shadcn/index.js +110 -0
  490. data/app/javascript/shadcn/popper.js +262 -0
  491. data/app/javascript/shadcn/scroll_geometry.js +267 -0
  492. data/app/javascript/shadcn/theme.js +161 -0
  493. data/app/javascript/shadcn/top_layer.js +59 -0
  494. data/app/javascript/shadcn/typeahead.js +99 -0
  495. data/config/importmap.rb +6 -0
  496. data/config/locales/en.yml +62 -0
  497. data/lib/generators/shadcn_view_component/install/install_generator.rb +149 -0
  498. data/lib/shadcn_view_component/engine.rb +73 -0
  499. data/lib/shadcn_view_component/form_builder.rb +264 -0
  500. data/lib/shadcn_view_component/icon_registry.rb +66 -0
  501. data/lib/shadcn_view_component/icons.rb +32 -0
  502. data/lib/shadcn_view_component/themes.rb +1065 -0
  503. data/lib/shadcn_view_component/version.rb +3 -0
  504. data/lib/shadcn_view_component.rb +82 -0
  505. data/lib/tasks/icons.rake +53 -0
  506. data/lib/tasks/themes.rake +171 -0
  507. metadata +608 -0
@@ -0,0 +1,154 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { uniqueId } from "shadcn/id"
3
+ import { pushLayer, removeLayer } from "shadcn/dismiss"
4
+ import { trapFocus, focusFirst, lockScroll, unlockScroll } from "shadcn/focus"
5
+ import * as topLayer from "shadcn/top_layer"
6
+ import { ExitQueue } from "shadcn/animation"
7
+
8
+ // Radix's Dialog, shared by Dialog, AlertDialog and Sheet: focus is trapped in
9
+ // the content, body scrolling is locked, and Escape closes it — except for
10
+ // AlertDialog, which Radix keeps dismiss-proof on outside clicks.
11
+ //
12
+ // Radix portals the overlay and content onto `document.body`; we deliberately
13
+ // leave them where they are. Both are `position: fixed` and the root is
14
+ // `display: contents`, so they still lay out against the viewport, and keeping
15
+ // them inside the controller's element is what lets the Stimulus actions on
16
+ // the close buttons keep working. (The one case this gives up is a host page
17
+ // that puts a `transform`/`filter`/`contain` ancestor above the dialog, which
18
+ // would become the containing block.)
19
+ export default class extends Controller {
20
+ static targets = [ "trigger", "overlay", "content" ]
21
+ static values = {
22
+ open: Boolean,
23
+ modal: { type: Boolean, default: true },
24
+ dismissable: { type: Boolean, default: true }
25
+ }
26
+
27
+ connect() {
28
+ this.releaseFocus = null
29
+ this.layer = null
30
+ this.exits = new ExitQueue()
31
+
32
+ this.render()
33
+ if (this.openValue) this.show()
34
+ }
35
+
36
+ disconnect() {
37
+ this.teardown()
38
+ // Turbo may be detaching the element; a continuation would then be
39
+ // operating on a subtree that has left the document.
40
+ this.exits.flushAll()
41
+ }
42
+
43
+ open() {
44
+ this.show()
45
+ }
46
+
47
+ close() {
48
+ this.hide()
49
+ }
50
+
51
+ toggle() {
52
+ this.openValue ? this.hide() : this.show()
53
+ }
54
+
55
+ show() {
56
+ if (!this.hasContentTarget || this.layer) return
57
+
58
+ this.previouslyFocused = document.activeElement
59
+ this.openValue = true
60
+ this.render()
61
+
62
+ if (this.modalValue) lockScroll()
63
+
64
+ this.releaseFocus = trapFocus(this.contentTarget, this.previouslyFocused)
65
+ focusFirst(this.contentTarget)
66
+
67
+ this.layer = pushLayer({
68
+ element: this.contentTarget,
69
+ anchors: this.hasTriggerTarget ? [ this.triggerTarget ] : [],
70
+ onDismiss: ({ reason }) => {
71
+ if (reason === "pointerdown" && !this.dismissableValue) return
72
+ this.hide()
73
+ }
74
+ })
75
+
76
+ this.dispatch("open")
77
+ }
78
+
79
+ hide() {
80
+ if (!this.layer) return
81
+
82
+ this.teardown()
83
+ this.openValue = false
84
+ this.render()
85
+ this.dispatch("close")
86
+ }
87
+
88
+ teardown() {
89
+ if (this.layer) removeLayer(this.layer)
90
+ this.layer = null
91
+
92
+ if (this.releaseFocus) this.releaseFocus()
93
+ this.releaseFocus = null
94
+
95
+ if (this.modalValue) unlockScroll()
96
+ }
97
+
98
+ render() {
99
+ const state = this.openValue ? "open" : "closed"
100
+
101
+ if (this.hasContentTarget) this.contentTarget.id ||= uniqueId("shadcn-dialog")
102
+
103
+ for (const element of this.layers) {
104
+ element.dataset.state = state
105
+
106
+ // A modal inside a `sticky z-40` header or an `isolate` card would
107
+ // otherwise be buried by whatever sits above that stacking context.
108
+ topLayer.enable(element)
109
+
110
+ if (this.openValue) {
111
+ // The overlay is faster than the content, so it can already have left
112
+ // the top layer while the content is still in it. Showing it again
113
+ // without taking the content out too would stack the backdrop *above*
114
+ // the dialog.
115
+ if (this.exits.has(element)) topLayer.hide(element)
116
+ this.exits.cancel(element)
117
+ element.hidden = false
118
+ topLayer.show(element)
119
+ } else if (element.hidden) {
120
+ // A morph can rewrite `hidden` back to true without going through an
121
+ // exit — `hidePopover()` on a never-shown popover costs nothing, so
122
+ // there is no reason to skip it just because nothing is animating.
123
+ topLayer.hide(element)
124
+ } else {
125
+ // Each element waits on its own animations: dialog content is
126
+ // `duration-200` (sheet content `duration-300`), while the overlay
127
+ // carries no `duration-*` class and falls back to `animate-out`'s
128
+ // default of 150ms.
129
+ this.exits.defer(element, () => {
130
+ element.hidden = true
131
+ topLayer.hide(element)
132
+ })
133
+ }
134
+ }
135
+
136
+ if (!this.hasTriggerTarget) return
137
+
138
+ this.triggerTarget.dataset.state = state
139
+ this.triggerTarget.setAttribute("aria-expanded", String(this.openValue))
140
+ // The id is generated above, so the server has nothing to point this at.
141
+ if (this.hasContentTarget) {
142
+ this.triggerTarget.setAttribute("aria-controls", this.contentTarget.id)
143
+ }
144
+ }
145
+
146
+ // Overlay first, content second: the top layer stacks in the order things are
147
+ // shown, so the dialog ends up above its own backdrop.
148
+ get layers() {
149
+ return [
150
+ this.hasOverlayTarget && this.overlayTarget,
151
+ this.hasContentTarget && this.contentTarget
152
+ ].filter(Boolean)
153
+ }
154
+ }
@@ -0,0 +1,265 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // vaul's own numbers (vaul/src/constants.ts). A release counts as a throw above
4
+ // 0.4 px/ms whatever the distance; below that it closes only once a quarter of
5
+ // the drawer has been dragged off screen.
6
+ const VELOCITY_THRESHOLD = 0.4
7
+ const CLOSE_THRESHOLD = 0.25
8
+
9
+ // After a scroll inside the drawer, a press within this window cannot start a
10
+ // drag — otherwise the finish of a flick-scroll drags the drawer away.
11
+ const SCROLL_LOCK_TIMEOUT = 100
12
+
13
+ // A press landing while the drawer is still coming in scrolls rather than drags.
14
+ const OPEN_GRACE = 500
15
+
16
+ // Dragging *into* the drawer has nowhere to go, so it is rubber-banded rather
17
+ // than stopped dead (vaul/src/helpers.ts:90-92).
18
+ const dampen = (value) => 8 * (Math.log(value + 1) - 2)
19
+
20
+ const VERTICAL = new Set([ "top", "bottom" ])
21
+
22
+ // A press that lands on one of these means the control, not the panel. Vaul's
23
+ // own `[data-vaul-no-drag]` is the escape hatch for anything else — it was
24
+ // already honoured, but only in `shouldDrag`, which runs on the first *move*,
25
+ // and by then the capture below has already happened.
26
+ const CONTROLS = "a, button, input, select, textarea, label, [data-vaul-no-drag]"
27
+
28
+ // The drag half of vaul's Drawer, which is the only half that is vaul's: the
29
+ // rest of this component is Radix's Dialog, and runs on `shadcn--dialog`, whose
30
+ // controller this one reaches for when a release means close.
31
+ //
32
+ // Four things vaul does are deliberately not here — snap points, scaling the
33
+ // page behind the drawer, its iOS `position: fixed` workaround, and nested
34
+ // drawers. See features/drawer.md for what each would cost and why.
35
+ export default class extends Controller {
36
+ static targets = [ "content", "overlay" ]
37
+
38
+ connect() {
39
+ this.openedAt = null
40
+ this.scrollPreventedAt = null
41
+ this.reset()
42
+
43
+ // The press that opens the drawer arrives before the animation does, and a
44
+ // drag started during it reads a transform that is still moving.
45
+ this.opened = () => {
46
+ this.openedAt = Date.now()
47
+ this.reset()
48
+ }
49
+ this.element.addEventListener("shadcn--dialog:open", this.opened)
50
+ }
51
+
52
+ disconnect() {
53
+ this.element.removeEventListener("shadcn--dialog:open", this.opened)
54
+ }
55
+
56
+ get direction() {
57
+ return this.hasContentTarget ? this.contentTarget.dataset.vaulDrawerDirection : "bottom"
58
+ }
59
+
60
+ get vertical() {
61
+ return VERTICAL.has(this.direction)
62
+ }
63
+
64
+ // Which way is "away". Bottom and right close by growing the coordinate, top
65
+ // and left by shrinking it, so one multiplier covers all four.
66
+ get multiplier() {
67
+ return this.direction === "bottom" || this.direction === "right" ? 1 : -1
68
+ }
69
+
70
+ press(event) {
71
+ if (!this.hasContentTarget || event.button !== 0) return
72
+ // Not a drag, and not merely "not worth dragging": the capture below
73
+ // retargets `pointerup` — and the compatibility `mouseup` with it — at the
74
+ // panel, and a click is dispatched at the common ancestor of down and up.
75
+ // Capturing on a button's press therefore means that button never gets a
76
+ // click at all. Typing kept working, because focus follows `pointerdown`,
77
+ // and so did Enter, because no pointer is involved: which is how this
78
+ // arrived — "with the keyboard it submits, with the mouse nothing happens".
79
+ if (event.target.closest?.(CONTROLS)) return
80
+
81
+ this.dragging = true
82
+ this.allowed = false
83
+ this.startedAt = Date.now()
84
+ this.start = this.vertical ? event.pageY : event.pageX
85
+ // Capture, which vaul does not do — and the one place this port knowingly
86
+ // adds something rather than leaving something out.
87
+ //
88
+ // A touch pointer is captured by the browser anyway, so on the device this
89
+ // component is designed for vaul already gets every move and every release.
90
+ // A mouse is not, and vaul binds its handlers to the panel, so with a mouse
91
+ // it loses the gesture the moment the cursor crosses the panel's edge —
92
+ // which a drag *upwards* does immediately, since the rubber band leaves the
93
+ // pointer above the panel by design. Both halves of that were reported here
94
+ // from the gallery: a drag that stopped following, and, before it, a drag
95
+ // released outside that never ended at all because the `pointerup` went to
96
+ // whatever was under the cursor.
97
+ //
98
+ // Capturing makes the mouse behave the way touch already does rather than
99
+ // changing what the component does. The one thing it costs is the target,
100
+ // recovered in `elementUnder` below.
101
+ this.contentTarget.setPointerCapture?.(event.pointerId)
102
+ }
103
+
104
+ move(event) {
105
+ if (!this.dragging) return
106
+
107
+
108
+ const moved = (this.start - (this.vertical ? event.pageY : event.pageX)) * this.multiplier
109
+ // Positive is *into* the drawer, which is the direction that scrolls.
110
+ const inward = moved > 0
111
+
112
+ if (!this.allowed && !this.shouldDrag(this.elementUnder(event), inward)) return
113
+ this.allowed = true
114
+
115
+ this.hold(this.contentTarget)
116
+ this.hold(this.overlayElement)
117
+
118
+ if (inward) {
119
+ this.translate(Math.min(dampen(moved) * -1, 0) * this.multiplier)
120
+ return
121
+ }
122
+
123
+ const distance = Math.abs(moved)
124
+ this.translate(distance * this.multiplier)
125
+ this.fade(1 - distance / this.size)
126
+ }
127
+
128
+ release(event) {
129
+ if (!this.dragging) return
130
+
131
+ this.dragging = false
132
+ if (!this.allowed) return
133
+ this.allowed = false
134
+
135
+ const moved = this.start - (this.vertical ? event.pageY : event.pageX)
136
+ const elapsed = Date.now() - this.startedAt
137
+ const velocity = elapsed > 0 ? Math.abs(moved) / elapsed : 0
138
+ const away = moved * this.multiplier < 0
139
+
140
+ if (!away) return this.reset()
141
+ if (velocity > VELOCITY_THRESHOLD) return this.dismiss()
142
+ if (Math.abs(moved) >= this.size * CLOSE_THRESHOLD) return this.dismiss()
143
+
144
+ this.reset()
145
+ }
146
+
147
+ // vaul's `shouldDrag`, minus the snap-point branches. It is what keeps a
148
+ // scrollable drawer usable: a press that means "scroll the list" must not
149
+ // also mean "throw the drawer away".
150
+ // What the pointer is really over. Capture retargets every move at the panel,
151
+ // and this is the one thing `shouldDrag` cannot do without: it climbs from
152
+ // there to find the scroller that owns the gesture, and a captured event says
153
+ // "the panel" every time.
154
+ elementUnder(event) {
155
+ return document.elementFromPoint(event.clientX, event.clientY) || event.target
156
+ }
157
+
158
+ shouldDrag(target, inward) {
159
+ let element = target
160
+
161
+ if (element.tagName === "SELECT") return false
162
+ if (element.closest("[data-vaul-no-drag]")) return false
163
+
164
+ // Sideways drawers have no scroll axis to compete with.
165
+ if (!this.vertical) return true
166
+
167
+ if (this.openedAt && Date.now() - this.openedAt < OPEN_GRACE) return false
168
+
169
+ // Already part-way out: keep dragging whatever is underneath.
170
+ if (this.translated !== 0) return true
171
+
172
+ if (window.getSelection()?.toString()) return false
173
+
174
+ if (this.scrollPreventedAt && Date.now() - this.scrollPreventedAt < SCROLL_LOCK_TIMEOUT) {
175
+ this.scrollPreventedAt = Date.now()
176
+ return false
177
+ }
178
+
179
+ if (inward) {
180
+ this.scrollPreventedAt = Date.now()
181
+ return false
182
+ }
183
+
184
+ // Climb to the first scrollable ancestor. One scrolled away from its top
185
+ // owns the gesture; the drawer itself is where the climb stops.
186
+ while (element && element !== this.element) {
187
+ if (element.scrollHeight > element.clientHeight) {
188
+ if (element.scrollTop !== 0) {
189
+ this.scrollPreventedAt = Date.now()
190
+ return false
191
+ }
192
+
193
+ if (element.getAttribute("role") === "dialog") return true
194
+ }
195
+
196
+ element = element.parentElement
197
+ }
198
+
199
+ return true
200
+ }
201
+
202
+ // How far the drawer has to travel to be gone, along the axis it moves on.
203
+ get size() {
204
+ const box = this.contentTarget.getBoundingClientRect()
205
+
206
+ return this.vertical
207
+ ? Math.min(box.height, window.innerHeight)
208
+ : Math.min(box.width, window.innerWidth)
209
+ }
210
+
211
+ // Read back rather than remembered: a `turbo:morph` or a second press can
212
+ // arrive between two drags, and the element is the only thing that knows.
213
+ get translated() {
214
+ const matrix = new DOMMatrixReadOnly(getComputedStyle(this.contentTarget).transform)
215
+
216
+ return this.vertical ? matrix.m42 : matrix.m41
217
+ }
218
+
219
+ get overlayElement() {
220
+ return this.hasOverlayTarget ? this.overlayTarget : null
221
+ }
222
+
223
+ translate(value) {
224
+ this.contentTarget.style.transform = this.vertical
225
+ ? `translate3d(0, ${value}px, 0)`
226
+ : `translate3d(${value}px, 0, 0)`
227
+ }
228
+
229
+ fade(opacity) {
230
+ if (this.overlayElement) this.overlayElement.style.opacity = String(opacity)
231
+ }
232
+
233
+ // The transition has to be off *while* a finger is on the drawer, or every
234
+ // frame chases the last one and the drawer lags behind the pointer.
235
+ hold(element) {
236
+ if (element) element.style.transition = "none"
237
+ }
238
+
239
+ reset() {
240
+ for (const element of [ this.hasContentTarget && this.contentTarget, this.overlayElement ]) {
241
+ if (!element) continue
242
+
243
+ element.style.transform = ""
244
+ element.style.opacity = ""
245
+ element.style.transition = ""
246
+ }
247
+ }
248
+
249
+ // The inline styles stay, which is vaul's own shape (`closeDrawer` cancels the
250
+ // drag and does not touch the transform, index.tsx:536-549). The exit is a CSS
251
+ // animation with no `from`, so it starts at whatever the element currently
252
+ // reads — which is exactly where the finger left the panel. Clearing first
253
+ // sent the panel back to full height for a frame and slid it away from there,
254
+ // which is what a person sees as it springing up before it goes.
255
+ //
256
+ // They are cleared on the way back *in* instead, so the next open is square
257
+ // rather than already pushed however far the last drag pushed it.
258
+ dismiss() {
259
+ this.dialog?.close()
260
+ }
261
+
262
+ get dialog() {
263
+ return this.application.getControllerForElementAndIdentifier(this.element, "shadcn--dialog")
264
+ }
265
+ }
@@ -0,0 +1,259 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { uniqueId } from "shadcn/id"
3
+ import { FloatingLayer } from "shadcn/floating"
4
+ import { Typeahead } from "shadcn/typeahead"
5
+
6
+ // Radix's own: 100ms before a hovered sub-trigger opens (menu.tsx:1123), and
7
+ // 300ms of grace after the pointer leaves it (menu.tsx:1157-1160).
8
+ const SUB_OPEN_DELAY = 100
9
+ const SUB_GRACE_DELAY = 300
10
+
11
+ // Radix's DropdownMenu: a `role="menu"` layer with the ARIA menu keyboard
12
+ // pattern — arrow keys move a `data-highlighted` cursor, typing jumps to a
13
+ // matching item, Escape closes and returns focus to the trigger.
14
+ export default class extends Controller {
15
+ static targets = [ "trigger", "content", "item" ]
16
+ static values = {
17
+ open: Boolean,
18
+ // The `--radix-*` custom properties the content's classes read. The context
19
+ // menu is this same controller with a different prefix and a different way
20
+ // in — its markup, its keyboard and its submenus are the dropdown's, which
21
+ // is why it has no controller of its own. The Sheet reuses the dialog's for
22
+ // the same reason.
23
+ prefix: { type: String, default: "dropdown-menu" },
24
+ side: { type: String, default: "bottom" },
25
+ align: { type: String, default: "start" },
26
+ sideOffset: { type: Number, default: 4 },
27
+ alignOffset: { type: Number, default: 0 },
28
+ loop: Boolean
29
+ }
30
+
31
+ connect() {
32
+ if (!this.hasContentTarget || !this.hasTriggerTarget) return
33
+
34
+ this.typeahead = new Typeahead()
35
+
36
+ this.contentTarget.hidden = true
37
+ this.contentTarget.id ||= uniqueId("shadcn-dropdown")
38
+
39
+ // The id is generated here, so the server has nothing to point this at.
40
+ this.triggerTarget.setAttribute("aria-controls", this.contentTarget.id)
41
+ this.triggerTarget.dataset.state = "closed"
42
+
43
+ this.layer = new FloatingLayer({
44
+ trigger: this.triggerTarget,
45
+ content: this.contentTarget,
46
+ prefix: this.prefixValue,
47
+ side: this.sideValue,
48
+ align: this.alignValue,
49
+ sideOffset: this.sideOffsetValue,
50
+ alignOffset: this.alignOffsetValue,
51
+ onOpen: () => {
52
+ this.triggerTarget.setAttribute("aria-expanded", "true")
53
+ this.contentTarget.focus({ preventScroll: true })
54
+ },
55
+ onClose: () => {
56
+ this.triggerTarget.setAttribute("aria-expanded", "false")
57
+ this.clearHighlight()
58
+ // Radix's menu clears the buffer on blur rather than on close
59
+ // (vendor/radix/ui/menu.tsx:585-590). Closing is where the focus this
60
+ // layer owns actually leaves — nothing here listens for focusout, so a
61
+ // menu that lost focus *without* closing would keep its buffer, which
62
+ // Radix's would not. No path in this gem does that: Tab, Escape and an
63
+ // outside click all close first.
64
+ this.typeahead.reset()
65
+ this.triggerTarget.focus({ preventScroll: true })
66
+ }
67
+ })
68
+
69
+ if (this.openValue) this.layer.show()
70
+ }
71
+
72
+ disconnect() {
73
+ this.cancelSubTimers()
74
+ if (this.layer) this.layer.destroy()
75
+ }
76
+
77
+ toggle() {
78
+ this.layer.toggle()
79
+ }
80
+
81
+ // A right-click opens the menu *at the pointer*, so the layer is measured
82
+ // against a point rather than against the element that was pressed. Radix
83
+ // does the same with a virtual element; `popper.js` only ever calls
84
+ // `getBoundingClientRect`, so a zero-sized rect at the coordinates is the
85
+ // whole of it.
86
+ openAtPointer(event) {
87
+ event.preventDefault()
88
+
89
+ const { clientX: x, clientY: y } = event
90
+
91
+ this.layer.anchor = {
92
+ getBoundingClientRect: () => ({
93
+ top: y, bottom: y, left: x, right: x, width: 0, height: 0, x, y
94
+ })
95
+ }
96
+
97
+ this.layer.hide()
98
+ this.layer.show()
99
+ }
100
+
101
+ // Submenus open on hover, like Radix's SubTrigger — after 100ms, so crossing
102
+ // a trigger on the way somewhere else does not open it (menu.tsx:1123).
103
+ open() {
104
+ this.cancelSubTimers()
105
+ this.subTimer = setTimeout(() => this.layer.show(), SUB_OPEN_DELAY)
106
+ }
107
+
108
+ close() {
109
+ this.cancelSubTimers()
110
+ this.layer.hide()
111
+ }
112
+
113
+ // Leaving the trigger, or the panel, starts a close the other one can cancel
114
+ // by being arrived at. Without it a submenu opened by hovering stays open for
115
+ // as long as the menu does, because nothing else was ever going to shut it.
116
+ //
117
+ // Radix grants the same grace but shapes it: a polygon from the exit point to
118
+ // the panel's edges, honoured only while the pointer is *moving toward* it
119
+ // (menu.tsx:1136-1160). This is the time half without the direction half —
120
+ // more forgiving, never less. See features/README.md.
121
+ closeLater() {
122
+ this.cancelSubTimers()
123
+ this.subTimer = setTimeout(() => this.layer.hide(), SUB_GRACE_DELAY)
124
+ }
125
+
126
+ cancelSubTimers() {
127
+ if (this.subTimer) clearTimeout(this.subTimer)
128
+ this.subTimer = null
129
+ }
130
+
131
+ // Opening with ArrowDown/ArrowUp lands on the first/last item, like Radix.
132
+ triggerKeydown(event) {
133
+ if (![ "ArrowDown", "ArrowUp", "Enter", " " ].includes(event.key)) return
134
+
135
+ event.preventDefault()
136
+ this.layer.show()
137
+
138
+ const items = this.enabledItems
139
+ if (items.length === 0) return
140
+ this.highlight(event.key === "ArrowUp" ? items[items.length - 1] : items[0])
141
+ }
142
+
143
+ contentKeydown(event) {
144
+ const items = this.enabledItems
145
+ const current = items.indexOf(this.highlighted)
146
+
147
+ switch (event.key) {
148
+ // Clamp unless asked to wrap, which is how RovingFocusGroup branches:
149
+ // `context.loop ? wrapArray(...) : candidateNodes.slice(currentIndex + 1)`
150
+ // (vendor/radix/ui/roving-focus-group.tsx:324-326) — the slice is empty
151
+ // at the end, so focus stays put. `loop` is opt-in in Radix's menu too
152
+ // (vendor/radix/ui/menu.tsx:387), and vendor/shadcn/ui/dropdown-menu.tsx
153
+ // never passes it, so `false` is the shadcn default rather than a choice
154
+ // made here.
155
+ case "ArrowDown":
156
+ event.preventDefault()
157
+ this.highlight(this.step(items, current, 1))
158
+ return
159
+ // With nothing highlighted yet — which is where a click-open leaves the
160
+ // menu — ArrowUp enters at the *end*: Radix lists it in `LAST_KEYS` and
161
+ // reverses the candidates before `focusFirst`
162
+ // (vendor/radix/ui/menu.tsx:576-583).
163
+ case "ArrowUp":
164
+ event.preventDefault()
165
+ this.highlight(current === -1 ? items[items.length - 1] : this.step(items, current, -1))
166
+ return
167
+ case "Home":
168
+ event.preventDefault()
169
+ this.highlight(items[0])
170
+ return
171
+ case "End":
172
+ event.preventDefault()
173
+ this.highlight(items[items.length - 1])
174
+ return
175
+ case "Enter":
176
+ case " ":
177
+ if (!this.highlighted) return
178
+ event.preventDefault()
179
+ this.highlighted.click()
180
+ return
181
+ case "Tab":
182
+ this.layer.hide()
183
+ return
184
+ }
185
+
186
+ if (event.key.length === 1 && !event.metaKey && !event.ctrlKey) {
187
+ const match = this.typeahead.search(event.key, this.enabledItems, this.highlighted)
188
+ if (match) this.highlight(match)
189
+ }
190
+ }
191
+
192
+ pointerenter(event) {
193
+ this.highlight(event.currentTarget)
194
+ }
195
+
196
+ select(event) {
197
+ const item = event.currentTarget
198
+ if (item.dataset.disabled !== undefined) return
199
+
200
+ if (item.getAttribute("role") === "menuitemcheckbox") {
201
+ const checked = item.getAttribute("aria-checked") === "true"
202
+ this.setChecked(item, !checked)
203
+ return
204
+ }
205
+
206
+ if (item.getAttribute("role") === "menuitemradio") {
207
+ this.radioSiblings(item).forEach((sibling) => this.setChecked(sibling, sibling === item))
208
+ return
209
+ }
210
+
211
+ this.layer.hide()
212
+ }
213
+
214
+ // --- helpers -------------------------------------------------------------
215
+
216
+ get enabledItems() {
217
+ return this.itemTargets.filter((item) => item.dataset.disabled === undefined)
218
+ }
219
+
220
+ // One step along `items` from `current`, wrapping only when `loop` is set.
221
+ // With nothing highlighted `current` is -1, so a forward step lands on the
222
+ // first item either way.
223
+ step(items, current, delta) {
224
+ const next = current + delta
225
+ return this.loopValue
226
+ ? items[(next + items.length) % items.length]
227
+ : items[Math.min(Math.max(next, 0), items.length - 1)]
228
+ }
229
+
230
+ get highlighted() {
231
+ return this.itemTargets.find((item) => item.dataset.highlighted !== undefined) || null
232
+ }
233
+
234
+ highlight(item) {
235
+ this.clearHighlight()
236
+ if (!item) return
237
+
238
+ item.dataset.highlighted = ""
239
+ item.focus({ preventScroll: true })
240
+ item.scrollIntoView({ block: "nearest" })
241
+ }
242
+
243
+ clearHighlight() {
244
+ this.itemTargets.forEach((item) => delete item.dataset.highlighted)
245
+ }
246
+
247
+ setChecked(item, checked) {
248
+ item.setAttribute("aria-checked", String(checked))
249
+ item.dataset.state = checked ? "checked" : "unchecked"
250
+
251
+ const indicator = item.querySelector("[data-slot$='-item-indicator']")
252
+ if (indicator) indicator.hidden = !checked
253
+ }
254
+
255
+ radioSiblings(item) {
256
+ const group = item.closest("[data-slot='dropdown-menu-radio-group']") || this.contentTarget
257
+ return Array.from(group.querySelectorAll("[role='menuitemradio']"))
258
+ }
259
+ }