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,267 @@
1
+ // The measurements a message scroller runs on, ported from
2
+ // `vendor/shadcn-react/message-scroller/geometry.ts`.
3
+ //
4
+ // Every function here is pure: it reads rects, `scrollTop`, computed padding
5
+ // and two `data-` attributes, and returns a number, an element or null. Nothing
6
+ // writes, nothing schedules, nothing holds state — which is why this is a module
7
+ // of its own rather than methods on the controller, and why it can be tested
8
+ // without a browser opening anything.
9
+ //
10
+ // One function of upstream's is deliberately missing:
11
+ // `getMessageScrollerVisibilityState`, the largest in the file, which exists to
12
+ // feed `useMessageScrollerVisibility`. That surface is not reproduced — see
13
+ // `.claude/docs/features/message-scroller.md` for the measurement behind that
14
+ // decision. Nothing else in this file referenced it.
15
+
16
+ // Upstream's `EMPTY_MESSAGE_SCROLLER_SCROLLABLE` (types.ts:193). A frozen
17
+ // object rather than a fresh one per call, so a caller cannot mutate the answer
18
+ // for the next.
19
+ const NOT_SCROLLABLE = Object.freeze({ start: false, end: false })
20
+
21
+ // The rows, in document order. The spacer is a sibling of the items and has to
22
+ // be excluded by identity rather than by a selector: it carries no `data-slot`
23
+ // of its own, and upstream filters it the same way (geometry.ts:108-116).
24
+ export function getMessageScrollerItems(content, spacer) {
25
+ return Array.from(content.children).filter(
26
+ (child) => child instanceof HTMLElement && child !== spacer
27
+ )
28
+ }
29
+
30
+ // Whether there is anything to scroll to in each direction — which is what
31
+ // decides the two buttons' `data-active`. `end` is measured against the
32
+ // content's own bottom rather than `scrollHeight`, because the tail spacer
33
+ // inflates `scrollHeight` on purpose and would keep the button lit forever.
34
+ export function getMessageScrollerScrollable({
35
+ content,
36
+ scrollEdgeThreshold,
37
+ spacer,
38
+ viewport
39
+ }) {
40
+ if (!viewport || !content) return NOT_SCROLLABLE
41
+
42
+ const contentBottom = getContentBottom({ content, spacer, viewport })
43
+
44
+ return {
45
+ start: viewport.scrollTop > scrollEdgeThreshold,
46
+ end:
47
+ contentBottom - viewport.scrollTop - viewport.clientHeight >
48
+ scrollEdgeThreshold
49
+ }
50
+ }
51
+
52
+ // The first anchor among rows added since the last count — what a newly
53
+ // arrived turn scrolls to.
54
+ export function getNewScrollAnchor(items, previousItemCount) {
55
+ for (let index = previousItemCount; index < items.length; index++) {
56
+ if (items[index]?.dataset.scrollAnchor === "true") return items[index]
57
+ }
58
+
59
+ return null
60
+ }
61
+
62
+ // `handledAnchors` is anything with a `has(element)` — upstream passes a
63
+ // `WeakSet` of anchors already scrolled to, so one is never handled twice.
64
+ export function getUnanchoredScrollAnchor(items, handledAnchors) {
65
+ for (const item of items) {
66
+ if (item.dataset.scrollAnchor === "true" && !handledAnchors.has(item)) {
67
+ return item
68
+ }
69
+ }
70
+
71
+ return null
72
+ }
73
+
74
+ // Two turns arriving in one batch cannot both be scrolled to, so the caller
75
+ // needs to know before it tries. Stops at two rather than counting them all.
76
+ export function hasMultipleNewScrollAnchors(items, previousItemCount) {
77
+ let count = 0
78
+
79
+ for (let index = previousItemCount; index < items.length; index++) {
80
+ if (items[index]?.dataset.scrollAnchor !== "true") continue
81
+
82
+ count += 1
83
+ if (count > 1) return true
84
+ }
85
+
86
+ return false
87
+ }
88
+
89
+ export function getLastScrollAnchor(items) {
90
+ for (let index = items.length - 1; index >= 0; index--) {
91
+ if (items[index]?.dataset.scrollAnchor === "true") return items[index]
92
+ }
93
+
94
+ return null
95
+ }
96
+
97
+ // Rows without a `data-message-id` are skipped: the spacer is already gone, but
98
+ // a host may put its own markup in the content column and it should not count
99
+ // as a message.
100
+ export function getFirstVisibleMessageItem({ content, spacer, viewport }) {
101
+ const viewportRect = viewport.getBoundingClientRect()
102
+
103
+ for (const item of getMessageScrollerItems(content, spacer)) {
104
+ if (!item.dataset.messageId) continue
105
+
106
+ const rect = item.getBoundingClientRect()
107
+
108
+ if (rect.bottom > viewportRect.top && rect.top < viewportRect.bottom) {
109
+ return item
110
+ }
111
+ }
112
+
113
+ return null
114
+ }
115
+
116
+ // Where the viewport has to be scrolled for `element` to sit at `align`.
117
+ // `scrollMargin` is the caller's breathing room above it; the content's own
118
+ // block padding is subtracted so the row lands where it looks right rather than
119
+ // where the box model puts it (geometry.ts:204-265).
120
+ export function getElementScrollTop({
121
+ align,
122
+ element,
123
+ scrollMargin,
124
+ spacer,
125
+ viewport
126
+ }) {
127
+ const elementTop = getElementTop(element, viewport)
128
+ const elementHeight = element.getBoundingClientRect().height
129
+ const contentPadding = getContentBlockPadding(spacer)
130
+
131
+ if (align === "center") {
132
+ const insetHeight = Math.max(
133
+ 0,
134
+ viewport.clientHeight - contentPadding.start - contentPadding.end
135
+ )
136
+
137
+ return (
138
+ elementTop -
139
+ contentPadding.start -
140
+ (insetHeight - elementHeight) / 2 -
141
+ scrollMargin
142
+ )
143
+ }
144
+
145
+ if (align === "end") {
146
+ return (
147
+ elementTop -
148
+ viewport.clientHeight +
149
+ elementHeight +
150
+ contentPadding.end +
151
+ scrollMargin
152
+ )
153
+ }
154
+
155
+ // `nearest` is the only one that can answer "do not move".
156
+ if (align === "nearest") {
157
+ const elementBottom = elementTop + elementHeight
158
+ const viewportTop = viewport.scrollTop + contentPadding.start
159
+ const viewportBottom =
160
+ viewport.scrollTop + viewport.clientHeight - contentPadding.end
161
+
162
+ if (elementTop >= viewportTop && elementBottom <= viewportBottom) {
163
+ return viewport.scrollTop
164
+ }
165
+
166
+ if (elementTop < viewportTop) {
167
+ return elementTop - contentPadding.start - scrollMargin
168
+ }
169
+
170
+ return (
171
+ elementBottom - viewport.clientHeight + contentPadding.end + scrollMargin
172
+ )
173
+ }
174
+
175
+ return elementTop - contentPadding.start - scrollMargin
176
+ }
177
+
178
+ // In the scroll container's own coordinates, not the viewport's: the rect
179
+ // difference plus the current `scrollTop` is what makes the answer survive
180
+ // being scrolled.
181
+ export function getElementTop(element, viewport) {
182
+ const elementRect = element.getBoundingClientRect()
183
+ const viewportRect = viewport.getBoundingClientRect()
184
+
185
+ return elementRect.top - viewportRect.top + viewport.scrollTop
186
+ }
187
+
188
+ export function getElementViewportTop(element, viewport) {
189
+ return (
190
+ element.getBoundingClientRect().top - viewport.getBoundingClientRect().top
191
+ )
192
+ }
193
+
194
+ // How tall the spacer under the last message has to be for that message to be
195
+ // able to reach the top of the viewport. Negative means none is needed.
196
+ export function getTailSpacerHeight({ content, scrollTop, spacer, viewport }) {
197
+ const contentBottom = getContentBottom({ content, spacer, viewport })
198
+
199
+ return scrollTop + viewport.clientHeight - contentBottom
200
+ }
201
+
202
+ // The bottom of the real content, spacer excluded — measured from the rows
203
+ // rather than read off `scrollHeight`, which the spacer inflates. Starts at the
204
+ // padding so an empty column still answers sensibly.
205
+ export function getContentBottom({ content, spacer, viewport }) {
206
+ const items = getMessageScrollerItems(content, spacer)
207
+ const padding = getBlockPadding(content)
208
+ const viewportRect = viewport.getBoundingClientRect()
209
+ const scrollTop = viewport.scrollTop
210
+ let contentBottom = padding.start + padding.end
211
+
212
+ for (const item of items) {
213
+ const rect = item.getBoundingClientRect()
214
+
215
+ contentBottom = Math.max(
216
+ contentBottom,
217
+ rect.bottom - viewportRect.top + scrollTop + padding.end
218
+ )
219
+ }
220
+
221
+ return contentBottom
222
+ }
223
+
224
+ export function getMaxScrollTop(viewport) {
225
+ return Math.max(0, viewport.scrollHeight - viewport.clientHeight)
226
+ }
227
+
228
+ // `paddingBlock*` first so a vertical writing mode measures the axis that
229
+ // actually scrolls; `paddingTop`/`paddingBottom` is the fallback.
230
+ export function getBlockPadding(element) {
231
+ const style = window.getComputedStyle(element)
232
+
233
+ return {
234
+ end: readCssPixel(style.paddingBlockEnd || style.paddingBottom),
235
+ start: readCssPixel(style.paddingBlockStart || style.paddingTop)
236
+ }
237
+ }
238
+
239
+ // Reached through the spacer rather than by taking the content element as an
240
+ // argument, because the callers that need it are holding the spacer.
241
+ export function getContentBlockPadding(spacer) {
242
+ const content = spacer?.parentElement
243
+
244
+ if (!content) return { end: 0, start: 0 }
245
+
246
+ return getBlockPadding(content)
247
+ }
248
+
249
+ export function getFlexGap(element) {
250
+ if (!element) return 0
251
+
252
+ const style = window.getComputedStyle(element)
253
+ const gap = style.rowGap === "normal" ? style.gap : style.rowGap
254
+
255
+ return readCssPixel(gap)
256
+ }
257
+
258
+ // `parseFloat` on a computed length, with everything unparseable — `normal`,
259
+ // `auto`, an empty string — collapsing to 0 rather than to `NaN`, which would
260
+ // poison every sum it reaches.
261
+ function readCssPixel(value) {
262
+ if (!value) return 0
263
+
264
+ const number = Number.parseFloat(value)
265
+
266
+ return Number.isFinite(number) ? number : 0
267
+ }
@@ -0,0 +1,161 @@
1
+ // The theming store — the port of what `next-themes` does for shadcn's own
2
+ // site, plus the colour-theme switching from its `ActiveThemeProvider`.
3
+ //
4
+ // Two independent axes, exactly as upstream:
5
+ //
6
+ // mode "light" | "dark" | "system" → toggles `.dark` on <html>
7
+ // theme a palette name → swaps `theme-*` on <body>
8
+ //
9
+ // Both are kept in `localStorage` and mirrored into a cookie, so a Rails layout
10
+ // can render the right classes server-side and avoid a flash without relying on
11
+ // the inline script.
12
+
13
+ export const MODE_STORAGE_KEY = "shadcn-ui-mode"
14
+ export const THEME_STORAGE_KEY = "shadcn-ui-theme"
15
+ export const CHANGE_EVENT = "shadcn:theme-change"
16
+
17
+ export const MODES = [ "light", "dark", "system" ]
18
+
19
+ const DARK_QUERY = "(prefers-color-scheme: dark)"
20
+
21
+ function read(key, fallback) {
22
+ try {
23
+ return window.localStorage.getItem(key) || fallback
24
+ } catch {
25
+ // Private browsing, or storage disabled.
26
+ return fallback
27
+ }
28
+ }
29
+
30
+ function write(key, value) {
31
+ try {
32
+ window.localStorage.setItem(key, value)
33
+ } catch {
34
+ // Ignore: the cookie mirror below is enough to survive a reload.
35
+ }
36
+
37
+ document.cookie = `${key}=${encodeURIComponent(value)}; path=/; max-age=31536000; samesite=lax`
38
+ }
39
+
40
+ export function systemMode() {
41
+ return window.matchMedia(DARK_QUERY).matches ? "dark" : "light"
42
+ }
43
+
44
+ export function getMode() {
45
+ const mode = read(MODE_STORAGE_KEY, "system")
46
+ return MODES.includes(mode) ? mode : "system"
47
+ }
48
+
49
+ // The mode actually in effect: `system` resolved against the OS preference.
50
+ export function resolvedMode() {
51
+ const mode = getMode()
52
+ return mode === "system" ? systemMode() : mode
53
+ }
54
+
55
+ export function setMode(mode) {
56
+ if (!MODES.includes(mode)) return
57
+
58
+ write(MODE_STORAGE_KEY, mode)
59
+ applyMode()
60
+ announce()
61
+ }
62
+
63
+ export function toggleMode() {
64
+ setMode(resolvedMode() === "dark" ? "light" : "dark")
65
+ }
66
+
67
+ // The palette in effect: what the visitor chose, or what the *document* says.
68
+ //
69
+ // The fallback used to be the literal "neutral", and that threw away the
70
+ // server's answer. `shadcn_theme_script_tag` writes the theme the page was
71
+ // rendered with onto `<html>` (`data-shadcn-theme`), and a host that sets
72
+ // `shadcn_theme_class(default: "sky")` renders `<body class="theme-sky">` — and
73
+ // then `applyTheme()` ran, found nothing in storage, and swapped the class back
74
+ // to `theme-neutral` on the first paint. The palette a host had chosen was
75
+ // visible only until the JavaScript booted, with nothing to explain it.
76
+ //
77
+ // Reading the document keeps one source of truth: the script tag has already
78
+ // resolved storage against the host's default, so this returns the same value
79
+ // when the visitor has chosen, and the host's when they have not.
80
+ export function getTheme() {
81
+ return read(THEME_STORAGE_KEY, documentTheme())
82
+ }
83
+
84
+ // Two places a host publishes it, and the rendered class comes first.
85
+ //
86
+ // The class is what the **server** decided, reading the cookie this module
87
+ // mirrors every choice into and falling back to the host's own default. The
88
+ // attribute is what the script tag decided a moment later, reading
89
+ // `localStorage` against the same default. They agree whenever both are
90
+ // present — the cookie is the mirror of the storage — and where they differ the
91
+ // class is the better informed of the two: storage cleared with the cookie
92
+ // still there is a visitor whose choice the server can still see and the script
93
+ // cannot.
94
+ function documentTheme() {
95
+ const target = document.body || document.documentElement
96
+ const rendered = Array.from(target.classList).find((name) =>
97
+ name.startsWith("theme-")
98
+ )
99
+ if (rendered) return rendered.slice("theme-".length)
100
+
101
+ return document.documentElement.dataset.shadcnTheme || "neutral"
102
+ }
103
+
104
+ export function setTheme(theme) {
105
+ if (!theme) return
106
+
107
+ write(THEME_STORAGE_KEY, theme)
108
+ applyTheme()
109
+ announce()
110
+ }
111
+
112
+ export function applyMode() {
113
+ const root = document.documentElement
114
+
115
+ root.classList.toggle("dark", resolvedMode() === "dark")
116
+ root.style.colorScheme = resolvedMode()
117
+ }
118
+
119
+ export function applyTheme() {
120
+ const target = document.body
121
+ if (!target) return
122
+
123
+ // Resolved **before** the classes come off. `getTheme()` falls back to what
124
+ // the document already says, and the loop below is what erases the answer:
125
+ // asking afterwards read a body with no `theme-*` left on it, and every boot
126
+ // fell back to the default no matter what the server had rendered.
127
+ const theme = getTheme()
128
+
129
+ for (const name of Array.from(target.classList)) {
130
+ if (name.startsWith("theme-")) target.classList.remove(name)
131
+ }
132
+ target.classList.add(`theme-${theme}`)
133
+ }
134
+
135
+ export function apply() {
136
+ applyMode()
137
+ applyTheme()
138
+ }
139
+
140
+ function announce() {
141
+ document.dispatchEvent(
142
+ new CustomEvent(CHANGE_EVENT, {
143
+ detail: { mode: getMode(), resolvedMode: resolvedMode(), theme: getTheme() }
144
+ })
145
+ )
146
+ }
147
+
148
+ // Follow the OS while the mode is "system", like next-themes does.
149
+ let watching = false
150
+
151
+ export function watchSystem() {
152
+ if (watching) return
153
+ watching = true
154
+
155
+ window.matchMedia(DARK_QUERY).addEventListener("change", () => {
156
+ if (getMode() !== "system") return
157
+
158
+ applyMode()
159
+ announce()
160
+ })
161
+ }
@@ -0,0 +1,59 @@
1
+ // Promotes floating content to the browser's top layer.
2
+ //
3
+ // `position: fixed` escapes overflow clipping but never escapes a *stacking
4
+ // context*: a layer's `z-50` is scoped to the nearest positioned ancestor with a
5
+ // z-index, so a `sticky z-40` header, an `isolate` card or anything with
6
+ // `opacity < 1` can bury it. No z-index can fix that from the inside.
7
+ //
8
+ // The Popover API does. `showPopover()` paints the element above every stacking
9
+ // context while leaving it exactly where it is in the DOM — which is what makes
10
+ // it usable here, since moving it would unbind the Stimulus actions on the close
11
+ // buttons and menu items.
12
+ //
13
+ // `manual` rather than `auto`: `auto` brings its own light-dismiss and its own
14
+ // nesting stack, which would fight the layer stack in `dismiss.js`.
15
+
16
+ export const SUPPORTED =
17
+ typeof HTMLElement !== "undefined" && Object.hasOwn(HTMLElement.prototype, "popover")
18
+
19
+ // Older browsers keep the previous behaviour: still `position: fixed`, still
20
+ // correct in the common case, just vulnerable to a stacking context above it.
21
+ const usable = (element) => SUPPORTED && !!element
22
+
23
+ export function enable(element) {
24
+ if (!usable(element)) return false
25
+
26
+ element.popover = "manual"
27
+ return true
28
+ }
29
+
30
+ export function show(element) {
31
+ if (!usable(element) || !element.popover) return false
32
+
33
+ // Throws if it is already showing, or not connected yet.
34
+ try { element.showPopover() } catch { /* already open */ }
35
+ return true
36
+ }
37
+
38
+ export function hide(element) {
39
+ if (!usable(element) || !element.popover) return false
40
+
41
+ try { element.hidePopover() } catch { /* already closed */ }
42
+ return true
43
+ }
44
+
45
+ // Hiding a popover does not put it back in the page's flow: the UA gives
46
+ // `[popover]` `position: fixed`, and that applies open or closed. `shadcn.css`
47
+ // neutralises the rest of those defaults but deliberately not `position`, which
48
+ // every other caller here wants — they enable it on a wrapper that is fixed
49
+ // anyway.
50
+ //
51
+ // The Sidebar is the one caller that enables it on an element the page is laid
52
+ // out around. Left enabled, the panel stays out of flow after the mobile sheet
53
+ // closes, and the page is drawn over a sidebar that is still visible.
54
+ export function disable(element) {
55
+ if (!usable(element)) return false
56
+
57
+ element.removeAttribute("popover")
58
+ return true
59
+ }
@@ -0,0 +1,99 @@
1
+ // A one-second character buffer shared by any roving-focus list (menu items,
2
+ // select options): each keystroke is appended to the buffer, which clears
3
+ // itself after a second of silence, and the caller gets back the item to
4
+ // move the highlight to next, or nothing when there is nowhere new to move —
5
+ // ported from Radix's `findNextItem` (vendor/radix/ui/select.tsx:1906-1921),
6
+ // whose body is identical to the dropdown menu's own `getNextMatch`
7
+ // (vendor/radix/ui/menu.tsx:1336-1347), so the search starts at the item
8
+ // currently highlighted and wraps, rather than always scanning from the top
9
+ // of the list.
10
+ //
11
+ // The input differs in Radix's *menu*, not in the file above: `getNextMatch`
12
+ // is handed an array of label strings and the winner mapped back to an item
13
+ // (vendor/radix/ui/menu.tsx:451-454), so two items sharing a label are one
14
+ // candidate there and two here. `findNextItem`'s own callers pass items and
15
+ // compare by identity, exactly as this does.
16
+ //
17
+ // **That difference is kept, and it is a decision rather than an oversight.**
18
+ // With `["Copy", "Copy", "Delete"]` and the second Copy highlighted, Radix's
19
+ // menu takes `values.indexOf("Copy")` — which is 0, the *first* one — then the
20
+ // single-character filter drops every value equal to the current match, so both
21
+ // Copies leave the candidates and the highlight does not move: the second
22
+ // duplicate is unreachable by typing. Here the two are separate elements, so
23
+ // pressing `c` walks from one to the other, which is what a typeahead is for
24
+ // and what a person with two identically named commands in a menu expects.
25
+ //
26
+ // Nothing else diverges: Radix's own *select* compares by identity too, and
27
+ // this is a port of that file. `spec/system/dropdown_menu_spec.rb` pins the
28
+ // duplicate-label case so the choice cannot be undone by accident.
29
+ //
30
+ // Deliberately does not know how the caller moves focus — that stays in each
31
+ // controller, which owns its own item list and highlighting. The current
32
+ // item is a parameter rather than something read off the DOM here.
33
+ export class Typeahead {
34
+ constructor() {
35
+ this.buffer = ""
36
+ this.timer = null
37
+ }
38
+
39
+ search(character, items, currentItem) {
40
+ clearTimeout(this.timer)
41
+ // Raw case, not lowercased: Radix compares the raw characters to decide
42
+ // whether they repeat (select.tsx:1911) and only lowercases for the
43
+ // `startsWith` match (select.tsx:1918) — "Bb" is not a repeat of "b".
44
+ this.buffer += character
45
+ this.timer = setTimeout(() => (this.buffer = ""), 1000)
46
+
47
+ return findNextItem(items, this.buffer, currentItem)
48
+ }
49
+
50
+ // Drops the buffer without waiting for its second to elapse. Radix does this
51
+ // at a *different moment* in each of the two components this class serves:
52
+ // the select on open — "reset typeahead when we open",
53
+ // vendor/radix/ui/select.tsx:331-336 — and the menu whenever focus leaves the
54
+ // content, vendor/radix/ui/menu.tsx:585-590. So the moment is each
55
+ // controller's to choose, and neither is hardcoded here.
56
+ reset() {
57
+ clearTimeout(this.timer)
58
+ this.buffer = ""
59
+ }
60
+ }
61
+
62
+ // This is the "meat" of the typeahead matching logic (vendor/radix/ui/select.tsx:1906-1921,
63
+ // vendor/radix/ui/menu.tsx:1336-1347). It takes a list of items, the search
64
+ // and the current item, and returns the next item (or `undefined`).
65
+ //
66
+ // The search is normalized because if a user has repeatedly pressed a
67
+ // character, we want the exact same behavior as if we only had that one
68
+ // character (ie. cycle through items starting with that character).
69
+ //
70
+ // The items are also reordered by wrapping the array around the current
71
+ // item, so we always look forward from the current item and picking the
72
+ // first match is always the correct one.
73
+ //
74
+ // Finally, if the normalized search is exactly one character, the current
75
+ // item is excluded from the candidates, because otherwise it would always be
76
+ // the first to match and focus would never move. This is as opposed to the
77
+ // regular case, where focus should stay put if the current item still
78
+ // matches.
79
+ function findNextItem(items, search, currentItem) {
80
+ const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0])
81
+ const normalizedSearch = isRepeated ? search[0] : search
82
+ const currentItemIndex = currentItem ? items.indexOf(currentItem) : -1
83
+ let wrapped = wrapArray(items, Math.max(currentItemIndex, 0))
84
+
85
+ if (normalizedSearch.length === 1) wrapped = wrapped.filter((item) => item !== currentItem)
86
+
87
+ const nextItem = wrapped.find((item) =>
88
+ item.textContent.trim().toLowerCase().startsWith(normalizedSearch.toLowerCase())
89
+ )
90
+
91
+ return nextItem !== currentItem ? nextItem : undefined
92
+ }
93
+
94
+ // Wraps an array around itself at a given start index
95
+ // (vendor/radix/ui/select.tsx:1923-1929).
96
+ // Example: `wrapArray([ "a", "b", "c", "d" ], 2)` is `[ "c", "d", "a", "b" ]`.
97
+ function wrapArray(array, startIndex) {
98
+ return array.map((_, index) => array[(startIndex + index) % array.length])
99
+ }
@@ -0,0 +1,6 @@
1
+ # Importmap pins contributed by the engine to the host application.
2
+ pin "shadcn", to: "shadcn/index.js"
3
+
4
+ pin_all_from ShadcnViewComponent::Engine.root.join("app/javascript/shadcn"),
5
+ under: "shadcn",
6
+ to: "shadcn"
@@ -0,0 +1,62 @@
1
+ # Every user-visible string the components render. shadcn hardcodes these in
2
+ # English; a Rails app is likely not English-only, so they go through I18n with
3
+ # the upstream text as the default. Override any key in your own locale files.
4
+ en:
5
+ shadcn_view_component:
6
+ breadcrumb:
7
+ label: breadcrumb
8
+ more: More
9
+ calendar:
10
+ previous: Go to the Previous Month
11
+ next: Go to the Next Month
12
+ today_label: "Today, %{date}"
13
+ selected_label: "%{date}, selected"
14
+ week_number_header: Week Number
15
+ week_number: "Week %{number}"
16
+ choose_month: Choose the Month
17
+ choose_year: Choose the Year
18
+ chart:
19
+ label: Chart
20
+ value: Value
21
+ combobox:
22
+ clear: Clear
23
+ toggle: Show options
24
+ remove: Remove
25
+ # The chips field's accessible name, when the caller gave it no
26
+ # placeholder to borrow one from.
27
+ add: Add an item
28
+ carousel:
29
+ previous: Previous slide
30
+ next: Next slide
31
+ dialog:
32
+ close: Close
33
+ pagination:
34
+ label: pagination
35
+ previous: Previous
36
+ next: Next
37
+ go_to_previous: Go to previous page
38
+ go_to_next: Go to next page
39
+ more: More pages
40
+ toaster:
41
+ label: Notifications
42
+ dismiss: Dismiss
43
+ select:
44
+ search_label: Search options
45
+ empty: No results found.
46
+ dialog_label: Select an option
47
+ list_label: Options
48
+ message_scroller:
49
+ scroll_to_end: Scroll to end
50
+ scroll_to_start: Scroll to start
51
+ sidebar:
52
+ toggle: Toggle Sidebar
53
+ title: Sidebar
54
+ description: Displays the mobile sidebar.
55
+ spinner:
56
+ loading: Loading
57
+ theme:
58
+ toggle: Toggle theme
59
+ label: Theme
60
+ light: Light
61
+ dark: Dark
62
+ system: System