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,558 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Calendar
5
+ # Port of registry/new-york-v4/ui/calendar.tsx.
6
+ #
7
+ # Upstream is 220 lines that hand every class to `react-day-picker` and let
8
+ # it render — so the class map is fully specified there, and the markup is
9
+ # the package's. That markup is a plain `<table role="grid">`, which is why
10
+ # this port has no dependency: what the package mostly *is* — 3,479 lines of
11
+ # locale data, 2,046 of Ethiopic, Hebrew, Hijri, Persian and Jalali
12
+ # calendars, and 576 wrapping `date-fns` — a Rails app already has in `Date`
13
+ # and `I18n`. See [features/calendar.md](../../../../.claude/docs/features/calendar.md).
14
+ #
15
+ # The grid is computed in `Calendar::Month` and rendered here, so a calendar
16
+ # is readable with no JavaScript at all — the one family in this gem where
17
+ # that is true of the whole component rather than of its resting state.
18
+ class Component < ApplicationViewComponent
19
+ # Upstream writes these two with `String.raw`, so the backslash is part of
20
+ # the class. Single-quoted here for the same reason: in a double-quoted
21
+ # Ruby string `\_` is just `_`, and the token would stop matching the
22
+ # element it names.
23
+ RTL_CHEVRONS = 'rtl:**:[.rdp-button\_next>svg]:rotate-180 rtl:**:[.rdp-button\_previous>svg]:rotate-180'
24
+
25
+ # The two `rdp-*` classes this port does render, and the only ones. They
26
+ # are not styling — they are what the two `RTL_CHEVRONS` selectors above
27
+ # point at, and upstream's own rule would match nothing without them.
28
+ # Every other `rdp-*` name comes from `getDefaultClassNames()`, a function
29
+ # call rather than a string in the vendored source, and nothing here
30
+ # styles them.
31
+ PREVIOUS_CLASS = "rdp-button_previous"
32
+ NEXT_CLASS = "rdp-button_next"
33
+
34
+ MODES = %i[single multiple range].freeze
35
+ CAPTION_LAYOUTS = %i[label dropdown dropdown_months dropdown_years].freeze
36
+
37
+ MONTHS_CLASS = "relative flex flex-col gap-4 md:flex-row"
38
+ MONTH_CLASS = "flex w-full flex-col gap-4"
39
+ NAV_CLASS = "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1"
40
+ CAPTION_CLASS = "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)"
41
+ CAPTION_LABEL_CLASS = "font-medium select-none"
42
+ CAPTION_LABEL_LABEL_CLASS = "text-sm"
43
+ CAPTION_LABEL_DROPDOWN_CLASS = "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm " \
44
+ "[&>svg]:size-3.5 [&>svg]:text-muted-foreground"
45
+ DROPDOWNS_CLASS = "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium"
46
+ DROPDOWN_ROOT_CLASS = "relative rounded-md border border-input shadow-xs has-focus:border-ring " \
47
+ "has-focus:ring-[3px] has-focus:ring-ring/50"
48
+ DROPDOWN_CLASS = "absolute inset-0 bg-popover opacity-0"
49
+ # What a screen reader is left with once the caption is two dropdowns: the
50
+ # month has to be announced when it changes, and the dropdowns announce
51
+ # themselves as controls rather than as the answer. Upstream's own inline
52
+ # style, which is `sr-only` written out.
53
+ STATUS_STYLE = "border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;" \
54
+ "position:absolute;width:1px;white-space:nowrap;word-wrap:normal"
55
+ GRID_CLASS = "w-full border-collapse"
56
+ WEEKDAYS_CLASS = "flex"
57
+ WEEKDAY_CLASS = "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none"
58
+ WEEK_CLASS = "mt-2 flex w-full"
59
+ WEEK_NUMBER_HEADER_CLASS = "w-(--cell-size) select-none"
60
+ WEEK_NUMBER_CLASS = "text-[0.8rem] text-muted-foreground select-none"
61
+ WEEK_NUMBER_INNER_CLASS = "flex size-(--cell-size) items-center justify-center text-center"
62
+ DAY_CLASS = "group/day relative aspect-square h-full w-full p-0 text-center select-none " \
63
+ "[&:last-child[data-selected=true]_button]:rounded-r-md"
64
+ DAY_FIRST_CLASS = "[&:first-child[data-selected=true]_button]:rounded-l-md"
65
+ DAY_FIRST_WITH_WEEK_NUMBER_CLASS = "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
66
+ TODAY_CLASS = "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none"
67
+ OUTSIDE_CLASS = "text-muted-foreground aria-selected:text-muted-foreground"
68
+ DISABLED_CLASS = "text-muted-foreground opacity-50"
69
+ HIDDEN_CLASS = "invisible"
70
+ RANGE_START_CLASS = "rounded-l-md bg-accent"
71
+ RANGE_MIDDLE_CLASS = "rounded-none"
72
+ RANGE_END_CLASS = "rounded-r-md bg-accent"
73
+ NAV_BUTTON_CLASS = "size-(--cell-size) p-0 select-none aria-disabled:opacity-50"
74
+ DAY_BUTTON_CLASS = "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none " \
75
+ "font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 " \
76
+ "group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] " \
77
+ "group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md " \
78
+ "data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary " \
79
+ "data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none " \
80
+ "data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground " \
81
+ "data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md " \
82
+ "data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground " \
83
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground " \
84
+ "dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70"
85
+ CHEVRON_CLASS = "size-4"
86
+
87
+ default_tag :div
88
+ slot_name :calendar
89
+
90
+ style do
91
+ base {
92
+ "w-fit group/calendar bg-background p-3 [--cell-size:--spacing(8)] " \
93
+ "[[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent " +
94
+ RTL_CHEVRONS
95
+ }
96
+ end
97
+
98
+ attr_reader :month, :months, :number_of_months, :mode, :selected, :disabled,
99
+ :button_variant, :caption_layout, :name, :required,
100
+ :show_outside_days, :show_week_number, :fixed_weeks,
101
+ :start_month, :end_month, :week_starts_on
102
+
103
+ # `month:` is which month is shown, `selected:` what is chosen in it, and
104
+ # they are separate on purpose — upstream's `month` and `selected` are two
105
+ # props for the same reason: a range can end in a month nobody is looking
106
+ # at.
107
+ #
108
+ # `disabled:` takes anything that answers `===` — a Date, a Range, a
109
+ # lambda — or an array of those. That is Ruby's own matcher protocol, and
110
+ # it covers what upstream's `Matcher` union covers without inventing a
111
+ # shape for it.
112
+ def initialize(month: nil, mode: :single, selected: nil, disabled: nil, name: nil, required: false,
113
+ caption_layout: :label, button_variant: :ghost, show_outside_days: true,
114
+ show_week_number: false, fixed_weeks: false, number_of_months: 1,
115
+ start_month: nil, end_month: nil, week_starts_on: nil,
116
+ **attributes)
117
+ @mode = MODES.include?(mode&.to_sym) ? mode.to_sym : :single
118
+ @selected = Array(selected.is_a?(Range) ? [ selected.begin, selected.end ] : selected).compact.map(&:to_date)
119
+ # A range arrives either as a `Range` or as the two ends of one, because
120
+ # that is what comes back from the controller and from a form. Both mean
121
+ # the same thing here, and the modifier classes read this rather than
122
+ # the argument's shape.
123
+ @range = if selected.is_a?(Range) then selected.begin.to_date..selected.end.to_date
124
+ elsif @mode == :range && @selected.size == 2 then @selected.first..@selected.last
125
+ end
126
+ @disabled = disabled
127
+ @name = name
128
+ @required = required
129
+ @caption_layout = CAPTION_LAYOUTS.include?(caption_layout&.to_sym) ? caption_layout.to_sym : :label
130
+ @button_variant = button_variant&.to_sym || :ghost
131
+ @show_outside_days = show_outside_days
132
+ @show_week_number = show_week_number
133
+ @fixed_weeks = fixed_weeks
134
+ @number_of_months = [ number_of_months.to_i, 1 ].max
135
+ @start_month = start_month&.to_date
136
+ @end_month = end_month&.to_date
137
+ @week_starts_on = week_starts_on
138
+ @month = Month.new(month || @selected.first || Date.current,
139
+ week_starts_on:, fixed_weeks:)
140
+ # Upstream's `numberOfMonths`: one nav for the lot, and a caption and a
141
+ # grid each. The months run forward from the one that was asked for.
142
+ @months = Array.new(@number_of_months) { |offset| offset.zero? ? @month : @month.plus_months(offset) }
143
+ super(**attributes)
144
+ end
145
+
146
+ # Everything the controller needs to draw a month the server never
147
+ # rendered, and nothing else. The names, the formats and the modifier
148
+ # classes all cross from here rather than being written in JavaScript:
149
+ # `I18n` stays the only thing that decides how a date reads, and Tailwind
150
+ # only ever sees a class in Ruby source.
151
+ def element_attributes(**defaults)
152
+ super(**{
153
+ "data-controller" => "shadcn--calendar",
154
+ "data-shadcn--calendar-month-value" => month.first.iso8601,
155
+ "data-shadcn--calendar-today-value" => Date.current.iso8601,
156
+ "data-shadcn--calendar-week-starts-on-value" => month.week_starts_on,
157
+ "data-shadcn--calendar-fixed-weeks-value" => fixed_weeks,
158
+ "data-shadcn--calendar-show-outside-days-value" => show_outside_days,
159
+ "data-shadcn--calendar-show-week-number-value" => show_week_number,
160
+ "data-shadcn--calendar-start-month-value" => start_month&.beginning_of_month&.iso8601,
161
+ "data-shadcn--calendar-end-month-value" => end_month&.end_of_month&.iso8601,
162
+ "data-shadcn--calendar-mode-value" => mode,
163
+ "data-shadcn--calendar-required-value" => required,
164
+ "data-shadcn--calendar-input-names-value" => input_names.to_json,
165
+ "data-shadcn--calendar-selected-value" => selected_dates.to_json,
166
+ "data-shadcn--calendar-disabled-value" => portable_matchers.to_json,
167
+ "data-shadcn--calendar-day-names-value" => day_names.to_json,
168
+ "data-shadcn--calendar-short-day-names-value" => short_day_names.to_json,
169
+ "data-shadcn--calendar-month-names-value" => month_names.to_json,
170
+ "data-shadcn--calendar-short-month-names-value" => short_month_names.to_json,
171
+ "data-shadcn--calendar-date-format-value" => day_label_format,
172
+ "data-shadcn--calendar-month-format-value" => MONTH_FORMAT,
173
+ # Fetched with the placeholder as its own argument, so what crosses is
174
+ # the template rather than a rendered sentence — `I18n` raises on a
175
+ # missing interpolation and the controller needs the `%{date}` intact.
176
+ "data-shadcn--calendar-today-label-value" => shadcn_t("calendar.today_label", date: "%{date}"),
177
+ "data-shadcn--calendar-selected-label-value" => shadcn_t("calendar.selected_label", date: "%{date}"),
178
+ "data-shadcn--calendar-week-number-label-value" => shadcn_t("calendar.week_number", number: "%{number}"),
179
+ "data-shadcn--calendar-today-class-value" => TODAY_CLASS,
180
+ "data-shadcn--calendar-outside-class-value" => OUTSIDE_CLASS,
181
+ "data-shadcn--calendar-disabled-class-value" => DISABLED_CLASS,
182
+ "data-shadcn--calendar-hidden-class-value" => HIDDEN_CLASS,
183
+ "data-shadcn--calendar-range-start-class-value" => RANGE_START_CLASS,
184
+ "data-shadcn--calendar-range-middle-class-value" => RANGE_MIDDLE_CLASS,
185
+ "data-shadcn--calendar-range-end-class-value" => RANGE_END_CLASS
186
+ }.compact.merge(defaults))
187
+ end
188
+
189
+ def call
190
+ # Block content lands after the months, which is where upstream's
191
+ # `footer` prop renders — and inside the root, which is what lets a
192
+ # preset button's `data-action` reach this controller at all.
193
+ render_element(body: safe_join([
194
+ tag.div(safe_join([ nav, *months.map { |shown| month_body(shown) } ]), class: MONTHS_CLASS),
195
+ inputs,
196
+ content
197
+ ].compact))
198
+ end
199
+
200
+ private
201
+
202
+ # The nav sits over the caption rather than beside it — `absolute` on the
203
+ # nav, and the caption padded by a cell on each side to leave room.
204
+ # `labelNav()` returns an empty string upstream, and an empty `aria-label`
205
+ # names nothing, so this port leaves the attribute off rather than
206
+ # rendering one that does no work.
207
+ def nav
208
+ tag.nav(safe_join([ step_button(:previous), step_button(:next) ]), class: NAV_CLASS)
209
+ end
210
+
211
+ def step_button(direction)
212
+ target = direction == :previous ? month.previous : month.next
213
+ unavailable = out_of_bounds?(target.first)
214
+
215
+ tag.button(
216
+ render(Icon::Component.new("chevron-#{direction == :previous ? 'left' : 'right'}", class: CHEVRON_CLASS)),
217
+ type: "button",
218
+ "data-shadcn--calendar-target": direction,
219
+ "data-action": "click->shadcn--calendar##{direction}",
220
+ class: Button::Component.variant_classes(
221
+ variant: button_variant,
222
+ class: [ NAV_BUTTON_CLASS, direction == :previous ? PREVIOUS_CLASS : NEXT_CLASS ].join(" ")
223
+ ),
224
+ tabindex: ("-1" if unavailable),
225
+ "aria-disabled": ("true" if unavailable),
226
+ "aria-label": shadcn_t("calendar.#{direction}")
227
+ )
228
+ end
229
+
230
+ def month_body(shown)
231
+ tag.div(safe_join([ caption(shown), grid(shown) ]), class: MONTH_CLASS)
232
+ end
233
+
234
+ # `role="status"` with `aria-live="polite"`, as upstream's CaptionLabel
235
+ # carries: the month is the one thing on the page that changes when the
236
+ # nav is pressed, and a screen reader has to hear it without being moved.
237
+ def caption(shown)
238
+ tag.div(caption_layout == :label ? plain_caption(shown) : dropdown_caption(shown), class: CAPTION_CLASS)
239
+ end
240
+
241
+ def plain_caption(shown)
242
+ tag.span(label_of(shown), class: "#{CAPTION_LABEL_CLASS} #{CAPTION_LABEL_LABEL_CLASS}",
243
+ id: caption_id(shown), role: "status", "aria-live": "polite",
244
+ "data-shadcn--calendar-target": "caption")
245
+ end
246
+
247
+ # Two native `<select>`s, laid invisible over the label they change — the
248
+ # same technique the one-time-code field uses, and upstream's here: the
249
+ # control a person operates is the real one, and what they read is the
250
+ # `aria-hidden` span underneath it.
251
+ def dropdown_caption(shown)
252
+ controls = [
253
+ (month_dropdown(shown) if caption_layout != :dropdown_years),
254
+ (tag.span(I18n.l(shown.date, format: "%b")) if caption_layout == :dropdown_years),
255
+ (year_dropdown(shown) if caption_layout != :dropdown_months),
256
+ (tag.span(shown.date.year) if caption_layout == :dropdown_months)
257
+ ].compact
258
+
259
+ tag.div(
260
+ safe_join(controls + [ tag.span(label_of(shown), id: caption_id(shown), role: "status",
261
+ "aria-live": "polite", style: STATUS_STYLE,
262
+ "data-shadcn--calendar-target": "caption") ]),
263
+ class: DROPDOWNS_CLASS
264
+ )
265
+ end
266
+
267
+ def month_dropdown(shown)
268
+ options = (1..12).map do |number|
269
+ { value: number, label: I18n.t("date.abbr_month_names")[number] }
270
+ end
271
+
272
+ dropdown(options, shown.date.month, shadcn_t("calendar.choose_month"), :monthSelect, :chooseMonth)
273
+ end
274
+
275
+ def year_dropdown(shown)
276
+ first = (start_month || shown.date - 100.years).year
277
+ last = (end_month || shown.date + 100.years).year
278
+ options = (first..last).map { |year| { value: year, label: year.to_s } }
279
+
280
+ dropdown(options, shown.date.year, shadcn_t("calendar.choose_year"), :yearSelect, :chooseYear)
281
+ end
282
+
283
+ def dropdown(options, value, label, target, action)
284
+ tag.span(
285
+ safe_join([
286
+ tag.select(
287
+ safe_join(options.map { |option| tag.option(option[:label], value: option[:value], selected: option[:value] == value) }),
288
+ class: DROPDOWN_CLASS,
289
+ "aria-label": label,
290
+ "data-shadcn--calendar-target": target,
291
+ "data-action": "change->shadcn--calendar##{action}"
292
+ ),
293
+ tag.span(
294
+ safe_join([
295
+ options.find { |option| option[:value] == value }&.dig(:label),
296
+ render(Icon::Component.new("chevron-down", class: CHEVRON_CLASS))
297
+ ].compact),
298
+ class: "#{CAPTION_LABEL_CLASS} #{CAPTION_LABEL_DROPDOWN_CLASS}",
299
+ "aria-hidden": "true"
300
+ )
301
+ ]),
302
+ class: DROPDOWN_ROOT_CLASS
303
+ )
304
+ end
305
+
306
+ # The grid is the control, so a caller who names this component names the
307
+ # *table* — and keeps the month, which is the other half of what a screen
308
+ # reader needs here. `aria-labelledby` wins over `aria-label` outright, so
309
+ # the two are combined by id rather than left to fight: "Starts on,
310
+ # August 2026".
311
+ def grid(shown)
312
+ tag.table(
313
+ safe_join([ weekday_header(shown), weeks(shown) ]),
314
+ class: GRID_CLASS,
315
+ role: "grid",
316
+ "data-shadcn--calendar-target": "grid",
317
+ "data-action": "keydown->shadcn--calendar#keydown focusin->shadcn--calendar#focused",
318
+ "aria-multiselectable": ("true" if mode != :single),
319
+ "aria-labelledby": "#{attributes[:"aria-labelledby"]} #{caption_id(shown)}".strip,
320
+ "aria-label": (label_of(shown) if attributes[:"aria-labelledby"].blank?)
321
+ )
322
+ end
323
+
324
+ def caption_id(shown)
325
+ @caption_ids ||= Hash.new { |ids, key| ids[key] = "shadcn-calendar-caption-#{SecureRandom.hex(4)}" }
326
+ @caption_ids[shown.first]
327
+ end
328
+
329
+ # `aria-hidden` on the whole header row, which is upstream's: the day
330
+ # names repeat in every cell's own label, so a screen reader reading the
331
+ # grid would say each one eight times.
332
+ def weekday_header(shown)
333
+ cells = shown.weekday_numbers.map do |number|
334
+ tag.th(short_day_names[number], class: WEEKDAY_CLASS, scope: "col",
335
+ "aria-label": day_names[number])
336
+ end
337
+ if show_week_number
338
+ # No text and a name of its own: upstream's `formatWeekNumberHeader`
339
+ # returns an empty string and the column is named by its `aria-label`.
340
+ cells.unshift(tag.th(nil, class: WEEK_NUMBER_HEADER_CLASS, scope: "col",
341
+ "aria-label": shadcn_t("calendar.week_number_header")))
342
+ end
343
+
344
+ tag.thead(tag.tr(safe_join(cells), class: WEEKDAYS_CLASS), "aria-hidden": "true")
345
+ end
346
+
347
+ def weeks(shown)
348
+ tag.tbody(safe_join(shown.weeks.map { |week| week_row(week, shown) }),
349
+ "data-shadcn--calendar-target": "weeks")
350
+ end
351
+
352
+ def week_row(week, shown)
353
+ cells = week.map { |day| day_cell(day, shown) }
354
+ cells.unshift(week_number_cell(week, shown)) if show_week_number
355
+
356
+ tag.tr(safe_join(cells), class: WEEK_CLASS)
357
+ end
358
+
359
+ # A `<td>`, not the `<th>` the package renders: `calendar.tsx` overrides
360
+ # `WeekNumber` with one (calendar.tsx:167-175), and the `scope` and role
361
+ # come from DayPicker either way.
362
+ def week_number_cell(week, shown)
363
+ number = shown.number_of(week)
364
+
365
+ tag.td(
366
+ tag.div(number, class: WEEK_NUMBER_INNER_CLASS),
367
+ class: WEEK_NUMBER_CLASS, scope: "row", role: "rowheader",
368
+ "aria-label": shadcn_t("calendar.week_number", number:)
369
+ )
370
+ end
371
+
372
+ # The cell carries the state and the button carries the label: upstream
373
+ # puts `data-selected`, `data-today` and the rest on the `<td>`, and the
374
+ # classes that read them are written against that.
375
+ def day_cell(day, shown)
376
+ outside = shown.outside?(day)
377
+ hidden = outside && !show_outside_days
378
+
379
+ tag.td(
380
+ (day_button(day, shown) unless hidden),
381
+ class: day_classes(day, outside:, hidden:),
382
+ role: "gridcell",
383
+ "data-day": day.iso8601,
384
+ "data-month": (day.strftime("%Y-%m") if outside),
385
+ "data-outside": ("true" if outside),
386
+ "data-today": ("true" if day == Date.current),
387
+ "data-selected": ("true" if selected?(day)),
388
+ "data-disabled": ("true" if disabled?(day)),
389
+ "data-hidden": ("true" if hidden),
390
+ "aria-selected": ("true" if selected?(day))
391
+ )
392
+ end
393
+
394
+ def day_classes(day, outside:, hidden:)
395
+ [
396
+ DAY_CLASS,
397
+ show_week_number ? DAY_FIRST_WITH_WEEK_NUMBER_CLASS : DAY_FIRST_CLASS,
398
+ (TODAY_CLASS if day == Date.current),
399
+ (OUTSIDE_CLASS if outside),
400
+ (DISABLED_CLASS if disabled?(day)),
401
+ (HIDDEN_CLASS if hidden),
402
+ (RANGE_START_CLASS if range_start?(day)),
403
+ (RANGE_MIDDLE_CLASS if range_middle?(day)),
404
+ (RANGE_END_CLASS if range_end?(day))
405
+ ].compact.join(" ")
406
+ end
407
+
408
+ # A real `Button`, as upstream's `CalendarDayButton` is — so it carries
409
+ # `data-slot="button"` and the variant classes rather than a copy of them.
410
+ #
411
+ # `data-day` here is the *formatted* date where the cell's is ISO, which
412
+ # is upstream's split too (`day.date.toLocaleDateString()` against
413
+ # `day.isoDate`). The machine-readable one is the cell's.
414
+ def day_button(day, shown)
415
+ render(Button::Component.new(
416
+ variant: :ghost,
417
+ size: :icon,
418
+ class: DAY_BUTTON_CLASS,
419
+ type: "button",
420
+ disabled: disabled?(day),
421
+ tabindex: (focus_target?(day, shown) ? "0" : "-1"),
422
+ "data-action": "click->shadcn--calendar#select",
423
+ "data-day": I18n.l(day, format: :default),
424
+ "data-selected-single": ("true" if mode == :single && selected?(day)),
425
+ "data-range-start": ("true" if range_start?(day)),
426
+ "data-range-middle": ("true" if range_middle?(day)),
427
+ "data-range-end": ("true" if range_end?(day)),
428
+ "aria-label": day_label(day)
429
+ )) { day.day.to_s }
430
+ end
431
+
432
+ # Exactly one day in the grid is tabbable, and the rest are reached with
433
+ # the arrow keys — the grid pattern, and what upstream's `isFocusTarget`
434
+ # does. The selected day if it is here, otherwise today, otherwise the
435
+ # first day of the month.
436
+ #
437
+ # A disabled candidate is passed over rather than taken: a tab stop on a
438
+ # disabled button is no tab stop at all, and the grid would drop out of
439
+ # the tab order for a calendar whose selected day happens to be blocked.
440
+ # The controller repeats this rule after every re-render, because a month
441
+ # with nothing tabbable is a month the keyboard cannot reach.
442
+ # One tab stop per grid, so a two-month calendar has two — each grid is a
443
+ # `role="grid"` of its own and the arrows walk between them by date.
444
+ def focus_target(shown)
445
+ @focus_targets ||= {}
446
+ @focus_targets[shown.first] ||= [
447
+ selected.find { |date| shown.days.include?(date) },
448
+ (Date.current if shown.days.include?(Date.current)),
449
+ shown.first
450
+ ].compact.find { |date| !disabled?(date) } || shown.first
451
+ end
452
+
453
+ def focus_target?(day, shown) = day == focus_target(shown)
454
+
455
+ def selected?(day)
456
+ return @range.cover?(day) if @range
457
+
458
+ selected.include?(day)
459
+ end
460
+
461
+ def range_start?(day) = @range && day == @range.begin
462
+ def range_end?(day) = @range && day == @range.end
463
+ def range_middle?(day) = @range && @range.cover?(day) && day != @range.begin && day != @range.end
464
+
465
+ def disabled?(day)
466
+ out_of_bounds?(day) || Array.wrap(disabled).any? { |matcher| matcher === day } # rubocop:disable Style/CaseEquality
467
+ end
468
+
469
+ def out_of_bounds?(day)
470
+ (start_month && day.end_of_month < start_month.beginning_of_month) ||
471
+ (end_month && day.beginning_of_month > end_month.end_of_month)
472
+ end
473
+
474
+ MONTH_FORMAT = "%B %Y"
475
+
476
+ # What a Rails form receives. One input per value, named the way the mode
477
+ # means it:
478
+ #
479
+ # | `mode:` | `name: "trip[on]"` becomes |
480
+ # |-------------|---------------------------------------|
481
+ # | `:single` | `trip[on]` |
482
+ # | `:multiple` | `trip[on][]`, one per day |
483
+ # | `:range` | `trip[on][from]` and `trip[on][to]` |
484
+ #
485
+ # A range can also be given two names of its own —
486
+ # `name: %w[trip[from] trip[to]]` — because two dates are usually two
487
+ # columns, and `starts_on`/`ends_on` is what a Rails model calls them.
488
+ #
489
+ # An empty value is still submitted, so that clearing a date reaches the
490
+ # controller: a parameter that simply vanishes leaves the old value in
491
+ # place, which is the same trap Rails' own checkbox hidden field exists to
492
+ # avoid.
493
+ def inputs
494
+ return if input_names.empty?
495
+
496
+ tag.span(safe_join(input_values.map { |field, value|
497
+ tag.input(type: "hidden", name: field, value:, autocomplete: "off")
498
+ }), "data-shadcn--calendar-target": "inputs")
499
+ end
500
+
501
+ def input_names
502
+ return [] if name.blank?
503
+ return Array(name) if name.is_a?(Array)
504
+
505
+ case mode
506
+ when :multiple then [ "#{name}[]" ]
507
+ when :range then [ "#{name}[from]", "#{name}[to]" ]
508
+ else [ name.to_s ]
509
+ end
510
+ end
511
+
512
+ def input_values
513
+ return input_names.zip(selected.map(&:iso8601)).map { |field, value| [ field, value.to_s ] } if mode == :range
514
+
515
+ return [ [ input_names.first, "" ] ] if selected.empty?
516
+
517
+ selected.map { |date| [ input_names.first, date.iso8601 ] }
518
+ end
519
+
520
+ def label_of(shown) = I18n.l(shown.date, format: MONTH_FORMAT)
521
+
522
+ # The two lists as the controller indexes them: weekdays by `Date#wday`,
523
+ # months from zero, which is what `getDay()` and `getMonth()` hand back.
524
+ def month_names = I18n.t("date.month_names").compact
525
+ def short_month_names = I18n.t("date.abbr_month_names").compact
526
+
527
+ def selected_dates = selected.map(&:iso8601)
528
+
529
+ # A `Date` and a `Range` of them cross to the browser as themselves. A
530
+ # callable cannot — it decided the month the server rendered, and says
531
+ # nothing about a month reached from the nav. Named here rather than left
532
+ # to be discovered; see features/calendar.md.
533
+ def portable_matchers
534
+ Array.wrap(disabled).filter_map do |matcher|
535
+ case matcher
536
+ when Date then matcher.iso8601
537
+ when Range then { from: matcher.begin&.to_date&.iso8601, to: matcher.end&.to_date&.iso8601 }
538
+ end
539
+ end
540
+ end
541
+
542
+ # One pattern, localized once and then handed to the controller as text —
543
+ # rather than a weekday glued onto a formatted date on one side and not
544
+ # the other, which is how the two first disagreed.
545
+ def day_label_format = "%A, #{I18n.t('date.formats.long')}"
546
+
547
+ def day_label(day)
548
+ label = I18n.l(day, format: day_label_format)
549
+ label = shadcn_t("calendar.today_label", date: label) if day == Date.current
550
+ label = shadcn_t("calendar.selected_label", date: label) if selected?(day)
551
+ label
552
+ end
553
+
554
+ def day_names = I18n.t("date.day_names")
555
+ def short_day_names = I18n.t("date.abbr_day_names")
556
+ end
557
+ end
558
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ module Calendar
5
+ # One month, as a grid of weeks.
6
+ #
7
+ # This is the part `react-day-picker` mostly *is* — `helpers/getDates.js`,
8
+ # `helpers/getWeeks.js` and the 576 lines of `classes/DateLib.js` that wrap
9
+ # `date-fns` — and it is the part Ruby already has. `Date` does the
10
+ # arithmetic and `I18n` does the names, so nothing here needs a dependency.
11
+ #
12
+ # A plain object rather than a component: it renders nothing, and being
13
+ # able to assert on a grid without a browser is worth more than the
14
+ # symmetry.
15
+ class Month
16
+ DAYS_IN_WEEK = 7
17
+
18
+ attr_reader :date, :week_starts_on, :fixed_weeks
19
+
20
+ # `week_starts_on` is a weekday number, 0 for Sunday, matching
21
+ # `Date#wday` — and defaulting to the host application's own
22
+ # `Date.beginning_of_week` rather than to a constant, because that is the
23
+ # setting a Rails app has already made. Upstream takes it from the locale.
24
+ def initialize(date = Date.current, week_starts_on: nil, fixed_weeks: false)
25
+ @date = date.to_date
26
+ @week_starts_on = week_starts_on || Date::DAYNAMES.index(Date.beginning_of_week.to_s.capitalize)
27
+ @fixed_weeks = fixed_weeks
28
+ end
29
+
30
+ def first = date.beginning_of_month
31
+ def last = date.end_of_month
32
+
33
+ # Every date the grid holds, including the days either side that fill the
34
+ # first and last weeks out. Six rows when `fixed_weeks` is asked for, so a
35
+ # calendar that is navigated does not change height under the pointer —
36
+ # upstream's `fixedWeeks`, and the reason it exists.
37
+ def weeks
38
+ start = shift_back(first)
39
+ rows = fixed_weeks ? 6 : ((shift_forward(last) - start + 1) / DAYS_IN_WEEK)
40
+
41
+ Array.new(rows) do |row|
42
+ Array.new(DAYS_IN_WEEK) { |column| start + (row * DAYS_IN_WEEK) + column }
43
+ end
44
+ end
45
+
46
+ def days = weeks.flatten
47
+
48
+ def outside?(day) = day.month != date.month
49
+
50
+ # ISO-8601, which is `Date#cweek`. Upstream's week number follows the
51
+ # locale and can differ — the US counts from a different week — and this
52
+ # port does not reproduce that; see features/calendar.md.
53
+ def number_of(week) = week.first.cweek
54
+
55
+ def previous = self.class.new(first.prev_month, **options)
56
+ def next = self.class.new(first.next_month, **options)
57
+ def plus_months(count) = self.class.new(first + count.months, **options)
58
+
59
+ # Sunday-first `Date::DAYNAMES`, rotated to whichever day the week starts
60
+ # on. The names themselves come from `I18n` at render time, not from here.
61
+ def weekday_numbers
62
+ Array.new(DAYS_IN_WEEK) { |offset| (week_starts_on + offset) % DAYS_IN_WEEK }
63
+ end
64
+
65
+ private
66
+
67
+ def options = { week_starts_on:, fixed_weeks: }
68
+
69
+ def shift_back(day) = day - ((day.wday - week_starts_on) % DAYS_IN_WEEK)
70
+
71
+ def shift_forward(day) = day + ((week_starts_on - 1 - day.wday) % DAYS_IN_WEEK)
72
+ end
73
+ end
74
+ end