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,320 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { uniqueId } from "shadcn/id"
3
+ import { FloatingLayer } from "shadcn/floating"
4
+ import { Typeahead } from "shadcn/typeahead"
5
+
6
+ // Radix's Select: a `role="combobox"` trigger driving a `role="listbox"` layer.
7
+ //
8
+ // The chosen item's label is copied into `[data-slot=select-value]` and the
9
+ // value is mirrored onto a hidden input so the control submits with the form,
10
+ // exactly like Radix's BubbleSelect.
11
+ export default class extends Controller {
12
+ // `input` is the hidden field that submits with the form, which is why the
13
+ // search box is `search`.
14
+ static targets = [ "trigger", "content", "item", "value", "input", "search", "list", "empty",
15
+ "scrollUpButton", "scrollDownButton", "viewport" ]
16
+ static values = {
17
+ open: Boolean,
18
+ value: String,
19
+ placeholder: String,
20
+ side: { type: String, default: "bottom" },
21
+ align: { type: String, default: "center" },
22
+ sideOffset: { type: Number, default: 4 },
23
+ searchable: Boolean
24
+ }
25
+
26
+ connect() {
27
+ if (!this.hasContentTarget || !this.hasTriggerTarget) return
28
+
29
+ this.typeahead = new Typeahead()
30
+
31
+ this.contentTarget.hidden = true
32
+ this.contentTarget.id ||= uniqueId("shadcn-select")
33
+
34
+ // The id is generated here, so the server has nothing to point this at.
35
+ this.triggerTarget.setAttribute("aria-controls", this.contentTarget.id)
36
+ this.triggerTarget.dataset.state = "closed"
37
+
38
+ if (this.searchableValue && this.hasSearchTarget && this.hasListTarget) {
39
+ this.listTarget.id ||= uniqueId("shadcn-select-list")
40
+ this.searchTarget.setAttribute("aria-controls", this.listTarget.id)
41
+ }
42
+
43
+ this.layer = new FloatingLayer({
44
+ trigger: this.triggerTarget,
45
+ content: this.contentTarget,
46
+ prefix: "select",
47
+ side: this.sideValue,
48
+ align: this.alignValue,
49
+ sideOffset: this.sideOffsetValue,
50
+ matchAnchorWidth: true,
51
+ onOpen: () => {
52
+ this.triggerTarget.setAttribute("aria-expanded", "true")
53
+ // Radix resets here rather than on close (vendor/radix/ui/select.tsx:331-336),
54
+ // so characters typed before this panel was last dismissed cannot join
55
+ // the next search.
56
+ this.typeahead.reset()
57
+ // Focus goes to the search field, not the popover: a searchable select
58
+ // is typed into the moment it opens.
59
+ if (this.searchableValue && this.hasSearchTarget) {
60
+ this.searchTarget.focus({ preventScroll: true })
61
+ } else {
62
+ this.contentTarget.focus({ preventScroll: true })
63
+ }
64
+ this.highlight(this.selectedItem || this.enabledItems[0])
65
+ this.syncScrollButtons()
66
+ },
67
+ onClose: () => {
68
+ this.triggerTarget.setAttribute("aria-expanded", "false")
69
+ this.clearHighlight()
70
+ // Clearing through `search()` rather than by hand keeps one definition
71
+ // of what the list looks like for a given query — the empty state and
72
+ // the list's own `hidden` included.
73
+ if (this.searchableValue && this.hasSearchTarget) {
74
+ this.searchTarget.value = ""
75
+ this.search()
76
+ }
77
+ this.triggerTarget.focus({ preventScroll: true })
78
+ }
79
+ })
80
+
81
+ this.render()
82
+ if (this.openValue) this.layer.show()
83
+ }
84
+
85
+ disconnect() {
86
+ this.stopAutoScroll()
87
+ if (this.layer) this.layer.destroy()
88
+ }
89
+
90
+ toggle() {
91
+ this.layer.toggle()
92
+ }
93
+
94
+ triggerKeydown(event) {
95
+ if (![ "ArrowDown", "ArrowUp", "Enter", " " ].includes(event.key)) return
96
+
97
+ event.preventDefault()
98
+ this.layer.show()
99
+ }
100
+
101
+ contentKeydown(event) {
102
+ const items = this.enabledItems
103
+ const current = items.indexOf(this.highlighted)
104
+
105
+ switch (event.key) {
106
+ // Clamp rather than wrap: Radix's own keydown handler slices the
107
+ // candidate list from the current index and never wraps it back around
108
+ // (vendor/radix/ui/select.tsx:904's
109
+ // `candidateNodes = candidateNodes.slice(currentIndex + 1)`).
110
+ case "ArrowDown":
111
+ event.preventDefault()
112
+ this.highlight(items[Math.min(current + 1, items.length - 1)])
113
+ return
114
+ // Radix reverses the candidates for ArrowUp before slicing
115
+ // (vendor/radix/ui/select.tsx:898-904), so an index of -1 slices nothing
116
+ // off the reversed list and focus enters at the end. `onOpen` always
117
+ // highlights, which leaves one way to be here with items to move
118
+ // through: a selected item that is *disabled*, which is what
119
+ // `selectedItem` finds and what `enabledItems` leaves out.
120
+ case "ArrowUp":
121
+ event.preventDefault()
122
+ this.highlight(current === -1 ? items[items.length - 1] : items[Math.max(current - 1, 0)])
123
+ return
124
+ // Home and End move the caret when a search field has focus. Jumping the
125
+ // highlight instead would make the field uneditable at either end.
126
+ case "Home":
127
+ if (this.searchableValue) return
128
+ event.preventDefault()
129
+ this.highlight(items[0])
130
+ return
131
+ case "End":
132
+ if (this.searchableValue) return
133
+ event.preventDefault()
134
+ this.highlight(items[items.length - 1])
135
+ return
136
+ case "Enter":
137
+ case " ":
138
+ // A space is a character while the search field has focus; Enter still
139
+ // chooses. Without this, typing a space picks an option and closes.
140
+ if (event.key === " " && this.searchableValue) return
141
+ if (!this.highlighted) return
142
+ event.preventDefault()
143
+ this.highlighted.click()
144
+ return
145
+ case "Tab":
146
+ event.preventDefault()
147
+ return
148
+ }
149
+
150
+ // The search field owns the characters when there is one; running the
151
+ // typeahead too would accumulate a second, invisible query alongside it.
152
+ if (!this.searchableValue && event.key.length === 1 && !event.metaKey && !event.ctrlKey) {
153
+ const match = this.typeahead.search(event.key, this.enabledItems, this.highlighted)
154
+ if (match) this.highlight(match)
155
+ }
156
+ }
157
+
158
+ // shadcn's aria variant filters on substring rather than prefix — "err"
159
+ // reaches Blueberry — and drops non-matching options from the DOM. Here they
160
+ // are hidden instead: the options are server-rendered ERB, and one removed
161
+ // cannot be put back.
162
+ search() {
163
+ const query = this.searchTarget.value.trim().toLowerCase()
164
+
165
+ this.itemTargets.forEach((item) => {
166
+ item.hidden = query !== "" && !item.textContent.trim().toLowerCase().includes(query)
167
+ })
168
+
169
+ const matches = this.enabledItems
170
+ if (this.hasListTarget) this.listTarget.hidden = matches.length === 0
171
+ if (this.hasEmptyTarget) this.emptyTarget.hidden = matches.length > 0
172
+
173
+ this.highlight(matches[0])
174
+ }
175
+
176
+ // Radix mounts each button only while the viewport can scroll that way and
177
+ // recomputes on every scroll (vendor/radix/ui/select.tsx:1585, :1630-1634).
178
+ // `Math.ceil` is theirs too: zoomed in, `scrollTop` is not always an integer.
179
+ syncScrollButtons() {
180
+ if (!this.hasScrollUpButtonTarget || !this.hasScrollDownButtonTarget) return
181
+
182
+ const box = this.scrollContainer
183
+ const maxScroll = box.scrollHeight - box.clientHeight
184
+
185
+ this.scrollUpButtonTarget.hidden = box.scrollTop <= 0
186
+ this.scrollDownButtonTarget.hidden = Math.ceil(box.scrollTop) >= maxScroll
187
+ }
188
+
189
+ // Pointer only. Radix starts the same 50ms interval on pointerdown and on
190
+ // pointermove (vendor/radix/ui/select.tsx:1697-1706) — move as well as down,
191
+ // so hovering scrolls without pressing.
192
+ startAutoScroll(event) {
193
+ if (this.autoScrollTimer) return
194
+
195
+ const up = event.currentTarget === this.scrollUpButtonTarget
196
+ this.autoScrollTimer = setInterval(() => this.autoScroll(up ? -1 : 1), 50)
197
+ }
198
+
199
+ stopAutoScroll() {
200
+ clearInterval(this.autoScrollTimer)
201
+ this.autoScrollTimer = null
202
+ }
203
+
204
+ autoScroll(direction) {
205
+ // One item per tick, as Radix does: it steps by the selected item's
206
+ // offsetHeight (vendor/radix/ui/select.tsx:1601, :1647). With nothing
207
+ // selected there is still a list to measure.
208
+ const step = (this.selectedItem || this.itemTargets[0])?.offsetHeight || 0
209
+
210
+ this.scrollContainer.scrollTop += direction * step
211
+ this.syncScrollButtons()
212
+ }
213
+
214
+ pointerenter(event) {
215
+ this.highlight(event.currentTarget)
216
+ }
217
+
218
+ select(event) {
219
+ const item = event.currentTarget
220
+ if (item.dataset.disabled !== undefined) return
221
+
222
+ this.valueValue = item.dataset.value || ""
223
+ this.render()
224
+ this.layer.hide()
225
+ this.dispatch("change", { detail: { value: this.valueValue } })
226
+
227
+ if (this.hasInputTarget) {
228
+ this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
229
+ }
230
+ }
231
+
232
+ // --- helpers -------------------------------------------------------------
233
+
234
+ // `!item.hidden` is what keeps the arrows, Home, End and `selectedItem` out
235
+ // of rows the filter has taken away. Nothing hides items unless `searchable`,
236
+ // so the plain select is unaffected.
237
+ // The viewport scrolls, as Radix's does (vendor/radix/ui/select.tsx:1247) —
238
+ // which is what keeps the scroll buttons pinned outside it. A searchable
239
+ // panel moves the scrolling one level further in, onto the list, so the
240
+ // search field stays put while the options move.
241
+ get scrollContainer() {
242
+ if (this.searchableValue && this.hasListTarget) return this.listTarget
243
+
244
+ return this.hasViewportTarget ? this.viewportTarget : this.contentTarget
245
+ }
246
+
247
+ get enabledItems() {
248
+ return this.itemTargets.filter((item) => item.dataset.disabled === undefined && !item.hidden)
249
+ }
250
+
251
+ get selectedItem() {
252
+ return this.itemTargets.find((item) => (item.dataset.value || "") === this.valueValue) || null
253
+ }
254
+
255
+ get highlighted() {
256
+ return this.itemTargets.find((item) => item.dataset.highlighted !== undefined) || null
257
+ }
258
+
259
+ highlight(item) {
260
+ this.clearHighlight()
261
+ if (!item) return
262
+
263
+ item.dataset.highlighted = ""
264
+
265
+ // Moving DOM focus would take it out of the search field and typing would
266
+ // stop, so a searchable select points at the item instead of focusing it.
267
+ if (this.searchableValue) {
268
+ // Assigned here rather than in `connect`, which only ever saw the items
269
+ // the server rendered. Anything added afterwards — a dependent select
270
+ // refilling its options when another one changes — arrived without an id,
271
+ // and `aria-activedescendant` then pointed at nothing: the highlight moved
272
+ // on screen and a screen reader followed none of it. Silent, and invisible
273
+ // to anyone not listening.
274
+ //
275
+ // `uniqueId` rather than `crypto.randomUUID()`, which is secure-context
276
+ // only and `undefined` over plain HTTP.
277
+ item.id ||= uniqueId("shadcn-select-item")
278
+ this.searchTarget.setAttribute("aria-activedescendant", item.id)
279
+ } else {
280
+ item.focus({ preventScroll: true })
281
+ }
282
+
283
+ item.scrollIntoView({ block: "nearest" })
284
+ }
285
+
286
+ clearHighlight() {
287
+ this.itemTargets.forEach((item) => delete item.dataset.highlighted)
288
+ if (this.searchableValue && this.hasSearchTarget) {
289
+ this.searchTarget.removeAttribute("aria-activedescendant")
290
+ }
291
+ }
292
+
293
+ render() {
294
+ const selected = this.selectedItem
295
+
296
+ this.itemTargets.forEach((item) => {
297
+ const isSelected = item === selected
298
+ item.setAttribute("aria-selected", String(isSelected))
299
+ item.dataset.state = isSelected ? "checked" : "unchecked"
300
+
301
+ const indicator = item.querySelector("[data-slot='select-item-indicator'] > *")
302
+ if (indicator) indicator.hidden = !isSelected
303
+ })
304
+
305
+ if (this.hasValueTarget) {
306
+ this.valueTarget.textContent = selected
307
+ ? selected.querySelector("[data-slot='select-item-text']")?.textContent.trim() ||
308
+ selected.textContent.trim()
309
+ : this.placeholderValue
310
+ }
311
+
312
+ if (selected) {
313
+ delete this.triggerTarget.dataset.placeholder
314
+ } else {
315
+ this.triggerTarget.dataset.placeholder = ""
316
+ }
317
+
318
+ if (this.hasInputTarget) this.inputTarget.value = this.valueValue
319
+ }
320
+ }
@@ -0,0 +1,273 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { pushLayer, removeLayer } from "shadcn/dismiss"
3
+ import { trapFocus, focusFirst, lockScroll, unlockScroll } from "shadcn/focus"
4
+ import * as topLayer from "shadcn/top_layer"
5
+ import { ExitQueue } from "shadcn/animation"
6
+
7
+ // Upstream's own name and lifetime (vendor/shadcn/ui/sidebar.tsx:28-29).
8
+ const COOKIE = "sidebar_state"
9
+ const COOKIE_MAX_AGE = 60 * 60 * 24 * 7
10
+ const SHORTCUT = "b"
11
+
12
+ // Not a number this port chose: upstream's desktop tree carries `md:block`
13
+ // (vendor/shadcn/ui/sidebar.tsx:210), and `md` is 768px in Tailwind's default
14
+ // scale. Nothing had to be vendored to learn it.
15
+ const MOBILE_QUERY = "(max-width: 767px)"
16
+
17
+ // Radix has no Sidebar — shadcn builds it on its own React context
18
+ // (vendor/shadcn/ui/sidebar.tsx:56), so this controller has no Radix source to
19
+ // answer to, only shadcn's own.
20
+ //
21
+ // The state is small: expanded or collapsed on desktop, and a separate
22
+ // ephemeral flag on mobile that is deliberately never persisted.
23
+ export default class extends Controller {
24
+ static targets = [ "sidebar", "trigger", "container", "overlay", "sheetHeader" ]
25
+ static values = { open: Boolean, openMobile: Boolean }
26
+
27
+ connect() {
28
+ // Holds the DOM half of a close until the sheet's slide-out has played.
29
+ // Same module and the same reason as `dialog_controller.js`.
30
+ this.exits = new ExitQueue()
31
+
32
+ // Bound on `window` rather than on the element, so it fires wherever focus
33
+ // happens to be — which is what upstream does (sidebar.tsx:96-111), and the
34
+ // point of a shortcut. Removed again in `disconnect()`: Turbo tears
35
+ // controllers down on every navigation, and a listener left behind would
36
+ // accumulate one per visit.
37
+ this.onKeydown = (event) => {
38
+ if (event.key !== SHORTCUT) return
39
+ // A bare "b" is a character somebody may be typing.
40
+ if (!event.metaKey && !event.ctrlKey) return
41
+
42
+ event.preventDefault()
43
+ this.toggle()
44
+ }
45
+
46
+ window.addEventListener("keydown", this.onKeydown)
47
+
48
+ // Feature-detected the way `top_layer.js` treats the Popover API: without
49
+ // matchMedia the desktop branch is the whole component.
50
+ this.media = typeof window.matchMedia === "function" ? window.matchMedia(MOBILE_QUERY) : null
51
+ this.onMediaChange = () => this.render()
52
+ this.media?.addEventListener("change", this.onMediaChange)
53
+
54
+ this.render()
55
+ }
56
+
57
+ disconnect() {
58
+ window.removeEventListener("keydown", this.onKeydown)
59
+ this.media?.removeEventListener("change", this.onMediaChange)
60
+ this.closeMobile()
61
+ // Turbo may be detaching the element: there is nothing to animate for, and
62
+ // the deferred teardown still has to run.
63
+ this.exits.flushAll()
64
+ }
65
+
66
+ get isMobile() {
67
+ return this.media?.matches === true
68
+ }
69
+
70
+ toggle() {
71
+ // Upstream branches here rather than in the state: mobile moves an
72
+ // ephemeral flag, desktop moves the persisted one
73
+ // (vendor/shadcn/ui/sidebar.tsx:92-95). A phone must not overwrite what the
74
+ // desktop remembered.
75
+ if (this.isMobile) {
76
+ this.openMobileValue = !this.openMobileValue
77
+ } else {
78
+ this.openValue = !this.openValue
79
+ this.persist()
80
+ }
81
+
82
+ this.render()
83
+ }
84
+
85
+ // Written, never read. A Rails layout reads it and passes the result back as
86
+ // the `open` value, the way a Next.js layout does with `defaultOpen`
87
+ // (vendor/shadcn/ui/sidebar.tsx:86) — which is the whole reason this is a
88
+ // cookie and not `localStorage`: the server has to be able to render the
89
+ // sidebar already collapsed, and on this component the alternative is a
90
+ // full-width layout shift on every page.
91
+ //
92
+ // `samesite=lax` is this port's one addition. Upstream's line carries no
93
+ // SameSite, and `theme.js` here already writes its cookie with it, so
94
+ // matching upstream character for character would leave one library
95
+ // disagreeing with itself.
96
+ //
97
+ // No `try`/`catch`: a blocked cookie assignment is a silent no-op, not an
98
+ // exception. `theme.js` wraps `localStorage`, which does throw in private
99
+ // browsing, and leaves its own cookie line outside the guard for this reason.
100
+ persist() {
101
+ document.cookie =
102
+ `${COOKIE}=${this.openValue}; path=/; max-age=${COOKIE_MAX_AGE}; samesite=lax`
103
+ }
104
+
105
+ // Named `render` because `index.js` re-runs exactly that on every `shadcn--*`
106
+ // controller after `turbo:morph`, which is what puts a controller back in
107
+ // agreement with markup the server has just rewritten underneath it.
108
+ render() {
109
+ if (!this.hasSidebarTarget) return
110
+
111
+ const sidebar = this.sidebarTarget
112
+
113
+ sidebar.dataset.state = this.openValue ? "expanded" : "collapsed"
114
+
115
+ // Upstream writes this empty while expanded and fills it only when
116
+ // collapsed (vendor/shadcn/ui/sidebar.tsx:212). The classes match on
117
+ // `group-data-[collapsible=offcanvas]:…`, so filling it always would style
118
+ // an open sidebar as a closed one. The value to collapse *to* therefore
119
+ // cannot live here; it lives in `data-sidebar-collapsible`.
120
+ //
121
+ // Empty on the mobile branch for a second reason: upstream's mobile tree
122
+ // has no collapsed state to carry (sidebar.tsx:182-204), and here the same
123
+ // element serves both, so a `collapsed` left over from the desktop would
124
+ // style the sheet as a rail of icons — or, with `offcanvas`, slide it off
125
+ // the screen it was just opened on.
126
+ sidebar.dataset.collapsible =
127
+ this.openValue || this.isMobile ? "" : (sidebar.dataset.sidebarCollapsible || "offcanvas")
128
+
129
+ if (this.isMobile && this.openMobileValue) this.openMobile()
130
+ else this.closeMobile()
131
+ }
132
+
133
+ // Everything a Sheet does, applied to markup the server already sent. The
134
+ // element never moves, so the Stimulus actions on the links inside it stay
135
+ // bound — which is why this port does not build a Sheet and relocate the
136
+ // content into it, the way React renders a different tree.
137
+ // `dialog_controller.js` composes the same three modules.
138
+ openMobile() {
139
+ const sidebar = this.sidebarTarget
140
+ if (sidebar.dataset.mobile === "true") return
141
+
142
+ // Reopened before the slide-out finished: both teardowns are still queued
143
+ // and would undo an open sheet a moment after it appeared.
144
+ if (this.hasContainerTarget) this.exits.cancel(this.containerTarget)
145
+ if (this.hasOverlayTarget) this.exits.cancel(this.overlayTarget)
146
+
147
+ sidebar.dataset.mobile = "true"
148
+ this.announceSheet()
149
+ // Undoes upstream's `hidden … md:block`, which is CSS-hidden below the
150
+ // breakpoint because React renders a different tree there rather than
151
+ // showing this one. Inline beats the utility without having to out-specify
152
+ // `md:block` at every other width, and removing the property below restores
153
+ // the class exactly.
154
+ sidebar.style.display = "block"
155
+
156
+ // `sheet-content` and `sheet-overlay` are animated by their own
157
+ // `data-[state=…]` classes, exactly as upstream writes them; nothing else
158
+ // here reads these two attributes.
159
+ this.setSheetState("open")
160
+
161
+ topLayer.enable(sidebar)
162
+ topLayer.show(sidebar)
163
+ lockScroll()
164
+
165
+ // The panel, not the sheet: the overlay is a child of `sidebar`, so a layer
166
+ // registered on `sidebar` counts a click on the dimmed backdrop as a click
167
+ // *inside* itself and never dismisses. Upstream has no such problem — its
168
+ // overlay is the content's sibling, portalled beside it. Here the two share
169
+ // a parent, and the layer has to be the half that is not the backdrop.
170
+ const panel = this.hasContainerTarget ? this.containerTarget : sidebar
171
+
172
+ this.releaseFocus = trapFocus(panel, this.hasTriggerTarget ? this.triggerTarget : undefined)
173
+ focusFirst(panel)
174
+ this.layer = pushLayer({
175
+ element: panel,
176
+ // Without the trigger as an anchor, the very click that opens the sheet
177
+ // reaches the dismiss layer as an outside click and closes it again.
178
+ anchors: this.hasTriggerTarget ? [ this.triggerTarget ] : [],
179
+ onDismiss: () => {
180
+ this.openMobileValue = false
181
+ this.render()
182
+ }
183
+ })
184
+ }
185
+
186
+ // A sheet is a modal dialog, and this one traps focus, locks the page's
187
+ // scroll and dims what is behind it while announcing nothing at all —
188
+ // upstream gets `role="dialog"`, `aria-modal` and a name for free by
189
+ // rendering a real Sheet below `md`, and this port has one tree instead.
190
+ //
191
+ // Applied here rather than in the markup because the server cannot know the
192
+ // viewport: on desktop this panel is not a dialog, and saying it is would be
193
+ // worse than saying nothing.
194
+ announceSheet() {
195
+ if (!this.hasContainerTarget) return
196
+
197
+ const panel = this.containerTarget
198
+
199
+ panel.setAttribute("role", "dialog")
200
+ panel.setAttribute("aria-modal", "true")
201
+ if (panel.dataset.sheetTitle) panel.setAttribute("aria-labelledby", panel.dataset.sheetTitle)
202
+ if (panel.dataset.sheetDescription) panel.setAttribute("aria-describedby", panel.dataset.sheetDescription)
203
+ if (this.hasSheetHeaderTarget) this.sheetHeaderTarget.hidden = false
204
+ }
205
+
206
+ silenceSheet() {
207
+ if (!this.hasContainerTarget) return
208
+
209
+ const panel = this.containerTarget
210
+
211
+ for (const name of [ "role", "aria-modal", "aria-labelledby", "aria-describedby" ]) {
212
+ panel.removeAttribute(name)
213
+ }
214
+ if (this.hasSheetHeaderTarget) this.sheetHeaderTarget.hidden = true
215
+ }
216
+
217
+ closeMobile() {
218
+ if (!this.hasSidebarTarget) return
219
+
220
+ const sidebar = this.sidebarTarget
221
+ if (sidebar.dataset.mobile !== "true") return
222
+
223
+ // Interaction state goes now: a sheet that is sliding out must not answer
224
+ // Escape, keep the page's scroll locked or hold focus. Only what is still
225
+ // being *looked at* waits — the same split `floating.js` makes.
226
+ unlockScroll()
227
+ this.releaseFocus?.()
228
+ this.releaseFocus = null
229
+ if (this.layer) removeLayer(this.layer)
230
+ this.layer = null
231
+
232
+ this.setSheetState("closed")
233
+
234
+ // Each element waits on its *own* animations, as the dialog's layers do.
235
+ // Hiding the backdrop on the container's clock instead looks broken rather
236
+ // than merely late: nothing in the compiled bundle sets
237
+ // `animation-fill-mode`, so the moment `fade-out-0` ends the overlay snaps
238
+ // back to a full `bg-black/50` and sits there — the panel gone, a grey
239
+ // sheet of glass left over it — until the longer animation finishes.
240
+ // Upstream's two clocks differ the same way, 150ms against `duration-300`.
241
+ if (this.hasOverlayTarget) {
242
+ this.exits.defer(this.overlayTarget, () => { this.overlayTarget.hidden = true })
243
+ }
244
+
245
+ // What ends the sheet is `data-mobile` going away — it is what
246
+ // `group-data-[mobile=true]:flex` reads — so it has to outlast the
247
+ // slide-out rather than start it.
248
+ const finish = () => {
249
+ this.silenceSheet()
250
+ delete sidebar.dataset.mobile
251
+ sidebar.style.removeProperty("display")
252
+
253
+ topLayer.hide(sidebar)
254
+ // Paired with the `enable` above, and not optional: this element is laid
255
+ // out around, so leaving it a popover leaves it `position: fixed` and the
256
+ // page draws over the desktop sidebar from then on.
257
+ topLayer.disable(sidebar)
258
+ }
259
+
260
+ if (this.hasContainerTarget) this.exits.defer(this.containerTarget, finish)
261
+ else finish()
262
+ }
263
+
264
+ // The overlay is `hidden` in the server's markup and stays that way on
265
+ // desktop, where nothing ever opens a sheet.
266
+ setSheetState(state) {
267
+ if (this.hasContainerTarget) this.containerTarget.dataset.state = state
268
+ if (!this.hasOverlayTarget) return
269
+
270
+ if (state === "open") this.overlayTarget.hidden = false
271
+ this.overlayTarget.dataset.state = state
272
+ }
273
+ }