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
data/README.md ADDED
@@ -0,0 +1,604 @@
1
+ # shadcn_view_component
2
+
3
+ [shadcn/ui](https://ui.shadcn.com) ported to Rails [ViewComponent](https://viewcomponent.org),
4
+ 1:1 — the same part names, the same variants, the same Tailwind classes and
5
+ `data-slot` attributes. Radix UI's behaviour is reimplemented with Stimulus, so
6
+ there is no React and no npm dependency.
7
+
8
+ Two specs hold the port to upstream. `spec/parity_spec.rb` reads the TSX
9
+ vendored in `vendor/shadcn/ui` and fails if a class React emits is missing from
10
+ the Ruby; `spec/snapshot_spec.rb` diffs the rendered HTML of every preview
11
+ against a committed golden file. See
12
+ [what the specs prove](#what-is-and-is-not-verified) for the honest version.
13
+
14
+ ## Installation
15
+
16
+ ```ruby
17
+ # Gemfile
18
+ gem "shadcn_view_component"
19
+ ```
20
+
21
+ ```sh
22
+ bin/rails generate shadcn_view_component:install
23
+ ```
24
+
25
+ The generator adds the imports to your Tailwind entrypoint and the Stimulus
26
+ registration to `application.js`. Those three CSS lines are the reason it
27
+ exists: each one names a path into the gem, and that path differs between a
28
+ system gem, `bundle config set path`, and a `path:` or `git:` source.
29
+
30
+ **They are filesystem paths, not asset-pipeline names.** `tailwindcss-rails`
31
+ runs the Tailwind CLI with `-i` and `-o` and no load path, so the CLI resolves
32
+ a bare `@import "shadcn.css"` the way Node would — beside the file, then in
33
+ `node_modules` — and a Rails app has neither. It stops the build with
34
+ `Can't resolve 'shadcn.css'`.
35
+
36
+ Doing it by hand, with `PATH` from `bundle show shadcn_view_component`:
37
+
38
+ ```css
39
+ /* app/assets/tailwind/application.css */
40
+ @import "tailwindcss";
41
+ @import "PATH/app/assets/stylesheets/shadcn.css";
42
+ @import "PATH/app/assets/stylesheets/shadcn-themes.css"; /* optional: the swappable palettes */
43
+
44
+ @source "PATH/app/components";
45
+ ```
46
+
47
+ `shadcn-themes.css` has to come after `shadcn.css`: `.theme-*` and `:root` have
48
+ the same specificity, so source order is what decides.
49
+
50
+ **Prefer a relative path where you can.** An absolute one is correct on the
51
+ machine that wrote it and wrong on every other, and the CSS is built on all of
52
+ them. With `bundle config set path vendor/bundle` — what CI and most containers
53
+ do — the gem lives inside the application and the three lines can be written
54
+ relative to the entrypoint, which then holds everywhere. That is what the
55
+ generator writes when it can.
56
+
57
+ ```js
58
+ // app/javascript/controllers/index.js
59
+ import { registerShadcnControllers } from "shadcn"
60
+ registerShadcnControllers(application)
61
+ ```
62
+
63
+ **That file, and not `app/javascript/application.js`.** `controllers/index.js`
64
+ is where importmap-rails puts `import { application } from
65
+ "controllers/application"`, so the registration has an instance to attach to.
66
+ `application.js` imports `"controllers"` for the side effect and defines no
67
+ such binding: register there and the browser says `ReferenceError: application
68
+ is not defined`, nothing is registered, and every dialog, select and menu is
69
+ inert — with the CSS working, so it looks installed. Starting a second
70
+ `Application.start()` instead is worse in a subtler way: two Stimulus instances
71
+ then fight over every element they both claim.
72
+
73
+ If your app has no Stimulus yet, start one and pass it that same way.
74
+
75
+ The engine contributes its own importmap pins, so `shadcn` resolves with no
76
+ further configuration when the host app uses `importmap-rails`.
77
+
78
+ ### Configuration
79
+
80
+ ```ruby
81
+ # config/application.rb
82
+ config.shadcn_view_component.cache_size = 50_000
83
+ ```
84
+
85
+ tailwind-merge keeps an LRU of merged class strings. Its own default is 500,
86
+ which a page built from these components can exhaust — and a Rails render is the
87
+ worst case for an LRU, since it cycles the same keys every request, so once the
88
+ working set exceeds the cache the hit rate collapses and stays collapsed. This
89
+ gem defaults to 10,000; raise it if you generate many distinct computed classes
90
+ (`w-[#{percent}%]` down a long table, say).
91
+
92
+ ## Usage
93
+
94
+ Every React part is one component class, under `Shadcn::`:
95
+
96
+ | React | Ruby |
97
+ |---|---|
98
+ | `<Button variant="outline" size="sm">` | `Shadcn::Button::Component.new(variant: :outline, size: :sm)` |
99
+ | `<CardHeader>` | `Shadcn::Card::Header::Component` |
100
+ | `<SelectItem value="apple">` | `Shadcn::Select::Item::Component.new(value: "apple")` |
101
+
102
+ Parents also expose slots, so the common case stays short:
103
+
104
+ ```erb
105
+ <%= render(Shadcn::Card::Component.new(class: "w-full max-w-sm")) do |card| %>
106
+ <% card.with_header do |header| %>
107
+ <% header.with_title { "Login to your account" } %>
108
+ <% header.with_description { "Enter your email below to login." } %>
109
+ <% end %>
110
+ <% card.with_card_content do %>
111
+ <%= render(Shadcn::Input::Component.new(type: "email", placeholder: "m@example.com")) %>
112
+ <% end %>
113
+ <% card.with_footer do %>
114
+ <%= render(Shadcn::Button::Component.new(class: "w-full")) { "Login" } %>
115
+ <% end %>
116
+ <% end %>
117
+ ```
118
+
119
+ The explicit form is always available, and is what you want when order matters
120
+ or when parts are interleaved with labels and separators:
121
+
122
+ ```erb
123
+ <%= render(Shadcn::Card::Component.new) do %>
124
+ <%= render(Shadcn::Card::Header::Component.new) do %>…<% end %>
125
+ <% end %>
126
+ ```
127
+
128
+ Slot content is rendered *before* block content, so don't mix the two inside one
129
+ parent unless you mean that ordering.
130
+
131
+ User-visible strings go through I18n under `shadcn_view_component.*`, with
132
+ shadcn's English as the default — override any key in your own locale files.
133
+
134
+ Anything you pass beyond the documented arguments lands on the element, exactly
135
+ like React's `{...props}`, and wins over what the component set itself:
136
+
137
+ ```erb
138
+ <%= render(Shadcn::Button::Component.new(type: "submit", disabled: true, data: { turbo: false })) { "Save" } %>
139
+ ```
140
+
141
+ `as:` is the port of shadcn's `asChild`:
142
+
143
+ ```erb
144
+ <%= render(Shadcn::Button::Component.new(as: :a, href: "/login", variant: :link)) { "Log in" } %>
145
+ ```
146
+
147
+ ## Rails forms
148
+
149
+ `shadcn_form_with` is `form_with` with a builder that wires the `Field` family
150
+ to a model. Ids and names come from Rails; the error text, `aria-invalid`,
151
+ `aria-describedby` and `Field`'s `data-invalid` come from `ActiveModel::Errors`.
152
+
153
+ ```erb
154
+ <%= shadcn_form_with model: @user do |f| %>
155
+ <%= f.shadcn_input_field :email, label: "Email",
156
+ description: "We never share it.",
157
+ type: "email" %>
158
+
159
+ <%= f.shadcn_field :plan, label: "Plan" do %>
160
+ <%= f.shadcn_select :plan, [["Free", "free"], ["Pro", "pro"]] %>
161
+ <% end %>
162
+
163
+ <%= f.shadcn_submit "Create account" %>
164
+ <% end %>
165
+ ```
166
+
167
+ `shadcn_field` is the wrapper — label, control, description, errors — and takes
168
+ any control in its block. `shadcn_<control>_field` is the shorthand for the
169
+ common case of exactly one.
170
+
171
+ | Control | Notes |
172
+ |---|---|
173
+ | `shadcn_input` | |
174
+ | `shadcn_textarea` | |
175
+ | `shadcn_native_select` | a real `<select>`: browser validation, autofill, native keyboard behaviour |
176
+ | `shadcn_select` | the styled listbox; submits through a hidden input, so `required` will **not** stop the form |
177
+ | `shadcn_checkbox`, `shadcn_switch` | boolean attributes |
178
+ | `shadcn_radio_group` | each option labelled and wired by id |
179
+ | `shadcn_submit` | |
180
+
181
+ Prefer `shadcn_native_select` unless you specifically want the styled listbox —
182
+ it is the one control here that a browser understands.
183
+
184
+ `form_with(..., builder: ShadcnViewComponent::FormBuilder)` does the same thing
185
+ if you would rather be explicit, and works with `fields_for` too.
186
+
187
+ ### A select you can filter
188
+
189
+ `searchable: true` puts a search field at the top of the open panel and narrows
190
+ the options as you type — case-insensitively, on a substring, so `err` finds
191
+ *Blueberry*. It works on the component and through the form builder:
192
+
193
+ ```erb
194
+ <%= render(Shadcn::Select::Component.new(name: "country", searchable: true)) do |s| %>
195
+ <% s.with_trigger { |t| t.with_value(placeholder: "Select a country") } %>
196
+ <% s.with_select_content do %>
197
+ <%= render(Shadcn::Select::Item::Component.new(value: "it")) { "Italy" } %>
198
+ <% end %>
199
+ <% end %>
200
+
201
+ <%= f.shadcn_select :country, choices, searchable: true %>
202
+ ```
203
+
204
+ Two things to know before reaching for it.
205
+
206
+ **The filter is client-side**, over the options already on the page. For a list
207
+ long enough that you would not render it all, listen for `input` on
208
+ `[data-slot=select-input-wrapper] input` and swap the options through a Turbo
209
+ Frame — the gem takes no position on that and ships no server mode.
210
+
211
+ **This one is not a port.** Every other component here reproduces a shadcn
212
+ component; no Radix-based shadcn select has a filter, so this one takes its
213
+ shape from the React Aria variant and is otherwise the gem's own. What that
214
+ means for you: its look is not guaranteed to match a future upstream, and
215
+ [`.claude/docs/decisions/01-architecture.md`](.claude/docs/decisions/01-architecture.md)
216
+ records where it deliberately differs.
217
+
218
+ ## How the mapping works
219
+
220
+ | shadcn (React) | this gem |
221
+ |---|---|
222
+ | `cva(base, { variants, defaultVariants })` | `ViewComponentContrib::StyleVariants` — the `style { base {} variants {} defaults {} }` DSL |
223
+ | `cn(...)` (clsx + tailwind-merge) | the `tailwind_merge` gem, wired in as the style postprocessor, so caller classes win conflicts |
224
+ | `data-slot="card-header"` | `slot_name :"card-header"` |
225
+ | `{...props}` | `**attributes`, splatted through Rails' tag builder |
226
+ | `asChild` | `as:` |
227
+ | Radix primitives | Stimulus controllers under `shadcn--*`, keeping the same `data-state`, `aria-*` and `--radix-*` custom properties in step with what the user does |
228
+ | `lucide-react` icons | `Shadcn::Icon::Component`, with the lucide SVGs inlined |
229
+
230
+ **The ARIA is the component's, not the controller's.** Whatever a part needs to
231
+ be what it is — its `role`, its `aria-haspopup`, a `tabindex`, the
232
+ `aria-expanded` it starts closed with — is rendered by the Ruby, so the markup
233
+ is right before any JavaScript runs and a `turbo:morph` cannot undo it. The
234
+ controllers only write what changes as the user acts — `aria-expanded` on open,
235
+ `aria-activedescendant` as the cursor moves, `aria-controls` once it knows
236
+ which element to point at. Four of them used to re-set the static half on
237
+ connect, and that was deliberately removed (commit `4e88573`): two places to
238
+ write one attribute is two places to drift.
239
+
240
+ The consequence, if you write your own markup: **`data-controller` and a
241
+ `data-shadcn--*-target` are not enough.** Hang them on a bare `<div>` and you
242
+ get a div — no role, no `aria-haspopup`, nothing for a screen reader to
243
+ announce, and no error to tell you. Render the component, or copy every
244
+ attribute it emits — `bin/console` prints them:
245
+
246
+ ```ruby
247
+ render Shadcn::Select::Trigger::Component.new
248
+ # => <button data-slot="select-trigger" type="button" role="combobox"
249
+ # aria-expanded="false" aria-autocomplete="none" …>
250
+ ```
251
+
252
+ 21 lucide icons are bundled — exactly the ones the ported components render,
253
+ out of lucide's ~1,500, and a spec fails if that stops being exactly true in
254
+ either direction. Their drawings are not typed into Ruby: they are the files lucide publishes,
255
+ vendored in the repository under `vendor/lucide/icons`, and `rake icons:build`
256
+ turns them into the registry the component reads. Only that registry ships in
257
+ the gem — the SVGs are a build-time source, like the upstream TSX.
258
+
259
+ **To add your own**, put SVG files in a directory and point the registry at it
260
+ from an initializer — that is where `cache_size` lives too, and the reason both
261
+ go through `ShadcnViewComponent::IconRegistry` is that nothing autoloadable
262
+ resolves there, `Shadcn::` included:
263
+
264
+ ```ruby
265
+ # config/initializers/shadcn_view_component.rb
266
+ ShadcnViewComponent::IconRegistry.load_directory(Rails.root.join("app/assets/icons"))
267
+ ```
268
+
269
+ The file's basename is the icon's name, and only what the `<svg>` element
270
+ contains is kept — the outer element is the component's, so lucide's own
271
+ attributes and yours never end up arguing. No asset pipeline is involved: the
272
+ files are read once, by you, when you call this. A single drawing can still be
273
+ registered by hand:
274
+
275
+ ```ruby
276
+ # config/initializers/shadcn_view_component.rb
277
+ ShadcnViewComponent::IconRegistry.register("star", %(<path d="M12 2 15 9l7 .5-5 4 1 7-6-3z"/>))
278
+ ```
279
+
280
+ ```erb
281
+ <%= render Shadcn::Icon::Component.new("star", class: "size-4") %>
282
+ ```
283
+
284
+ Registering a name the gem already bundles replaces it — `register("check", …)`
285
+ changes the tick in every checkbox, select and dropdown item — so what ships is
286
+ a set of defaults, not a fixed set.
287
+
288
+ `Shadcn::Icon.register` / `.registered` delegate to the same registry and read
289
+ more naturally from a view or another component — anywhere autoloading has
290
+ already run, which is everywhere except an initializer.
291
+
292
+ An unknown name raises where `Rails.env.local?` is true — development and
293
+ test, where a typo can still be fixed — and renders nothing everywhere else,
294
+ staging included: the gem cannot know every icon a host will ever pass it,
295
+ and a missing one is not worth a 500.
296
+
297
+ Components live in sidecar directories following the
298
+ [Evil Martians layout](https://evilmartians.com/chronicles/viewcomponent-in-the-wild-building-modern-rails-frontends):
299
+ `app/components/shadcn/<family>/[<part>/]{component.rb,preview.rb,previews/*.html.erb}`.
300
+
301
+ Everything is namespaced under `Shadcn::` rather than sitting at the top level,
302
+ because names like `Card`, `Table`, `Field` and `Select` would otherwise collide
303
+ with a host application's own models.
304
+
305
+ ## Theming
306
+
307
+ Theming works exactly as shadcn documents it: semantic CSS variables under
308
+ `:root` and `.dark`, mapped into Tailwind utilities by `@theme inline`. Override
309
+ `--primary` and every `bg-primary` in the app follows. The full token table is
310
+ in [the shadcn theming docs](https://ui.shadcn.com/docs/theming); this gem ships
311
+ the same tokens and the same `--radius` scale.
312
+
313
+ On top of that there are two independent switchable axes, ported from what runs
314
+ on ui.shadcn.com:
315
+
316
+ | Axis | Values | Applied as |
317
+ |---|---|---|
318
+ | mode | `light`, `dark`, `system` | `.dark` on `<html>` |
319
+ | theme | one of 24 palettes | `theme-<name>` on `<body>` |
320
+
321
+ ### Wiring it up
322
+
323
+ ```erb
324
+ <%# app/views/layouts/application.html.erb %>
325
+ <html>
326
+ <head>
327
+ <%= shadcn_theme_script_tag %>
328
+ </head>
329
+ <body class="<%= shadcn_theme_class %>">
330
+ <%= render(Shadcn::ModeToggle::Component.new) %>
331
+ <%= render(Shadcn::ThemeSelector::Component.new(value: shadcn_theme_name)) %>
332
+ ```
333
+
334
+ `shadcn_theme_script_tag` is the piece that matters: it runs before the first
335
+ paint, reads the stored preference and sets `.dark` and `data-shadcn-theme` on
336
+ `<html>` — which is why there is no flash of the wrong palette. It is the
337
+ equivalent of what `next-themes` injects.
338
+
339
+ The preference is stored in `localStorage` (as upstream) and mirrored into a
340
+ cookie, so `shadcn_theme_class` and `shadcn_mode` can render the right thing
341
+ server-side on the very first byte.
342
+
343
+ ### The switchers
344
+
345
+ | Component | Upstream | What it is |
346
+ |---|---|---|
347
+ | `Shadcn::ModeToggle::Component` | `examples/mode-toggle.tsx` | The documented dropdown: Light / Dark / System |
348
+ | `Shadcn::ModeSwitcher::Component` | `components/mode-switcher.tsx` | The single button in shadcn's own header, straight light↔dark |
349
+ | `Shadcn::ThemeSelector::Component` | `components/theme-selector.tsx` | A select over the palettes |
350
+
351
+ Driving it yourself is a matter of one Stimulus action inside a
352
+ `shadcn--theme` controller:
353
+
354
+ ```erb
355
+ <div data-controller="shadcn--theme">
356
+ <button data-action="shadcn--theme#toggle">Flip the mode</button>
357
+ <button data-mode="dark" data-action="click->shadcn--theme#setMode">Dark</button>
358
+ <button data-value="zinc" data-action="click->shadcn--theme#setTheme">Zinc</button>
359
+ </div>
360
+ ```
361
+
362
+ Or from your own JavaScript:
363
+
364
+ ```js
365
+ import { setMode, setTheme, resolvedMode } from "shadcn/theme"
366
+ ```
367
+
368
+ ### The palettes
369
+
370
+ Seven base greys — neutral, stone, zinc, mauve, olive, mist, taupe — and
371
+ seventeen accents: amber, blue, cyan, emerald, fuchsia, green, indigo, lime,
372
+ orange, pink, purple, red, rose, sky, teal, violet, yellow.
373
+
374
+ The base greys carry a complete token set. The accents are overlays: they
375
+ redefine `--primary` and the chart colours and let the rest fall through to
376
+ `:root`, which is why dropping one onto `<body>` recolours an app without
377
+ restating the neutrals.
378
+
379
+ Palettes also scope, so you can theme one region of a page:
380
+
381
+ ```erb
382
+ <div class="theme-blue">…</div>
383
+ ```
384
+
385
+ `ShadcnViewComponent::Themes` is the registry behind all of this
386
+ (`ALL`, `BASE_COLORS`, `ACCENTS`, `find`). Both the registry and
387
+ `shadcn-themes.css` are generated from `vendor/shadcn/themes.json` — run
388
+ `rake themes:build` after refreshing it.
389
+
390
+ ### A few rules resist an ordinary `!important`
391
+
392
+ `[data-slot][hidden]`, `[data-slot][data-exiting]` and the two
393
+ `animate-accordion-*` reduced-motion overrides in `shadcn.css` are
394
+ `!important` inside a cascade layer, which beats an `!important` of your own
395
+ at any specificity. An inline `style` attribute gets past it, and so does a
396
+ `@layer` declared earlier than this stylesheet in your document.
397
+
398
+ ## Components
399
+
400
+ **Theming** — mode-toggle, mode-switcher, theme-selector
401
+
402
+ **Presentational** — button, badge, card, alert, avatar, separator, skeleton,
403
+ spinner, kbd, aspect-ratio, progress, table, breadcrumb, pagination, label,
404
+ input, textarea, native-select, field, empty, item, button-group
405
+
406
+ **Form controls** — checkbox, radio-group, switch, toggle, toggle-group,
407
+ input-group, input-otp
408
+
409
+ **Interactive** — accordion, collapsible, tabs, dialog, alert-dialog, sheet,
410
+ dropdown-menu (including submenus), context-menu, menubar, popover, tooltip,
411
+ hover-card, select,
412
+ scroll-area, navigation-menu, slider, sidebar, drawer, carousel
413
+
414
+ **AI chat** — message, bubble, attachment, marker, message-scroller. The first
415
+ four are markup and variants with no behaviour of their own; the scroller
416
+ follows its own live end, holds the view still when older history loads above,
417
+ and is the one component here reimplemented from a package shadcn publishes
418
+ rather than from Radix.
419
+
420
+ These are one of the two places hand-written CSS was unavoidable: `shimmer`,
421
+ `scroll-fade-x`, `scroll-fade-b`, `scrollbar-none`, `scrollbar-thin` and
422
+ `scrollbar-gutter-stable` are shadcn's own utilities rather than Tailwind's, and
423
+ are reproduced at the end of `shadcn.css`. The drawer is the other.
424
+
425
+ **Drawer** — dragged down or thrown, it closes; dragged up it gives a little and
426
+ comes back. Upstream builds this on vaul rather than Radix, and vaul is a Radix
427
+ dialog with a drag on top, so the open/close half is the dialog's here too and
428
+ only the gesture is new. Four of vaul's features are deliberately not ported —
429
+ snap points, scaling the page behind the drawer, its iOS `position: fixed`
430
+ workaround and nested drawers. Its stylesheet is the second piece of
431
+ hand-written CSS: `drawer.tsx` renders no entrance animation and no
432
+ `touch-action`, and both are load-bearing.
433
+
434
+ **Reading direction** — write `dir="rtl"` on any ancestor and the components
435
+ that navigate with arrow keys follow it. shadcn ships a `DirectionProvider` for
436
+ this; there is none here, because the browser already resolves `dir` before a
437
+ Stimulus controller runs.
438
+
439
+ **Forms** — `shadcn_form_with` and `f.shadcn_input_field`, `f.shadcn_select`,
440
+ `f.shadcn_switch` and the rest. shadcn's Form component is react-hook-form's
441
+ per-field state given five wrappers; there is no such state on a server, and
442
+ what the wrappers do with it is what Rails' `FormBuilder` already does with a
443
+ model. So that family is a FormBuilder here rather than components, built over
444
+ `field`. It emits `field-*` slots where upstream's form emits `form-*`, errors
445
+ come from `ActiveModel::Errors` after a round trip rather than as you type, and
446
+ all of an attribute's messages are shown rather than the first.
447
+
448
+ **Toaster** — notifications, stacked one behind another and fanned out under
449
+ the pointer. shadcn's is a forty-line wrapper around `sonner` and renders no
450
+ markup of its own, so this one is not a port: it keeps sonner's measurements and
451
+ its stacking, and adds the two ways a Rails app actually raises a notification —
452
+ a flash rendered with the page, and a `turbo_stream.append` onto the list.
453
+
454
+ **Calendar** — a month is a `<table role="grid">`, built in Ruby with `Date` and
455
+ `I18n` rather than by `react-day-picker`: a third of that package is locale data
456
+ and another fifth is other calendar systems, both of which a Rails app already
457
+ has. It is the one component here that renders correctly with no JavaScript at
458
+ all; the controller adds the nav, the keyboard and the selection, and takes its
459
+ month names and its idea of *today* from the server rather than from the
460
+ browser's locale and clock.
461
+
462
+ **Chart** — `chart.tsx` draws nothing: it is a container that publishes
463
+ `--color-<key>` per series, plus the contents of a tooltip and a legend that
464
+ `recharts` fills in. That frame is ported 1:1 and the shapes are drawn here, as
465
+ SVG, from the server. The pie is the first — pass a Hash of key to number, which
466
+ is what `group(:x).sum(:y)` already returns. Bars, lines and areas are not drawn
467
+ yet.
468
+
469
+ **Resizable** — panels a pointer or the arrow keys can resize. Upstream wraps
470
+ `react-resizable-panels`; here a panel is a share of a flex container, so the
471
+ layout is the browser's and the controller only moves two numbers. The handle is
472
+ a `role="separator"` with the package's own keyboard: five points an arrow, all
473
+ the way on Home and End.
474
+
475
+ **Command** — the palette. Upstream wraps `cmdk`; here the items are
476
+ server-rendered and the controller filters, *ranks* and walks them. The ranking
477
+ matters enough that `cmdk`'s own fuzzy scorer is ported rather than replaced
478
+ with a substring match: type `gp` and Group Policy comes before Groups.
479
+ `keywords:` are searched and never shown.
480
+
481
+ **Combobox** — a field that filters a list and keeps the caret. It is the one
482
+ component shadcn writes against Base UI rather than Radix, so this family alone
483
+ emits `data-open` and reads `--anchor-width`; the port publishes those names
484
+ here and nowhere else. Single selection is complete; the chips markup is there
485
+ and adding one is not wired yet.
486
+
487
+ Every component in the registry is now ported, adapted or decided against with
488
+ a reason — see [the per-component notes](.claude/docs/features/README.md).
489
+
490
+ ## What is and is not verified
491
+
492
+ Worth being precise about, because "1:1" invites more trust than any test here
493
+ earns.
494
+
495
+ | Spec | Catches |
496
+ |---|---|
497
+ | `parity_spec.rb` | a class upstream emits that the port dropped or mistyped. Per *family*, not per part — swap two variants' bodies and it stays green |
498
+ | `snapshot_spec.rb` | anything that changes rendered HTML: wrong part, wrong variant, attribute drift, extra classes |
499
+ | `stimulus_contract_spec.rb` | a controller action, target or value a component names but the JavaScript does not define |
500
+ | `system/` | the behaviour, in a real browser: open/close, keyboard navigation, focus trapping, positioning, persistence, Turbo Drive and morph refreshes |
501
+ | `system/accessibility_spec.rb` | axe over every preview, **in both palettes**, at rest and with each layer open |
502
+ | `form_builder_spec.rb`, `theming_spec.rb` | the Rails form wiring, the generated palettes and the switchers |
503
+
504
+ The system specs drive headless Chrome against the gallery, so they exercise the
505
+ compiled Tailwind and the actual Stimulus controllers — `popper.js`, the layer
506
+ stack in `dismiss.js` and the focus trap in `focus.js` included.
507
+
508
+ Two things remain unverified:
509
+
510
+ - **Parity runs one way.** When upstream *removes* a class, the port keeps it
511
+ and nothing fails. Read the TSX diff when you re-sync.
512
+ - **Accessibility is audited by axe, not by a person.** Every preview is checked
513
+ against WCAG 2.1 AA, at rest and with its layer open, in the light palette and
514
+ the dark one. axe catches names, roles, required parents and contrast; it does
515
+ not replace a screen reader, and nothing here has been through one.
516
+
517
+ The colour scheme the audit runs in is pinned, and that is not housekeeping:
518
+ headless Chrome follows the desktop it runs on, so the suite spent this
519
+ project's life auditing whichever palette the author was sitting in front of.
520
+ The first run on a Linux CI runner audited the other one and found eleven real
521
+ contrast violations.
522
+
523
+ ### One thing to know if you have to meet AA
524
+
525
+ **shadcn's light palette puts `text-muted-foreground` on `bg-muted` at 4.34:1**,
526
+ where WCAG AA wants 4.5:1 — its own tokens, `muted: oklch(0.97 0 0)` against
527
+ `muted-foreground: oklch(0.556 0 0)`, and its own class string on the avatar
528
+ fallback (`avatar.tsx:49`). The same pair measures 5.85:1 in the dark palette.
529
+ `text-destructive` on `bg-destructive/10`, which the attachment's error state
530
+ and the destructive bubble use, comes to 4.00:1.
531
+
532
+ This port renders them unchanged, because upstream wins on markup and altering
533
+ them would put classes in your bundle that upstream does not emit. If you need
534
+ AA, override the token in your own CSS — one line, and it reaches every
535
+ component at once:
536
+
537
+ ```css
538
+ :root { --muted-foreground: oklch(0.52 0 0); } /* measured: 5.04:1 on --muted */
539
+ ```
540
+
541
+ That is the smallest step off upstream's `0.556` that clears AA — measured in
542
+ Chrome by painting both colours and computing the WCAG ratio, the same
543
+ arithmetic axe does.
544
+
545
+ ## Known differences from the React DOM
546
+
547
+ Four deliberate ones, all documented at the point where they happen:
548
+
549
+ 1. **Context-only roots render an element.** Radix's `Dialog.Root`,
550
+ `Popover.Root`, `Select.Root` and friends render no DOM at all. Stimulus
551
+ needs something to attach to, so those roots emit a `display: contents`
552
+ wrapper. It has no box and no effect on layout, and it gives shadcn's
553
+ `data-slot="dialog"` — which Radix silently drops — somewhere to live.
554
+
555
+ 2. **Nothing is portalled to `document.body`.** Radix moves overlays and
556
+ floating content onto the body. Here they stay inside the component, wrapped
557
+ in the same `data-slot="*-portal"` / `data-radix-popper-content-wrapper`
558
+ elements, because moving them out of the controller's element would unbind
559
+ the Stimulus actions on the close buttons and menu items.
560
+
561
+ Staying put means a stacking context above the component could bury it —
562
+ `position: fixed` escapes overflow clipping but never a stacking context, and
563
+ a `sticky z-40` header or an `isolate` card is ordinary markup. The Popover
564
+ API solves exactly that: opening calls `showPopover()`, which paints the
565
+ layer above every stacking context *without* moving it in the DOM. Browsers
566
+ without the API fall back to plain `position: fixed`, which is correct except
567
+ under such an ancestor. `spec/system/stacking_context_spec.rb` holds this.
568
+
569
+ One case does remain: an ancestor with `transform`, `filter` or `contain`
570
+ becomes the containing block for fixed descendants, which affects where the
571
+ layer is positioned rather than what paints over it.
572
+
573
+ 3. **A morph refresh resets a component to the server's state.** Turbo's morph
574
+ rewrites attributes in place without disconnecting the Stimulus controllers,
575
+ so `connect()` never runs again. The controllers re-sync on `turbo:morph`, so
576
+ the DOM and the controller always agree — but what a component was showing is
577
+ whatever the server just rendered. If a component's open state should survive
578
+ a refresh, mark it `data-turbo-permanent`; that is your call, not the
579
+ library's.
580
+
581
+ 4. **Indicators are hidden, not unmounted, before JavaScript runs.** Radix
582
+ mounts a checkbox tick or a select checkmark only while checked. The server
583
+ renders them with `hidden` so the markup is right without JavaScript, and the
584
+ controller detaches them on connect to match Radix exactly.
585
+
586
+ ## Development
587
+
588
+ ```sh
589
+ bin/setup # bundle install + build Tailwind
590
+ bundle exec rake # the whole suite
591
+ bin/rubocop # Ruby style
592
+ bin/eslint # the controllers; Node is a dev dependency only
593
+ bin/console # IRB with the dummy app, to render components
594
+ cd test/dummy && bin/rails s
595
+ ```
596
+
597
+ Then open <http://localhost:3000/lookbook> for the component gallery.
598
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for adding a component and re-syncing
599
+ with upstream.
600
+
601
+ ## License
602
+
603
+ MIT. The vendored shadcn/ui sources under `vendor/shadcn` are MIT too; see
604
+ `vendor/shadcn/LICENSE.md`.