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,66 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { uniqueId } from "shadcn/id"
3
+ import { FloatingLayer } from "shadcn/floating"
4
+ import { trapFocus, focusFirst } from "shadcn/focus"
5
+
6
+ // Radix's Popover: a `role="dialog"` layer anchored to its trigger.
7
+ export default class extends Controller {
8
+ static targets = [ "trigger", "content", "anchor" ]
9
+ static values = {
10
+ open: Boolean,
11
+ side: { type: String, default: "bottom" },
12
+ align: { type: String, default: "center" },
13
+ sideOffset: { type: Number, default: 4 },
14
+ alignOffset: { type: Number, default: 0 },
15
+ modal: Boolean
16
+ }
17
+
18
+ connect() {
19
+ if (!this.hasContentTarget || !this.hasTriggerTarget) return
20
+
21
+ this.contentTarget.hidden = true
22
+ this.contentTarget.id ||= uniqueId("shadcn-popover")
23
+
24
+ // The id is generated here, so the server has nothing to point this at.
25
+ this.triggerTarget.setAttribute("aria-controls", this.contentTarget.id)
26
+ this.triggerTarget.dataset.state = "closed"
27
+
28
+ this.layer = new FloatingLayer({
29
+ trigger: this.hasAnchorTarget ? this.anchorTarget : this.triggerTarget,
30
+ content: this.contentTarget,
31
+ prefix: "popover",
32
+ side: this.sideValue,
33
+ align: this.alignValue,
34
+ sideOffset: this.sideOffsetValue,
35
+ alignOffset: this.alignOffsetValue,
36
+ onOpen: () => {
37
+ this.triggerTarget.setAttribute("aria-expanded", "true")
38
+ this.releaseFocus = trapFocus(this.contentTarget, this.triggerTarget)
39
+ focusFirst(this.contentTarget)
40
+ },
41
+ onClose: () => {
42
+ this.triggerTarget.setAttribute("aria-expanded", "false")
43
+ if (this.releaseFocus) this.releaseFocus()
44
+ this.releaseFocus = null
45
+ }
46
+ })
47
+
48
+ if (this.openValue) this.layer.show()
49
+ }
50
+
51
+ disconnect() {
52
+ if (this.layer) this.layer.destroy()
53
+ }
54
+
55
+ toggle() {
56
+ this.layer.toggle()
57
+ }
58
+
59
+ open() {
60
+ this.layer.show()
61
+ }
62
+
63
+ close() {
64
+ this.layer.hide()
65
+ }
66
+ }
@@ -0,0 +1,69 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { directionAwareKey, readDirection } from "shadcn/direction"
3
+
4
+ // Radix's RadioGroup: `role="radiogroup"` with roving tabindex over its items
5
+ // and arrow-key navigation that also selects, per the ARIA radio pattern.
6
+ export default class extends Controller {
7
+ static targets = [ "item", "input" ]
8
+ static values = { value: String, disabled: Boolean }
9
+
10
+ connect() {
11
+ this.render()
12
+ }
13
+
14
+ select(event) {
15
+ const item = event.currentTarget
16
+ if (item.disabled || this.disabledValue) return
17
+
18
+ this.valueValue = item.dataset.value || ""
19
+ this.render()
20
+ item.focus({ preventScroll: true })
21
+ this.dispatch("change", { detail: { value: this.valueValue } })
22
+
23
+ if (this.hasInputTarget) {
24
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
25
+ }
26
+ }
27
+
28
+ keydown(event) {
29
+ const keys = {
30
+ ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1
31
+ }
32
+ const step = keys[directionAwareKey(event.key, readDirection(this.element))]
33
+ if (!step) return
34
+
35
+ event.preventDefault()
36
+
37
+ const items = this.enabledItems
38
+ if (items.length === 0) return
39
+
40
+ const current = items.indexOf(event.currentTarget)
41
+ const next = items[(current + step + items.length) % items.length]
42
+ next.click()
43
+ }
44
+
45
+ get enabledItems() {
46
+ return this.itemTargets.filter((item) => !item.disabled)
47
+ }
48
+
49
+ render() {
50
+ const items = this.itemTargets
51
+ const selected = items.find((item) => item.dataset.value === this.valueValue)
52
+
53
+ items.forEach((item) => {
54
+ const checked = item === selected
55
+ item.dataset.state = checked ? "checked" : "unchecked"
56
+ item.setAttribute("aria-checked", String(checked))
57
+ // Roving tabindex: only the selected item (or the first one) is tabbable.
58
+ const tabbable = selected ? checked : item === this.enabledItems[0]
59
+ item.setAttribute("tabindex", tabbable ? "0" : "-1")
60
+
61
+ const indicator = item.querySelector("[data-slot='radio-group-indicator']")
62
+ if (!indicator) return
63
+ indicator.hidden = !checked
64
+ indicator.dataset.state = checked ? "checked" : "unchecked"
65
+ })
66
+
67
+ if (this.hasInputTarget) this.inputTarget.value = this.valueValue
68
+ }
69
+ }
@@ -0,0 +1,167 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Panels are shares of a flex container, so the layout is the browser's: this
4
+ // only moves two numbers when a handle is dragged, and the browser lays the
5
+ // group out again.
6
+ //
7
+ // The reference is `react-resizable-panels`, 2,252 lines — most of which is
8
+ // state, persistence and conditional panels. The arithmetic underneath is a
9
+ // delta in pixels turned into a percentage of the group.
10
+ export default class extends Controller {
11
+ static targets = [ "panel", "handle" ]
12
+ static values = { orientation: { type: String, default: "horizontal" } }
13
+
14
+ // The package's own step for the arrow keys, and its own answer for Home and
15
+ // End: push the separator all the way (`react-resizable-panels.js`, the
16
+ // keydown switch).
17
+ static STEP = 5
18
+ static ALL = 100
19
+
20
+ connect() {
21
+ this.publish()
22
+ }
23
+
24
+ press(event) {
25
+ const handle = event.currentTarget
26
+ if (handle.getAttribute("aria-disabled") === "true") return
27
+
28
+ event.preventDefault()
29
+ handle.setPointerCapture(event.pointerId)
30
+ handle.dataset.separator = "active"
31
+
32
+ this.drag = {
33
+ handle,
34
+ from: this.positionOf(event),
35
+ sizes: this.neighbours(handle).map((panel) => this.shareOf(panel))
36
+ }
37
+
38
+ this.onMove = (moved) => this.move(moved)
39
+ this.onUp = () => this.release()
40
+ handle.addEventListener("pointermove", this.onMove)
41
+ handle.addEventListener("pointerup", this.onUp)
42
+ handle.addEventListener("pointercancel", this.onUp)
43
+ }
44
+
45
+ move(event) {
46
+ if (!this.drag) return
47
+
48
+ // In percent, because that is what a share is. The group's own size is the
49
+ // whole, so a drag of 40px in a 400px group is ten points.
50
+ const travelled = (this.positionOf(event) - this.drag.from) / this.extent * 100
51
+
52
+ this.resize(this.drag.handle, travelled, this.drag.sizes)
53
+ }
54
+
55
+ release() {
56
+ if (!this.drag) return
57
+
58
+ const { handle } = this.drag
59
+ handle.removeEventListener("pointermove", this.onMove)
60
+ handle.removeEventListener("pointerup", this.onUp)
61
+ handle.removeEventListener("pointercancel", this.onUp)
62
+ handle.dataset.separator = "inactive"
63
+ this.drag = null
64
+ }
65
+
66
+ // The separator pattern: the arrows that lie along the group's own axis move
67
+ // it, and the ones across it do nothing — which is why the orientation is a
68
+ // value rather than something read from the class.
69
+ keydown(event) {
70
+ const handle = event.currentTarget
71
+ if (handle.getAttribute("aria-disabled") === "true") return
72
+
73
+ const horizontal = this.orientationValue === "horizontal"
74
+ const steps = {
75
+ ArrowLeft: horizontal ? -this.constructor.STEP : 0,
76
+ ArrowRight: horizontal ? this.constructor.STEP : 0,
77
+ ArrowUp: horizontal ? 0 : -this.constructor.STEP,
78
+ ArrowDown: horizontal ? 0 : this.constructor.STEP,
79
+ Home: -this.constructor.ALL,
80
+ End: this.constructor.ALL
81
+ }
82
+ const step = steps[event.key]
83
+ if (!step) return
84
+
85
+ event.preventDefault()
86
+ this.resize(handle, step, this.neighbours(handle).map((panel) => this.shareOf(panel)))
87
+ }
88
+
89
+ // Two panels and one number: what one gives the other takes, so the group
90
+ // always adds up to what it added up to before.
91
+ resize(handle, delta, [ before, after ]) {
92
+ const [ first, second ] = this.neighbours(handle)
93
+ if (!first || !second) return
94
+
95
+ const total = before + after
96
+ const wanted = before + delta
97
+ const size = Math.min(
98
+ Math.max(wanted, this.limits(first).min, total - this.limits(second).max),
99
+ this.limits(first).max,
100
+ total - this.limits(second).min
101
+ )
102
+
103
+ first.style.flexGrow = String(this.round(size))
104
+ second.style.flexGrow = String(this.round(total - size))
105
+ this.publish()
106
+ this.dispatch("resize", { detail: { sizes: this.panelTargets.map((panel) => this.shareOf(panel)) } })
107
+ }
108
+
109
+ // `aria-valuenow` is the share of the panel a separator controls, and it is
110
+ // written from here rather than rendered: the server would have to know how
111
+ // many panels the caller put in the block, and it does not.
112
+ publish() {
113
+ for (const handle of this.handleTargets) {
114
+ const [ first ] = this.neighbours(handle)
115
+ if (!first) continue
116
+
117
+ handle.setAttribute("aria-controls", first.id)
118
+ handle.setAttribute("aria-valuenow", String(Math.round(this.shareOf(first))))
119
+ handle.setAttribute("aria-valuemin", String(this.limits(first).min))
120
+ handle.setAttribute("aria-valuemax", String(this.limits(first).max))
121
+ }
122
+ }
123
+
124
+ // The panels either side of a handle, in DOM order — which is what "either
125
+ // side" means in a flex row that may also be a column.
126
+ neighbours(handle) {
127
+ const parts = [ ...this.element.children ]
128
+ const at = parts.indexOf(handle)
129
+
130
+ return [ parts[at - 1], parts[at + 1] ].map((part) => (part?.matches("[data-shadcn--resizable-target=panel]") ? part : null))
131
+ }
132
+
133
+ limits(panel) {
134
+ return {
135
+ min: Number(panel.dataset.minSize ?? 0),
136
+ max: Number(panel.dataset.maxSize ?? 100)
137
+ }
138
+ }
139
+
140
+ // A share read from the layout rather than from the style, so a group whose
141
+ // panels were never given sizes still has numbers to move: `flex-grow: 1`
142
+ // each renders as equal shares, and this is what they are.
143
+ shareOf(panel) {
144
+ const sizes = this.panelTargets.map((each) => this.lengthOf(each))
145
+ const total = sizes.reduce((sum, size) => sum + size, 0)
146
+
147
+ return total ? this.lengthOf(panel) / total * 100 : 0
148
+ }
149
+
150
+ lengthOf(element) {
151
+ const box = element.getBoundingClientRect()
152
+
153
+ return this.orientationValue === "horizontal" ? box.width : box.height
154
+ }
155
+
156
+ get extent() {
157
+ return this.lengthOf(this.element) || 1
158
+ }
159
+
160
+ positionOf(event) {
161
+ return this.orientationValue === "horizontal" ? event.clientX : event.clientY
162
+ }
163
+
164
+ round(value) {
165
+ return Math.round(value * 100) / 100
166
+ }
167
+ }
@@ -0,0 +1,253 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Radix's ScrollArea (vendor/radix/ui/scroll-area.tsx): native scrollbars
4
+ // hidden, a drawn one in their place.
5
+ //
6
+ // The layout is CSS. This computes two numbers per axis — how long the thumb is
7
+ // and how far along it sits — and publishes the first as the custom property
8
+ // the thumb's width or height reads. That split is Radix's, measured on the
9
+ // live demo, and it is why 1,189 lines of TSX come down to this: the scrollbar
10
+ // is positioned by a stylesheet, not by script.
11
+ //
12
+ // Two of Radix's four `type` strategies are here: `hover`, its default, and
13
+ // `always`. `scroll` and `auto` are not — see features/scroll-area.md.
14
+
15
+ // Radix's own, and the comment is Radix's too: "minimum of 18 matches macOS
16
+ // minimum" (scroll-area.tsx:1065).
17
+ const MIN_THUMB = 18
18
+
19
+ export default class extends Controller {
20
+ static targets = [ "viewport", "content", "scrollbar", "corner" ]
21
+ static values = {
22
+ type: { type: String, default: "hover" },
23
+ hideDelay: { type: Number, default: 600 }
24
+ }
25
+
26
+ connect() {
27
+ if (!this.hasViewportTarget) return
28
+
29
+ this.dragging = null
30
+ this.hideTimer = null
31
+
32
+ this.onScroll = () => this.render()
33
+ this.viewportTarget.addEventListener("scroll", this.onScroll, { passive: true })
34
+
35
+ // Content and viewport both change the numbers, and neither change is an
36
+ // event: a row growing is not a scroll.
37
+ this.resizes = new ResizeObserver(() => this.render())
38
+ this.resizes.observe(this.viewportTarget)
39
+ if (this.hasContentTarget) this.resizes.observe(this.contentTarget)
40
+
41
+ this.render()
42
+ if (this.typeValue === "always") this.show()
43
+ }
44
+
45
+ disconnect() {
46
+ this.viewportTarget?.removeEventListener("scroll", this.onScroll)
47
+ this.resizes?.disconnect()
48
+ if (this.hideTimer) clearTimeout(this.hideTimer)
49
+ this.endDrag()
50
+ }
51
+
52
+ // ------------------------------------------------------------ visibility
53
+
54
+ pointerEnter() {
55
+ if (this.typeValue !== "hover") return
56
+
57
+ if (this.hideTimer) clearTimeout(this.hideTimer)
58
+ this.show()
59
+ }
60
+
61
+ pointerLeave() {
62
+ if (this.typeValue !== "hover" || this.dragging) return
63
+
64
+ this.hideTimer = setTimeout(() => this.hide(), this.hideDelayValue)
65
+ }
66
+
67
+ show() {
68
+ for (const bar of this.scrollbarTargets) {
69
+ // A bar for an axis with nothing to scroll stays hidden however the
70
+ // pointer moves — showing it would draw a full-length thumb that cannot
71
+ // go anywhere.
72
+ bar.dataset.state = this.overflows(this.axisOf(bar)) ? "visible" : "hidden"
73
+ }
74
+ }
75
+
76
+ hide() {
77
+ for (const bar of this.scrollbarTargets) bar.dataset.state = "hidden"
78
+ }
79
+
80
+ // --------------------------------------------------------------- geometry
81
+
82
+ render() {
83
+ for (const bar of this.scrollbarTargets) this.renderBar(bar)
84
+
85
+ if (this.typeValue === "always") this.show()
86
+ else if (this.scrollbarTargets.some((bar) => bar.dataset.state === "visible")) this.show()
87
+
88
+ this.renderCorner()
89
+ }
90
+
91
+ renderBar(bar) {
92
+ const axis = this.axisOf(bar)
93
+ const sizes = this.sizes(axis, bar)
94
+ const thumb = bar.querySelector("[data-slot=scroll-area-thumb]")
95
+
96
+ if (!thumb) return
97
+
98
+ const thumbSize = this.thumbSize(sizes)
99
+ const offset = this.thumbOffset(sizes, thumbSize)
100
+
101
+ if (axis === "horizontal") {
102
+ bar.style.setProperty("--radix-scroll-area-thumb-width", `${thumbSize}px`)
103
+ thumb.style.width = "var(--radix-scroll-area-thumb-width)"
104
+ thumb.style.transform = `translate3d(${offset}px, 0, 0)`
105
+ } else {
106
+ bar.style.setProperty("--radix-scroll-area-thumb-height", `${thumbSize}px`)
107
+ thumb.style.height = "var(--radix-scroll-area-thumb-height)"
108
+ thumb.style.transform = `translate3d(0, ${offset}px, 0)`
109
+ }
110
+ }
111
+
112
+ // Radix's `getThumbSize` (scroll-area.tsx:1061): the track's length times the
113
+ // fraction of the content on screen, never below 18px — past that the thumb
114
+ // stops being a proportion and starts being a handle.
115
+ thumbSize({ viewport, content, track }) {
116
+ const ratio = content > 0 ? viewport / content : 0
117
+
118
+ return Math.max(track * ratio, MIN_THUMB)
119
+ }
120
+
121
+ // `getThumbOffsetFromScroll` (:1087), which is a linear scale from how far
122
+ // the content has scrolled to how far the thumb can travel.
123
+ thumbOffset({ viewport, content, track, scroll }, thumbSize) {
124
+ const maxScroll = content - viewport
125
+ const maxThumb = track - thumbSize
126
+
127
+ if (maxScroll <= 0 || maxThumb <= 0) return 0
128
+
129
+ return this.clamp(scroll, 0, maxScroll) * (maxThumb / maxScroll)
130
+ }
131
+
132
+ sizes(axis, bar) {
133
+ const viewport = this.viewportTarget
134
+ const horizontal = axis === "horizontal"
135
+
136
+ return {
137
+ viewport: horizontal ? viewport.clientWidth : viewport.clientHeight,
138
+ content: horizontal ? viewport.scrollWidth : viewport.scrollHeight,
139
+ scroll: horizontal ? viewport.scrollLeft : viewport.scrollTop,
140
+ // The track is the bar minus its own padding, which is where the 1px of
141
+ // `p-px` goes.
142
+ track: this.trackSize(bar, horizontal)
143
+ }
144
+ }
145
+
146
+ trackSize(bar, horizontal) {
147
+ const style = window.getComputedStyle(bar)
148
+ const padding = horizontal
149
+ ? parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)
150
+ : parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)
151
+
152
+ return (horizontal ? bar.clientWidth : bar.clientHeight) - (padding || 0)
153
+ }
154
+
155
+ overflows(axis) {
156
+ const { viewport, content } = this.sizes(axis, this.scrollbarFor(axis) || this.element)
157
+
158
+ return content - viewport > 1
159
+ }
160
+
161
+ // The two bars stop short of each other by exactly the other's thickness, and
162
+ // the corner fills what is left. Published as custom properties because that
163
+ // is where the bars' inline `bottom` and `right` already look.
164
+ renderCorner() {
165
+ if (!this.hasCornerTarget) return
166
+
167
+ const horizontal = this.scrollbarFor("horizontal")
168
+ const vertical = this.scrollbarFor("vertical")
169
+
170
+ this.element.style.setProperty(
171
+ "--radix-scroll-area-corner-width", `${vertical ? vertical.offsetWidth : 0}px`
172
+ )
173
+ this.element.style.setProperty(
174
+ "--radix-scroll-area-corner-height", `${horizontal ? horizontal.offsetHeight : 0}px`
175
+ )
176
+ }
177
+
178
+ // ------------------------------------------------------------------ drag
179
+
180
+ // On the bar rather than the thumb, so a press on the track jumps there —
181
+ // which is what a native scrollbar does and what Radix's
182
+ // `getScrollPositionFromPointer` computes (:1069).
183
+ startDrag(event) {
184
+ if (event.button !== 0) return
185
+
186
+ const bar = event.currentTarget
187
+ const axis = this.axisOf(bar)
188
+ const thumb = bar.querySelector("[data-slot=scroll-area-thumb]")
189
+
190
+ event.preventDefault()
191
+ bar.setPointerCapture(event.pointerId)
192
+
193
+ const thumbRect = thumb.getBoundingClientRect()
194
+ const pointer = axis === "horizontal" ? event.clientX : event.clientY
195
+ const thumbStart = axis === "horizontal" ? thumbRect.left : thumbRect.top
196
+ const thumbLength = axis === "horizontal" ? thumbRect.width : thumbRect.height
197
+ const inside = pointer >= thumbStart && pointer <= thumbStart + thumbLength
198
+
199
+ // Grabbing the thumb keeps the point you took hold of under the pointer;
200
+ // pressing the track centres it on the press instead.
201
+ this.dragging = { bar, axis, grip: inside ? pointer - thumbStart : thumbLength / 2 }
202
+
203
+ this.onMove = (moveEvent) => this.drag(moveEvent)
204
+ this.onUp = () => this.endDrag()
205
+ bar.addEventListener("pointermove", this.onMove)
206
+ bar.addEventListener("pointerup", this.onUp)
207
+
208
+ this.drag(event)
209
+ }
210
+
211
+ drag(event) {
212
+ if (!this.dragging) return
213
+
214
+ const { bar, axis, grip } = this.dragging
215
+ const horizontal = axis === "horizontal"
216
+ const sizes = this.sizes(axis, bar)
217
+ const thumbSize = this.thumbSize(sizes)
218
+ const rect = bar.getBoundingClientRect()
219
+ const pointer = (horizontal ? event.clientX - rect.left : event.clientY - rect.top) - grip
220
+ const maxThumb = sizes.track - thumbSize
221
+ const maxScroll = sizes.content - sizes.viewport
222
+
223
+ if (maxThumb <= 0 || maxScroll <= 0) return
224
+
225
+ const scroll = this.clamp(pointer, 0, maxThumb) * (maxScroll / maxThumb)
226
+
227
+ if (horizontal) this.viewportTarget.scrollLeft = scroll
228
+ else this.viewportTarget.scrollTop = scroll
229
+ }
230
+
231
+ endDrag() {
232
+ if (!this.dragging) return
233
+
234
+ const { bar } = this.dragging
235
+ bar.removeEventListener("pointermove", this.onMove)
236
+ bar.removeEventListener("pointerup", this.onUp)
237
+ this.dragging = null
238
+ }
239
+
240
+ // ----------------------------------------------------------------- helpers
241
+
242
+ axisOf(bar) {
243
+ return bar.dataset.orientation === "horizontal" ? "horizontal" : "vertical"
244
+ }
245
+
246
+ scrollbarFor(axis) {
247
+ return this.scrollbarTargets.find((bar) => this.axisOf(bar) === axis)
248
+ }
249
+
250
+ clamp(value, min, max) {
251
+ return Math.min(Math.max(value, min), max)
252
+ }
253
+ }