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,42 @@
1
+ <%# Upstream's "Sizes" and "Spacing".
2
+
3
+ Neither is an argument. A slide is `basis-full` until the caller says
4
+ `basis-1/2` or `basis-1/3`, and the gutter is the track's negative margin
5
+ against the item's padding — `-ml-4`/`pl-4` by default, and any matching
6
+ pair the caller likes. Both are upstream's own instructions rather than this
7
+ port's: `carousel.tsx` has no prop for either. %>
8
+ <div class="flex flex-col gap-10 px-14">
9
+ <%= render(Shadcn::Carousel::Component.new(class: "w-full max-w-sm")) do |c| %>
10
+ <% c.with_carousel_content do |content| %>
11
+ <% 6.times do |i| %>
12
+ <% content.with_item(class: "basis-1/2") do %>
13
+ <div class="p-1">
14
+ <%= render(Shadcn::Card::Component.new) do |card| %>
15
+ <% card.with_card_content(class: "flex aspect-square items-center justify-center p-6") do %>
16
+ <span class="text-3xl font-semibold"><%= i + 1 %></span>
17
+ <% end %>
18
+ <% end %>
19
+ </div>
20
+ <% end %>
21
+ <% end %>
22
+ <% end %>
23
+ <% c.with_previous %>
24
+ <% c.with_next_control %>
25
+ <% end %>
26
+
27
+ <%= render(Shadcn::Carousel::Component.new(class: "w-full max-w-sm")) do |c| %>
28
+ <% c.with_carousel_content(class: "-ml-2") do |content| %>
29
+ <% 6.times do |i| %>
30
+ <% content.with_item(class: "basis-1/3 pl-2") do %>
31
+ <%= render(Shadcn::Card::Component.new) do |card| %>
32
+ <% card.with_card_content(class: "flex aspect-square items-center justify-center p-6") do %>
33
+ <span class="text-2xl font-semibold"><%= i + 1 %></span>
34
+ <% end %>
35
+ <% end %>
36
+ <% end %>
37
+ <% end %>
38
+ <% end %>
39
+ <% c.with_previous %>
40
+ <% c.with_next_control %>
41
+ <% end %>
42
+ </div>
@@ -0,0 +1,27 @@
1
+ <%# Upstream's "Orientation". The axis reaches three places — the track becomes
2
+ a column, the items pad from the top, and the controls move above and below
3
+ and turn — so it is the root's argument and each part reads it.
4
+
5
+ `basis-1/2` is upstream's own example and it is the caller's, not the
6
+ component's: two slides in the window rather than one. Without it a card
7
+ sized by its own text floats in the middle of a 240px slot, which is what
8
+ this preview shipped as and what a person reported. %>
9
+ <div class="py-14">
10
+ <%= render(Shadcn::Carousel::Component.new(orientation: :vertical, class: "w-full max-w-xs")) do |c| %>
11
+ <% c.with_carousel_content(orientation: :vertical, class: "h-[340px]") do |content| %>
12
+ <% 5.times do |i| %>
13
+ <% content.with_item(orientation: :vertical, class: "basis-1/2") do %>
14
+ <div class="p-1">
15
+ <%= render(Shadcn::Card::Component.new) do |card| %>
16
+ <% card.with_card_content(class: "flex items-center justify-center p-6") do %>
17
+ <span class="text-3xl font-semibold"><%= i + 1 %></span>
18
+ <% end %>
19
+ <% end %>
20
+ </div>
21
+ <% end %>
22
+ <% end %>
23
+ <% end %>
24
+ <% c.with_previous(orientation: :vertical) %>
25
+ <% c.with_next_control(orientation: :vertical) %>
26
+ <% end %>
27
+ </div>
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Carousel
5
+ module Previous
6
+ # CarouselPrevious — the button's own variants, as `pagination-link` takes
7
+ # them: upstream renders a `<Button variant="outline" size="icon">` and
8
+ # adds its placement on top, so the classes come from
9
+ # `Button.variant_classes` rather than from a style block of this
10
+ # component's own. Redefining `style` here would *replace* the button's
11
+ # base rather than add to it, which is a round control with no round.
12
+ class Component < ApplicationViewComponent
13
+ default_tag :button
14
+ slot_name :"carousel-previous"
15
+
16
+ PLACEMENT = {
17
+ horizontal: "top-1/2 -left-12 -translate-y-1/2",
18
+ vertical: "-top-12 left-1/2 -translate-x-1/2 rotate-90"
19
+ }.freeze
20
+
21
+ attr_reader :orientation, :variant, :size
22
+
23
+ def initialize(orientation: :horizontal, variant: :outline, size: :icon, **attributes)
24
+ @orientation = orientation&.to_sym == :vertical ? :vertical : :horizontal
25
+ @variant = variant&.to_sym || :outline
26
+ @size = size&.to_sym || :icon
27
+ super(**attributes)
28
+ end
29
+
30
+ # Disabled until the controller says otherwise: at rest a carousel is at
31
+ # one end, and which end has room is a question about a viewport the
32
+ # server has never measured.
33
+ def element_attributes(**defaults)
34
+ super(**{
35
+ type: "button",
36
+ disabled: true,
37
+ "data-shadcn--carousel-target" => "previous",
38
+ "data-action" => "click->shadcn--carousel#previous"
39
+ }.merge(defaults))
40
+ end
41
+
42
+ def css_classes(extra = nil)
43
+ Button::Component.variant_classes(
44
+ variant:,
45
+ size:,
46
+ class: [ "absolute size-8 rounded-full", PLACEMENT.fetch(orientation), extra ].compact.join(" ")
47
+ )
48
+ end
49
+
50
+ def call
51
+ render_element(body: safe_join([
52
+ render(Icon::Component.new("arrow-left")),
53
+ tag.span(shadcn_t("carousel.previous"), class: "sr-only")
54
+ ]))
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ module Area
6
+ # Upstream's "Area Chart": the line chart with the space beneath each
7
+ # series filled in.
8
+ #
9
+ # chart.with_area(data: { "January" => { desktop: 186, mobile: 80 } })
10
+ class Component < Line::Component
11
+ # Flat, and see-through enough that two overlapping series both stay
12
+ # readable. Upstream's docs example fills from a `<linearGradient>`
13
+ # instead: that needs a `<defs>` with an id per series, and an id that
14
+ # has to stay unique in a page this gem cannot see is a cost the fill
15
+ # does not repay.
16
+ OPACITY = 0.4
17
+
18
+ slot_name :"chart-area"
19
+
20
+ private
21
+
22
+ # The fills go down before every line and dot, or a series drawn later
23
+ # would wash out the one before it.
24
+ def shapes = plot.series.map { |key| fill(key) } + super
25
+
26
+ def fill(key)
27
+ tag.polygon(points: closed(key), fill: colour_of(key), "fill-opacity": OPACITY)
28
+ end
29
+
30
+ # A line, then back along the baseline to where it started — the two
31
+ # corners that turn a stroke into a shape.
32
+ def closed(key)
33
+ points = plot.points(key)
34
+ return "" if points.empty?
35
+
36
+ [ "#{points.first[:x]},#{plot.baseline.round(2)}",
37
+ *points.map { |point| "#{point[:x]},#{point[:y]}" },
38
+ "#{points.last[:x]},#{plot.baseline.round(2)}" ].join(" ")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ module Bar
6
+ # Upstream's "Bar Chart" and "Bar Chart - Stacked", drawn on the server.
7
+ #
8
+ # chart.with_bar(data: { "January" => { desktop: 186, mobile: 80 } })
9
+ #
10
+ # `data:` is a Hash of category to a Hash of series to number — one
11
+ # `each_with_object` away from `group(:month, :platform).sum(:visits)`.
12
+ class Component < Cartesian::Component
13
+ # `radius={4}`, which upstream's bar examples pass at the call site in
14
+ # the docs. Nothing of recharts is vendored here, so unlike a class
15
+ # string this is checked against the rendered example and not against
16
+ # `vendor/`.
17
+ RADIUS = 4
18
+
19
+ slot_name :"chart-bar"
20
+
21
+ attr_reader :stacked
22
+
23
+ # `stacked:` lives here and not on the frame because it is a bar's
24
+ # question. A stacked *line* would need cumulative points, and `Plot`
25
+ # does not compute them — so the option is absent where it would lie.
26
+ def initialize(stacked: false, **attributes)
27
+ @stacked = stacked
28
+ super(**attributes)
29
+ end
30
+
31
+ private
32
+
33
+ def plot = @plot ||= Plot.new(data: @data, series: @series, stacked:)
34
+
35
+ # A `<rect>` rounds all four corners or none, because `rx` takes one
36
+ # number. A stack needs its seams square and only its two ends round,
37
+ # which is the per-corner form of the same prop. So a bar is a path,
38
+ # like the pie's slices.
39
+ def shapes
40
+ plot.bars.group_by { |bar| bar[:category] }.values.flat_map do |bars|
41
+ bars.map.with_index do |bar, position|
42
+ tag.path(d: outline(bar, **corners(position, bars.size)), fill: colour_of(bar[:key]),
43
+ **mark_attributes(**bar.slice(:category, :key, :value)))
44
+ end
45
+ end
46
+ end
47
+
48
+ # Grouped, every bar stands on its own and rounds both ends. Stacked,
49
+ # only the bottom of the first segment and the top of the last are on
50
+ # the outside of anything.
51
+ def corners(position, count)
52
+ return { top: true, bottom: true } unless stacked
53
+
54
+ { top: position == count - 1, bottom: position.zero? }
55
+ end
56
+
57
+ # One formula for all four cases: a zero-radius arc is drawn as a
58
+ # straight line, which is the SVG spec's own answer to `A 0 0`.
59
+ def outline(bar, top:, bottom:)
60
+ radius = [ RADIUS, bar[:width] / 2.0, bar[:height] / 2.0 ].min.round(2)
61
+ t = top ? radius : 0
62
+ b = bottom ? radius : 0
63
+ x, y = bar.values_at(:x, :y)
64
+ right = (x + bar[:width]).round(2)
65
+ base = (y + bar[:height]).round(2)
66
+
67
+ "M #{x} #{(y + t).round(2)} A #{t} #{t} 0 0 1 #{(x + t).round(2)} #{y} " \
68
+ "L #{(right - t).round(2)} #{y} A #{t} #{t} 0 0 1 #{right} #{(y + t).round(2)} " \
69
+ "L #{right} #{(base - b).round(2)} A #{b} #{b} 0 0 1 #{(right - b).round(2)} #{base} " \
70
+ "L #{(x + b).round(2)} #{base} A #{b} #{b} 0 0 1 #{x} #{(base - b).round(2)} Z"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ module Cartesian
6
+ # What a bar, a line and an area all need and the pie did not: a grid, an
7
+ # axis down the left, category labels along the bottom, and one accessible
8
+ # name carrying every number. Abstract — `shapes` is the subclass's half.
9
+ #
10
+ # **Ours**, like the pie: `chart.tsx` draws nothing, so there is no
11
+ # upstream markup for this to be 1:1 with. What *is* reproduced is the
12
+ # contract around it — every mark is filled from `--color-<key>`, which
13
+ # the container publishes from `config`, so a host restyles this chart
14
+ # from the same stylesheet that restyles upstream's.
15
+ #
16
+ # The axis is drawn in SVG attributes and CSS custom properties rather
17
+ # than Tailwind classes. `stroke="var(--border)"` is a token `shadcn.css`
18
+ # already defines, and it keeps the grid out of `reverse_parity_spec`'s
19
+ # way: a class here would be one no vendored source contains, and every
20
+ # such class has to be listed with its reason.
21
+ class Component < ApplicationViewComponent
22
+ include Focusable
23
+
24
+ default_tag :svg
25
+ # No `slot_name` of its own: this class is never rendered, and a
26
+ # `data-slot="chart-cartesian"` would name an element no page contains.
27
+ # Each shape declares its own.
28
+
29
+ style do
30
+ base { "w-full" }
31
+ end
32
+
33
+ attr_reader :config, :label
34
+
35
+ def initialize(data: {}, config: {}, series: nil, label: nil, **attributes)
36
+ @data = data
37
+ @series = series
38
+ @config = config
39
+ @label = label
40
+ super(**attributes)
41
+ end
42
+
43
+ def element_attributes(**defaults)
44
+ super(**{ viewBox: "0 0 #{plot.width} #{plot.height}" }.merge(keyboard).merge(defaults))
45
+ end
46
+
47
+ # The grid goes down first so the shapes are drawn over it, which is the
48
+ # one thing SVG's painter order decides for us. The table follows the
49
+ # graphic it describes, which is the order a reader meets them in.
50
+ def call
51
+ drawing = tag.g(safe_join([ grid, tick_labels, category_labels, *shapes ]), "aria-hidden": "true")
52
+
53
+ safe_join([ render_element(body: drawing), table ])
54
+ end
55
+
56
+ private
57
+
58
+ def drawn? = plot.categories.any?
59
+
60
+ # No rows, no table: an empty scope on a quiet week is a thing a host's
61
+ # data does, and a table of nothing announces a name and then leaves a
62
+ # reader in an empty grid.
63
+ def table
64
+ return unless drawn?
65
+
66
+ render(Table::Component.new(caption: label, columns: plot.series.map { |key| label_for(key) },
67
+ rows: table_rows))
68
+ end
69
+
70
+ def table_rows
71
+ plot.data.map do |category, values|
72
+ [ category, *plot.series.map { |key| number(values[key]) } ]
73
+ end
74
+ end
75
+
76
+ def plot = @plot ||= Plot.new(data: @data, series: @series)
77
+
78
+ def shapes = raise NotImplementedError, "#{self.class} draws no shape"
79
+
80
+ def grid
81
+ safe_join(plot.ticks.map do |value|
82
+ y = plot.y_of(value).round(2)
83
+
84
+ tag.line(x1: Plot::PADDING[:left], x2: plot.width - Plot::PADDING[:right], y1: y, y2: y,
85
+ stroke: "var(--border)", "stroke-dasharray": ("3 3" unless value.zero?))
86
+ end)
87
+ end
88
+
89
+ def tick_labels
90
+ safe_join(plot.ticks.map do |value|
91
+ tag.text(number(value), x: Plot::PADDING[:left] - 6, y: (plot.y_of(value) + 3).round(2),
92
+ "text-anchor": "end", **label_attributes)
93
+ end)
94
+ end
95
+
96
+ # Only every `label_every`th, and the first is always drawn: a chart
97
+ # whose axis starts at "Week 5" reads as though the data does.
98
+ def category_labels
99
+ every = plot.label_every
100
+
101
+ safe_join(plot.categories.each_with_index.filter_map do |category, index|
102
+ next unless (index % every).zero?
103
+
104
+ x = category_x(index)
105
+
106
+ tag.text(category, x: x.round(2), y: plot.height - 8,
107
+ "text-anchor": anchor_at(x), **label_attributes)
108
+ end)
109
+ end
110
+
111
+ # Under the middle of the band, which is where a bar stands. A line
112
+ # chart puts its readings on a point scale instead and says so.
113
+ def category_x(index) = plot.x_of(index) + plot.band / 2
114
+
115
+ # A label centred on a reading that sits on the edge of the plot has
116
+ # half of itself outside the viewBox, and an SVG clips at its own box:
117
+ # the line chart's "June" was drawn as "Jun". Only a point scale reaches
118
+ # the edge, so this changes nothing for a bar.
119
+ def anchor_at(x)
120
+ return "start" if x <= Plot::PADDING[:left]
121
+ return "end" if x >= plot.width - Plot::PADDING[:right]
122
+
123
+ "middle"
124
+ end
125
+
126
+ def label_attributes = { fill: "var(--muted-foreground)", "font-size": 10 }
127
+
128
+ # What every mark hands the controller, so the tooltip is filled from
129
+ # the DOM rather than from the series sent a second time as JSON.
130
+ def mark_attributes(category:, key:, value:)
131
+ {
132
+ "data-shadcn--chart-target": "mark",
133
+ "data-action": "pointerenter->shadcn--chart#show pointermove->shadcn--chart#move " \
134
+ "pointerleave->shadcn--chart#hide",
135
+ "data-key": key,
136
+ "data-label": category,
137
+ "data-name": label_for(key),
138
+ "data-display": number(value)
139
+ }
140
+ end
141
+
142
+ def colour_of(key) = "var(--color-#{key.to_s.parameterize.underscore.dasherize})"
143
+
144
+ def label_for(key) = config.dig(key.to_sym, :label) || config.dig(key.to_s, :label) || key.to_s.humanize
145
+
146
+ # `|| 0` because a series may be missing from a category — a month
147
+ # where nothing was sold is a gap in the Hash, not a zero someone typed.
148
+ def number(value) = ActiveSupport::NumberHelper.number_to_delimited(value || 0)
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ # Port of registry/new-york-v4/ui/chart.tsx — the half of it that is markup.
6
+ #
7
+ # `chart.tsx` draws nothing. It is a *frame*: a container that publishes one
8
+ # custom property per series, and the contents of a tooltip and a legend
9
+ # that `recharts` fills in. The chart itself — the SVG, the scales, the
10
+ # axes — is `recharts`, 29,091 lines of compiled ES6 over eleven packages,
11
+ # and the caller writes it in JSX.
12
+ #
13
+ # So this family is the frame ported 1:1 and the drawing written here: a
14
+ # pie, and the three cartesian shapes — bar, line, area — over the grid and
15
+ # axes `Chart::Plot` computes. See
16
+ # [features/chart.md](../../../../.claude/docs/features/chart.md).
17
+ #
18
+ # `config:` is upstream's `ChartConfig`, key for key:
19
+ #
20
+ # config: {
21
+ # chrome: { label: "Chrome", color: "var(--chart-1)" },
22
+ # safari: { label: "Safari", theme: { light: "…", dark: "…" } }
23
+ # }
24
+ #
25
+ # Every colour is published as `--color-<key>` on the container, which is
26
+ # what lets a slice say `fill="var(--color-chrome)"` and a host restyle the
27
+ # chart from its own stylesheet.
28
+ class Component < ApplicationViewComponent
29
+ THEMES = { light: "", dark: ".dark" }.freeze
30
+
31
+ default_tag :div
32
+ slot_name :chart
33
+
34
+ style do
35
+ base {
36
+ # Upstream's own, minus thirteen `[&_.recharts-*]` variants: they
37
+ # select `recharts`' DOM, which nothing here renders, so they would
38
+ # compile to rules that match nothing. `parity_spec` holds them in
39
+ # `allowed_missing` with that reason.
40
+ #
41
+ # `relative` is this port's: upstream's tooltip is positioned by
42
+ # recharts inside a wrapper of its own, and here it is positioned
43
+ # against the container.
44
+ "relative flex aspect-video justify-center text-xs"
45
+ }
46
+ end
47
+
48
+ renders_one :pie, ->(**options) { Pie::Component.new(config:, **options) }
49
+ renders_one :bar, ->(**options) { Bar::Component.new(config:, **options) }
50
+ renders_one :line, ->(**options) { Line::Component.new(config:, **options) }
51
+ renders_one :area, ->(**options) { Area::Component.new(config:, **options) }
52
+ renders_one :chart_tooltip, ->(**options) { Tooltip::Component.new(**options) }
53
+ renders_one :chart_legend, ->(**options) { Legend::Component.new(config:, **options) }
54
+
55
+ attr_reader :config
56
+
57
+ def initialize(config: {}, **attributes)
58
+ @config = config.transform_keys(&:to_s)
59
+ super(**attributes)
60
+ end
61
+
62
+ def element_attributes(**defaults)
63
+ super(**{
64
+ "data-chart" => chart_id,
65
+ "data-controller" => "shadcn--chart"
66
+ }.merge(defaults))
67
+ end
68
+
69
+ # Slots rather than block content, and rendered in an order this decides:
70
+ # the tooltip has to come after the shape it floats over, and slot content
71
+ # is emitted before block content — the trap that has shipped twice here.
72
+ def call
73
+ render_element(body: safe_join([ colours, stack, chart_tooltip ].compact))
74
+ end
75
+
76
+ private
77
+
78
+ # Upstream's container is a flex *row* because it has exactly one child:
79
+ # recharts draws the legend inside the SVG. Here the legend is its own
80
+ # element, so the two need a column of their own — and it is a plain div
81
+ # rather than a class on the container, which stays as `chart.tsx` wrote
82
+ # it.
83
+ def stack
84
+ tag.div(safe_join([ pie, bar, line, area, content, chart_legend ].compact),
85
+ class: "flex w-full min-w-0 flex-col items-center justify-center gap-2")
86
+ end
87
+
88
+ def chart_id
89
+ @chart_id ||= "chart-#{SecureRandom.hex(4)}"
90
+ end
91
+
92
+ # Upstream's `ChartStyle`, which is a real `<style>` element rather than
93
+ # inline properties: a custom property has to be readable by descendants
94
+ # *and* by a `.dark` rule, and an inline style cannot carry the second.
95
+ def colours
96
+ rules = THEMES.filter_map do |theme, prefix|
97
+ declarations = config.filter_map do |key, item|
98
+ colour = safe_colour(item.dig(:theme, theme) || item[:color])
99
+ " --color-#{key.parameterize.underscore.dasherize}: #{colour};" if colour
100
+ end
101
+
102
+ "#{prefix} [data-chart=#{chart_id}] {\n#{declarations.join("\n")}\n}" if declarations.any?
103
+ end
104
+ return if rules.empty?
105
+
106
+ tag.style(rules.join("\n").html_safe) # rubocop:disable Rails/OutputSafety
107
+ end
108
+
109
+ # A colour reaches a `<style>` element, where a `}` would end the rule and
110
+ # everything after it would be the caller's own CSS running in the host's
111
+ # page. Upstream writes the same string through
112
+ # `dangerouslySetInnerHTML` and is exposed to exactly this; a library that
113
+ # runs inside an application it cannot see does not get to be.
114
+ #
115
+ # What is allowed is what a colour is made of: `oklch(0.7 0.1 20)`,
116
+ # `var(--chart-1)`, `#0f172a`, `rgb(1 2 3 / 40%)`.
117
+ COLOUR = %r{\A[\w\s().,%#/-]+\z}
118
+
119
+ def safe_colour(value)
120
+ value.to_s.strip.presence&.then { |colour| colour if colour.match?(COLOUR) }
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ # What makes a chart reachable without a pointer, shared by every shape.
6
+ #
7
+ # This is upstream's own shape, read from the rendered example rather than
8
+ # from source — nothing of recharts is vendored here. Its
9
+ # `accessibilityLayer` puts `role="application" tabindex="0"` on the
10
+ # surface, and `application` is not decoration: it is what stops a screen
11
+ # reader from swallowing the arrow keys before they reach the page.
12
+ #
13
+ # Where this port diverges: upstream leaves the graphic itself exposed, so
14
+ # its axis labels reach the accessibility tree as loose text — *"400 300 200
15
+ # 100 0 January February …"*. Here the drawing sits inside an
16
+ # `aria-hidden` group, so the element that takes focus is not hidden — the
17
+ # pair is what axe calls `aria-hidden-focus` — and nothing inside it is
18
+ # read twice. The numbers are the table's job.
19
+ module Focusable
20
+ private
21
+
22
+ def keyboard
23
+ return {} unless drawn?
24
+
25
+ {
26
+ tabindex: 0,
27
+ role: "application",
28
+ "aria-label" => label.presence || shadcn_t("chart.label"),
29
+ "data-action" => "keydown->shadcn--chart#navigate blur->shadcn--chart#hide"
30
+ }
31
+ end
32
+
33
+ # A chart of nothing has nowhere for a cursor to go, and a tab stop that
34
+ # answers no key is worse than no tab stop.
35
+ def drawn? = raise NotImplementedError, "#{self.class} cannot say whether it drew anything"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ module Legend
6
+ # `ChartLegendContent`, 1:1 in markup.
7
+ #
8
+ # Upstream's is handed a payload by recharts' `Legend`; here it is the
9
+ # `config` itself, which is the same list arrived at without a round trip
10
+ # through a chart library.
11
+ class Component < ApplicationViewComponent
12
+ default_tag :div
13
+ slot_name :"chart-legend"
14
+
15
+ style do
16
+ base { "flex items-center justify-center gap-4" }
17
+ end
18
+
19
+ attr_reader :config, :vertical_align, :hide_icon
20
+
21
+ def initialize(config: {}, vertical_align: :bottom, hide_icon: false, **attributes)
22
+ @config = config
23
+ @vertical_align = vertical_align&.to_sym || :bottom
24
+ @hide_icon = hide_icon
25
+ super(**attributes)
26
+ end
27
+
28
+ def css_classes(extra = nil)
29
+ super([ vertical_align == :top ? "pb-3" : "pt-3", extra ].compact.join(" ").presence)
30
+ end
31
+
32
+ def call
33
+ render_element(body: safe_join(entries))
34
+ end
35
+
36
+ private
37
+
38
+ def entries
39
+ config.map do |key, item|
40
+ tag.div(class: "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground") do
41
+ safe_join([ swatch(key), item[:label] ].compact)
42
+ end
43
+ end
44
+ end
45
+
46
+ def swatch(key)
47
+ return if hide_icon
48
+
49
+ tag.div(class: "h-2 w-2 shrink-0 rounded-[2px]",
50
+ style: "background-color: var(--color-#{key.to_s.parameterize.underscore.dasherize})")
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ module Line
6
+ # Upstream's "Line Chart", drawn on the server: one polyline per series,
7
+ # with a dot at each reading.
8
+ #
9
+ # chart.with_line(data: { "January" => { desktop: 186, mobile: 80 } })
10
+ class Component < Cartesian::Component
11
+ DOT = 4
12
+ # The dot a pointer has to hit, which is not the dot a reader sees. Four
13
+ # pixels is a target nobody can hold, so a transparent circle takes the
14
+ # events and the visible one takes the colour.
15
+ REACH = 10
16
+
17
+ slot_name :"chart-line"
18
+
19
+ private
20
+
21
+ # A label belongs under the reading it names, and a line's readings run
22
+ # edge to edge rather than band by band.
23
+ def category_x(index) = plot.point_x(index)
24
+
25
+ def shapes
26
+ plot.series.flat_map { |key| [ path(key), *dots(key) ] }
27
+ end
28
+
29
+ def path(key)
30
+ tag.polyline(points: coordinates(key), fill: "none", stroke: colour_of(key),
31
+ "stroke-width": 2, "stroke-linecap": "round", "stroke-linejoin": "round")
32
+ end
33
+
34
+ def coordinates(key)
35
+ plot.points(key).map { |point| "#{point[:x]},#{point[:y]}" }.join(" ")
36
+ end
37
+
38
+ def dots(key)
39
+ plot.points(key).flat_map do |point|
40
+ [
41
+ tag.circle(cx: point[:x], cy: point[:y], r: DOT, fill: colour_of(key)),
42
+ tag.circle(cx: point[:x], cy: point[:y], r: REACH, fill: "transparent",
43
+ **mark_attributes(category: point[:category], key:, value: point[:value]))
44
+ ]
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end