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,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ module Pie
6
+ # A pie, drawn on the server.
7
+ #
8
+ # **Ours.** `chart.tsx` has no markup for a shape — the caller writes
9
+ # `<PieChart><Pie …/></PieChart>` and `recharts` renders it. What is
10
+ # reproduced instead is the *contract* around it: each slice is filled
11
+ # from `--color-<key>`, which the container publishes from `config`, so
12
+ # the same stylesheet that themes upstream's chart themes this one.
13
+ #
14
+ # An arc is trigonometry and an SVG path, which is why the pie is the
15
+ # shape to draw first: no scales, no ticks, no axis labels to collide.
16
+ #
17
+ # render(Shadcn::Chart::Component.new(config:)) do |chart|
18
+ # chart.with_pie(data: { chrome: 275, safari: 200, firefox: 187 })
19
+ # end
20
+ #
21
+ # `data:` is a Hash of key to number, which is what `group(:x).sum(:y)`
22
+ # already hands back.
23
+ class Component < ApplicationViewComponent
24
+ include Focusable
25
+
26
+ SIZE = 250
27
+ PADDING = 4
28
+ # Twelve o'clock, where a reader starts. SVG angles start at three.
29
+ START = -90
30
+
31
+ default_tag :svg
32
+ slot_name :"chart-pie"
33
+
34
+ style do
35
+ base { "mx-auto aspect-square max-h-[250px]" }
36
+ end
37
+
38
+ attr_reader :data, :config, :inner_radius, :percentage, :label
39
+
40
+ # `inner_radius:` is a fraction of the radius, as upstream's own
41
+ # examples pass one — 0 is a pie and 0.6 is the donut the docs show.
42
+ def initialize(data: {}, config: {}, inner_radius: 0, percentage: false, label: nil, **attributes)
43
+ @data = data.to_h.transform_keys(&:to_s).select { |_, value| value.to_f.positive? }
44
+ @config = config
45
+ @inner_radius = inner_radius.to_f.clamp(0, 0.95)
46
+ @percentage = percentage
47
+ @label = label
48
+ super(**attributes)
49
+ end
50
+
51
+ # No SVG `<title>`: it would say what the table says and then be drawn
52
+ # by the browser as a native tooltip on every hover, over this
53
+ # component's own. Reported from a screenshot — the grey box covering
54
+ # the panel was Chrome's, not ours.
55
+ def element_attributes(**defaults)
56
+ super(**{ viewBox: "0 0 #{SIZE} #{SIZE}" }.merge(keyboard).merge(defaults))
57
+ end
58
+
59
+ # The table follows the graphic it describes, which is the order a
60
+ # reader meets them in.
61
+ def call
62
+ safe_join([ render_element(body: tag.g(safe_join(slices), "aria-hidden": "true")), table ])
63
+ end
64
+
65
+ private
66
+
67
+ def drawn? = data.any?
68
+
69
+ # A pie of nothing draws nothing, and a table of nothing announces a
70
+ # name and then leaves a reader in an empty grid. A filtered scope
71
+ # reaches this.
72
+ def table
73
+ return unless drawn?
74
+
75
+ render(Table::Component.new(caption: label, columns: [ value_heading ], rows: table_rows))
76
+ end
77
+
78
+ def table_rows
79
+ data.map { |key, value| [ label_for(key), display_for(value, value.to_f / total) ] }
80
+ end
81
+
82
+ # One column, and it needs a name: a pie's numbers are counts, or the
83
+ # shares `percentage:` turns them into. The chart's own label heads it
84
+ # where there is one, since "Visitors" says more than "Value".
85
+ def value_heading = label.presence || shadcn_t("chart.value")
86
+
87
+ def total = @total ||= data.values.sum(&:to_f)
88
+
89
+ # Each slice carries what the tooltip needs, so the controller reads the
90
+ # DOM rather than being handed the series a second time as JSON.
91
+ def slices
92
+ angle = START
93
+
94
+ data.map do |key, value|
95
+ share = value.to_f / total
96
+ from = angle
97
+ angle += share * 360
98
+
99
+ tag.path(
100
+ d: path_for(from, angle),
101
+ fill: "var(--color-#{key.parameterize.underscore.dasherize})",
102
+ "data-shadcn--chart-target": "mark",
103
+ "data-action": "pointerenter->shadcn--chart#show pointermove->shadcn--chart#move " \
104
+ "pointerleave->shadcn--chart#hide",
105
+ "data-key": key,
106
+ "data-label": label_for(key),
107
+ "data-display": display_for(value, share)
108
+ )
109
+ end
110
+ end
111
+
112
+ def label_for(key) = config.dig(key.to_sym, :label) || config.dig(key.to_s, :label) || key.to_s.humanize
113
+
114
+ # What the tooltip shows. A share where one was asked for, because a
115
+ # pie's question is usually "how much of the whole" — and doing it here
116
+ # keeps the tooltip's own markup 1:1 with upstream, which has one cell
117
+ # for a value and no room for a second number.
118
+ def display_for(value, share)
119
+ return "#{(share * 100).round(1).to_s.delete_suffix('.0')}%" if percentage
120
+
121
+ ActiveSupport::NumberHelper.number_to_delimited(value)
122
+ end
123
+
124
+ def centre = SIZE / 2.0
125
+
126
+ def radius = centre - PADDING
127
+
128
+ # A full circle has no arc: its two ends are the same point, so the path
129
+ # would draw nothing at all. One slice is the case a Rails application
130
+ # reaches by filtering, not an odd one.
131
+ def path_for(from, to)
132
+ return ring if (to - from) >= 359.999
133
+
134
+ outer_start = point(from, radius)
135
+ outer_end = point(to, radius)
136
+ large = (to - from) > 180 ? 1 : 0
137
+
138
+ return "M #{centre} #{centre} L #{outer_start} A #{radius} #{radius} 0 #{large} 1 #{outer_end} Z" if hole.zero?
139
+
140
+ inner_start = point(to, hole)
141
+ inner_end = point(from, hole)
142
+
143
+ "M #{outer_start} A #{radius} #{radius} 0 #{large} 1 #{outer_end} " \
144
+ "L #{inner_start} A #{hole} #{hole} 0 #{large} 0 #{inner_end} Z"
145
+ end
146
+
147
+ def ring
148
+ top = "#{centre} #{centre - radius}"
149
+ bottom = "#{centre} #{centre + radius}"
150
+ circle = "M #{top} A #{radius} #{radius} 0 1 1 #{bottom} A #{radius} #{radius} 0 1 1 #{top} Z"
151
+ return circle if hole.zero?
152
+
153
+ inner_top = "#{centre} #{centre - hole}"
154
+ inner_bottom = "#{centre} #{centre + hole}"
155
+
156
+ "#{circle} M #{inner_top} A #{hole} #{hole} 0 1 0 #{inner_bottom} A #{hole} #{hole} 0 1 0 #{inner_top} Z"
157
+ end
158
+
159
+ def hole = @hole ||= (radius * inner_radius).round(2)
160
+
161
+ def point(degrees, distance)
162
+ radians = degrees * Math::PI / 180
163
+
164
+ "#{(centre + distance * Math.cos(radians)).round(2)} #{(centre + distance * Math.sin(radians)).round(2)}"
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ # The arithmetic a cartesian chart needs, and nothing that draws.
6
+ #
7
+ # This is the part the pie did not have and the reason the todo said the
8
+ # axis is the work: a bar is a rectangle, but *where* it goes is a scale, a
9
+ # nice maximum, a set of ticks and a band per category. A plain object, so
10
+ # all of that is asserted directly rather than through a browser — the same
11
+ # trade `Calendar::Month` makes.
12
+ #
13
+ # `data` is a Hash of category to a Hash of series to number, which is what
14
+ # a `group(:month, :platform).sum(:visits)` becomes after one `each_with_object`:
15
+ #
16
+ # { "January" => { desktop: 186, mobile: 80 },
17
+ # "February" => { desktop: 305, mobile: 200 } }
18
+ class Plot
19
+ SIZE = { width: 560, height: 240 }.freeze
20
+ # Room for the tick labels on the left and the category labels below.
21
+ PADDING = { left: 34, right: 8, top: 12, bottom: 24 }.freeze
22
+ TICKS = 4
23
+ # The share of a band left empty, so neighbouring categories do not touch.
24
+ INSET = 0.15
25
+
26
+ attr_reader :data, :series, :stacked, :width, :height
27
+
28
+ def initialize(data: {}, series: nil, stacked: false, width: SIZE[:width], height: SIZE[:height])
29
+ @data = data.to_h { |category, values| [ category.to_s, values.to_h.transform_keys(&:to_s) ] }
30
+ @series = (series || @data.values.flat_map(&:keys).uniq).map(&:to_s)
31
+ @stacked = stacked
32
+ @width = width
33
+ @height = height
34
+ end
35
+
36
+ def categories = data.keys
37
+
38
+ # The tallest thing the chart has to fit: one bar's value, or a whole
39
+ # stack's, rounded up to something a person would choose as a maximum.
40
+ def max
41
+ @max ||= nice(data.values.map { |values| tallest(values) }.max.to_f)
42
+ end
43
+
44
+ def ticks = @ticks ||= (0..TICKS).map { |step| max * step / TICKS }
45
+
46
+ # The horizontal room one category gets, including the space between.
47
+ def band = @band ||= (plot_width / [ categories.size, 1 ].max.to_f)
48
+
49
+ def x_of(index) = PADDING[:left] + band * index
50
+
51
+ def y_of(value) = PADDING[:top] + (plot_height - plot_height * value.to_f / max)
52
+
53
+ def baseline = y_of(0)
54
+
55
+ def plot_width = width - PADDING[:left] - PADDING[:right]
56
+
57
+ def plot_height = height - PADDING[:top] - PADDING[:bottom]
58
+
59
+ # One rectangle per series per category. Grouped side by side, or stacked
60
+ # one on top of the other — which is the same arithmetic with the bottom
61
+ # moved up by what is already there.
62
+ def bars
63
+ data.flat_map.with_index do |(category, values), index|
64
+ stacked ? stacked_bars(category, values, index) : grouped_bars(category, values, index)
65
+ end
66
+ end
67
+
68
+ # Where a series' line goes. Not the middle of a band: a line's axis is a
69
+ # *point* scale, so the first reading sits on the left edge and the last
70
+ # on the right, and upstream's own line and area charts touch both. A bar
71
+ # cannot — it needs a band to be wide in — which is why the two shapes
72
+ # place their category labels differently.
73
+ def point_x(index)
74
+ return x_of(index) + band / 2 if categories.size < 2
75
+
76
+ PADDING[:left] + plot_width * index / (categories.size - 1).to_f
77
+ end
78
+
79
+ def points(key)
80
+ key = key.to_s
81
+
82
+ data.map.with_index do |(category, values), index|
83
+ { category:, value: values[key],
84
+ x: point_x(index).round(2), y: y_of(values[key]).round(2) }
85
+ end
86
+ end
87
+
88
+ # Every label would collide long before a year of days fits, so only every
89
+ # `n`th is drawn — `n` from the room the longest label needs against the
90
+ # room a band gives it. Upstream's own charts rotate them instead; a Rails
91
+ # app is likelier to pass twelve months than a thousand points, so this
92
+ # stays a skip rather than a rotation.
93
+ def label_every(pixels_per_character: 7)
94
+ longest = categories.map { |category| category.length }.max.to_i
95
+
96
+ [ ((longest * pixels_per_character) / band).ceil, 1 ].max
97
+ end
98
+
99
+ private
100
+
101
+ def tallest(values)
102
+ stacked ? series.sum { |key| values[key].to_f } : values.values.map(&:to_f).max.to_f
103
+ end
104
+
105
+ def grouped_bars(category, values, index)
106
+ gutter = band * INSET
107
+ slot = (band - gutter * 2) / [ series.size, 1 ].max
108
+
109
+ series.each_with_index.map do |key, position|
110
+ value = values[key]
111
+ top = y_of(value)
112
+
113
+ bar(category:, key:, value:,
114
+ x: (x_of(index) + gutter + slot * position).round(2), y: top.round(2),
115
+ width: slot.round(2), height: (baseline - top).round(2))
116
+ end
117
+ end
118
+
119
+ def stacked_bars(category, values, index)
120
+ gutter = band * INSET
121
+ bottom = baseline
122
+
123
+ series.map do |key|
124
+ value = values[key]
125
+ height = plot_height * value.to_f / max
126
+ bottom -= height
127
+
128
+ bar(category:, key:, value:,
129
+ x: (x_of(index) + gutter).round(2), y: bottom.round(2),
130
+ width: (band - gutter * 2).round(2), height: height.round(2))
131
+ end
132
+ end
133
+
134
+ def bar(**attributes) = attributes
135
+
136
+ # A maximum a person would have chosen: the next round number above the
137
+ # tallest value, so the ticks read 0, 100, 200 rather than 0, 78.5, 157.
138
+ def nice(value)
139
+ return 1 if value <= 0
140
+
141
+ step = 10**Math.log10(value).floor
142
+
143
+ (value / step).ceil * step
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ class Preview < ApplicationViewComponentPreview
6
+ # Upstream's "Pie Chart" example, drawn here rather than by recharts.
7
+ def default
8
+ render_with_template
9
+ end
10
+
11
+ # Upstream's "Pie Chart - Donut", and the shape most of its own examples
12
+ # use: `inner_radius:` is a fraction of the radius.
13
+ def donut
14
+ render_with_template
15
+ end
16
+
17
+ # What a pie is usually asked — how much of the whole — with the share in
18
+ # the tooltip instead of the count.
19
+ def percentages
20
+ render_with_template
21
+ end
22
+
23
+ # Upstream's "Bar Chart - Multiple", and the first shape here with an axis
24
+ # under it.
25
+ def bar
26
+ render_with_template
27
+ end
28
+
29
+ # Upstream's "Bar Chart - Stacked": one bar a month, split by device.
30
+ def bar_stacked
31
+ render_with_template
32
+ end
33
+
34
+ # Upstream's "Line Chart - Multiple".
35
+ def line
36
+ render_with_template
37
+ end
38
+
39
+ # Upstream's "Area Chart".
40
+ def area
41
+ render_with_template
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,31 @@
1
+ <%# Upstream's "Area Chart": the line with the space beneath it filled in. Flat
2
+ rather than the gradient the docs show — a gradient needs a `<defs>` with an
3
+ id per series, and an id unique in a page this gem cannot see is a cost the
4
+ fill does not repay. %>
5
+ <%
6
+ config = {
7
+ desktop: { label: "Desktop", color: "var(--chart-1)" },
8
+ mobile: { label: "Mobile", color: "var(--chart-2)" }
9
+ }
10
+ data = {
11
+ "January" => { desktop: 186, mobile: 80 },
12
+ "February" => { desktop: 305, mobile: 200 },
13
+ "March" => { desktop: 237, mobile: 120 },
14
+ "April" => { desktop: 73, mobile: 190 },
15
+ "May" => { desktop: 209, mobile: 130 },
16
+ "June" => { desktop: 214, mobile: 140 }
17
+ }
18
+ %>
19
+ <%= render(Shadcn::Card::Component.new(class: "max-w-xl")) do |card| %>
20
+ <% card.with_header do |header| %>
21
+ <% header.with_title { "Visitors" } %>
22
+ <% header.with_description { "January – June 2026" } %>
23
+ <% end %>
24
+ <% card.with_card_content do %>
25
+ <%= render(Shadcn::Chart::Component.new(config:)) do |chart| %>
26
+ <% chart.with_area(data:, label: "Visitors a month, by device") %>
27
+ <% chart.with_chart_tooltip %>
28
+ <% chart.with_chart_legend %>
29
+ <% end %>
30
+ <% end %>
31
+ <% end %>
@@ -0,0 +1,32 @@
1
+ <%# Upstream's "Bar Chart - Multiple": two series a month, side by side.
2
+
3
+ `data:` is a Hash of category to a Hash of series to number — which is one
4
+ `each_with_object` away from what `group(:month, :platform).sum(:visits)`
5
+ already returns. %>
6
+ <%
7
+ config = {
8
+ desktop: { label: "Desktop", color: "var(--chart-1)" },
9
+ mobile: { label: "Mobile", color: "var(--chart-2)" }
10
+ }
11
+ data = {
12
+ "January" => { desktop: 186, mobile: 80 },
13
+ "February" => { desktop: 305, mobile: 200 },
14
+ "March" => { desktop: 237, mobile: 120 },
15
+ "April" => { desktop: 73, mobile: 190 },
16
+ "May" => { desktop: 209, mobile: 130 },
17
+ "June" => { desktop: 214, mobile: 140 }
18
+ }
19
+ %>
20
+ <%= render(Shadcn::Card::Component.new(class: "max-w-xl")) do |card| %>
21
+ <% card.with_header do |header| %>
22
+ <% header.with_title { "Visitors" } %>
23
+ <% header.with_description { "January – June 2026" } %>
24
+ <% end %>
25
+ <% card.with_card_content do %>
26
+ <%= render(Shadcn::Chart::Component.new(config:)) do |chart| %>
27
+ <% chart.with_bar(data:, label: "Visitors a month, by device") %>
28
+ <% chart.with_chart_tooltip %>
29
+ <% chart.with_chart_legend %>
30
+ <% end %>
31
+ <% end %>
32
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <%# Upstream's "Bar Chart - Stacked": the same months asking a different
2
+ question — not which series is larger, but what the month came to. The axis
3
+ grows to fit the whole stack rather than its tallest part. %>
4
+ <%
5
+ config = {
6
+ desktop: { label: "Desktop", color: "var(--chart-1)" },
7
+ mobile: { label: "Mobile", color: "var(--chart-2)" }
8
+ }
9
+ data = {
10
+ "January" => { desktop: 186, mobile: 80 },
11
+ "February" => { desktop: 305, mobile: 200 },
12
+ "March" => { desktop: 237, mobile: 120 },
13
+ "April" => { desktop: 73, mobile: 190 },
14
+ "May" => { desktop: 209, mobile: 130 },
15
+ "June" => { desktop: 214, mobile: 140 }
16
+ }
17
+ %>
18
+ <%= render(Shadcn::Card::Component.new(class: "max-w-xl")) do |card| %>
19
+ <% card.with_header do |header| %>
20
+ <% header.with_title { "Visitors" } %>
21
+ <% header.with_description { "Every device, month by month" } %>
22
+ <% end %>
23
+ <% card.with_card_content do %>
24
+ <%= render(Shadcn::Chart::Component.new(config:)) do |chart| %>
25
+ <% chart.with_bar(data:, stacked: true, label: "Visitors a month, by device") %>
26
+ <% chart.with_chart_tooltip %>
27
+ <% chart.with_chart_legend %>
28
+ <% end %>
29
+ <% end %>
30
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <%# Upstream's "Pie Chart": five browsers, one slice each, coloured from the
2
+ `--chart-*` tokens the gem's themes already define. The data is a plain Hash
3
+ of key to number — which is what `group(:browser).sum(:visitors)` returns.
4
+
5
+ `hide_label:` because a pie's label and its item name are the same word, and
6
+ without it the tooltip reads "Chrome / Chrome 275". Upstream's own pie
7
+ examples pass it for the same reason. %>
8
+ <%
9
+ config = {
10
+ chrome: { label: "Chrome", color: "var(--chart-1)" },
11
+ safari: { label: "Safari", color: "var(--chart-2)" },
12
+ firefox: { label: "Firefox", color: "var(--chart-3)" },
13
+ edge: { label: "Edge", color: "var(--chart-4)" },
14
+ other: { label: "Other", color: "var(--chart-5)" }
15
+ }
16
+ %>
17
+ <%= render(Shadcn::Card::Component.new(class: "max-w-sm")) do |card| %>
18
+ <% card.with_header do |header| %>
19
+ <% header.with_title { "Visitors by browser" } %>
20
+ <% header.with_description { "January – June 2026" } %>
21
+ <% end %>
22
+ <% card.with_card_content do %>
23
+ <%= render(Shadcn::Chart::Component.new(config:)) do |chart| %>
24
+ <% chart.with_pie(data: { chrome: 275, safari: 200, firefox: 187, edge: 173, other: 90 },
25
+ label: "Visitors by browser") %>
26
+ <% chart.with_chart_tooltip(hide_label: true) %>
27
+ <% chart.with_chart_legend %>
28
+ <% end %>
29
+ <% end %>
30
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <%# Upstream's "Pie Chart - Donut": the same slices with the middle taken out.
2
+ `inner_radius:` is a fraction, so the ring keeps its proportions at any size. %>
3
+ <%
4
+ config = {
5
+ chrome: { label: "Chrome", color: "var(--chart-1)" },
6
+ safari: { label: "Safari", color: "var(--chart-2)" },
7
+ firefox: { label: "Firefox", color: "var(--chart-3)" },
8
+ edge: { label: "Edge", color: "var(--chart-4)" }
9
+ }
10
+ %>
11
+ <%= render(Shadcn::Chart::Component.new(config:, class: "max-w-sm")) do |chart| %>
12
+ <% chart.with_pie(data: { chrome: 275, safari: 200, firefox: 187, edge: 173 },
13
+ inner_radius: 0.6, label: "Visitors by browser") %>
14
+ <% chart.with_chart_tooltip(hide_label: true) %>
15
+ <% chart.with_chart_legend %>
16
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <%# Upstream's "Line Chart - Multiple": one polyline a series, with a dot at
2
+ each reading. Each dot carries its own number, so the tooltip is filled from
3
+ the DOM rather than from the series sent a second time as JSON. %>
4
+ <%
5
+ config = {
6
+ desktop: { label: "Desktop", color: "var(--chart-1)" },
7
+ mobile: { label: "Mobile", color: "var(--chart-2)" }
8
+ }
9
+ data = {
10
+ "January" => { desktop: 186, mobile: 80 },
11
+ "February" => { desktop: 305, mobile: 200 },
12
+ "March" => { desktop: 237, mobile: 120 },
13
+ "April" => { desktop: 73, mobile: 190 },
14
+ "May" => { desktop: 209, mobile: 130 },
15
+ "June" => { desktop: 214, mobile: 140 }
16
+ }
17
+ %>
18
+ <%= render(Shadcn::Card::Component.new(class: "max-w-xl")) do |card| %>
19
+ <% card.with_header do |header| %>
20
+ <% header.with_title { "Visitors" } %>
21
+ <% header.with_description { "January – June 2026" } %>
22
+ <% end %>
23
+ <% card.with_card_content do %>
24
+ <%= render(Shadcn::Chart::Component.new(config:)) do |chart| %>
25
+ <% chart.with_line(data:, label: "Visitors a month, by device") %>
26
+ <% chart.with_chart_tooltip %>
27
+ <% chart.with_chart_legend %>
28
+ <% end %>
29
+ <% end %>
30
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <%# A pie's question is usually "how much of the whole", so `percentage: true`
2
+ puts the share in the tooltip where the count would be. Doing it in the data
3
+ rather than in the tooltip keeps that part 1:1 with upstream, which has one
4
+ cell for a value and no room for a second number. %>
5
+ <%
6
+ config = {
7
+ margin: { label: "Margine", color: "var(--chart-1)" },
8
+ costs: { label: "Costi", color: "var(--chart-2)" },
9
+ deductions: { label: "Ritenute", color: "var(--chart-3)" }
10
+ }
11
+ %>
12
+ <%= render(Shadcn::Chart::Component.new(config:, class: "max-w-sm")) do |chart| %>
13
+ <% chart.with_pie(data: { margin: 42_500, costs: 118_200, deductions: 9_800 },
14
+ percentage: true, inner_radius: 0.55, label: "Margine commessa") %>
15
+ <% chart.with_chart_tooltip(indicator: :line, hide_label: true) %>
16
+ <% chart.with_chart_legend %>
17
+ <% end %>
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Chart
5
+ module Table
6
+ # The chart's numbers, as a table only a screen reader sees.
7
+ #
8
+ # **Ours.** `chart.tsx` has nothing like it — upstream's answer is
9
+ # recharts' `accessibilityLayer`, which makes the chart arrow-navigable
10
+ # and announces a category at a time.
11
+ #
12
+ # A table is what this port reaches for instead, because a table is
13
+ # already a table: a screen reader enters it in table mode, moves by row
14
+ # and by column, and rereads the column header on demand. What it
15
+ # replaces is one long sentence — *"January: Desktop 186, Mobile 80;
16
+ # February: …"* — which cannot be navigated, cannot be reread in pieces,
17
+ # and by the time a year of months is in it is not a sentence anyone
18
+ # listens to.
19
+ #
20
+ # It is rendered by the shape rather than by the container, so that a
21
+ # shape rendered on its own is still complete, and so that the graphic
22
+ # and its table can never disagree about who carries the data — the same
23
+ # object emits both.
24
+ #
25
+ # Not `Shadcn::Table`, despite the name it shadows inside this module:
26
+ # that family's classes are all visual — borders, padding, hover — and
27
+ # every one of them would be dead CSS on an element no one sees.
28
+ class Component < ApplicationViewComponent
29
+ default_tag :table
30
+ slot_name :"chart-table"
31
+
32
+ style do
33
+ base { "sr-only" }
34
+ end
35
+
36
+ attr_reader :caption, :columns, :rows
37
+
38
+ # `rows` is one array per row: the row's own header, then a cell per
39
+ # column.
40
+ def initialize(caption: nil, columns: [], rows: [], **attributes)
41
+ @caption = caption
42
+ @columns = columns
43
+ @rows = rows
44
+ super(**attributes)
45
+ end
46
+
47
+ def call
48
+ render_element(body: safe_join([ heading, head, body ].compact))
49
+ end
50
+
51
+ private
52
+
53
+ # Not an endless method with a trailing `if`: that reads as *define
54
+ # this method only when the condition holds*, and the condition is
55
+ # evaluated once, against the class.
56
+ def heading
57
+ tag.caption(caption) if caption.present?
58
+ end
59
+
60
+ # The corner cell is a `<td>` and not an empty `<th>`: it heads
61
+ # nothing, and a header cell with no text is a header a screen reader
62
+ # still announces.
63
+ def head
64
+ tag.thead(tag.tr(safe_join([ tag.td, *columns.map { |column| tag.th(column, scope: "col") } ])))
65
+ end
66
+
67
+ def body
68
+ tag.tbody(safe_join(rows.map do |header, *cells|
69
+ tag.tr(safe_join([ tag.th(header, scope: "row"), *cells.map { |cell| tag.td(cell) } ]))
70
+ end))
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end