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,633 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { readDirection } from "shadcn/direction"
3
+
4
+ // The calendar is rendered by the server — a month is a `<table>`, and Ruby
5
+ // builds it in `Calendar::Month`. This moves it: the nav, the two dropdowns and
6
+ // the grid's keyboard, plus the re-render each of those asks for.
7
+ //
8
+ // Which means the grid exists twice, and that is the one duplication this port
9
+ // introduces. Two rules keep it from drifting, and both are the reason the
10
+ // values below are as long as they are:
11
+ //
12
+ // 1. **No text of its own.** Month names, weekday names, the date format and
13
+ // every label template come from the server, so `I18n` stays the only thing
14
+ // that decides how a date reads. A `toLocaleDateString` here would answer to
15
+ // the *browser's* locale, and the month you navigated to would be spelled
16
+ // differently from the one you started on.
17
+ // 2. **No class of its own.** The modifier classes arrive as values. Tailwind
18
+ // scans source text and does not read this file, and `parity_spec` reads the
19
+ // Ruby, so a class written here would be both uncompiled and unchecked.
20
+ //
21
+ // What it does build is structure, and even that is cloned from what the server
22
+ // rendered rather than written out.
23
+ export default class extends Controller {
24
+ static targets = [ "grid", "weeks", "caption", "previous", "next", "monthSelect", "yearSelect", "inputs" ]
25
+
26
+ static values = {
27
+ month: String,
28
+ // The application's today, not the browser's. `Time.zone` and a laptop's
29
+ // clock disagree by a day for a few hours out of every twenty-four, and the
30
+ // server has already marked a cell `data-today` from its own — so a
31
+ // `new Date()` here would light one day and count presets from another.
32
+ today: String,
33
+ weekStartsOn: { type: Number, default: 1 },
34
+ fixedWeeks: Boolean,
35
+ showOutsideDays: { type: Boolean, default: true },
36
+ showWeekNumber: Boolean,
37
+ startMonth: String,
38
+ endMonth: String,
39
+ mode: { type: String, default: "single" },
40
+ required: Boolean,
41
+ inputNames: Array,
42
+ selected: Array,
43
+ disabled: Array,
44
+ dayNames: Array,
45
+ shortDayNames: Array,
46
+ monthNames: Array,
47
+ shortMonthNames: Array,
48
+ dateFormat: String,
49
+ monthFormat: String,
50
+ todayLabel: String,
51
+ selectedLabel: String,
52
+ weekNumberLabel: String,
53
+ todayClass: String,
54
+ outsideClass: String,
55
+ disabledClass: String,
56
+ hiddenClass: String,
57
+ rangeStartClass: String,
58
+ rangeMiddleClass: String,
59
+ rangeEndClass: String
60
+ }
61
+
62
+ connect() {
63
+ this.direction = readDirection(this.element)
64
+ // One nav, one keyboard, and as many grids as `number_of_months` asked
65
+ // for — so everything below indexes by grid, and the month a grid shows is
66
+ // the root's month plus its own offset.
67
+ // The prototypes for a re-render, taken from the server's own output: a
68
+ // week row, and a cell with every modifier class stripped back off it. The
69
+ // alternative is markup in a template literal here, which is the thing
70
+ // rule 2 above is about.
71
+ this.rowPrototype = this.weeksTargets[0].querySelector("tr").cloneNode(true)
72
+ this.cellPrototype = this.plainCell()
73
+ this.baseCellClass = this.cellPrototype.className
74
+ this.numberPrototype = this.showWeekNumberValue
75
+ ? this.weeksTargets[0].querySelector("td:not([data-day])").cloneNode(true)
76
+ : null
77
+ }
78
+
79
+ previous(event) {
80
+ event.preventDefault()
81
+ this.goTo(this.shiftMonths(this.month, -1))
82
+ }
83
+
84
+ next(event) {
85
+ event.preventDefault()
86
+ this.goTo(this.shiftMonths(this.month, 1))
87
+ }
88
+
89
+ chooseMonth(event) {
90
+ const month = this.month
91
+ month.setMonth(Number(event.target.value) - 1)
92
+ this.goTo(month)
93
+ }
94
+
95
+ chooseYear(event) {
96
+ const year = this.month
97
+ year.setFullYear(Number(event.target.value))
98
+ this.goTo(year)
99
+ }
100
+
101
+ // `react-day-picker`'s own three, read from `selection/useSingle.js`,
102
+ // `selection/useMulti.js` and `utils/addToRange.js` rather than invented:
103
+ // single toggles off unless it was asked to be `required`, multiple toggles
104
+ // each day, and a range grows from one end or starts again.
105
+ select(event) {
106
+ const cell = event.target.closest("td[data-day]")
107
+ if (!cell || cell.dataset.disabled) return
108
+
109
+ event.preventDefault()
110
+ const day = cell.dataset.day
111
+ this.selectedValue = this.selectionWith(day)
112
+ this.repaint()
113
+ this.writeInputs()
114
+ this.dispatch("select", { detail: { selected: this.selectedValue, day } })
115
+ }
116
+
117
+ // A day counted from today, which is what a preset is: upstream's own example
118
+ // holds five of them in React state, and a Rails app has no state to hold
119
+ // them in — so the offset rides on the button and the controller does the
120
+ // arithmetic it is already doing.
121
+ preset({ params: { offset } }) {
122
+ const day = this.parse(this.todayValue)
123
+ day.setDate(day.getDate() + Number(offset || 0))
124
+
125
+ this.selectedValue = this.modeValue === "range" ? [ this.iso(day), this.iso(day) ] : [ this.iso(day) ]
126
+ this.goTo(day)
127
+ this.repaint()
128
+ this.writeInputs()
129
+ this.dispatch("select", { detail: { selected: this.selectedValue, day: this.iso(day) } })
130
+ }
131
+
132
+ selectionWith(day) {
133
+ if (this.modeValue === "multiple") return this.multipleWith(day)
134
+ if (this.modeValue === "range") return this.rangeWith(day)
135
+
136
+ return this.selectedValue.includes(day) && !this.requiredValue ? [] : [ day ]
137
+ }
138
+
139
+ multipleWith(day) {
140
+ return this.selectedValue.includes(day)
141
+ ? this.selectedValue.filter((selected) => selected !== day)
142
+ : [ ...this.selectedValue, day ].sort()
143
+ }
144
+
145
+ // `addToRange`, with its cases in its order: nothing yet, one end only, or a
146
+ // finished range that a third click starts over.
147
+ rangeWith(day) {
148
+ const [ from, to ] = this.selectedValue
149
+
150
+ if (!from) return [ day, day ]
151
+
152
+ if (from && !to) return day < from ? [ day, from ] : [ from, day ]
153
+
154
+ if (day === from && day === to) return this.requiredValue ? [ from, to ] : []
155
+ if (day === from || day === to) return [ day, day ]
156
+ if (day < from) return [ day, to ]
157
+
158
+ return [ from, day ]
159
+ }
160
+
161
+ // What a Rails form receives, rewritten from the selection. `<input
162
+ // type=hidden>` is the one element this file builds rather than clones: it
163
+ // carries no class, so nothing here is invisible to Tailwind or to parity.
164
+ writeInputs() {
165
+ if (!this.hasInputsTarget || !this.inputNamesValue.length) return
166
+
167
+ const pairs = this.modeValue === "range"
168
+ ? this.inputNamesValue.map((name, index) => [ name, this.selectedValue[index] || "" ])
169
+ : (this.selectedValue.length
170
+ ? this.selectedValue.map((value) => [ this.inputNamesValue[0], value ])
171
+ : [ [ this.inputNamesValue[0], "" ] ])
172
+
173
+ this.inputsTarget.replaceChildren(...pairs.map(([ name, value ]) => {
174
+ const input = document.createElement("input")
175
+ input.type = "hidden"
176
+ input.autocomplete = "off"
177
+ input.name = name
178
+ input.value = value
179
+ return input
180
+ }))
181
+ }
182
+
183
+ // The grid pattern: one day is tabbable and the arrows reach the rest. Radix
184
+ // is not the reference here — `react-day-picker`'s own `DayPicker.js:176-190`
185
+ // is, and it moves by day, by week, to the ends of the week, and by month or
186
+ // year on Page keys.
187
+ keydown(event) {
188
+ const from = this.dayOf(event.target)
189
+ if (!from) return
190
+
191
+ const back = this.direction === "rtl" ? 1 : -1
192
+ const moves = {
193
+ ArrowLeft: [ "day", back ],
194
+ ArrowRight: [ "day", -back ],
195
+ ArrowUp: [ "week", -1 ],
196
+ ArrowDown: [ "week", 1 ],
197
+ Home: [ "weekStart", 0 ],
198
+ End: [ "weekEnd", 0 ],
199
+ PageUp: [ event.shiftKey ? "year" : "month", -1 ],
200
+ PageDown: [ event.shiftKey ? "year" : "month", 1 ]
201
+ }
202
+ const move = moves[event.key]
203
+ if (!move) return
204
+
205
+ event.preventDefault()
206
+ this.focusDay(this.step(from, ...move))
207
+ }
208
+
209
+ // A day that is focused becomes the tabbable one, so tabbing away and back
210
+ // returns to where the reader was rather than to the top of the month.
211
+ focused(event) {
212
+ const day = this.dayOf(event.target)
213
+ if (day) this.markFocusTarget(event.target)
214
+ }
215
+
216
+ get month() {
217
+ return this.parse(this.monthValue)
218
+ }
219
+
220
+ // --- moving ---------------------------------------------------------------
221
+
222
+ step(from, unit, by) {
223
+ const to = new Date(from)
224
+
225
+ if (unit === "day") to.setDate(to.getDate() + by)
226
+ if (unit === "week") to.setDate(to.getDate() + by * 7)
227
+ if (unit === "month") to.setMonth(to.getMonth() + by)
228
+ if (unit === "year") to.setFullYear(to.getFullYear() + by)
229
+ if (unit === "weekStart") to.setDate(to.getDate() - this.offsetInWeek(to))
230
+ if (unit === "weekEnd") to.setDate(to.getDate() + (6 - this.offsetInWeek(to)))
231
+
232
+ return this.clampToBounds(to)
233
+ }
234
+
235
+ // Focus follows the day even when the day is in another month — which is what
236
+ // makes ArrowRight on the last of the month work at all. The month changes
237
+ // under the focus, and the focus is restored once the new grid is there.
238
+ focusDay(date) {
239
+ const iso = this.iso(date)
240
+ if (!this.isDisplayed(date)) this.goTo(date)
241
+
242
+ const button = this.buttonFor(iso)
243
+ if (!button) return
244
+
245
+ this.markFocusTarget(button)
246
+ button.focus()
247
+ }
248
+
249
+ goTo(date) {
250
+ const target = this.clampToBounds(date)
251
+ const first = new Date(target.getFullYear(), target.getMonth(), 1)
252
+ if (this.iso(first) === this.monthValue) return
253
+
254
+ this.monthValue = this.iso(first)
255
+ this.render()
256
+ this.dispatch("month", { detail: { month: this.monthValue } })
257
+ }
258
+
259
+ // Compared as ISO text rather than as `Date`s: the bounds arrive as strings,
260
+ // and `new Date("2026-01-01")` is parsed as UTC where `new Date(y, m, d)` is
261
+ // local — a comparison between the two is off by a day for half the world.
262
+ clampToBounds(date) {
263
+ const iso = this.iso(date)
264
+
265
+ if (this.startMonthValue && iso < this.startMonthValue) return this.parse(this.startMonthValue)
266
+ if (this.endMonthValue && iso > this.endMonthValue) return this.parse(this.endMonthValue)
267
+
268
+ return date
269
+ }
270
+
271
+ // --- rendering ------------------------------------------------------------
272
+
273
+ render() {
274
+ this.weeksTargets.forEach((weeks, index) => {
275
+ const month = this.monthAt(index)
276
+
277
+ weeks.replaceChildren(...this.weeks(month).map((week) => this.row(week, month)))
278
+ this.captionTargets[index].textContent = this.formatMonth(month)
279
+ // Only where this port put one: with a caller's own `aria-labelledby` the
280
+ // grid is named by that and by the caption's id, and neither changes here.
281
+ if (this.gridTargets[index].hasAttribute("aria-label")) {
282
+ this.gridTargets[index].setAttribute("aria-label", this.formatMonth(month))
283
+ }
284
+ })
285
+
286
+ this.updateNav()
287
+ this.updateDropdowns()
288
+ this.markInitialFocusTarget()
289
+ }
290
+
291
+ monthAt(index) {
292
+ return new Date(this.month.getFullYear(), this.month.getMonth() + index, 1)
293
+ }
294
+
295
+ // The Ruby rule, repeated: the selected day if it is in this grid, otherwise
296
+ // today, otherwise the first of the month — and never a disabled one. Without
297
+ // it a navigated month has no tabbable day at all and the grid leaves the tab
298
+ // order, which is how this first worked.
299
+ // One tab stop per grid, which is what the server renders too: each grid is a
300
+ // `role="grid"` of its own.
301
+ markInitialFocusTarget() {
302
+ this.weeksTargets.forEach((weeks, index) => {
303
+ const month = this.monthAt(index)
304
+ const candidates = [ ...this.selectedValue, this.todayValue, this.iso(month) ]
305
+
306
+ for (const iso of candidates) {
307
+ const button = weeks.querySelector(`td[data-day="${iso}"]:not([data-outside]) button`)
308
+ if (button && !button.disabled) {
309
+ for (const other of weeks.querySelectorAll("button")) other.tabIndex = -1
310
+ button.tabIndex = 0
311
+ return
312
+ }
313
+ }
314
+ })
315
+ }
316
+
317
+ // The same grid `Calendar::Month#weeks` builds, and it has to stay the same:
318
+ // the first day of the week the month starts in, through the last day of the
319
+ // week it ends in, six rows when a fixed height was asked for.
320
+ weeks(month) {
321
+ const first = new Date(month.getFullYear(), month.getMonth(), 1)
322
+ const start = new Date(first)
323
+ start.setDate(start.getDate() - this.offsetInWeek(first))
324
+
325
+ const last = new Date(month.getFullYear(), month.getMonth() + 1, 0)
326
+ const span = Math.round((last - start) / 86400000) + 1
327
+ const rows = this.fixedWeeksValue ? 6 : Math.ceil(span / 7)
328
+
329
+ return Array.from({ length: rows }, (_, row) =>
330
+ Array.from({ length: 7 }, (_, column) => {
331
+ const day = new Date(start)
332
+ day.setDate(start.getDate() + row * 7 + column)
333
+ return day
334
+ }))
335
+ }
336
+
337
+ row(week, month) {
338
+ const row = this.rowPrototype.cloneNode(false)
339
+
340
+ if (this.numberPrototype) row.appendChild(this.weekNumber(week))
341
+ for (const day of week) row.appendChild(this.cell(day, month))
342
+
343
+ return row
344
+ }
345
+
346
+ weekNumber(week) {
347
+ const cell = this.numberPrototype.cloneNode(true)
348
+ const number = this.isoWeek(week[0])
349
+
350
+ cell.querySelector("div").textContent = number
351
+ cell.setAttribute("aria-label", this.weekNumberLabelValue.replace("%{number}", number))
352
+
353
+ return cell
354
+ }
355
+
356
+ cell(day, month) {
357
+ const cell = this.cellPrototype.cloneNode(true)
358
+ this.applyState(cell, day, month)
359
+
360
+ return cell
361
+ }
362
+
363
+ // Selecting repaints rather than rebuilds: replacing the rows would take the
364
+ // button the pointer or the keyboard is on out of the document, and the focus
365
+ // with it.
366
+ repaint() {
367
+ this.weeksTargets.forEach((weeks, index) => {
368
+ for (const cell of weeks.querySelectorAll("td[data-day]")) {
369
+ this.applyState(cell, this.parse(cell.dataset.day), this.monthAt(index))
370
+ }
371
+ })
372
+ }
373
+
374
+ applyState(cell, day, month) {
375
+ const outside = day.getMonth() !== month.getMonth()
376
+ const hidden = outside && !this.showOutsideDaysValue
377
+ const selected = this.isSelected(day)
378
+ const disabled = this.isDisabled(day)
379
+
380
+ cell.className = [
381
+ this.baseCellClass,
382
+ this.isToday(day) && this.todayClassValue,
383
+ outside && this.outsideClassValue,
384
+ disabled && this.disabledClassValue,
385
+ hidden && this.hiddenClassValue,
386
+ this.isRangeStart(day) && this.rangeStartClassValue,
387
+ this.isRangeMiddle(day) && this.rangeMiddleClassValue,
388
+ this.isRangeEnd(day) && this.rangeEndClassValue
389
+ ].filter(Boolean).join(" ")
390
+
391
+ this.set(cell, "data-day", this.iso(day))
392
+ this.set(cell, "data-month", outside ? this.iso(day).slice(0, 7) : null)
393
+ this.set(cell, "data-outside", outside ? "true" : null)
394
+ this.set(cell, "data-today", this.isToday(day) ? "true" : null)
395
+ this.set(cell, "data-selected", selected ? "true" : null)
396
+ this.set(cell, "data-disabled", disabled ? "true" : null)
397
+ this.set(cell, "data-hidden", hidden ? "true" : null)
398
+ this.set(cell, "aria-selected", selected ? "true" : null)
399
+
400
+ const button = cell.querySelector("button")
401
+ if (hidden) button?.remove()
402
+ else if (button) this.dayButton(button, day, { selected, disabled })
403
+ }
404
+
405
+ dayButton(button, day, { selected, disabled }) {
406
+ button.textContent = String(day.getDate())
407
+ button.disabled = disabled
408
+ if (button.tabIndex !== 0) button.tabIndex = -1
409
+ this.set(button, "data-day", this.format(day, this.dateFormatValue))
410
+ this.set(button, "aria-label", this.dayLabel(day, selected))
411
+ this.set(button, "data-selected-single", this.modeValue === "single" && selected ? "true" : null)
412
+ this.set(button, "data-range-start", this.isRangeStart(day) ? "true" : null)
413
+ this.set(button, "data-range-middle", this.isRangeMiddle(day) ? "true" : null)
414
+ this.set(button, "data-range-end", this.isRangeEnd(day) ? "true" : null)
415
+ }
416
+
417
+ // Exactly one button in the grid is tabbable: the selected day if it is in
418
+ // this month, otherwise today, otherwise the first of the month.
419
+ markFocusTarget(button) {
420
+ for (const other of this.buttons) other.tabIndex = -1
421
+ button.tabIndex = 0
422
+ }
423
+
424
+ // A day shown in two grids at once — the last of August is also an outside
425
+ // day in September's — belongs to the grid it is not outside of.
426
+ buttonFor(iso) {
427
+ return this.element.querySelector(`td[data-day="${iso}"]:not([data-outside]) button`) ||
428
+ this.element.querySelector(`td[data-day="${iso}"] button`)
429
+ }
430
+
431
+ updateNav() {
432
+ const back = this.shiftMonths(this.month, -1)
433
+ const forward = this.shiftMonths(this.month, 1)
434
+
435
+ this.mark(this.previousTarget, this.outOfBounds(back))
436
+ this.mark(this.nextTarget, this.outOfBounds(forward))
437
+ }
438
+
439
+ mark(button, unavailable) {
440
+ this.set(button, "aria-disabled", unavailable ? "true" : null)
441
+ this.set(button, "tabindex", unavailable ? "-1" : null)
442
+ }
443
+
444
+ // The dropdowns are the caption, so they follow it rather than lead it — a
445
+ // month reached with the arrow keys has to leave them showing where it went.
446
+ updateDropdowns() {
447
+ if (this.hasMonthSelectTarget) {
448
+ this.monthSelectTarget.value = String(this.month.getMonth() + 1)
449
+ // The *short* name: the options the server rendered are the abbreviated
450
+ // ones, and what is shown is the selected option's own label. Writing the
451
+ // full name here made the two renderers disagree in the one place a
452
+ // person is looking.
453
+ this.label(this.monthSelectTarget, this.shortMonthNamesValue[this.month.getMonth()])
454
+ }
455
+
456
+ if (this.hasYearSelectTarget) {
457
+ this.yearSelectTarget.value = String(this.month.getFullYear())
458
+ this.label(this.yearSelectTarget, String(this.month.getFullYear()))
459
+ }
460
+ }
461
+
462
+ // What is read is the `aria-hidden` span under the invisible select, so the
463
+ // two have to be set together or the calendar says one month and shows
464
+ // another.
465
+ label(select, text) {
466
+ const shown = select.parentElement.querySelector("[aria-hidden=true]")
467
+ if (!shown?.firstChild) return
468
+
469
+ shown.firstChild.textContent = text
470
+ }
471
+
472
+ // --- what a day is --------------------------------------------------------
473
+
474
+ isToday(day) {
475
+ return this.iso(day) === this.todayValue
476
+ }
477
+
478
+ isSelected(day) {
479
+ if (this.range) return this.iso(day) >= this.range.from && this.iso(day) <= this.range.to
480
+
481
+ return this.selectedValue.includes(this.iso(day))
482
+ }
483
+
484
+ isRangeStart(day) { return Boolean(this.range) && this.iso(day) === this.range.from }
485
+ isRangeEnd(day) { return Boolean(this.range) && this.iso(day) === this.range.to }
486
+
487
+ isRangeMiddle(day) {
488
+ return this.isSelected(day) && Boolean(this.range) && !this.isRangeStart(day) && !this.isRangeEnd(day)
489
+ }
490
+
491
+ get range() {
492
+ return this.modeValue === "range" && this.selectedValue.length === 2
493
+ ? { from: this.selectedValue[0], to: this.selectedValue[1] }
494
+ : null
495
+ }
496
+
497
+ // A `disabled:` that is a Date or a Range crosses to the browser as itself. A
498
+ // callable cannot, and does not: it decided the month the server rendered and
499
+ // says nothing about the ones reached from here — see features/calendar.md.
500
+ isDisabled(day) {
501
+ const iso = this.iso(day)
502
+ if (this.outOfBounds(day)) return true
503
+
504
+ return this.disabledValue.some((matcher) =>
505
+ typeof matcher === "string" ? matcher === iso : iso >= matcher.from && iso <= matcher.to)
506
+ }
507
+
508
+ // At the granularity the bounds are about: `start_month` and `end_month` name
509
+ // months, so a day in the same month as the bound is inside it.
510
+ outOfBounds(date) {
511
+ const month = this.iso(date).slice(0, 7)
512
+
513
+ return Boolean(this.startMonthValue && month < this.startMonthValue.slice(0, 7)) ||
514
+ Boolean(this.endMonthValue && month > this.endMonthValue.slice(0, 7))
515
+ }
516
+
517
+ // True of any month on screen, not only the first: with two months shown,
518
+ // stepping off the end of the first lands in the second and nothing moves.
519
+ isDisplayed(date) {
520
+ return this.weeksTargets.some((_, index) => {
521
+ const month = this.monthAt(index)
522
+ return date.getFullYear() === month.getFullYear() && date.getMonth() === month.getMonth()
523
+ })
524
+ }
525
+
526
+ // --- reading the DOM ------------------------------------------------------
527
+
528
+ get buttons() {
529
+ return this.weeksTargets.flatMap((weeks) => [ ...weeks.querySelectorAll("button") ])
530
+ }
531
+
532
+ dayOf(element) {
533
+ const cell = element.closest("td[data-day]")
534
+
535
+ return cell ? this.parse(cell.dataset.day) : null
536
+ }
537
+
538
+ // A cell with the state stripped off it, which is what every later cell is
539
+ // built from. Taken from a plain day rather than assembled: an outside or a
540
+ // selected one would carry classes this then has to guess at removing.
541
+ plainCell() {
542
+ const plain = this.weeksTargets[0].querySelector(
543
+ "td[data-day]:not([data-today]):not([data-outside]):not([data-selected]):not([data-disabled])"
544
+ )
545
+ const cell = (plain || this.weeksTargets[0].querySelector("td[data-day]")).cloneNode(true)
546
+
547
+ for (const state of [ "day", "month", "outside", "today", "selected", "disabled", "hidden" ]) {
548
+ delete cell.dataset[state]
549
+ }
550
+ cell.removeAttribute("aria-selected")
551
+ // The cell this was taken from may have been the tabbable one, and every
552
+ // later cell is a copy of it — one tab stop would have become thirty-five.
553
+ const button = cell.querySelector("button")
554
+ if (button) button.tabIndex = -1
555
+
556
+ return cell
557
+ }
558
+
559
+ set(element, name, value) {
560
+ if (value === null || value === undefined || value === false) element.removeAttribute(name)
561
+ else element.setAttribute(name, value)
562
+ }
563
+
564
+ // --- dates ----------------------------------------------------------------
565
+
566
+ parse(iso) {
567
+ const [ year, month, day ] = iso.split("-").map(Number)
568
+
569
+ return new Date(year, month - 1, day)
570
+ }
571
+
572
+ shiftMonths(date, by) {
573
+ return new Date(date.getFullYear(), date.getMonth() + by, 1)
574
+ }
575
+
576
+ offsetInWeek(date) {
577
+ return (date.getDay() - this.weekStartsOnValue + 7) % 7
578
+ }
579
+
580
+ iso(date) {
581
+ return [
582
+ date.getFullYear(),
583
+ String(date.getMonth() + 1).padStart(2, "0"),
584
+ String(date.getDate()).padStart(2, "0")
585
+ ].join("-")
586
+ }
587
+
588
+ // ISO-8601, matching `Date#cweek` on the Ruby side: the week holding the
589
+ // Thursday of this week is the week of its year.
590
+ isoWeek(date) {
591
+ const thursday = new Date(date)
592
+ thursday.setDate(thursday.getDate() + 3 - ((date.getDay() + 6) % 7))
593
+ const first = new Date(thursday.getFullYear(), 0, 4)
594
+
595
+ return 1 + Math.round(((thursday - first) / 86400000 - 3 + ((first.getDay() + 6) % 7)) / 7)
596
+ }
597
+
598
+ dayLabel(day, selected) {
599
+ let label = this.format(day, this.dateFormatValue)
600
+ if (this.isToday(day)) label = this.todayLabelValue.replace("%{date}", label)
601
+ if (selected) label = this.selectedLabelValue.replace("%{date}", label)
602
+
603
+ return label
604
+ }
605
+
606
+ formatMonth(date) {
607
+ return this.format(date, this.monthFormatValue)
608
+ }
609
+
610
+ // Enough `strftime` to read the formats Rails' own locales are written in,
611
+ // and no more. The pattern comes from `I18n` with the rest of the text, so a
612
+ // host that has translated `date.formats.long` gets its own order and its own
613
+ // punctuation here rather than this file's idea of them.
614
+ format(date, pattern) {
615
+ const day = date.getDate()
616
+ const month = date.getMonth() + 1
617
+ const tokens = {
618
+ "%A": this.dayNamesValue[date.getDay()],
619
+ "%a": this.shortDayNamesValue[date.getDay()],
620
+ "%B": this.monthNamesValue[date.getMonth()],
621
+ "%b": this.shortMonthNamesValue[date.getMonth()],
622
+ "%d": String(day).padStart(2, "0"),
623
+ "%-d": String(day),
624
+ "%e": String(day).padStart(2, " "),
625
+ "%m": String(month).padStart(2, "0"),
626
+ "%-m": String(month),
627
+ "%Y": String(date.getFullYear()),
628
+ "%y": String(date.getFullYear()).slice(-2)
629
+ }
630
+
631
+ return pattern.replace(/%-?[A-Za-z]/g, (token) => (token in tokens ? tokens[token] : token))
632
+ }
633
+ }