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,281 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { ExitQueue } from "shadcn/animation"
3
+
4
+ // sonner's own numbers, read from its source: four seconds of life, 200ms to
5
+ // leave. The lifetime is a value so a caller can change it; the exit is not,
6
+ // because it has to agree with the classes on the toast.
7
+ const DEFAULT_DURATION = 4000
8
+
9
+ // sonner's, read from its source: 14px between toasts.
10
+ const GAP = 14
11
+
12
+ // The toaster is a *list* and this is its clock. Toasts arrive three ways —
13
+ // rendered with the page from a flash, appended by a Turbo Stream, or asked for
14
+ // in JavaScript — and this treats all three the same: anything that appears in
15
+ // the list starts its own timer and leaves when it runs out.
16
+ //
17
+ // Which is why there is a MutationObserver rather than a method every route has
18
+ // to remember to call. A `turbo_stream.append` knows nothing about this
19
+ // controller and should not have to.
20
+ export default class extends Controller {
21
+ static targets = [ "list", "toast", "template" ]
22
+ static values = { duration: { type: Number, default: DEFAULT_DURATION } }
23
+
24
+ connect() {
25
+ this.timers = new Map()
26
+ this.exits = new ExitQueue()
27
+ this.hovering = false
28
+ this.focused = false
29
+ this.expanded = false
30
+ this.floor = 0
31
+
32
+ this.watch = new MutationObserver((records) => {
33
+ for (const record of records) {
34
+ for (const node of record.addedNodes) {
35
+ if (node.nodeType === Node.ELEMENT_NODE && node.dataset.slot === "toast") this.start(node)
36
+ }
37
+ }
38
+
39
+ this.place()
40
+ })
41
+ this.watch.observe(this.listTarget, { childList: true })
42
+
43
+ for (const toast of this.toastElements) this.start(toast)
44
+
45
+ // A toast's own height decides where the one behind it sits, and a height
46
+ // changes when the window narrows and a line wraps.
47
+ this.resizes = new ResizeObserver(() => this.place())
48
+ this.resizes.observe(this.listTarget)
49
+
50
+ this.place()
51
+ }
52
+
53
+ disconnect() {
54
+ this.watch.disconnect()
55
+ this.resizes.disconnect()
56
+ for (const timer of this.timers.values()) clearTimeout(timer)
57
+ this.timers.clear()
58
+ this.exits.flushAll()
59
+ }
60
+
61
+ // `document.dispatchEvent(new CustomEvent("shadcn--toast", { detail: … }))`.
62
+ // The detail is the component's own arguments, so the two routes read alike.
63
+ raise(event) {
64
+ const { title, description, variant = "default", duration } = event.detail ?? {}
65
+ // One template per variant: a variant carries an icon or does not, and an
66
+ // element that was never rendered cannot be brought back by an attribute.
67
+ const template = this.templateTargets.find((t) => t.dataset.variant === variant) ||
68
+ this.templateTargets.find((t) => t.dataset.variant === "default")
69
+ if (!template) return
70
+
71
+ const toast = template.content.firstElementChild.cloneNode(true)
72
+ // `if (duration)` would drop a 0, which is the one value that means
73
+ // something here: no clock. The same slip twice in one file.
74
+ if (duration !== undefined && duration !== null) toast.dataset.duration = duration
75
+ this.fill(toast, "toast-title", title)
76
+ this.fill(toast, "toast-description", description)
77
+
78
+ this.listTarget.appendChild(toast)
79
+ }
80
+
81
+ dismiss(event) {
82
+ this.close(event.currentTarget.closest("[data-slot=toast]"))
83
+ }
84
+
85
+ // Hovering does two things, and they are the same gesture: it stops every
86
+ // clock, and it opens the stack. On the list rather than on each toast — a
87
+ // pointer crossing from one to another must not restart the others, and must
88
+ // not collapse the stack under itself on the way.
89
+ //
90
+ // A pointer and a keyboard can hold it open at the same time, and they are
91
+ // tracked apart rather than as one flag: a `focusout` while the pointer is
92
+ // still on the stack is not the stack being let go.
93
+ hold(event) {
94
+ if (event?.type === "focusin") this.focused = true
95
+ else this.hovering = true
96
+
97
+ this.settle()
98
+ }
99
+
100
+ // Only the half the event is about. Closing a toast takes away the button the
101
+ // focus was on, and the browser reports that as focus leaving the stack — so
102
+ // a `focusout` that clears both would shut a stack the pointer is still on,
103
+ // which is what was reported. Focus moving *within* the stack needs nothing
104
+ // here: the `focusin` that follows it arrives in the same tick.
105
+ release(event) {
106
+ if (event?.type === "focusout") this.focused = false
107
+ else this.hovering = false
108
+
109
+ this.settle()
110
+ }
111
+
112
+ // Open while either a pointer or the keyboard is on it; counting down only
113
+ // once neither is.
114
+ settle() {
115
+ this.expanded = this.hovering || this.focused
116
+
117
+ for (const timer of this.timers.values()) clearTimeout(timer)
118
+ this.timers.clear()
119
+
120
+ this.place()
121
+
122
+ if (this.expanded) return
123
+
124
+ for (const toast of this.toastElements) this.start(toast)
125
+ }
126
+
127
+ // A toast on its way out is not one of these any more. It keeps the transform
128
+ // it had and fades where it stands, while the ones left take their new places
129
+ // — which is the difference between a stack that scrolls and one that jumps
130
+ // once the element finally goes.
131
+ get toastElements() {
132
+ return [ ...this.listTarget.querySelectorAll("[data-slot=toast]:not([data-state=closed])") ]
133
+ }
134
+
135
+ get lift() {
136
+ return this.element.dataset.position?.startsWith("top") ? 1 : -1
137
+ }
138
+
139
+ // Where every toast sits, collapsed and expanded, and how tall the box a
140
+ // pointer has to be inside is.
141
+ //
142
+ // sonner's arrangement, read from its stylesheet: collapsed, the newest is at
143
+ // the front at its own height and each one behind it is lifted by a gap,
144
+ // scaled down a twentieth per place, and has its content faded out — so a
145
+ // stack reads as one toast with others behind it. Expanded, each returns to
146
+ // its own height and is lifted past the ones in front of it.
147
+ place() {
148
+ const toasts = this.toastElements
149
+ if (toasts.length === 0) {
150
+ this.floor = 0
151
+ this.listTarget.style.height = "0px"
152
+ this.listTarget.style.pointerEvents = "none"
153
+ return
154
+ }
155
+
156
+ // Front first: the newest is nearest the edge, and for a stack anchored at
157
+ // the bottom that is the last one in the list.
158
+ const front = this.lift > 0 ? toasts : [ ...toasts ].reverse()
159
+ const heights = front.map((toast) => this.heightOf(toast))
160
+ const frontHeight = heights[0]
161
+ const edge = this.lift > 0 ? "top" : "bottom"
162
+ let stacked = 0
163
+ let reach = 0
164
+
165
+ front.forEach((toast, index) => {
166
+ const behind = index > 0
167
+ const offset = this.expanded ? stacked + GAP * index : GAP * index
168
+ const scale = this.expanded || !behind ? 1 : 1 - index * 0.05
169
+ const height = this.expanded || !behind ? heights[index] : frontHeight
170
+
171
+ toast.style.zIndex = String(front.length - index)
172
+ toast.style.transform = `translateY(${this.lift * offset}px) scale(${scale})`
173
+ // Scaled from the edge it is anchored to, not from its middle: a toast
174
+ // scaled about its centre grows *upwards* as well as inwards, and reaches
175
+ // past the box it is meant to be inside.
176
+ toast.style.transformOrigin = `${edge} center`
177
+ toast.style.height = this.expanded || !behind ? "" : `${frontHeight}px`
178
+ toast.style.opacity = behind && index > 2 && !this.expanded ? "0" : "1"
179
+ toast.style[edge] = "0px"
180
+
181
+ // Only the front one is readable while the stack is closed; the rest are
182
+ // shapes behind it, which is what makes a stack look like one toast.
183
+ for (const child of toast.children) {
184
+ child.style.opacity = behind && !this.expanded ? "0" : "1"
185
+ }
186
+
187
+ stacked += heights[index]
188
+ reach = Math.max(reach, offset + height * scale)
189
+ })
190
+
191
+ // The box has to hold every toast, because it is the box a pointer must stay
192
+ // inside for the stack to stay open. Computed from how far the toasts
193
+ // actually reach rather than from a sum that looked right.
194
+ this.listTarget.style.height = `${this.box(Math.round(reach))}px`
195
+
196
+ // And while the stack is open the box itself has to take the pointer.
197
+ //
198
+ // The region is `pointer-events: none` so the page underneath stays usable
199
+ // around a toast, and each toast turns them back on for itself — which
200
+ // leaves the gaps *between* toasts as holes. A pointer crossing one
201
+ // hit-tests straight through to the page, leaves the list, and the stack
202
+ // shuts and reopens as it lands on the next one: the flicker that was
203
+ // reported. Closed, the holes are wanted; open, they are the reason the
204
+ // stack keeps closing under the pointer that opened it.
205
+ this.listTarget.style.pointerEvents = this.expanded ? "auto" : "none"
206
+ }
207
+
208
+ // How tall the box is, which is not always how far the toasts reach: while the
209
+ // stack is open it may grow and must not shrink.
210
+ //
211
+ // The box is anchored at its edge, so a toast going away pulls the far end in
212
+ // by its own height and a gap — past the pointer that is holding the stack
213
+ // open, if that pointer was on the toast that went. The stack then shuts under
214
+ // the hand that opened it, which is what was reported. It is let back down on
215
+ // the way out, where the pointer has already gone.
216
+ box(reach) {
217
+ if (!this.expanded) {
218
+ this.floor = 0
219
+ return reach
220
+ }
221
+
222
+ this.floor = Math.max(this.floor, reach)
223
+ return this.floor
224
+ }
225
+
226
+ // Measured with the toast at its own size, and remembered: once it is scaled
227
+ // and clipped to the front one's height, the box no longer knows how tall it
228
+ // wants to be.
229
+ heightOf(toast) {
230
+ if (toast.dataset.height) return Number(toast.dataset.height)
231
+
232
+ const height = Math.round(toast.getBoundingClientRect().height)
233
+ if (height > 0) toast.dataset.height = String(height)
234
+
235
+ return height
236
+ }
237
+
238
+ start(toast) {
239
+ if (this.timers.has(toast)) clearTimeout(this.timers.get(toast))
240
+
241
+ // `||` would read a `0` as "not given" and hand back the default, which is
242
+ // the opposite of what 0 means here: no clock at all.
243
+ const asked = toast.dataset.duration
244
+ const duration = asked === undefined || asked === "" ? this.durationValue : Number(asked)
245
+ // A toast that is still loading has no business disappearing on a clock.
246
+ if (toast.dataset.variant === "loading" || duration <= 0) return
247
+
248
+ this.timers.set(toast, setTimeout(() => this.close(toast), duration))
249
+ }
250
+
251
+ close(toast) {
252
+ if (!toast || toast.dataset.state === "closed") return
253
+
254
+ clearTimeout(this.timers.get(toast))
255
+ this.timers.delete(toast)
256
+ toast.dataset.state = "closed"
257
+ // `place()` wrote an inline opacity on every toast, and an inline style
258
+ // beats the class that fades this one out. Handing the property back is what
259
+ // lets `data-[state=closed]:opacity-0` play at all — and the queue below
260
+ // waits on that animation, so without this the toast is gone in the same
261
+ // tick rather than on its way out.
262
+ toast.style.opacity = ""
263
+ // And the rest close the gap it leaves while it fades, rather than after.
264
+ this.place()
265
+
266
+ // The element waits on its own animation rather than on a number here, so
267
+ // the exit and the classes that draw it cannot drift apart.
268
+ this.exits.defer(toast, () => {
269
+ toast.remove()
270
+ this.place()
271
+ })
272
+ }
273
+
274
+ fill(toast, slot, text) {
275
+ const element = toast.querySelector(`[data-slot=${slot}]`)
276
+ if (!element) return
277
+
278
+ if (text) element.textContent = text
279
+ else element.remove()
280
+ }
281
+ }
@@ -0,0 +1,24 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Radix's Toggle: an `aria-pressed` button whose styling keys off
4
+ // `data-state="on" | "off"`.
5
+ export default class extends Controller {
6
+ static values = { pressed: Boolean, disabled: Boolean }
7
+
8
+ connect() {
9
+ this.render()
10
+ }
11
+
12
+ toggle() {
13
+ if (this.disabledValue) return
14
+
15
+ this.pressedValue = !this.pressedValue
16
+ this.render()
17
+ this.dispatch("change", { detail: { pressed: this.pressedValue } })
18
+ }
19
+
20
+ render() {
21
+ this.element.dataset.state = this.pressedValue ? "on" : "off"
22
+ this.element.setAttribute("aria-pressed", String(this.pressedValue))
23
+ }
24
+ }
@@ -0,0 +1,73 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { directionAwareKey, readDirection } from "shadcn/direction"
3
+
4
+ // Radix's ToggleGroup, in both `single` and `multiple` modes, with the roving
5
+ // tabindex its items use.
6
+ export default class extends Controller {
7
+ static targets = [ "item", "input" ]
8
+ static values = { type: { type: String, default: "single" }, value: String }
9
+
10
+ connect() {
11
+ this.render()
12
+ }
13
+
14
+ get multiple() {
15
+ return this.typeValue === "multiple"
16
+ }
17
+
18
+ get selected() {
19
+ if (!this.valueValue) return []
20
+ return this.valueValue.split(",").filter(Boolean)
21
+ }
22
+
23
+ set selected(values) {
24
+ this.valueValue = values.join(",")
25
+ }
26
+
27
+ toggle(event) {
28
+ const item = event.currentTarget
29
+ if (item.disabled) return
30
+
31
+ const value = item.dataset.value || ""
32
+ const current = this.selected
33
+
34
+ if (this.multiple) {
35
+ this.selected = current.includes(value)
36
+ ? current.filter((v) => v !== value)
37
+ : current.concat(value)
38
+ } else {
39
+ this.selected = current.includes(value) ? [] : [ value ]
40
+ }
41
+
42
+ this.render()
43
+ this.dispatch("change", { detail: { value: this.valueValue } })
44
+ }
45
+
46
+ keydown(event) {
47
+ const key = directionAwareKey(event.key, readDirection(this.element))
48
+ const step = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[key]
49
+ if (!step) return
50
+
51
+ event.preventDefault()
52
+
53
+ const items = this.itemTargets.filter((item) => !item.disabled)
54
+ if (items.length === 0) return
55
+
56
+ const current = items.indexOf(event.currentTarget)
57
+ items[(current + step + items.length) % items.length].focus({ preventScroll: true })
58
+ }
59
+
60
+ render() {
61
+ const selected = this.selected
62
+ const enabled = this.itemTargets.filter((item) => !item.disabled)
63
+
64
+ this.itemTargets.forEach((item) => {
65
+ const on = selected.includes(item.dataset.value || "")
66
+ item.dataset.state = on ? "on" : "off"
67
+ item.setAttribute("aria-pressed", String(on))
68
+ item.setAttribute("tabindex", item === (enabled.find((i) => selected.includes(i.dataset.value)) || enabled[0]) ? "0" : "-1")
69
+ })
70
+
71
+ if (this.hasInputTarget) this.inputTarget.value = this.valueValue
72
+ }
73
+ }
@@ -0,0 +1,58 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { uniqueId } from "shadcn/id"
3
+ import { FloatingLayer } from "shadcn/floating"
4
+
5
+ // Radix's Tooltip: opens on hover or focus, closes on leave, blur or Escape,
6
+ // and never takes focus itself.
7
+ export default class extends Controller {
8
+ static targets = [ "trigger", "content" ]
9
+ static values = {
10
+ side: { type: String, default: "top" },
11
+ align: { type: String, default: "center" },
12
+ sideOffset: { type: Number, default: 0 },
13
+ delay: { type: Number, default: 0 }
14
+ }
15
+
16
+ connect() {
17
+ if (!this.hasContentTarget || !this.hasTriggerTarget) return
18
+
19
+ this.contentTarget.hidden = true
20
+ this.contentTarget.setAttribute("role", "tooltip")
21
+ this.contentTarget.id ||= uniqueId("shadcn-tooltip")
22
+
23
+ this.layer = new FloatingLayer({
24
+ trigger: this.triggerTarget,
25
+ content: this.contentTarget,
26
+ prefix: "tooltip",
27
+ side: this.sideValue,
28
+ align: this.alignValue,
29
+ sideOffset: this.sideOffsetValue,
30
+ onOpen: () => this.triggerTarget.setAttribute("aria-describedby", this.contentTarget.id),
31
+ onClose: () => this.triggerTarget.removeAttribute("aria-describedby")
32
+ })
33
+ }
34
+
35
+ disconnect() {
36
+ this.cancel()
37
+ if (this.layer) this.layer.destroy()
38
+ }
39
+
40
+ show() {
41
+ this.cancel()
42
+ if (this.delayValue > 0) {
43
+ this.timer = setTimeout(() => this.layer.show(), this.delayValue)
44
+ } else {
45
+ this.layer.show()
46
+ }
47
+ }
48
+
49
+ hide() {
50
+ this.cancel()
51
+ this.layer.hide()
52
+ }
53
+
54
+ cancel() {
55
+ if (this.timer) clearTimeout(this.timer)
56
+ this.timer = null
57
+ }
58
+ }
@@ -0,0 +1,41 @@
1
+ // Reading direction, for the controllers whose arrow keys depend on it.
2
+ //
3
+ // This is the port of shadcn's `direction.tsx`, and the port is a module rather
4
+ // than a component because there is nothing to render: upstream's
5
+ // `DirectionProvider` wraps Radix's, which is a React context and emits no DOM
6
+ // at all (vendor/shadcn/ui/direction.tsx). React needs that context because a
7
+ // component cannot easily read inherited DOM state while rendering. A Stimulus
8
+ // controller has no such problem — the browser has already resolved `dir` for
9
+ // every element by the time one runs.
10
+ //
11
+ // So a host writes `<html dir="rtl">`, or `dir` on any ancestor, and these
12
+ // controllers follow. There is no provider to mount and no value to thread
13
+ // through, which is the whole of what this component becomes here.
14
+
15
+ // `getComputedStyle().direction` rather than walking up looking for a `dir`
16
+ // attribute: it resolves inheritance, the CSS `direction` property, and
17
+ // `dir="auto"` — which has no fixed answer until the browser has looked at the
18
+ // text. Reading the attribute would get the first two wrong and the third
19
+ // impossible.
20
+ export function readDirection(element) {
21
+ if (!element) return "ltr"
22
+
23
+ return window.getComputedStyle(element).direction === "rtl" ? "rtl" : "ltr"
24
+ }
25
+
26
+ // Radix's `getDirectionAwareKey` (vendor/radix/ui/roving-focus-group.tsx:359).
27
+ // Right-to-left swaps the two horizontal arrows and nothing else — `ArrowUp`
28
+ // and `ArrowDown` are unaffected, because reading direction is horizontal and
29
+ // a column is a column either way.
30
+ //
31
+ // Applied *before* a key is mapped to an intent, never after: the point is that
32
+ // in RTL the key labelled Left is the one that means "next", so every caller
33
+ // can keep one map and translate the key on the way in.
34
+ export function directionAwareKey(key, direction) {
35
+ if (direction !== "rtl") return key
36
+
37
+ if (key === "ArrowLeft") return "ArrowRight"
38
+ if (key === "ArrowRight") return "ArrowLeft"
39
+
40
+ return key
41
+ }
@@ -0,0 +1,73 @@
1
+ // Port of Radix's DismissableLayer.
2
+ //
3
+ // Layers form a stack. Escape only closes the topmost one; a pointer press
4
+ // outside a layer closes it and everything above it. Elements registered as
5
+ // "anchors" (the trigger that opened the layer) do not count as outside, so a
6
+ // click on the trigger toggles rather than close-then-reopen.
7
+
8
+ const layers = []
9
+ let listening = false
10
+
11
+ function topLayer() {
12
+ return layers[layers.length - 1]
13
+ }
14
+
15
+ function isInside(layer, target) {
16
+ if (!target) return false
17
+ if (layer.element.contains(target)) return true
18
+ return layer.anchors.some((anchor) => anchor && anchor.contains(target))
19
+ }
20
+
21
+ function onPointerDown(event) {
22
+ // Close from the top down, stopping at the first layer that owns the target.
23
+ for (let i = layers.length - 1; i >= 0; i--) {
24
+ const layer = layers[i]
25
+ if (isInside(layer, event.target)) break
26
+ layer.onDismiss({ reason: "pointerdown", originalEvent: event })
27
+ }
28
+ }
29
+
30
+ function onKeyDown(event) {
31
+ if (event.key !== "Escape") return
32
+
33
+ const layer = topLayer()
34
+ if (!layer) return
35
+
36
+ layer.onDismiss({ reason: "escape", originalEvent: event })
37
+ }
38
+
39
+ function startListening() {
40
+ if (listening) return
41
+ // Capture phase so a layer closes before the click reaches the page.
42
+ document.addEventListener("pointerdown", onPointerDown, true)
43
+ // Escape listens on the bubble phase, and deliberately does not stop
44
+ // propagation: a capture-phase `stopPropagation` here would swallow Escape
45
+ // for the whole document — including the host application's own handlers —
46
+ // whenever any layer, even a tooltip, happened to be open.
47
+ document.addEventListener("keydown", onKeyDown)
48
+ listening = true
49
+ }
50
+
51
+ function stopListening() {
52
+ if (!listening) return
53
+ document.removeEventListener("pointerdown", onPointerDown, true)
54
+ document.removeEventListener("keydown", onKeyDown)
55
+ listening = false
56
+ }
57
+
58
+ export function pushLayer({ element, anchors = [], onDismiss }) {
59
+ const layer = { element, anchors: [].concat(anchors), onDismiss }
60
+ layers.push(layer)
61
+ startListening()
62
+ return layer
63
+ }
64
+
65
+ export function removeLayer(layer) {
66
+ const index = layers.indexOf(layer)
67
+ if (index !== -1) layers.splice(index, 1)
68
+ if (layers.length === 0) stopListening()
69
+ }
70
+
71
+ export function isTopLayer(layer) {
72
+ return topLayer() === layer
73
+ }