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,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module ShadcnViewComponent
6
+ module Generators
7
+ # `bin/rails generate shadcn_view_component:install`
8
+ #
9
+ # Wires the gem into a host app. The part worth automating is every line
10
+ # that names a path into this gem: they have to point inside whatever
11
+ # directory bundler put us in, and that differs between a system gem,
12
+ # `bundle config set path`, a `path:` checkout and a `git:` source.
13
+ #
14
+ # All three lines, not just `@source`. This generator used to write
15
+ # `@import "shadcn.css"`, which reads like an asset-pipeline path and is
16
+ # not one: `tailwindcss-rails` runs the CLI with `-i` and `-o` and no load
17
+ # path, so the CLI resolves a bare name the way Node does — beside the file,
18
+ # then `node_modules` — and a Rails app has neither. It stopped the build
19
+ # with `Can't resolve 'shadcn.css'`. The dummy never showed it because its
20
+ # own entrypoint reaches the engine with `../../../../..`, which is a
21
+ # relationship no host has.
22
+ class InstallGenerator < Rails::Generators::Base
23
+ source_root File.expand_path("templates", __dir__)
24
+
25
+ # Where a Stimulus application is in scope. `controllers/index.js` is the
26
+ # file importmap-rails generates, and it opens with
27
+ # `import { application } from "controllers/application"` — so a line
28
+ # appended there can register onto the app's own instance.
29
+ #
30
+ # `app/javascript/application.js` is not that file. It imports
31
+ # `"controllers"` for the side effect and defines no `application`
32
+ # binding, which is where this generator used to append: a stock Rails 8
33
+ # app got `ReferenceError: application is not defined`, no controllers
34
+ # registered, and every dialog, select and menu inert — with the CSS
35
+ # working, so it looked installed.
36
+ JAVASCRIPT_ENTRYPOINT = "app/javascript/controllers/index.js"
37
+
38
+ TAILWIND_ENTRYPOINTS = [
39
+ "app/assets/tailwind/application.css",
40
+ "app/assets/stylesheets/application.tailwind.css",
41
+ "app/assets/stylesheets/application.css"
42
+ ].freeze
43
+
44
+ def add_tailwind_imports
45
+ entrypoint = TAILWIND_ENTRYPOINTS.find { |path| File.exist?(Rails.root.join(path)) }
46
+
47
+ unless entrypoint
48
+ say_status :skip, "no Tailwind entrypoint found; add the block below yourself", :yellow
49
+ say "(paths are written for #{TAILWIND_ENTRYPOINTS.first}; adjust them if yours sits elsewhere)"
50
+ say tailwind_block(TAILWIND_ENTRYPOINTS.first)
51
+ return
52
+ end
53
+
54
+ if File.read(Rails.root.join(entrypoint)).include?("shadcn.css")
55
+ say_status :identical, entrypoint
56
+ return
57
+ end
58
+
59
+ append_to_file entrypoint, "\n#{tailwind_block(entrypoint)}"
60
+ end
61
+
62
+ def add_javascript
63
+ return say_status :skip, "no importmap; import \"shadcn\" yourself", :yellow unless importmap?
64
+
65
+ unless File.exist?(Rails.root.join(JAVASCRIPT_ENTRYPOINT))
66
+ say_status :skip, "#{JAVASCRIPT_ENTRYPOINT} not found; register the controllers yourself", :yellow
67
+ say javascript_block
68
+ return
69
+ end
70
+
71
+ if File.read(Rails.root.join(JAVASCRIPT_ENTRYPOINT)).include?("registerShadcnControllers")
72
+ return say_status :identical, JAVASCRIPT_ENTRYPOINT
73
+ end
74
+
75
+ append_to_file JAVASCRIPT_ENTRYPOINT, "\n#{javascript_block}"
76
+ end
77
+
78
+ def show_layout_hint
79
+ say "\nAdd the theming tags to your layout:", :green
80
+ say <<~ERB
81
+ <head>
82
+ <%= shadcn_theme_script_tag %>
83
+ </head>
84
+ <body class="<%= shadcn_theme_class %>">
85
+ ERB
86
+ end
87
+
88
+ private
89
+
90
+ def importmap?
91
+ File.exist?(Rails.root.join("config/importmap.rb"))
92
+ end
93
+
94
+ # Resolved from the loaded gem spec, so it is correct wherever bundler put
95
+ # us — including a `path:` or `git:` source.
96
+ def gem_root
97
+ Pathname(ShadcnViewComponent::Engine.root)
98
+ end
99
+
100
+ # Relative to the file the line is written into, whenever this gem sits
101
+ # inside the application — which is what `bundle config set path
102
+ # vendor/bundle` does, and what CI and most containers do. An absolute
103
+ # path is correct only on the machine that generated it, and the CSS is
104
+ # built on every machine.
105
+ #
106
+ # Computed against the entrypoint rather than against a fixed depth: the
107
+ # three conventional locations happen to be three deep today, and a
108
+ # generator that would break on the fourth is a generator waiting.
109
+ def path_to(target, entrypoint)
110
+ return target.to_s unless inside_application?(target)
111
+
112
+ target.relative_path_from(Rails.root.join(entrypoint).dirname).to_s
113
+ end
114
+
115
+ # A relative path is worth having only while it stays inside the
116
+ # application: between two unrelated trees — a system gem, a sibling
117
+ # checkout — it encodes the distance between them, which is as personal
118
+ # to one machine as an absolute path and harder to read.
119
+ def inside_application?(target)
120
+ !target.relative_path_from(Rails.root).to_s.start_with?("..")
121
+ rescue ArgumentError
122
+ # Different volumes: no relative path exists at all.
123
+ false
124
+ end
125
+
126
+ # `application` is the binding `controllers/index.js` already has. Adding
127
+ # an `Application.start()` here instead would start a *second* Stimulus
128
+ # instance beside the app's own, and two instances fight over every
129
+ # element they both claim.
130
+ def javascript_block
131
+ <<~JS
132
+ import { registerShadcnControllers } from "shadcn"
133
+ registerShadcnControllers(application)
134
+ JS
135
+ end
136
+
137
+ def tailwind_block(entrypoint)
138
+ stylesheets = gem_root.join("app/assets/stylesheets")
139
+
140
+ <<~CSS
141
+ /* shadcn_view_component */
142
+ @import "#{path_to(stylesheets.join('shadcn.css'), entrypoint)}";
143
+ @import "#{path_to(stylesheets.join('shadcn-themes.css'), entrypoint)}";
144
+ @source "#{path_to(gem_root.join('app/components'), entrypoint)}";
145
+ CSS
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,73 @@
1
+ module ShadcnViewComponent
2
+ # Rails engine wiring.
3
+ #
4
+ # `app/components` is picked up automatically as an autoload/eager-load root
5
+ # by the `app/{*,*/concerns}` glob every Rails::Engine declares, so
6
+ # `app/components/shadcn/card/component.rb` resolves to `Shadcn::Card::Component`
7
+ # with no extra configuration. Nesting everything under `shadcn/` keeps names
8
+ # such as `Card`, `Table` or `Field` out of the host application's top level,
9
+ # where they would clash with its own models.
10
+ class Engine < ::Rails::Engine
11
+ COMPONENTS_PATH = "app/components".freeze
12
+
13
+ # Host apps configure the gem through `config.shadcn_view_component` in
14
+ # their `application.rb`, e.g.
15
+ #
16
+ # config.shadcn_view_component.cache_size = 50_000
17
+ config.shadcn_view_component = ActiveSupport::OrderedOptions.new
18
+
19
+ # Register component previews (sidecar `preview.rb` next to `component.rb`).
20
+ initializer "shadcn_view_component.previews" do |app|
21
+ options = app.config.view_component
22
+ options.previews.paths << root.join(COMPONENTS_PATH).to_s
23
+
24
+ ActiveSupport.on_load(:view_component) do
25
+ ViewComponent::Preview.extend ViewComponentContrib::Preview::Sidecarable
26
+ end
27
+ end
28
+
29
+ # Build the tailwind-merge instance at boot rather than on the first
30
+ # request: it costs a few milliseconds to construct, and two Puma threads
31
+ # racing on a lazy `||=` would each build one and discard a warm cache.
32
+ initializer "shadcn_view_component.merger" do |app|
33
+ configured = app.config.shadcn_view_component.cache_size
34
+ ShadcnViewComponent.cache_size = configured if configured
35
+
36
+ ShadcnViewComponent.build_merger
37
+
38
+ # Style blocks are redefined when components reload, so the memoised
39
+ # class strings have to go with them.
40
+ app.config.to_prepare { ShadcnViewComponent.class_cache.clear }
41
+ end
42
+
43
+ # Make the theming and form helpers available to the host application's
44
+ # views. The form builder is required here rather than at the top of the gem
45
+ # because it subclasses ActionView's.
46
+ initializer "shadcn_view_component.helpers" do
47
+ ActiveSupport.on_load(:action_view) do
48
+ require "shadcn_view_component/form_builder"
49
+ end
50
+
51
+ ActiveSupport.on_load(:action_controller_base) do
52
+ helper ShadcnViewComponent::ThemeHelper
53
+ helper ShadcnViewComponent::FormHelper
54
+ end
55
+ end
56
+
57
+ # Expose the bundled stylesheet and Stimulus controllers to the asset pipeline.
58
+ initializer "shadcn_view_component.assets" do |app|
59
+ next unless app.config.respond_to?(:assets)
60
+
61
+ app.config.assets.paths << root.join("app/assets/stylesheets")
62
+ app.config.assets.paths << root.join("app/javascript")
63
+ end
64
+
65
+ # Make `shadcn/*` importable when the host app uses importmap-rails.
66
+ initializer "shadcn_view_component.importmap", before: "importmap" do |app|
67
+ next unless app.config.respond_to?(:importmap)
68
+
69
+ app.config.importmap.paths << root.join("config/importmap.rb")
70
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShadcnViewComponent
4
+ # Bridges the components to Rails forms.
5
+ #
6
+ # shadcn's `Field` family is the right shape for this — label, control,
7
+ # description, error — but on its own it knows nothing about a model. This
8
+ # builder wires the two together: ids and names come from Rails, the error
9
+ # text and `aria-invalid` come from `ActiveModel::Errors`, and `Field` gets the
10
+ # `data-invalid` its styles key off.
11
+ #
12
+ # <%= shadcn_form_with model: @user do |f| %>
13
+ # <%= f.shadcn_field :email, label: "Email", description: "We never share it." do %>
14
+ # <%= f.shadcn_input :email, type: "email" %>
15
+ # <% end %>
16
+ # <% end %>
17
+ #
18
+ # Or, when the control needs no arguments of its own:
19
+ #
20
+ # <%= f.shadcn_input_field :email, label: "Email", type: "email" %>
21
+ class FormBuilder < ActionView::Helpers::FormBuilder
22
+ # A labelled control with its description and any validation errors, wired
23
+ # together by id. `Field` styles itself from `data-invalid`, so that is set
24
+ # from the model rather than passed in.
25
+ #
26
+ # @param method [Symbol] the attribute
27
+ # @param label [String] omit to leave the label out entirely
28
+ # @param description [String] hint text below the control
29
+ # @param orientation [Symbol] :vertical, :horizontal or :responsive
30
+ def shadcn_field(method, label: nil, description: nil, orientation: :vertical, **options, &block)
31
+ messages = errors_for(method)
32
+ described[method.to_s] = description.present?
33
+
34
+ @template.render(Shadcn::Field::Component.new(
35
+ orientation:,
36
+ "data-invalid": (true if messages.any?),
37
+ **options
38
+ )) do |field|
39
+ field.with_label(id: label_id(method), for: field_id(method)) { label } if label
40
+ field.with_field_content { @template.capture(&block) }
41
+ field.with_description(id: description_id(method)) { description } if description
42
+ field.with_error(id: error_id(method), errors: messages) if messages.any?
43
+ end
44
+ end
45
+
46
+ # `shadcn_input_field`, `shadcn_textarea_field`, … — the common case where
47
+ # the field wraps exactly one control.
48
+ %i[input textarea native_select switch checkbox].each do |control|
49
+ define_method(:"shadcn_#{control}_field") do |method, label: nil, description: nil, orientation: nil, **options|
50
+ shadcn_field(
51
+ method,
52
+ label:,
53
+ description:,
54
+ orientation: orientation || (%i[switch checkbox].include?(control) ? :horizontal : :vertical)
55
+ ) { public_send(:"shadcn_#{control}", method, **options) }
56
+ end
57
+ end
58
+
59
+ def shadcn_input(method, **options)
60
+ @template.render(Shadcn::Input::Component.new(**control_options(method, **options)))
61
+ end
62
+
63
+ def shadcn_textarea(method, **options)
64
+ @template.render(Shadcn::Textarea::Component.new(**control_options(method, **options))) do
65
+ value_for(method).to_s
66
+ end
67
+ end
68
+
69
+ # A real `<select>`: browser validation, autofill and keyboard behaviour for
70
+ # free. Prefer it over `shadcn_select` unless you need the styled listbox.
71
+ def shadcn_native_select(method, choices = [], **options)
72
+ @template.render(Shadcn::NativeSelect::Component.new(**control_options(method, **options))) do
73
+ @template.safe_join(Array(choices).map { |choice| native_option(method, choice) })
74
+ end
75
+ end
76
+
77
+ # The Radix-style listbox. It submits through a hidden input, so it has no
78
+ # browser validation — `required` will not stop the form.
79
+ def shadcn_select(method, choices = [], placeholder: nil, **options)
80
+ selected = value_for(method).to_s
81
+
82
+ @template.render(Shadcn::Select::Component.new(
83
+ name: field_name(method), value: selected, placeholder:, **options
84
+ )) do |select|
85
+ select.with_trigger(**aria_labelled(method,
86
+ id: field_id(method),
87
+ placeholder: selected.blank?,
88
+ "aria-invalid": invalid(method))) do |trigger|
89
+ trigger.with_value(placeholder:) { label_for(choices, selected) }
90
+ end
91
+ select.with_select_content do
92
+ @template.safe_join(Array(choices).map { |choice|
93
+ label, value = option_pair(choice)
94
+ @template.render(Shadcn::Select::Item::Component.new(value:, selected: value == selected)) { label }
95
+ })
96
+ end
97
+ end
98
+ end
99
+
100
+ # A calendar bound to the model, in any of its three modes.
101
+ #
102
+ # f.shadcn_calendar :starts_on
103
+ # f.shadcn_calendar :dates, mode: :multiple
104
+ # f.shadcn_calendar :starts_on, mode: :range, to: :ends_on
105
+ #
106
+ # `to:` is what makes a range Rails-shaped: two dates are usually two
107
+ # columns, so the two ends are named after two attributes and each comes
108
+ # back as itself. Without it a range submits `name[from]` and `name[to]`
109
+ # under one parameter, which suits a form object rather than a record.
110
+ #
111
+ # The month shown follows the value rather than today, so reopening a form
112
+ # lands on the date already chosen.
113
+ def shadcn_calendar(method, mode: :single, to: nil, **options)
114
+ selected = calendar_selection(method, mode:, to:)
115
+
116
+ @template.render(Shadcn::Calendar::Component.new(
117
+ mode:,
118
+ selected:,
119
+ name: to ? [ field_name(method), field_name(to) ] : field_name(method),
120
+ month: options.delete(:month) || Array(selected).compact.first,
121
+ **aria_labelled(method, options)
122
+ ))
123
+ end
124
+
125
+ def shadcn_checkbox(method, **options)
126
+ @template.render(Shadcn::Checkbox::Component.new(
127
+ name: field_name(method),
128
+ checked: !!value_for(method),
129
+ **aria_labelled(method, **control_options(method, **options))
130
+ ))
131
+ end
132
+
133
+ def shadcn_switch(method, **options)
134
+ @template.render(Shadcn::Switch::Component.new(
135
+ name: field_name(method),
136
+ checked: !!value_for(method),
137
+ **aria_labelled(method, **control_options(method, **options))
138
+ ))
139
+ end
140
+
141
+ # Each option is rendered as block content rather than through the `item`
142
+ # slot: the items have to sit inside their own label rows, and slot content
143
+ # is emitted before block content, which would pull them all out of place.
144
+ def shadcn_radio_group(method, choices = [], **options)
145
+ selected = value_for(method).to_s
146
+
147
+ @template.render(Shadcn::RadioGroup::Component.new(
148
+ name: field_name(method), value: selected, **options
149
+ )) do
150
+ @template.safe_join(Array(choices).map { |choice|
151
+ label, value = option_pair(choice)
152
+ id = "#{field_id(method)}_#{value.parameterize.underscore}"
153
+
154
+ @template.tag.div(class: "flex items-center gap-2") do
155
+ @template.safe_join([
156
+ @template.render(Shadcn::RadioGroup::Item::Component.new(
157
+ value:, checked: value == selected, id:
158
+ )),
159
+ @template.render(Shadcn::Label::Component.new(for: id)) { label }
160
+ ])
161
+ end
162
+ })
163
+ end
164
+ end
165
+
166
+ def shadcn_submit(text = nil, **options, &block)
167
+ @template.render(Shadcn::Button::Component.new(type: "submit", **options)) do
168
+ block ? @template.capture(&block) : (text || submit_default_value)
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ # Which attributes have actually had a description rendered. `aria-describedby`
175
+ # may only name elements that exist — a reference to a missing id is invalid
176
+ # ARIA and simply resolves to nothing, so the control ends up describing
177
+ # itself as having a description it does not have.
178
+ #
179
+ # Upstream has the same defect and it is not reproduced on purpose:
180
+ # `FormControl` sets `aria-describedby` to the description id unconditionally
181
+ # (form.tsx:113-119), whether or not a `FormDescription` was ever rendered.
182
+ # This port shipped it too, until writing features/form.md checked the claim
183
+ # that it did not.
184
+ #
185
+ # A control rendered outside any `shadcn_field` is the same case and gets the
186
+ # same answer: no description element, so nothing to point at.
187
+ def described
188
+ @described ||= {}
189
+ end
190
+
191
+ def errors_for(method)
192
+ return [] unless object.respond_to?(:errors)
193
+
194
+ object.errors[method] || []
195
+ end
196
+
197
+ def invalid(method)
198
+ "true" if errors_for(method).any?
199
+ end
200
+
201
+ # What is already chosen, in the shape the component takes: one date, a
202
+ # list of them, or the two ends of a range.
203
+ def calendar_selection(method, mode:, to:)
204
+ return [ value_for(method), value_for(to) ].compact if to
205
+ return Array(value_for(method)).compact if mode == :multiple
206
+
207
+ value_for(method)
208
+ end
209
+
210
+ def value_for(method)
211
+ object.respond_to?(method) ? object.public_send(method) : nil
212
+ end
213
+
214
+ # Everything a control needs to belong to the form and to announce itself.
215
+ def control_options(method, **options)
216
+ wants_description = options.delete(:described_by) != false
217
+ described_by = [
218
+ (description_id(method) if wants_description && described[method.to_s]),
219
+ (error_id(method) if errors_for(method).any?)
220
+ ].compact
221
+
222
+ {
223
+ id: field_id(method),
224
+ name: field_name(method),
225
+ value: value_for(method),
226
+ "aria-invalid": invalid(method),
227
+ "aria-describedby": described_by.presence&.join(" ")
228
+ }.compact.merge(options)
229
+ end
230
+
231
+ def description_id(method) = "#{field_id(method)}_description"
232
+ def error_id(method) = "#{field_id(method)}_error"
233
+ def label_id(method) = "#{field_id(method)}_label"
234
+
235
+ # Select, Checkbox and Switch render a `<button>` carrying an ARIA role, and
236
+ # a `<label for>` does not name a button — `role="combobox"` goes further and
237
+ # forbids taking the name from its own content. So they are named by pointing
238
+ # at the label instead. Without this they reach the browser unnamed, which
239
+ # `spec/system/accessibility_spec.rb` catches.
240
+ def aria_labelled(method, options)
241
+ return options if options.key?(:"aria-label") || options.key?(:"aria-labelledby")
242
+
243
+ options.merge("aria-labelledby": label_id(method))
244
+ end
245
+
246
+ # Accepts the same shapes as Rails' `options_for_select`: bare values, or
247
+ # `[label, value]` pairs.
248
+ def option_pair(choice)
249
+ choice.is_a?(Array) ? [ choice.first.to_s, choice.last.to_s ] : [ choice.to_s, choice.to_s ]
250
+ end
251
+
252
+ def label_for(choices, value)
253
+ Array(choices).map { |choice| option_pair(choice) }.find { |(_label, v)| v == value }&.first
254
+ end
255
+
256
+ def native_option(method, choice)
257
+ label, value = option_pair(choice)
258
+
259
+ @template.render(Shadcn::NativeSelect::Option::Component.new(
260
+ value:, selected: value == value_for(method).to_s
261
+ )) { label }
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShadcnViewComponent
4
+ # Backs `Shadcn::Icon.register` / `.registered`. Deliberately outside
5
+ # `app/components`, which Rails reloads in development: a hash living on
6
+ # the `Shadcn::Icon` module object would be discarded with it on the next
7
+ # code reload, silently un-registering every icon a host added at boot.
8
+ # `lib/` is not reloaded, so this survives.
9
+ module IconRegistry
10
+ # lucide-react's own aliases, kept so call sites read like the TSX imports
11
+ # — `Icon::Component.new("more-horizontal")` is what `pagination.tsx`
12
+ # writes. They live here rather than beside the drawings because *this* is
13
+ # the file that has to know about them: a host registering
14
+ # `"more-horizontal"` and the gem rendering `"ellipsis"` are the same icon,
15
+ # and until they were resolved in one place the registration was stored
16
+ # under a key nothing ever looked up.
17
+ ALIASES = {
18
+ "loader-2" => "loader-circle",
19
+ "more-horizontal" => "ellipsis"
20
+ }.freeze
21
+
22
+ def self.registered
23
+ @registered ||= {}
24
+ end
25
+
26
+ # Stored under the canonical name, so registering under either spelling
27
+ # reaches every call site that uses either — one icon, one entry.
28
+ def self.register(name, path)
29
+ registered[canonical(name)] = path
30
+ end
31
+
32
+ def self.canonical(name)
33
+ ALIASES.fetch(name.to_s, name.to_s)
34
+ end
35
+
36
+ # Register every `*.svg` in a directory, under its own basename. This is
37
+ # how a host adds icons the gem does not bundle: download the SVGs it wants
38
+ # — lucide publishes them as files, and so does everyone else — drop them
39
+ # in a directory and point here from an initializer.
40
+ #
41
+ # ShadcnViewComponent::IconRegistry.load_directory(
42
+ # Rails.root.join("app/assets/icons")
43
+ # )
44
+ #
45
+ # It reads files, so it is the caller who decides when: an initializer
46
+ # pays once at boot. Nothing in the gem calls it — the bundled icons are
47
+ # generated into `Icons::PATHS` at build time, so a host that adds none
48
+ # reads no files at all.
49
+ def self.load_directory(path)
50
+ Dir.glob(File.join(path, "*.svg")).sort.each do |file|
51
+ register(File.basename(file, ".svg"), drawing(File.read(file)))
52
+ end
53
+ end
54
+
55
+ # What a `<svg>` element contains, with the whitespace its author used to
56
+ # lay it out taken back out. The outer element is deliberately dropped:
57
+ # `Icon::Component` renders that itself, with lucide's own attributes and
58
+ # `lucide lucide-<name>` classes, so a file's own `<svg …>` would arrive as
59
+ # a second opinion about the thing the component already owns.
60
+ def self.drawing(source)
61
+ inside = source[%r{<svg\b[^>]*>(.*)</svg>}m, 1].to_s
62
+
63
+ inside.gsub(/>\s+</, "><").gsub(%r{\s+/>}, "/>").strip
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated from the vendored lucide SVGs (`vendor/lucide/icons`, taken from
4
+ # lucide-static at the version in `vendor/lucide/REVISION`) — do not edit by
5
+ # hand, run `rake icons:build`.
6
+ module ShadcnViewComponent
7
+ module Icons
8
+ PATHS = {
9
+ "arrow-left" => %(<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>),
10
+ "arrow-right" => %(<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>),
11
+ "check" => %(<path d="M20 6 9 17l-5-5"/>),
12
+ "chevron-down" => %(<path d="m6 9 6 6 6-6"/>),
13
+ "chevron-left" => %(<path d="m15 18-6-6 6-6"/>),
14
+ "chevron-right" => %(<path d="m9 18 6-6-6-6"/>),
15
+ "chevron-up" => %(<path d="m18 15-6-6-6 6"/>),
16
+ "circle-check" => %(<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>),
17
+ "circle" => %(<circle cx="12" cy="12" r="10"/>),
18
+ "ellipsis" => %(<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>),
19
+ "grip-vertical" => %(<circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/>),
20
+ "info" => %(<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>),
21
+ "loader-circle" => %(<path d="M21 12a9 9 0 1 1-6.219-8.56"/>),
22
+ "minus" => %(<path d="M5 12h14"/>),
23
+ "moon" => %(<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/>),
24
+ "octagon-x" => %(<path d="m15 9-6 6"/><path d="M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"/><path d="m9 9 6 6"/>),
25
+ "panel-left" => %(<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/>),
26
+ "search" => %(<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>),
27
+ "sun" => %(<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>),
28
+ "triangle-alert" => %(<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>),
29
+ "x" => %(<path d="M18 6 6 18"/><path d="m6 6 12 12"/>)
30
+ }.freeze
31
+ end
32
+ end