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,207 @@
1
+ // Shared open/close machinery for the popper-based components (popover,
2
+ // dropdown menu, select, tooltip).
3
+ //
4
+ // Radix renders floating content through a portal on `document.body`. We keep
5
+ // it inside the component instead, wrapped in the same fixed-position element
6
+ // Radix uses (`data-radix-popper-content-wrapper`). The component roots are
7
+ // `display: contents`, so a fixed wrapper still positions against the viewport,
8
+ // and staying in place is what keeps the Stimulus actions on the items — menu
9
+ // items, select options, close buttons — bound and working.
10
+
11
+ import { createWrapper, position } from "shadcn/popper"
12
+ import * as topLayer from "shadcn/top_layer"
13
+ import { pushLayer, removeLayer } from "shadcn/dismiss"
14
+ import { ExitQueue } from "shadcn/animation"
15
+
16
+ export class FloatingLayer {
17
+ constructor(options) {
18
+ this.trigger = options.trigger
19
+ // What the content is *measured* against, which is not always what opened
20
+ // it: a context menu is anchored to the point you pressed, not to the area
21
+ // you pressed it in. Anything with a `getBoundingClientRect()` will do —
22
+ // `popper.js` asks for nothing else — so a caller can hand over a bare
23
+ // object standing for a point. Everything else the trigger is used for
24
+ // (its `data-state`, the dismiss anchor, the resize observer) still wants
25
+ // the real element.
26
+ this.anchor = options.anchor || options.trigger
27
+ this.content = options.content
28
+ this.home = options.home || this.content.parentElement
29
+ this.prefix = options.prefix || "popper"
30
+ this.side = options.side || "bottom"
31
+ this.align = options.align || "center"
32
+ this.sideOffset = options.sideOffset || 0
33
+ this.alignOffset = options.alignOffset || 0
34
+ this.matchAnchorWidth = options.matchAnchorWidth || false
35
+ // Base UI's four unprefixed custom properties, for the one family
36
+ // answerable to Base UI rather than to Radix. Asked for rather than always
37
+ // published: `--anchor-width` is a name a host could plausibly use for
38
+ // something of its own, and only the combobox's classes read it.
39
+ this.publishBaseUiVariables = options.publishBaseUiVariables || false
40
+ this.onOpen = options.onOpen || (() => {})
41
+ this.onClose = options.onClose || (() => {})
42
+ this.onDismiss = options.onDismiss || null
43
+
44
+ this.open = false
45
+ this.mounted = false
46
+ this.wrapper = null
47
+ this.layer = null
48
+ this.placeholder = null
49
+ this.frame = null
50
+ this.exits = new ExitQueue()
51
+ this.reposition = this.reposition.bind(this)
52
+ this.resizes = new ResizeObserver(this.reposition)
53
+ }
54
+
55
+ show() {
56
+ if (this.open) return
57
+ this.open = true
58
+
59
+ // Reopened before the exit finished — the wrapper, the placeholder and the
60
+ // content are all still in place, so only the interaction state has to come
61
+ // back. Rebuilding them would strand the old placeholder and leave a second
62
+ // wrapper in the DOM.
63
+ if (this.exits.has(this.content)) {
64
+ this.exits.cancel(this.content)
65
+ } else {
66
+ this.mount()
67
+ }
68
+
69
+ this.content.hidden = false
70
+ this.content.dataset.state = "open"
71
+ if (this.trigger) this.trigger.dataset.state = "open"
72
+
73
+ this.applyPosition()
74
+
75
+ window.addEventListener("scroll", this.reposition, true)
76
+ window.addEventListener("resize", this.reposition)
77
+
78
+ // A `resize` event only fires for the window, and the anchor can change
79
+ // size while it stays still — a sidebar menu button goes from 239px to
80
+ // 32px when the panel collapses under an open tooltip, which left the
81
+ // label 200px out to the right of the icon it names. floating-ui's
82
+ // `autoUpdate` observes both the reference and the floating element; this
83
+ // observes the anchor only, which is the side that moves the result.
84
+ if (this.trigger) this.resizes.observe(this.trigger)
85
+
86
+ this.layer = pushLayer({
87
+ element: this.content,
88
+ anchors: [ this.trigger ],
89
+ onDismiss: (event) => (this.onDismiss ? this.onDismiss(event) : this.hide())
90
+ })
91
+
92
+ this.onOpen()
93
+ }
94
+
95
+ // Leave a marker where the content was, so closing puts it back exactly
96
+ // there. Appending to `home` instead would quietly reorder the markup —
97
+ // after one open/close a Select's content ends up after the hidden input.
98
+ mount() {
99
+ this.mounted = true
100
+
101
+ this.placeholder = document.createComment("shadcn-floating-content")
102
+ this.content.replaceWith(this.placeholder)
103
+
104
+ this.wrapper = createWrapper()
105
+ this.placeholder.parentNode.insertBefore(this.wrapper, this.placeholder)
106
+ this.wrapper.appendChild(this.content)
107
+
108
+ // Above every stacking context, without leaving the DOM.
109
+ topLayer.enable(this.wrapper)
110
+ topLayer.show(this.wrapper)
111
+ }
112
+
113
+ hide() {
114
+ if (!this.open) return
115
+ this.open = false
116
+
117
+ // Interaction state is immediate: a layer that is fading out must not
118
+ // answer Escape or an outside click. Positioning is not interaction
119
+ // state — the content stays mounted through the exit (`this.mounted`
120
+ // goes false only in `dismount()`), so a scroll or resize during the fade
121
+ // still moves it instead of leaving it stranded over its old anchor.
122
+ if (this.layer) removeLayer(this.layer)
123
+ this.layer = null
124
+
125
+ // This is what starts the exit animation, so it has to be set before the
126
+ // queue looks for one.
127
+ this.content.dataset.state = "closed"
128
+ if (this.trigger) this.trigger.dataset.state = "closed"
129
+
130
+ this.exits.defer(this.content, () => this.dismount())
131
+
132
+ this.onClose()
133
+ }
134
+
135
+ dismount() {
136
+ window.removeEventListener("scroll", this.reposition, true)
137
+ window.removeEventListener("resize", this.reposition)
138
+ this.resizes.disconnect()
139
+
140
+ if (this.frame) cancelAnimationFrame(this.frame)
141
+ this.frame = null
142
+
143
+ // Not what keeps a closed layer still — both listeners are gone and the
144
+ // pending frame is cancelled, so nothing can reach `reposition()` or
145
+ // `applyPosition()` from here. It is what keeps `mounted` answering "is the
146
+ // content in the wrapper", which is the only thing either of them asks
147
+ // before moving it, and the layer is reopened rather than replaced.
148
+ this.mounted = false
149
+
150
+ this.content.hidden = true
151
+
152
+ if (this.placeholder?.parentNode) {
153
+ this.placeholder.replaceWith(this.content)
154
+ } else {
155
+ this.home.appendChild(this.content)
156
+ }
157
+ this.placeholder = null
158
+
159
+ if (this.wrapper) {
160
+ topLayer.hide(this.wrapper)
161
+ this.wrapper.remove()
162
+ }
163
+ this.wrapper = null
164
+ }
165
+
166
+ toggle() {
167
+ this.open ? this.hide() : this.show()
168
+ }
169
+
170
+ // Bound to `scroll` (capture, so every scrolling ancestor fires it), to
171
+ // `resize`, and to the anchor's own `ResizeObserver` — which also fires once
172
+ // on `observe()`, harmlessly: `show()` has already positioned synchronously.
173
+ // Positioning writes a transform and then reads `offsetWidth`,
174
+ // which forces a synchronous layout, so coalesce into one frame instead of
175
+ // reflowing on every event.
176
+ reposition() {
177
+ if (!this.mounted || !this.trigger || this.frame) return
178
+
179
+ this.frame = requestAnimationFrame(() => {
180
+ this.frame = null
181
+ this.applyPosition()
182
+ })
183
+ }
184
+
185
+ // The opening position cannot wait for a frame: the wrapper starts at the
186
+ // top-left of the viewport, so a deferred first placement flashes there.
187
+ applyPosition() {
188
+ if (!this.mounted || !this.anchor) return
189
+
190
+ position(this.anchor, this.content, {
191
+ side: this.side,
192
+ align: this.align,
193
+ sideOffset: this.sideOffset,
194
+ alignOffset: this.alignOffset,
195
+ prefix: this.prefix,
196
+ matchAnchorWidth: this.matchAnchorWidth,
197
+ publishBaseUiVariables: this.publishBaseUiVariables
198
+ })
199
+ }
200
+
201
+ destroy() {
202
+ this.hide()
203
+ // Turbo may be detaching the element; there is nothing to animate for and
204
+ // nowhere to put the content back afterwards.
205
+ this.exits.flushAll()
206
+ }
207
+ }
@@ -0,0 +1,86 @@
1
+ // Port of Radix's FocusScope: keep Tab inside a container while it is open and
2
+ // hand focus back to whatever had it when the container closes.
3
+
4
+ const FOCUSABLE = [
5
+ "a[href]",
6
+ "button:not([disabled])",
7
+ "input:not([disabled]):not([type='hidden'])",
8
+ "select:not([disabled])",
9
+ "textarea:not([disabled])",
10
+ "[tabindex]:not([tabindex='-1'])",
11
+ "audio[controls]",
12
+ "video[controls]",
13
+ "[contenteditable]:not([contenteditable='false'])"
14
+ ].join(",")
15
+
16
+ function isVisible(element) {
17
+ return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length)
18
+ }
19
+
20
+ export function tabbable(container) {
21
+ return Array.from(container.querySelectorAll(FOCUSABLE)).filter(
22
+ (element) => !element.hasAttribute("disabled") && isVisible(element)
23
+ )
24
+ }
25
+
26
+ export function focusFirst(container) {
27
+ const candidates = tabbable(container)
28
+ const target = candidates[0] || container
29
+ target.focus({ preventScroll: true })
30
+ }
31
+
32
+ // Traps Tab/Shift+Tab inside `container`. Returns a function that releases the
33
+ // trap and restores focus to `restoreTo` (defaults to the previously focused
34
+ // element).
35
+ export function trapFocus(container, restoreTo = document.activeElement) {
36
+ function onKeyDown(event) {
37
+ if (event.key !== "Tab") return
38
+
39
+ const candidates = tabbable(container)
40
+ if (candidates.length === 0) {
41
+ event.preventDefault()
42
+ return
43
+ }
44
+
45
+ const first = candidates[0]
46
+ const last = candidates[candidates.length - 1]
47
+
48
+ if (event.shiftKey && document.activeElement === first) {
49
+ event.preventDefault()
50
+ last.focus({ preventScroll: true })
51
+ } else if (!event.shiftKey && document.activeElement === last) {
52
+ event.preventDefault()
53
+ first.focus({ preventScroll: true })
54
+ }
55
+ }
56
+
57
+ container.addEventListener("keydown", onKeyDown)
58
+
59
+ return function release() {
60
+ container.removeEventListener("keydown", onKeyDown)
61
+ if (restoreTo && document.contains(restoreTo)) {
62
+ restoreTo.focus({ preventScroll: true })
63
+ }
64
+ }
65
+ }
66
+
67
+ // Radix locks body scrolling while a modal layer is open. Reference counted so
68
+ // nested layers behave.
69
+ let scrollLocks = 0
70
+ let previousOverflow = null
71
+
72
+ export function lockScroll() {
73
+ if (scrollLocks === 0) {
74
+ previousOverflow = document.body.style.overflow
75
+ document.body.style.overflow = "hidden"
76
+ }
77
+ scrollLocks += 1
78
+ }
79
+
80
+ export function unlockScroll() {
81
+ scrollLocks = Math.max(0, scrollLocks - 1)
82
+ if (scrollLocks === 0) {
83
+ document.body.style.overflow = previousOverflow || ""
84
+ previousOverflow = null
85
+ }
86
+ }
@@ -0,0 +1,14 @@
1
+ // Unique DOM ids for the ARIA wiring between triggers and their content.
2
+ //
3
+ // Deliberately not `crypto.randomUUID()`: that is only defined in a secure
4
+ // context, so over plain HTTP — a LAN IP, a staging box, `rails s` reached from
5
+ // another device — it is `undefined` and every controller that called it threw
6
+ // in `connect()`, silently killing the component. A counter is enough; these ids
7
+ // only have to be unique within one document.
8
+
9
+ let counter = 0
10
+
11
+ export function uniqueId(prefix = "shadcn") {
12
+ counter += 1
13
+ return `${prefix}-${counter}`
14
+ }
@@ -0,0 +1,110 @@
1
+ // Entry point for the Stimulus controllers that reproduce the Radix UI
2
+ // behaviour of the interactive components.
3
+ //
4
+ // import { Application } from "@hotwired/stimulus"
5
+ // import { registerShadcnControllers } from "shadcn"
6
+ //
7
+ // registerShadcnControllers(Application.start())
8
+ //
9
+ // Controllers register under the `shadcn--` prefix, which is what the
10
+ // components emit: `data-controller="shadcn--accordion"`.
11
+
12
+ import AccordionController from "shadcn/controllers/accordion_controller"
13
+ import AvatarController from "shadcn/controllers/avatar_controller"
14
+ import CalendarController from "shadcn/controllers/calendar_controller"
15
+ import CarouselController from "shadcn/controllers/carousel_controller"
16
+ import ChartController from "shadcn/controllers/chart_controller"
17
+ import ComboboxController from "shadcn/controllers/combobox_controller"
18
+ import CommandController from "shadcn/controllers/command_controller"
19
+ import CheckboxController from "shadcn/controllers/checkbox_controller"
20
+ import CollapsibleController from "shadcn/controllers/collapsible_controller"
21
+ import DialogController from "shadcn/controllers/dialog_controller"
22
+ import DrawerController from "shadcn/controllers/drawer_controller"
23
+ import DropdownMenuController from "shadcn/controllers/dropdown_menu_controller"
24
+ import HoverCardController from "shadcn/controllers/hover_card_controller"
25
+ import InputGroupController from "shadcn/controllers/input_group_controller"
26
+ import InputOtpController from "shadcn/controllers/input_otp_controller"
27
+ import MenubarController from "shadcn/controllers/menubar_controller"
28
+ import MessageScrollerController from "shadcn/controllers/message_scroller_controller"
29
+ import NavigationMenuController from "shadcn/controllers/navigation_menu_controller"
30
+ import PopoverController from "shadcn/controllers/popover_controller"
31
+ import RadioGroupController from "shadcn/controllers/radio_group_controller"
32
+ import ResizableController from "shadcn/controllers/resizable_controller"
33
+ import ScrollAreaController from "shadcn/controllers/scroll_area_controller"
34
+ import SelectController from "shadcn/controllers/select_controller"
35
+ import SidebarController from "shadcn/controllers/sidebar_controller"
36
+ import SliderController from "shadcn/controllers/slider_controller"
37
+ import SwitchController from "shadcn/controllers/switch_controller"
38
+ import TabsController from "shadcn/controllers/tabs_controller"
39
+ import ToasterController from "shadcn/controllers/toaster_controller"
40
+ import ThemeController from "shadcn/controllers/theme_controller"
41
+ import ToggleController from "shadcn/controllers/toggle_controller"
42
+ import ToggleGroupController from "shadcn/controllers/toggle_group_controller"
43
+ import TooltipController from "shadcn/controllers/tooltip_controller"
44
+
45
+ const CONTROLLERS = {
46
+ accordion: AccordionController,
47
+ avatar: AvatarController,
48
+ calendar: CalendarController,
49
+ carousel: CarouselController,
50
+ chart: ChartController,
51
+ checkbox: CheckboxController,
52
+ combobox: ComboboxController,
53
+ command: CommandController,
54
+ collapsible: CollapsibleController,
55
+ dialog: DialogController,
56
+ drawer: DrawerController,
57
+ "dropdown-menu": DropdownMenuController,
58
+ "hover-card": HoverCardController,
59
+ "input-group": InputGroupController,
60
+ "input-otp": InputOtpController,
61
+ menubar: MenubarController,
62
+ "message-scroller": MessageScrollerController,
63
+ "navigation-menu": NavigationMenuController,
64
+ popover: PopoverController,
65
+ "radio-group": RadioGroupController,
66
+ resizable: ResizableController,
67
+ "scroll-area": ScrollAreaController,
68
+ select: SelectController,
69
+ sidebar: SidebarController,
70
+ slider: SliderController,
71
+ switch: SwitchController,
72
+ tabs: TabsController,
73
+ theme: ThemeController,
74
+ toggle: ToggleController,
75
+ "toggle-group": ToggleGroupController,
76
+ toaster: ToasterController,
77
+ tooltip: TooltipController
78
+ }
79
+
80
+ export function registerShadcnControllers(application) {
81
+ for (const [ name, controller ] of Object.entries(CONTROLLERS)) {
82
+ application.register(`shadcn--${name}`, controller)
83
+ }
84
+
85
+ resyncOnMorph(application)
86
+
87
+ return application
88
+ }
89
+
90
+ // A Turbo morph refresh rewrites attributes in place without disconnecting the
91
+ // controllers, so `connect()` never runs again: the DOM goes back to whatever
92
+ // the server rendered while the controller still holds the old ids, targets and
93
+ // state. Re-running `render()` puts the two back in agreement.
94
+ //
95
+ // The server's state wins, which is what a refresh means. To keep a component's
96
+ // open state across a morph, mark it `data-turbo-permanent` in your own markup —
97
+ // that is the application's call, not the library's.
98
+ function resyncOnMorph(application) {
99
+ if (typeof document === "undefined") return
100
+
101
+ document.addEventListener("turbo:morph", () => {
102
+ for (const controller of application.controllers) {
103
+ if (!controller.identifier.startsWith("shadcn--")) continue
104
+ if (typeof controller.render === "function") controller.render()
105
+ }
106
+ })
107
+ }
108
+
109
+ export { CONTROLLERS }
110
+ export default registerShadcnControllers
@@ -0,0 +1,262 @@
1
+ // Port of the positioning half of Radix's Popper.
2
+ //
3
+ // Radix wraps floating content in a fixed-position element carrying
4
+ // `data-radix-popper-content-wrapper` and publishes a set of CSS custom
5
+ // properties that the shadcn Tailwind classes read directly — e.g.
6
+ // `origin-(--radix-popover-content-transform-origin)` and
7
+ // `max-h-(--radix-select-content-available-height)`. Those names have to match
8
+ // exactly, so `prefix` names the component the variables belong to.
9
+
10
+ const SIDES = [ "top", "right", "bottom", "left" ]
11
+
12
+ const OPPOSITE = { top: "bottom", bottom: "top", left: "right", right: "left" }
13
+
14
+ export function createWrapper() {
15
+ const wrapper = document.createElement("div")
16
+ wrapper.setAttribute("data-radix-popper-content-wrapper", "")
17
+ Object.assign(wrapper.style, {
18
+ position: "fixed",
19
+ left: "0",
20
+ top: "0",
21
+ minWidth: "max-content",
22
+ zIndex: "50"
23
+ })
24
+ return wrapper
25
+ }
26
+
27
+ function measure(anchor) {
28
+ const rect = anchor.getBoundingClientRect()
29
+ return {
30
+ top: rect.top,
31
+ left: rect.left,
32
+ right: rect.right,
33
+ bottom: rect.bottom,
34
+ width: rect.width,
35
+ height: rect.height
36
+ }
37
+ }
38
+
39
+ function place(side, align, anchorRect, size, sideOffset, alignOffset) {
40
+ let x
41
+ let y
42
+
43
+ switch (side) {
44
+ case "top":
45
+ y = anchorRect.top - size.height - sideOffset
46
+ break
47
+ case "bottom":
48
+ y = anchorRect.bottom + sideOffset
49
+ break
50
+ case "left":
51
+ x = anchorRect.left - size.width - sideOffset
52
+ break
53
+ case "right":
54
+ x = anchorRect.right + sideOffset
55
+ break
56
+ }
57
+
58
+ const vertical = side === "top" || side === "bottom"
59
+
60
+ if (vertical) {
61
+ if (align === "start") x = anchorRect.left + alignOffset
62
+ else if (align === "end") x = anchorRect.right - size.width - alignOffset
63
+ else x = anchorRect.left + anchorRect.width / 2 - size.width / 2 + alignOffset
64
+ } else {
65
+ if (align === "start") y = anchorRect.top + alignOffset
66
+ else if (align === "end") y = anchorRect.bottom - size.height - alignOffset
67
+ else y = anchorRect.top + anchorRect.height / 2 - size.height / 2 + alignOffset
68
+ }
69
+
70
+ return { x, y }
71
+ }
72
+
73
+ function fits(side, anchorRect, size, sideOffset, padding, viewport) {
74
+ switch (side) {
75
+ case "top":
76
+ return anchorRect.top - size.height - sideOffset >= padding
77
+ case "bottom":
78
+ return anchorRect.bottom + size.height + sideOffset <= viewport.height - padding
79
+ case "left":
80
+ return anchorRect.left - size.width - sideOffset >= padding
81
+ case "right":
82
+ return anchorRect.right + size.width + sideOffset <= viewport.width - padding
83
+ }
84
+ return true
85
+ }
86
+
87
+ function availableSpace(side, anchorRect, sideOffset, padding, viewport) {
88
+ switch (side) {
89
+ case "top":
90
+ return anchorRect.top - sideOffset - padding
91
+ case "bottom":
92
+ return viewport.height - anchorRect.bottom - sideOffset - padding
93
+ case "left":
94
+ return anchorRect.left - sideOffset - padding
95
+ case "right":
96
+ return viewport.width - anchorRect.right - sideOffset - padding
97
+ }
98
+ return 0
99
+ }
100
+
101
+ // Positions `content` (already inside a wrapper from `createWrapper`) relative
102
+ // to `anchor`, flipping and shifting to stay on screen. Returns the resolved
103
+ // side and align, which callers mirror onto `data-side` / `data-align`.
104
+ // Radix places the arrow itself, through Popper: a wrapper pinned to the side
105
+ // the content ended up on, offset along the cross axis so it points at the
106
+ // anchor's middle (vendor's `@radix-ui/react-popper`, measured on the live
107
+ // tooltip on 2026-08-12 — `position:absolute; bottom:0; transform:translateY(100%);
108
+ // left:43.5px` for a tooltip placed on top).
109
+ //
110
+ // The tooltip's content renders the same two elements Radix does — a bare
111
+ // wrapper and, inside it, the rotated square shadcn styles — because the
112
+ // styling classes set `rotate` and `translate`, which are their own CSS
113
+ // properties in Tailwind v4 and compose with `transform` rather than losing to
114
+ // it. Placement written on the same element fights them; on the wrapper it does
115
+ // not. Without any of this the arrow is laid out in the text flow after the
116
+ // label: rendered, never seen, which is how it shipped.
117
+ // Radix's own, verbatim: the wrapper is pinned to the edge opposite the side
118
+ // the content took, and each side turns it so the same square points outwards.
119
+ const ARROW_TRANSFORM = {
120
+ top: "translateY(100%)",
121
+ bottom: "rotate(180deg)",
122
+ left: "translateY(50%) rotate(-90deg) translateX(50%)",
123
+ right: "translateY(50%) rotate(90deg) translateX(-50%)"
124
+ }
125
+
126
+ // One origin per side, and they are not decoration: three of these transforms
127
+ // rotate, and a rotation is only as right as the point it turns about. A single
128
+ // origin for all four — which is what this had, and what put the left and right
129
+ // arrows somewhere nobody could see them — spins two of them off their own edge.
130
+ const ARROW_ORIGIN = {
131
+ top: "",
132
+ right: "0 0",
133
+ bottom: "center 0",
134
+ left: "100% 0"
135
+ }
136
+
137
+ function placeArrow(arrow, side, anchorRect, size, wrapperX, wrapperY) {
138
+ const width = arrow.offsetWidth || 10
139
+ const height = arrow.offsetHeight || 10
140
+ const vertical = side === "top" || side === "bottom"
141
+ const padding = 6
142
+
143
+ Object.assign(arrow.style, {
144
+ position: "absolute",
145
+ top: "", right: "", bottom: "", left: "",
146
+ transform: ARROW_TRANSFORM[side],
147
+ transformOrigin: ARROW_ORIGIN[side]
148
+ })
149
+
150
+ if (vertical) {
151
+ const centre = anchorRect.left + anchorRect.width / 2 - wrapperX - width / 2
152
+ arrow.style.left = `${Math.round(Math.min(Math.max(padding, centre), Math.max(padding, size.width - width - padding)))}px`
153
+ arrow.style[side === "top" ? "bottom" : "top"] = "0px"
154
+ } else {
155
+ const centre = anchorRect.top + anchorRect.height / 2 - wrapperY - height / 2
156
+ arrow.style.top = `${Math.round(Math.min(Math.max(padding, centre), Math.max(padding, size.height - height - padding)))}px`
157
+ arrow.style[side === "left" ? "right" : "left"] = "0px"
158
+ }
159
+ }
160
+
161
+ export function position(anchor, content, options = {}) {
162
+ const {
163
+ side = "bottom",
164
+ align = "center",
165
+ sideOffset = 0,
166
+ alignOffset = 0,
167
+ collisionPadding = 8,
168
+ prefix = "popper",
169
+ matchAnchorWidth = false,
170
+ publishBaseUiVariables = false
171
+ } = options
172
+
173
+ const wrapper = content.parentElement
174
+ const viewport = { width: window.innerWidth, height: window.innerHeight }
175
+ const anchorRect = measure(anchor)
176
+
177
+ // An arrow has to fit between the panel and what it points at, so its height
178
+ // is part of the offset rather than something to subtract later — Radix does
179
+ // the same, `offset({ mainAxis: sideOffset + arrowHeight })` in
180
+ // `@radix-ui/react-popper`. Which is why a tooltip whose `sideOffset` is 0,
181
+ // as shadcn's is (tooltip.tsx:47), still stands clear of its trigger: without
182
+ // this the panel sits flush and the arrow lies across the thing it came from.
183
+ const arrow = content.querySelector("[data-slot$='-arrow']")
184
+ const offset = sideOffset + (arrow ? arrow.offsetHeight : 0)
185
+
186
+ if (matchAnchorWidth) content.style.minWidth = `${anchorRect.width}px`
187
+
188
+ // Measure without a stale transform in the way.
189
+ wrapper.style.transform = "translate(0, 0)"
190
+ const size = { width: content.offsetWidth, height: content.offsetHeight }
191
+
192
+ let resolvedSide = side
193
+ if (!fits(side, anchorRect, size, offset, collisionPadding, viewport)) {
194
+ const flipped = OPPOSITE[side]
195
+ if (fits(flipped, anchorRect, size, offset, collisionPadding, viewport)) {
196
+ resolvedSide = flipped
197
+ } else {
198
+ // Neither fits: take whichever has more room.
199
+ resolvedSide =
200
+ availableSpace(flipped, anchorRect, offset, collisionPadding, viewport) >
201
+ availableSpace(side, anchorRect, offset, collisionPadding, viewport)
202
+ ? flipped
203
+ : side
204
+ }
205
+ }
206
+
207
+ let { x, y } = place(resolvedSide, align, anchorRect, size, offset, alignOffset)
208
+
209
+ // Shift back into the viewport along the cross axis.
210
+ const maxX = viewport.width - size.width - collisionPadding
211
+ const maxY = viewport.height - size.height - collisionPadding
212
+ x = Math.min(Math.max(collisionPadding, x), Math.max(collisionPadding, maxX))
213
+ y = Math.min(Math.max(collisionPadding, y), Math.max(collisionPadding, maxY))
214
+
215
+ wrapper.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`
216
+
217
+ const available = availableSpace(resolvedSide, anchorRect, offset, collisionPadding, viewport)
218
+ const originX = resolvedSide === "left" ? "100%" : resolvedSide === "right" ? "0%" : "50%"
219
+ const originY = resolvedSide === "top" ? "100%" : resolvedSide === "bottom" ? "0%" : "50%"
220
+ const transformOrigin = `${originX} ${originY}`
221
+
222
+ const vars = {
223
+ [`--radix-${prefix}-content-transform-origin`]: transformOrigin,
224
+ [`--radix-${prefix}-content-available-width`]: `${viewport.width - collisionPadding * 2}px`,
225
+ [`--radix-${prefix}-content-available-height`]: `${Math.max(0, available)}px`,
226
+ "--radix-popper-transform-origin": transformOrigin,
227
+ "--radix-popper-available-width": `${viewport.width - collisionPadding * 2}px`,
228
+ "--radix-popper-available-height": `${Math.max(0, available)}px`,
229
+ "--radix-popper-anchor-width": `${anchorRect.width}px`,
230
+ "--radix-popper-anchor-height": `${anchorRect.height}px`,
231
+ [`--radix-${prefix}-trigger-width`]: `${anchorRect.width}px`,
232
+ [`--radix-${prefix}-trigger-height`]: `${anchorRect.height}px`,
233
+ // Base UI spells the same four numbers without a prefix, and
234
+ // `combobox.tsx` — the one file of the 61 written against Base UI — reads
235
+ // them: `w-(--anchor-width)`, `max-w-(--available-width)`,
236
+ // `origin-(--transform-origin)`, and the list's `--available-height`.
237
+ // Published only where a caller asks, because an unprefixed name in a
238
+ // host's page is a name a host may already be using.
239
+ ...(publishBaseUiVariables
240
+ ? {
241
+ "--anchor-width": `${anchorRect.width}px`,
242
+ "--available-width": `${viewport.width - collisionPadding * 2}px`,
243
+ "--available-height": `${Math.max(0, available)}px`,
244
+ "--transform-origin": transformOrigin
245
+ }
246
+ : {})
247
+ }
248
+
249
+ for (const [ name, value ] of Object.entries(vars)) {
250
+ content.style.setProperty(name, value)
251
+ wrapper.style.setProperty(name, value)
252
+ }
253
+
254
+ content.dataset.side = resolvedSide
255
+ content.dataset.align = align
256
+
257
+ if (arrow) placeArrow(arrow, resolvedSide, anchorRect, size, x, y)
258
+
259
+ return { side: resolvedSide, align }
260
+ }
261
+
262
+ export { SIDES }