velvetkit 0.1.0.beta

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 (481) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +197 -0
  5. data/lib/generators/velvetkit/add/add_generator.rb +111 -0
  6. data/lib/generators/velvetkit/ai/ai_generator.rb +78 -0
  7. data/lib/generators/velvetkit/helpers.rb +64 -0
  8. data/lib/generators/velvetkit/install/install_generator.rb +108 -0
  9. data/lib/velvetkit/namespace.rb +57 -0
  10. data/lib/velvetkit/reference.rb +108 -0
  11. data/lib/velvetkit/registry.rb +162 -0
  12. data/lib/velvetkit/version.rb +5 -0
  13. data/lib/velvetkit.rb +28 -0
  14. data/registry/app/assets/tailwind/velvetkit.css +342 -0
  15. data/registry/app/components/ui/accordion/content.html.erb +5 -0
  16. data/registry/app/components/ui/accordion/content.rb +34 -0
  17. data/registry/app/components/ui/accordion/item.html.erb +1 -0
  18. data/registry/app/components/ui/accordion/item.rb +40 -0
  19. data/registry/app/components/ui/accordion/trigger.html.erb +1 -0
  20. data/registry/app/components/ui/accordion/trigger.rb +37 -0
  21. data/registry/app/components/ui/accordion.html.erb +7 -0
  22. data/registry/app/components/ui/accordion.rb +34 -0
  23. data/registry/app/components/ui/alert/description.html.erb +1 -0
  24. data/registry/app/components/ui/alert/description.rb +12 -0
  25. data/registry/app/components/ui/alert/title.html.erb +1 -0
  26. data/registry/app/components/ui/alert/title.rb +12 -0
  27. data/registry/app/components/ui/alert.html.erb +16 -0
  28. data/registry/app/components/ui/alert.rb +55 -0
  29. data/registry/app/components/ui/avatar/fallback.html.erb +1 -0
  30. data/registry/app/components/ui/avatar/fallback.rb +12 -0
  31. data/registry/app/components/ui/avatar/image.rb +20 -0
  32. data/registry/app/components/ui/avatar.html.erb +1 -0
  33. data/registry/app/components/ui/avatar.rb +56 -0
  34. data/registry/app/components/ui/avatar_group.html.erb +1 -0
  35. data/registry/app/components/ui/avatar_group.rb +35 -0
  36. data/registry/app/components/ui/badge.html.erb +1 -0
  37. data/registry/app/components/ui/badge.rb +38 -0
  38. data/registry/app/components/ui/banner.html.erb +10 -0
  39. data/registry/app/components/ui/banner.rb +47 -0
  40. data/registry/app/components/ui/base.rb +53 -0
  41. data/registry/app/components/ui/breadcrumb/auto_ellipsis.html.erb +23 -0
  42. data/registry/app/components/ui/breadcrumb/auto_ellipsis.rb +12 -0
  43. data/registry/app/components/ui/breadcrumb/ellipsis.html.erb +18 -0
  44. data/registry/app/components/ui/breadcrumb/ellipsis.rb +8 -0
  45. data/registry/app/components/ui/breadcrumb/item.html.erb +1 -0
  46. data/registry/app/components/ui/breadcrumb/item.rb +12 -0
  47. data/registry/app/components/ui/breadcrumb/item_slot.html.erb +10 -0
  48. data/registry/app/components/ui/breadcrumb/item_slot.rb +47 -0
  49. data/registry/app/components/ui/breadcrumb/list.html.erb +1 -0
  50. data/registry/app/components/ui/breadcrumb/list.rb +12 -0
  51. data/registry/app/components/ui/breadcrumb/separator.html.erb +1 -0
  52. data/registry/app/components/ui/breadcrumb/separator.rb +12 -0
  53. data/registry/app/components/ui/breadcrumb.html.erb +30 -0
  54. data/registry/app/components/ui/breadcrumb.rb +83 -0
  55. data/registry/app/components/ui/button.html.erb +1 -0
  56. data/registry/app/components/ui/button.rb +81 -0
  57. data/registry/app/components/ui/button_to.html.erb +1 -0
  58. data/registry/app/components/ui/button_to.rb +33 -0
  59. data/registry/app/components/ui/card/content.html.erb +1 -0
  60. data/registry/app/components/ui/card/content.rb +12 -0
  61. data/registry/app/components/ui/card/description.html.erb +1 -0
  62. data/registry/app/components/ui/card/description.rb +12 -0
  63. data/registry/app/components/ui/card/footer.html.erb +1 -0
  64. data/registry/app/components/ui/card/footer.rb +12 -0
  65. data/registry/app/components/ui/card/header.html.erb +1 -0
  66. data/registry/app/components/ui/card/header.rb +15 -0
  67. data/registry/app/components/ui/card/title.html.erb +1 -0
  68. data/registry/app/components/ui/card/title.rb +12 -0
  69. data/registry/app/components/ui/card.html.erb +1 -0
  70. data/registry/app/components/ui/card.rb +18 -0
  71. data/registry/app/components/ui/checkbox.html.erb +18 -0
  72. data/registry/app/components/ui/checkbox.rb +133 -0
  73. data/registry/app/components/ui/data_grid.html.erb +2 -0
  74. data/registry/app/components/ui/data_grid.rb +84 -0
  75. data/registry/app/components/ui/date_picker.html.erb +70 -0
  76. data/registry/app/components/ui/date_picker.rb +111 -0
  77. data/registry/app/components/ui/dialog/close.html.erb +7 -0
  78. data/registry/app/components/ui/dialog/close.rb +53 -0
  79. data/registry/app/components/ui/dialog/content.html.erb +26 -0
  80. data/registry/app/components/ui/dialog/content.rb +102 -0
  81. data/registry/app/components/ui/dialog/description.html.erb +1 -0
  82. data/registry/app/components/ui/dialog/description.rb +16 -0
  83. data/registry/app/components/ui/dialog/footer.html.erb +1 -0
  84. data/registry/app/components/ui/dialog/footer.rb +14 -0
  85. data/registry/app/components/ui/dialog/header.html.erb +1 -0
  86. data/registry/app/components/ui/dialog/header.rb +16 -0
  87. data/registry/app/components/ui/dialog/title.html.erb +1 -0
  88. data/registry/app/components/ui/dialog/title.rb +16 -0
  89. data/registry/app/components/ui/dialog/trigger.html.erb +5 -0
  90. data/registry/app/components/ui/dialog/trigger.rb +75 -0
  91. data/registry/app/components/ui/dialog.html.erb +3 -0
  92. data/registry/app/components/ui/dialog.rb +41 -0
  93. data/registry/app/components/ui/divider.html.erb +9 -0
  94. data/registry/app/components/ui/divider.rb +41 -0
  95. data/registry/app/components/ui/icon.html.erb +1 -0
  96. data/registry/app/components/ui/icon.rb +41 -0
  97. data/registry/app/components/ui/image.html.erb +1 -0
  98. data/registry/app/components/ui/image.rb +74 -0
  99. data/registry/app/components/ui/input.html.erb +9 -0
  100. data/registry/app/components/ui/input.rb +127 -0
  101. data/registry/app/components/ui/input_group/addon.html.erb +6 -0
  102. data/registry/app/components/ui/input_group/addon.rb +51 -0
  103. data/registry/app/components/ui/input_group/field.rb +26 -0
  104. data/registry/app/components/ui/input_group.html.erb +5 -0
  105. data/registry/app/components/ui/input_group.rb +55 -0
  106. data/registry/app/components/ui/label.html.erb +2 -0
  107. data/registry/app/components/ui/label.rb +48 -0
  108. data/registry/app/components/ui/link.html.erb +1 -0
  109. data/registry/app/components/ui/link.rb +64 -0
  110. data/registry/app/components/ui/menu/content.html.erb +5 -0
  111. data/registry/app/components/ui/menu/content.rb +106 -0
  112. data/registry/app/components/ui/menu/group.html.erb +1 -0
  113. data/registry/app/components/ui/menu/group.rb +19 -0
  114. data/registry/app/components/ui/menu/item.html.erb +1 -0
  115. data/registry/app/components/ui/menu/item.rb +68 -0
  116. data/registry/app/components/ui/menu/label.html.erb +1 -0
  117. data/registry/app/components/ui/menu/label.rb +33 -0
  118. data/registry/app/components/ui/menu/separator.html.erb +1 -0
  119. data/registry/app/components/ui/menu/separator.rb +20 -0
  120. data/registry/app/components/ui/menu/trigger.html.erb +1 -0
  121. data/registry/app/components/ui/menu/trigger.rb +29 -0
  122. data/registry/app/components/ui/menu.html.erb +3 -0
  123. data/registry/app/components/ui/menu.rb +23 -0
  124. data/registry/app/components/ui/native_select.html.erb +16 -0
  125. data/registry/app/components/ui/native_select.rb +109 -0
  126. data/registry/app/components/ui/pagination.html.erb +34 -0
  127. data/registry/app/components/ui/pagination.rb +62 -0
  128. data/registry/app/components/ui/password_input.html.erb +20 -0
  129. data/registry/app/components/ui/password_input.rb +94 -0
  130. data/registry/app/components/ui/popover/content.html.erb +12 -0
  131. data/registry/app/components/ui/popover/content.rb +83 -0
  132. data/registry/app/components/ui/popover/trigger.html.erb +1 -0
  133. data/registry/app/components/ui/popover/trigger.rb +29 -0
  134. data/registry/app/components/ui/popover.html.erb +3 -0
  135. data/registry/app/components/ui/popover.rb +24 -0
  136. data/registry/app/components/ui/radio_group/item.html.erb +18 -0
  137. data/registry/app/components/ui/radio_group/item.rb +97 -0
  138. data/registry/app/components/ui/radio_group.html.erb +1 -0
  139. data/registry/app/components/ui/radio_group.rb +35 -0
  140. data/registry/app/components/ui/select.html.erb +60 -0
  141. data/registry/app/components/ui/select.rb +181 -0
  142. data/registry/app/components/ui/sheet/close.rb +4 -0
  143. data/registry/app/components/ui/sheet/content.html.erb +24 -0
  144. data/registry/app/components/ui/sheet/content.rb +97 -0
  145. data/registry/app/components/ui/sheet/trigger.rb +4 -0
  146. data/registry/app/components/ui/sheet.html.erb +3 -0
  147. data/registry/app/components/ui/sheet.rb +37 -0
  148. data/registry/app/components/ui/sidebar/content.html.erb +1 -0
  149. data/registry/app/components/ui/sidebar/content.rb +18 -0
  150. data/registry/app/components/ui/sidebar/footer.html.erb +3 -0
  151. data/registry/app/components/ui/sidebar/footer.rb +16 -0
  152. data/registry/app/components/ui/sidebar/group.html.erb +1 -0
  153. data/registry/app/components/ui/sidebar/group.rb +19 -0
  154. data/registry/app/components/ui/sidebar/group_label.html.erb +1 -0
  155. data/registry/app/components/ui/sidebar/group_label.rb +18 -0
  156. data/registry/app/components/ui/sidebar/header.html.erb +1 -0
  157. data/registry/app/components/ui/sidebar/header.rb +16 -0
  158. data/registry/app/components/ui/sidebar/inset.html.erb +1 -0
  159. data/registry/app/components/ui/sidebar/inset.rb +17 -0
  160. data/registry/app/components/ui/sidebar/menu.html.erb +1 -0
  161. data/registry/app/components/ui/sidebar/menu.rb +18 -0
  162. data/registry/app/components/ui/sidebar/menu_button.html.erb +1 -0
  163. data/registry/app/components/ui/sidebar/menu_button.rb +30 -0
  164. data/registry/app/components/ui/sidebar/menu_item.html.erb +1 -0
  165. data/registry/app/components/ui/sidebar/menu_item.rb +23 -0
  166. data/registry/app/components/ui/sidebar/trigger.html.erb +1 -0
  167. data/registry/app/components/ui/sidebar/trigger.rb +24 -0
  168. data/registry/app/components/ui/sidebar/wrapper.html.erb +1 -0
  169. data/registry/app/components/ui/sidebar/wrapper.rb +40 -0
  170. data/registry/app/components/ui/sidebar.html.erb +11 -0
  171. data/registry/app/components/ui/sidebar.rb +29 -0
  172. data/registry/app/components/ui/spinner.html.erb +1 -0
  173. data/registry/app/components/ui/spinner.rb +20 -0
  174. data/registry/app/components/ui/switch.html.erb +12 -0
  175. data/registry/app/components/ui/switch.rb +109 -0
  176. data/registry/app/components/ui/table/cell.html.erb +1 -0
  177. data/registry/app/components/ui/table/cell.rb +16 -0
  178. data/registry/app/components/ui/table/column.html.erb +1 -0
  179. data/registry/app/components/ui/table/column.rb +18 -0
  180. data/registry/app/components/ui/table/head.html.erb +1 -0
  181. data/registry/app/components/ui/table/head.rb +13 -0
  182. data/registry/app/components/ui/table/row.html.erb +1 -0
  183. data/registry/app/components/ui/table/row.rb +26 -0
  184. data/registry/app/components/ui/table.html.erb +4 -0
  185. data/registry/app/components/ui/table.rb +45 -0
  186. data/registry/app/components/ui/tabs/content.html.erb +1 -0
  187. data/registry/app/components/ui/tabs/content.rb +43 -0
  188. data/registry/app/components/ui/tabs/list.html.erb +1 -0
  189. data/registry/app/components/ui/tabs/list.rb +28 -0
  190. data/registry/app/components/ui/tabs/trigger.html.erb +1 -0
  191. data/registry/app/components/ui/tabs/trigger.rb +91 -0
  192. data/registry/app/components/ui/tabs.html.erb +3 -0
  193. data/registry/app/components/ui/tabs.rb +27 -0
  194. data/registry/app/components/ui/text.html.erb +1 -0
  195. data/registry/app/components/ui/text.rb +48 -0
  196. data/registry/app/components/ui/theme_toggle.html.erb +9 -0
  197. data/registry/app/components/ui/theme_toggle.rb +63 -0
  198. data/registry/app/components/ui/tooltip/content.html.erb +1 -0
  199. data/registry/app/components/ui/tooltip/content.rb +65 -0
  200. data/registry/app/components/ui/tooltip/trigger.html.erb +1 -0
  201. data/registry/app/components/ui/tooltip/trigger.rb +33 -0
  202. data/registry/app/components/ui/tooltip.html.erb +3 -0
  203. data/registry/app/components/ui/tooltip.rb +24 -0
  204. data/registry/app/components/ui/typography/blockquote.html.erb +1 -0
  205. data/registry/app/components/ui/typography/blockquote.rb +15 -0
  206. data/registry/app/components/ui/typography/code.html.erb +1 -0
  207. data/registry/app/components/ui/typography/code.rb +25 -0
  208. data/registry/app/components/ui/typography/h1.rb +7 -0
  209. data/registry/app/components/ui/typography/h2.rb +7 -0
  210. data/registry/app/components/ui/typography/h3.rb +7 -0
  211. data/registry/app/components/ui/typography/h4.rb +7 -0
  212. data/registry/app/components/ui/typography/large.rb +7 -0
  213. data/registry/app/components/ui/typography/lead.rb +7 -0
  214. data/registry/app/components/ui/typography/muted.rb +7 -0
  215. data/registry/app/components/ui/typography/small.rb +7 -0
  216. data/registry/app/javascript/ag_grid.ts +26 -0
  217. data/registry/app/javascript/cally.ts +8 -0
  218. data/registry/app/javascript/controllers/ui/_internal/use_dismiss.ts +88 -0
  219. data/registry/app/javascript/controllers/ui/_internal/use_floating.ts +112 -0
  220. data/registry/app/javascript/controllers/ui/accordion_controller.ts +100 -0
  221. data/registry/app/javascript/controllers/ui/alert_controller.ts +7 -0
  222. data/registry/app/javascript/controllers/ui/banner_controller.ts +7 -0
  223. data/registry/app/javascript/controllers/ui/data_grid_controller.ts +198 -0
  224. data/registry/app/javascript/controllers/ui/date_picker_controller.ts +141 -0
  225. data/registry/app/javascript/controllers/ui/dialog_controller.ts +70 -0
  226. data/registry/app/javascript/controllers/ui/menu_controller.ts +201 -0
  227. data/registry/app/javascript/controllers/ui/password_visibility_controller.ts +43 -0
  228. data/registry/app/javascript/controllers/ui/popover_controller.ts +108 -0
  229. data/registry/app/javascript/controllers/ui/select_controller.ts +274 -0
  230. data/registry/app/javascript/controllers/ui/sidebar_controller.ts +64 -0
  231. data/registry/app/javascript/controllers/ui/tabs_controller.ts +74 -0
  232. data/registry/app/javascript/controllers/ui/theme_controller.ts +101 -0
  233. data/registry/app/javascript/controllers/ui/tooltip_controller.ts +102 -0
  234. data/registry/app/javascript/velvetkit_icons.ts +58 -0
  235. data/registry/app/views/layouts/_theme_script.html.erb +19 -0
  236. data/registry/components.yml +102 -0
  237. data/registry/config/locales/velvetkit.en.yml +56 -0
  238. data/registry/docs/agents.md +62 -0
  239. data/registry/test/components/previews/ui/accordion_preview/faq_style.html.erb +20 -0
  240. data/registry/test/components/previews/ui/accordion_preview/multiple.html.erb +27 -0
  241. data/registry/test/components/previews/ui/accordion_preview/single.html.erb +22 -0
  242. data/registry/test/components/previews/ui/accordion_preview.rb +31 -0
  243. data/registry/test/components/previews/ui/alert_preview/title_only.html.erb +12 -0
  244. data/registry/test/components/previews/ui/alert_preview/variants.html.erb +22 -0
  245. data/registry/test/components/previews/ui/alert_preview/with_icon.html.erb +12 -0
  246. data/registry/test/components/previews/ui/alert_preview/with_link.html.erb +18 -0
  247. data/registry/test/components/previews/ui/alert_preview.rb +34 -0
  248. data/registry/test/components/previews/ui/avatar_group_preview/default.html.erb +11 -0
  249. data/registry/test/components/previews/ui/avatar_group_preview/overflow.html.erb +23 -0
  250. data/registry/test/components/previews/ui/avatar_group_preview/small.html.erb +12 -0
  251. data/registry/test/components/previews/ui/avatar_group_preview.rb +23 -0
  252. data/registry/test/components/previews/ui/avatar_preview/colors.html.erb +8 -0
  253. data/registry/test/components/previews/ui/avatar_preview/shapes.html.erb +10 -0
  254. data/registry/test/components/previews/ui/avatar_preview/sizes.html.erb +8 -0
  255. data/registry/test/components/previews/ui/avatar_preview/with_image.html.erb +10 -0
  256. data/registry/test/components/previews/ui/avatar_preview.rb +24 -0
  257. data/registry/test/components/previews/ui/badge_preview/sizes.html.erb +5 -0
  258. data/registry/test/components/previews/ui/badge_preview/variants.html.erb +8 -0
  259. data/registry/test/components/previews/ui/badge_preview/with_icon.html.erb +14 -0
  260. data/registry/test/components/previews/ui/badge_preview.rb +20 -0
  261. data/registry/test/components/previews/ui/banner_preview/introduction.html.erb +9 -0
  262. data/registry/test/components/previews/ui/banner_preview/promotion.html.erb +9 -0
  263. data/registry/test/components/previews/ui/banner_preview/with_illustration.html.erb +12 -0
  264. data/registry/test/components/previews/ui/banner_preview.rb +25 -0
  265. data/registry/test/components/previews/ui/breadcrumb_preview/collapsed.html.erb +50 -0
  266. data/registry/test/components/previews/ui/breadcrumb_preview/default.html.erb +8 -0
  267. data/registry/test/components/previews/ui/breadcrumb_preview/long_path.html.erb +8 -0
  268. data/registry/test/components/previews/ui/breadcrumb_preview/medium.html.erb +7 -0
  269. data/registry/test/components/previews/ui/breadcrumb_preview/with_avatar.html.erb +8 -0
  270. data/registry/test/components/previews/ui/breadcrumb_preview.rb +66 -0
  271. data/registry/test/components/previews/ui/button_preview/disabled.html.erb +39 -0
  272. data/registry/test/components/previews/ui/button_preview/icon_only.html.erb +17 -0
  273. data/registry/test/components/previews/ui/button_preview/loading.html.erb +12 -0
  274. data/registry/test/components/previews/ui/button_preview/sizes.html.erb +6 -0
  275. data/registry/test/components/previews/ui/button_preview/variants.html.erb +8 -0
  276. data/registry/test/components/previews/ui/button_preview/with_both_icons.html.erb +12 -0
  277. data/registry/test/components/previews/ui/button_preview/with_leading_icon.html.erb +22 -0
  278. data/registry/test/components/previews/ui/button_preview/with_trailing_icon.html.erb +14 -0
  279. data/registry/test/components/previews/ui/button_preview.rb +65 -0
  280. data/registry/test/components/previews/ui/button_to_preview/states.html.erb +8 -0
  281. data/registry/test/components/previews/ui/button_to_preview/variants.html.erb +5 -0
  282. data/registry/test/components/previews/ui/button_to_preview/with_icon.html.erb +11 -0
  283. data/registry/test/components/previews/ui/button_to_preview.rb +21 -0
  284. data/registry/test/components/previews/ui/card_preview/complete.html.erb +31 -0
  285. data/registry/test/components/previews/ui/card_preview/default.html.erb +7 -0
  286. data/registry/test/components/previews/ui/card_preview/settings_form.html.erb +25 -0
  287. data/registry/test/components/previews/ui/card_preview/with_footer.html.erb +13 -0
  288. data/registry/test/components/previews/ui/card_preview/with_header.html.erb +11 -0
  289. data/registry/test/components/previews/ui/card_preview.rb +44 -0
  290. data/registry/test/components/previews/ui/checkbox_preview/in_form.html.erb +9 -0
  291. data/registry/test/components/previews/ui/checkbox_preview/states.html.erb +22 -0
  292. data/registry/test/components/previews/ui/checkbox_preview/with_label.html.erb +5 -0
  293. data/registry/test/components/previews/ui/checkbox_preview.rb +27 -0
  294. data/registry/test/components/previews/ui/data_grid_preview.rb +18 -0
  295. data/registry/test/components/previews/ui/date_picker_preview/default.html.erb +3 -0
  296. data/registry/test/components/previews/ui/date_picker_preview/states.html.erb +7 -0
  297. data/registry/test/components/previews/ui/date_picker_preview.rb +13 -0
  298. data/registry/test/components/previews/ui/dialog_preview/confirmation.html.erb +11 -0
  299. data/registry/test/components/previews/ui/dialog_preview/custom_trigger.html.erb +16 -0
  300. data/registry/test/components/previews/ui/dialog_preview/default.html.erb +15 -0
  301. data/registry/test/components/previews/ui/dialog_preview/long_content.html.erb +16 -0
  302. data/registry/test/components/previews/ui/dialog_preview/playground.html.erb +12 -0
  303. data/registry/test/components/previews/ui/dialog_preview/scrollable_body.html.erb +29 -0
  304. data/registry/test/components/previews/ui/dialog_preview/sizes.html.erb +46 -0
  305. data/registry/test/components/previews/ui/dialog_preview/without_close_button.html.erb +11 -0
  306. data/registry/test/components/previews/ui/dialog_preview/without_title.html.erb +13 -0
  307. data/registry/test/components/previews/ui/dialog_preview.rb +38 -0
  308. data/registry/test/components/previews/ui/divider_preview/horizontal.html.erb +5 -0
  309. data/registry/test/components/previews/ui/divider_preview/vertical.html.erb +9 -0
  310. data/registry/test/components/previews/ui/divider_preview/with_label.html.erb +5 -0
  311. data/registry/test/components/previews/ui/divider_preview.rb +19 -0
  312. data/registry/test/components/previews/ui/icon_preview/common.html.erb +8 -0
  313. data/registry/test/components/previews/ui/icon_preview/sizes.html.erb +7 -0
  314. data/registry/test/components/previews/ui/icon_preview.rb +20 -0
  315. data/registry/test/components/previews/ui/image_preview/radii.html.erb +8 -0
  316. data/registry/test/components/previews/ui/image_preview/sizing.html.erb +9 -0
  317. data/registry/test/components/previews/ui/image_preview.rb +16 -0
  318. data/registry/test/components/previews/ui/input_group_preview/addons.html.erb +29 -0
  319. data/registry/test/components/previews/ui/input_group_preview/playground.html.erb +12 -0
  320. data/registry/test/components/previews/ui/input_group_preview.rb +17 -0
  321. data/registry/test/components/previews/ui/input_preview/sizes.html.erb +5 -0
  322. data/registry/test/components/previews/ui/input_preview/states.html.erb +7 -0
  323. data/registry/test/components/previews/ui/input_preview/with_labels.html.erb +10 -0
  324. data/registry/test/components/previews/ui/input_preview.rb +33 -0
  325. data/registry/test/components/previews/ui/label_preview/sizes.html.erb +6 -0
  326. data/registry/test/components/previews/ui/label_preview/states.html.erb +6 -0
  327. data/registry/test/components/previews/ui/label_preview/weights.html.erb +5 -0
  328. data/registry/test/components/previews/ui/label_preview.rb +22 -0
  329. data/registry/test/components/previews/ui/link_preview/sizes.html.erb +5 -0
  330. data/registry/test/components/previews/ui/link_preview/variants.html.erb +8 -0
  331. data/registry/test/components/previews/ui/link_preview/with_icons.html.erb +11 -0
  332. data/registry/test/components/previews/ui/link_preview.rb +20 -0
  333. data/registry/test/components/previews/ui/menu_preview/positions.html.erb +122 -0
  334. data/registry/test/components/previews/ui/menu_preview/variants.html.erb +218 -0
  335. data/registry/test/components/previews/ui/menu_preview.rb +26 -0
  336. data/registry/test/components/previews/ui/native_select_preview/sizes.html.erb +6 -0
  337. data/registry/test/components/previews/ui/native_select_preview/states.html.erb +6 -0
  338. data/registry/test/components/previews/ui/native_select_preview.rb +23 -0
  339. data/registry/test/components/previews/ui/pagination_preview/states.html.erb +15 -0
  340. data/registry/test/components/previews/ui/pagination_preview.rb +15 -0
  341. data/registry/test/components/previews/ui/password_input_preview/states.html.erb +19 -0
  342. data/registry/test/components/previews/ui/password_input_preview.rb +21 -0
  343. data/registry/test/components/previews/ui/popover_preview/default.html.erb +7 -0
  344. data/registry/test/components/previews/ui/popover_preview/positions.html.erb +30 -0
  345. data/registry/test/components/previews/ui/popover_preview/with_close_button.html.erb +8 -0
  346. data/registry/test/components/previews/ui/popover_preview/with_rich_content.html.erb +17 -0
  347. data/registry/test/components/previews/ui/popover_preview/without_title.html.erb +9 -0
  348. data/registry/test/components/previews/ui/popover_preview.rb +32 -0
  349. data/registry/test/components/previews/ui/radio_group_preview/default.html.erb +10 -0
  350. data/registry/test/components/previews/ui/radio_group_preview/disabled.html.erb +17 -0
  351. data/registry/test/components/previews/ui/radio_group_preview/with_required.html.erb +10 -0
  352. data/registry/test/components/previews/ui/radio_group_preview.rb +25 -0
  353. data/registry/test/components/previews/ui/select_preview/default.html.erb +4 -0
  354. data/registry/test/components/previews/ui/select_preview/long_list.html.erb +5 -0
  355. data/registry/test/components/previews/ui/select_preview/multiple.html.erb +6 -0
  356. data/registry/test/components/previews/ui/select_preview/sizes.html.erb +5 -0
  357. data/registry/test/components/previews/ui/select_preview/states.html.erb +6 -0
  358. data/registry/test/components/previews/ui/select_preview.rb +45 -0
  359. data/registry/test/components/previews/ui/sheet_preview/bottom.html.erb +12 -0
  360. data/registry/test/components/previews/ui/sheet_preview/left.html.erb +10 -0
  361. data/registry/test/components/previews/ui/sheet_preview/playground.html.erb +15 -0
  362. data/registry/test/components/previews/ui/sheet_preview/right.html.erb +11 -0
  363. data/registry/test/components/previews/ui/sheet_preview/top.html.erb +8 -0
  364. data/registry/test/components/previews/ui/sheet_preview.rb +22 -0
  365. data/registry/test/components/previews/ui/sidebar_preview/_brand.html.erb +14 -0
  366. data/registry/test/components/previews/ui/sidebar_preview/_shell.html.erb +60 -0
  367. data/registry/test/components/previews/ui/sidebar_preview/collapsible.html.erb +55 -0
  368. data/registry/test/components/previews/ui/sidebar_preview/default.html.erb +17 -0
  369. data/registry/test/components/previews/ui/sidebar_preview/with_dropdown_header.html.erb +33 -0
  370. data/registry/test/components/previews/ui/sidebar_preview/with_groups.html.erb +22 -0
  371. data/registry/test/components/previews/ui/sidebar_preview.rb +15 -0
  372. data/registry/test/components/previews/ui/spinner_preview/in_buttons.html.erb +7 -0
  373. data/registry/test/components/previews/ui/spinner_preview/sizes.html.erb +7 -0
  374. data/registry/test/components/previews/ui/spinner_preview.rb +19 -0
  375. data/registry/test/components/previews/ui/switch_preview/default.html.erb +4 -0
  376. data/registry/test/components/previews/ui/switch_preview/sizes.html.erb +5 -0
  377. data/registry/test/components/previews/ui/switch_preview/states.html.erb +7 -0
  378. data/registry/test/components/previews/ui/switch_preview.rb +26 -0
  379. data/registry/test/components/previews/ui/table_preview/customers.html.erb +15 -0
  380. data/registry/test/components/previews/ui/table_preview/empty.html.erb +8 -0
  381. data/registry/test/components/previews/ui/table_preview/with_caption.html.erb +14 -0
  382. data/registry/test/components/previews/ui/table_preview.rb +12 -0
  383. data/registry/test/components/previews/ui/tabs_preview/default.html.erb +39 -0
  384. data/registry/test/components/previews/ui/tabs_preview/pill_variant.html.erb +18 -0
  385. data/registry/test/components/previews/ui/tabs_preview/settings_form.html.erb +42 -0
  386. data/registry/test/components/previews/ui/tabs_preview/with_icons.html.erb +34 -0
  387. data/registry/test/components/previews/ui/tabs_preview.rb +33 -0
  388. data/registry/test/components/previews/ui/text_preview/sizes.html.erb +10 -0
  389. data/registry/test/components/previews/ui/text_preview/tones.html.erb +5 -0
  390. data/registry/test/components/previews/ui/text_preview/weights.html.erb +6 -0
  391. data/registry/test/components/previews/ui/text_preview.rb +26 -0
  392. data/registry/test/components/previews/ui/theme_toggle_preview/in_a_header.html.erb +14 -0
  393. data/registry/test/components/previews/ui/theme_toggle_preview/palette.html.erb +47 -0
  394. data/registry/test/components/previews/ui/theme_toggle_preview.rb +15 -0
  395. data/registry/test/components/previews/ui/tooltip_preview/default.html.erb +8 -0
  396. data/registry/test/components/previews/ui/tooltip_preview/positions.html.erb +34 -0
  397. data/registry/test/components/previews/ui/tooltip_preview/with_button.html.erb +8 -0
  398. data/registry/test/components/previews/ui/tooltip_preview/with_icon.html.erb +10 -0
  399. data/registry/test/components/previews/ui/tooltip_preview.rb +29 -0
  400. data/registry/test/components/previews/ui/typography_preview/article.html.erb +11 -0
  401. data/registry/test/components/previews/ui/typography_preview/code_and_quotes.html.erb +6 -0
  402. data/registry/test/components/previews/ui/typography_preview/headings.html.erb +6 -0
  403. data/registry/test/components/previews/ui/typography_preview/text_styles.html.erb +7 -0
  404. data/registry/test/components/previews/ui/typography_preview.rb +23 -0
  405. data/registry/test/components/ui/accordion/content_test.rb +64 -0
  406. data/registry/test/components/ui/accordion/item_test.rb +61 -0
  407. data/registry/test/components/ui/accordion/trigger_test.rb +80 -0
  408. data/registry/test/components/ui/accordion_test.rb +96 -0
  409. data/registry/test/components/ui/alert/description_test.rb +37 -0
  410. data/registry/test/components/ui/alert/title_test.rb +38 -0
  411. data/registry/test/components/ui/alert_test.rb +184 -0
  412. data/registry/test/components/ui/avatar_group_test.rb +79 -0
  413. data/registry/test/components/ui/avatar_test.rb +129 -0
  414. data/registry/test/components/ui/badge_test.rb +112 -0
  415. data/registry/test/components/ui/banner_test.rb +116 -0
  416. data/registry/test/components/ui/base_test.rb +98 -0
  417. data/registry/test/components/ui/breadcrumb/ellipsis_test.rb +51 -0
  418. data/registry/test/components/ui/breadcrumb/item_test.rb +39 -0
  419. data/registry/test/components/ui/breadcrumb/list_test.rb +34 -0
  420. data/registry/test/components/ui/breadcrumb/separator_test.rb +36 -0
  421. data/registry/test/components/ui/breadcrumb_test.rb +276 -0
  422. data/registry/test/components/ui/button_test.rb +276 -0
  423. data/registry/test/components/ui/button_to_test.rb +74 -0
  424. data/registry/test/components/ui/card/content_test.rb +41 -0
  425. data/registry/test/components/ui/card/description_test.rb +32 -0
  426. data/registry/test/components/ui/card/footer_test.rb +34 -0
  427. data/registry/test/components/ui/card/header_test.rb +57 -0
  428. data/registry/test/components/ui/card/title_test.rb +34 -0
  429. data/registry/test/components/ui/card_test.rb +67 -0
  430. data/registry/test/components/ui/checkbox_test.rb +236 -0
  431. data/registry/test/components/ui/data_grid_test.rb +88 -0
  432. data/registry/test/components/ui/date_picker_test.rb +168 -0
  433. data/registry/test/components/ui/dialog/content_test.rb +156 -0
  434. data/registry/test/components/ui/dialog/trigger_test.rb +74 -0
  435. data/registry/test/components/ui/dialog_test.rb +54 -0
  436. data/registry/test/components/ui/divider_test.rb +69 -0
  437. data/registry/test/components/ui/icon_test.rb +82 -0
  438. data/registry/test/components/ui/image_test.rb +74 -0
  439. data/registry/test/components/ui/input_group_test.rb +143 -0
  440. data/registry/test/components/ui/input_test.rb +251 -0
  441. data/registry/test/components/ui/label_test.rb +96 -0
  442. data/registry/test/components/ui/link_test.rb +95 -0
  443. data/registry/test/components/ui/menu/content_test.rb +233 -0
  444. data/registry/test/components/ui/menu/group_test.rb +24 -0
  445. data/registry/test/components/ui/menu/item_test.rb +76 -0
  446. data/registry/test/components/ui/menu/label_test.rb +43 -0
  447. data/registry/test/components/ui/menu/separator_test.rb +32 -0
  448. data/registry/test/components/ui/menu/trigger_test.rb +109 -0
  449. data/registry/test/components/ui/menu_test.rb +152 -0
  450. data/registry/test/components/ui/native_select_test.rb +118 -0
  451. data/registry/test/components/ui/pagination_test.rb +92 -0
  452. data/registry/test/components/ui/password_input_test.rb +98 -0
  453. data/registry/test/components/ui/popover/content_test.rb +226 -0
  454. data/registry/test/components/ui/popover/trigger_test.rb +83 -0
  455. data/registry/test/components/ui/popover_test.rb +101 -0
  456. data/registry/test/components/ui/radio_group/item_test.rb +14 -0
  457. data/registry/test/components/ui/radio_group_test.rb +139 -0
  458. data/registry/test/components/ui/select_test.rb +181 -0
  459. data/registry/test/components/ui/sheet/content_test.rb +135 -0
  460. data/registry/test/components/ui/sheet/trigger_test.rb +23 -0
  461. data/registry/test/components/ui/sheet_test.rb +43 -0
  462. data/registry/test/components/ui/sidebar/menu_button_test.rb +43 -0
  463. data/registry/test/components/ui/sidebar/trigger_test.rb +17 -0
  464. data/registry/test/components/ui/sidebar/wrapper_test.rb +15 -0
  465. data/registry/test/components/ui/sidebar_test.rb +43 -0
  466. data/registry/test/components/ui/spinner_test.rb +63 -0
  467. data/registry/test/components/ui/switch_test.rb +206 -0
  468. data/registry/test/components/ui/table_test.rb +106 -0
  469. data/registry/test/components/ui/tabs/content_test.rb +93 -0
  470. data/registry/test/components/ui/tabs/list_test.rb +65 -0
  471. data/registry/test/components/ui/tabs/trigger_test.rb +159 -0
  472. data/registry/test/components/ui/tabs_test.rb +88 -0
  473. data/registry/test/components/ui/text_test.rb +84 -0
  474. data/registry/test/components/ui/theme_toggle_test.rb +88 -0
  475. data/registry/test/components/ui/tooltip/content_test.rb +135 -0
  476. data/registry/test/components/ui/tooltip/trigger_test.rb +52 -0
  477. data/registry/test/components/ui/tooltip_test.rb +97 -0
  478. data/registry/test/components/ui/typography/code_blockquote_test.rb +57 -0
  479. data/registry/test/components/ui/typography/heading_test.rb +71 -0
  480. data/registry/test/components/ui/typography/text_variants_test.rb +71 -0
  481. metadata +540 -0
@@ -0,0 +1,70 @@
1
+ <div class="<%= classes %>" data-controller="ui--date-picker"
2
+ data-ui--date-picker-module-url-value="<%= helpers.asset_path('cally.js') %>"
3
+ data-ui--date-picker-order-value="<%= part_order %>"
4
+ data-ui--date-picker-separator-value="<%= separator %>"
5
+ data-action="click@window->ui--date-picker#closeOnOutsideClick keydown@window->ui--date-picker#closeOnEscape"
6
+ <%= tag.attributes(@html_attrs) %>>
7
+ <% if render_label? %>
8
+ <label for="<%= input_id %>" class="mb-1.5 block text-sm font-medium text-foreground">
9
+ <%= label_text %><% if @required %><span class="ml-0.5 text-destructive">*</span><% end %>
10
+ </label>
11
+ <% end %>
12
+
13
+ <div class="relative">
14
+ <%# What the user types. Not name'd: it is never submitted, so a
15
+ half-typed date cannot reach the server. %>
16
+ <input type="text" id="<%= input_id %>" inputmode="numeric" autocomplete="off"
17
+ placeholder="<%= placeholder %>" class="<%= input_classes %>"
18
+ value="<%= typed_value %>"
19
+ <%= "required" if @required %> <%= "disabled" if @disabled %>
20
+ <%= "aria-describedby=\"#{input_id}-hint\"".html_safe if render_hint? %>
21
+ data-ui--date-picker-target="input"
22
+ data-action="input->ui--date-picker#typed">
23
+
24
+ <%# What the form submits. %>
25
+ <input type="hidden" name="<%= field_name %>" value="<%= iso_value %>"
26
+ data-ui--date-picker-target="hidden">
27
+
28
+ <button type="button" aria-haspopup="dialog" aria-expanded="false"
29
+ aria-controls="<%= panel_id %>"
30
+ aria-label="<%= t("ui.date_picker.open") %>"
31
+ class="absolute inset-y-0 right-0 flex w-9 items-center justify-center rounded-r-md text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
32
+ <%= "disabled" if @disabled %>
33
+ data-ui--date-picker-target="toggle"
34
+ data-action="ui--date-picker#toggle">
35
+ <%= render(UI::Icon.new(name: "calendar", class: "size-4")) %>
36
+ </button>
37
+ </div>
38
+
39
+ <% if render_hint? %>
40
+ <p id="<%= input_id %>-hint" class="mt-1 text-xs text-muted-foreground">
41
+ <%= t("ui.date_picker.hint", pattern: placeholder) %>
42
+ </p>
43
+ <% end %>
44
+
45
+ <div id="<%= panel_id %>" role="dialog" hidden
46
+ class="absolute z-30 mt-1 rounded-md border bg-popover p-3 text-popover-foreground shadow-md"
47
+ data-ui--date-picker-target="panel">
48
+ <%# calendar-select-year is why this library was chosen: reaching 2012 for
49
+ a date of birth is one click, not a hundred and seventy. %>
50
+ <%# Cally puts each arrow inside its own button, so ours are icons with
51
+ words for screen readers; the month and year selects fill its heading
52
+ slot, between the arrows. %>
53
+ <%= tag.send(:"calendar-date",
54
+ safe_join([
55
+ tag.span(safe_join([ render(UI::Icon.new(name: "chevron-left", size: :sm)),
56
+ tag.span(t("ui.date_picker.previous"), class: "sr-only") ]),
57
+ slot: "previous", class: "inline-flex"),
58
+ tag.span(safe_join([ render(UI::Icon.new(name: "chevron-right", size: :sm)),
59
+ tag.span(t("ui.date_picker.next"), class: "sr-only") ]),
60
+ slot: "next", class: "inline-flex"),
61
+ tag.div(safe_join([ tag.send(:"calendar-select-month"), tag.send(:"calendar-select-year") ]),
62
+ slot: "heading", class: "flex items-center gap-1.5"),
63
+ tag.send(:"calendar-month")
64
+ ]),
65
+ locale: I18n.locale == :en ? "en-IN" : I18n.locale.to_s,
66
+ "first-day-of-week": 1, min: @min, max: @max,
67
+ data: { "ui--date-picker-target": "calendar",
68
+ action: "change->ui--date-picker#picked" }) %>
69
+ </div>
70
+ </div>
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI::DatePicker — a typed date field with a calendar beside it.
4
+ #
5
+ # Three inputs in one: what the user types (DD/MM/YYYY), what the form submits
6
+ # (ISO, hidden), and the calendar. The typed field comes first because people
7
+ # who know the date type it in a second: reaching a date years away in a
8
+ # calendar means clicking back month after month.
9
+ #
10
+ # The written format is not decided here. It comes from `date.formats.typed`
11
+ # in the locale, and this component derives the placeholder, the parse order
12
+ # and the separator from that one string — so a locale that writes dates
13
+ # differently changes what the field accepts, what it shows and what the hint
14
+ # says, together, with nothing hardcoded.
15
+ #
16
+ # The shipped English format is day-first; change `date.formats.typed` for
17
+ # month-first. A native date input follows the browser's locale instead, which
18
+ # is how the same form reads MM/DD on one machine and DD/MM on another, and how
19
+ # the 2nd of May gets recorded as the 5th of February.
20
+ #
21
+ # Cally is ~9 KB gzipped and loaded only when the calendar is first opened, so
22
+ # a form nobody opens a calendar on downloads nothing.
23
+ #
24
+ # Usage:
25
+ # <%= render(UI::DatePicker.new(form: f, field: :starts_on, required: true)) %>
26
+ # <%= render(UI::DatePicker.new(name: "event[date]", value: @date, label: "Date")) %>
27
+ class UI::DatePicker < UI::Base
28
+ def initialize(name: nil, value: nil, form: nil, field: nil, label: nil,
29
+ required: false, error: false, disabled: false, hint: true,
30
+ min: nil, max: nil, class: nil, **html_attrs)
31
+ @name = name
32
+ @value = value
33
+ @form = form
34
+ @field = field
35
+ @label = label
36
+ @required = required
37
+ # The hint teaches the format, which a form field wants and a filter
38
+ # toolbar does not — there the value is already on
39
+ # screen in the format the hint would describe.
40
+ @hint = hint
41
+ @error = error
42
+ @disabled = disabled
43
+ @min = min
44
+ @max = max
45
+ @user_class = binding.local_variable_get(:class)
46
+ @html_attrs = html_attrs
47
+ end
48
+
49
+ def field_name = @name || (@form && @field && "#{@form.object_name}[#{@field}]")
50
+
51
+ # The value the form submits, always ISO, whatever was typed.
52
+ def iso_value
53
+ raw = @value || (@form && @field && @form.object&.public_send(@field))
54
+ raw.respond_to?(:strftime) ? raw.strftime("%Y-%m-%d") : raw.presence
55
+ end
56
+
57
+ def label_text
58
+ return @label if @label.present?
59
+ return nil unless @form && @field
60
+
61
+ @form.object.class.try(:human_attribute_name, @field) || @field.to_s.humanize
62
+ end
63
+
64
+ def render_label? = @label != false && label_text.present?
65
+ def render_hint? = @hint != false
66
+
67
+ def input_id = @input_id ||= "ui-date-#{SecureRandom.hex(4)}"
68
+ def panel_id = "#{input_id}-panel"
69
+
70
+ def classes = cn("relative", @user_class)
71
+
72
+ def input_classes
73
+ cn(
74
+ "flex w-full rounded-md border border-input bg-background text-foreground",
75
+ "placeholder:text-muted-foreground transition-all focus:outline-none",
76
+ "focus:ring-2 focus:ring-primary focus:ring-offset-1 ring-offset-background",
77
+ "disabled:cursor-not-allowed disabled:opacity-50 h-9 pl-3 pr-10 py-2 text-sm",
78
+ # aria-invalid rather than a Ruby flag, so a date the user mistypes is
79
+ # marked without a round trip.
80
+ "aria-invalid:border-destructive aria-invalid:ring-destructive",
81
+ @error && "border-destructive"
82
+ )
83
+ end
84
+
85
+ # The one string everything else here is derived from.
86
+ def date_format = I18n.t("date.formats.typed")
87
+
88
+ # "%d/%m/%Y" -> "DD/MM/YYYY". Shown as the placeholder and in the hint, so
89
+ # what the user is told matches what the parser accepts by construction.
90
+ def placeholder
91
+ date_format.gsub("%d", "DD").gsub("%m", "MM").gsub("%Y", "YYYY").gsub("%y", "YY")
92
+ end
93
+
94
+ # "%d/%m/%Y" -> "DMY". The controller builds its parser from this rather
95
+ # than assuming an order.
96
+ def part_order = date_format.scan(/%([dmyY])/).flatten.map(&:upcase).join
97
+
98
+ def separator = date_format[/[^%a-zA-Z]/] || "/"
99
+
100
+ # Rendered server-side in the locale's format, so the field is already right
101
+ # before any JavaScript has loaded.
102
+ def typed_value
103
+ iso = iso_value
104
+ return nil if iso.blank?
105
+
106
+ date = iso.respond_to?(:strftime) ? iso : Date.parse(iso.to_s)
107
+ I18n.l(date, format: :typed)
108
+ rescue Date::Error
109
+ iso
110
+ end
111
+ end
@@ -0,0 +1,7 @@
1
+ <%= render(UI::Button.new(**button_options)) do %>
2
+ <% if content.present? %>
3
+ <%= content %>
4
+ <% else %>
5
+ <%= render(UI::Icon.new(name: :x, size: :sm)) %>
6
+ <% end %>
7
+ <% end %>
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Dialog::Close < UI::Base
4
+ def initialize(variant: :ghost, size: :icon_sm, class: nil, **html_attrs)
5
+ @variant = variant
6
+ @size = size
7
+ @user_class = binding.local_variable_get(:class)
8
+ @html_attrs = html_attrs
9
+ end
10
+
11
+ def set_content_context(content_context)
12
+ @content_context = content_context
13
+ end
14
+
15
+ def button_class
16
+ if content.present?
17
+ cn(@user_class)
18
+ else
19
+ cn("absolute right-4 top-4 p-0", @user_class)
20
+ end
21
+ end
22
+
23
+ def button_options
24
+ opts = @html_attrs.deep_dup
25
+ opts[:variant] = @variant
26
+ opts[:size] = effective_size
27
+ opts[:type] = :button
28
+ opts[:class] = button_class
29
+ opts[:data] = merged_data
30
+ opts[:aria] = merged_aria
31
+ opts
32
+ end
33
+
34
+ private
35
+
36
+ def merged_data
37
+ user_data = (@html_attrs[:data] || {}).deep_dup
38
+ action = [ user_data.delete(:action) || user_data.delete("action"), "click->ui--dialog#close" ].compact.join(" ")
39
+
40
+ user_data.merge(
41
+ action: action,
42
+ ui_dialog_close: true
43
+ )
44
+ end
45
+
46
+ def merged_aria
47
+ (@html_attrs[:aria] || {}).deep_dup.reverse_merge(label: I18n.t("ui.dialog.close_label", default: "Close"))
48
+ end
49
+
50
+ def effective_size
51
+ content.present? && [ :icon, :icon_sm ].include?(@size) ? :default : @size
52
+ end
53
+ end
@@ -0,0 +1,26 @@
1
+ <%# Force the slot block to run first so with_title/with_description/with_footer
2
+ side effects materialise BEFORE we check title?/description?/footer?. %>
3
+ <% body = content %>
4
+ <%= tag.dialog(**html_options) do %>
5
+ <%= tag.div(class: panel_classes, data: { state: "closed", "ui--dialog-part": "panel" }) do %>
6
+ <% if show_close? %>
7
+ <%= render(UI::Dialog::Close.new) %>
8
+ <% end %>
9
+ <% if title? || description? %>
10
+ <header class="flex flex-col space-y-1.5 text-center sm:text-left">
11
+ <% if title? %>
12
+ <%= tag.h2(title_html, id: title_id, class: "text-lg font-semibold leading-none tracking-tight") %>
13
+ <% end %>
14
+ <% if description? %>
15
+ <%= tag.p(description_html, id: description_id, class: "text-sm text-muted-foreground") %>
16
+ <% end %>
17
+ </header>
18
+ <% end %>
19
+ <% if body.present? %>
20
+ <div class="overflow-y-auto flex-1 min-h-0 <%= title? || description? ? 'mt-4' : '' %>"><%= body %></div>
21
+ <% end %>
22
+ <% if footer? %>
23
+ <footer class="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"><%= footer_html %></footer>
24
+ <% end %>
25
+ <% end %>
26
+ <% end %>
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI::Dialog::Content — the native <dialog> element. Exposes block helpers for
4
+ # the optional title, description, and footer regions instead of nested
5
+ # ViewComponent sub-classes (one file, fewer indirections).
6
+ class UI::Dialog::Content < UI::Base
7
+ PANEL_BASE =
8
+ "relative flex flex-col w-full max-h-[calc(100vh-2rem)] overflow-hidden " \
9
+ "rounded-lg border bg-background p-6 shadow-lg " \
10
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 " \
11
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95"
12
+
13
+ variants(
14
+ base: PANEL_BASE,
15
+ variants: {
16
+ size: {
17
+ sm: "max-w-md",
18
+ default: "max-w-lg",
19
+ lg: "max-w-2xl",
20
+ xl: "max-w-4xl"
21
+ }
22
+ },
23
+ defaults: { size: :default }
24
+ )
25
+
26
+ def initialize(size: :default, show_close: true, class: nil, **html_attrs)
27
+ @size = size
28
+ @show_close = show_close
29
+ @user_class = binding.local_variable_get(:class)
30
+ @html_attrs = html_attrs
31
+ end
32
+
33
+ def set_dialog_context(dialog)
34
+ @dialog = dialog
35
+ end
36
+
37
+ # ── Inline slot helpers (block ↔ text) ────────────────────────────────────
38
+
39
+ def with_title(text = nil, &block)
40
+ @title_text = text
41
+ @title_block = block
42
+ self
43
+ end
44
+
45
+ def with_description(text = nil, &block)
46
+ @description_text = text
47
+ @description_block = block
48
+ self
49
+ end
50
+
51
+ def with_footer(&block)
52
+ @footer_block = block
53
+ self
54
+ end
55
+
56
+ def title?; @title_text.present? || @title_block.present?; end
57
+ def description?; @description_text.present? || @description_block.present?; end
58
+ def footer?; @footer_block.present?; end
59
+ def show_close?; @show_close; end
60
+
61
+ def title_html
62
+ @title_block ? view_context.capture(&@title_block) : @title_text
63
+ end
64
+
65
+ def description_html
66
+ @description_block ? view_context.capture(&@description_block) : @description_text
67
+ end
68
+
69
+ def footer_html
70
+ view_context.capture(&@footer_block) if @footer_block
71
+ end
72
+
73
+ # ── Class strings ────────────────────────────────────────────────────────
74
+
75
+ def panel_classes
76
+ cn(variant_classes(size: @size), @user_class)
77
+ end
78
+
79
+ # The native <dialog> takes the full viewport and centers the panel inside.
80
+ # Backdrop styling is owned by the ::backdrop pseudo-element so we don't
81
+ # need a separate overlay div.
82
+ def dialog_classes
83
+ "fixed inset-0 m-0 p-0 max-w-none max-h-none w-full h-full " \
84
+ "bg-transparent border-0 overflow-visible " \
85
+ "open:flex open:items-center open:justify-center " \
86
+ "backdrop:bg-black/60 backdrop:backdrop-blur-[1px]"
87
+ end
88
+
89
+ def html_options
90
+ opts = @html_attrs.dup
91
+ opts[:id] = @dialog.id
92
+ opts[:class] = dialog_classes
93
+ opts[:"aria-labelledby"] = @dialog.title_id if title?
94
+ opts[:"aria-describedby"] = @dialog.description_id if description?
95
+ opts[:"data-ui--dialog-target"] = "content"
96
+ opts[:"data-state"] = "closed"
97
+ opts
98
+ end
99
+
100
+ def title_id; @dialog.title_id; end
101
+ def description_id; @dialog.description_id; end
102
+ end
@@ -0,0 +1 @@
1
+ <%= tag.p(id: @content_context.description_id, class: classes, **@html_attrs) do %><%= content %><% end %>
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Dialog::Description < UI::Base
4
+ def initialize(class: nil, **html_attrs)
5
+ @user_class = binding.local_variable_get(:class)
6
+ @html_attrs = html_attrs
7
+ end
8
+
9
+ def set_content_context(content_context)
10
+ @content_context = content_context
11
+ end
12
+
13
+ def classes
14
+ cn("text-sm text-muted-foreground", @user_class)
15
+ end
16
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div(class: classes, data: { "ui--dialog-part": "footer" }, **@html_attrs) do %><%= content %><% end %>
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Dialog::Footer < UI::Base
4
+ def initialize(class: nil, **html_attrs)
5
+ @user_class = binding.local_variable_get(:class)
6
+ @html_attrs = html_attrs
7
+ end
8
+
9
+ def set_content_context(_content_context); end
10
+
11
+ def classes
12
+ cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", @user_class)
13
+ end
14
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div(class: classes, data: { "ui--dialog-part": "header" }, **@html_attrs) do %><%= content %><% end %>
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Dialog::Header < UI::Base
4
+ def initialize(class: nil, **html_attrs)
5
+ @user_class = binding.local_variable_get(:class)
6
+ @html_attrs = html_attrs
7
+ end
8
+
9
+ def set_content_context(content_context)
10
+ @content_context = content_context
11
+ end
12
+
13
+ def classes
14
+ cn("flex flex-col space-y-1.5 text-center sm:text-left", @user_class)
15
+ end
16
+ end
@@ -0,0 +1 @@
1
+ <%= tag.h2(id: @content_context.title_id, class: classes, **@html_attrs) do %><%= content %><% end %>
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Dialog::Title < UI::Base
4
+ def initialize(class: nil, **html_attrs)
5
+ @user_class = binding.local_variable_get(:class)
6
+ @html_attrs = html_attrs
7
+ end
8
+
9
+ def set_content_context(content_context)
10
+ @content_context = content_context
11
+ end
12
+
13
+ def classes
14
+ cn("text-lg font-semibold leading-none tracking-tight", @user_class)
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ <% if @as == :button %>
2
+ <%= render(UI::Button.new(**button_options)) do %><%= content %><% end %>
3
+ <% else %>
4
+ <%= tag.public_send(@as, **html_options) do %><%= content %><% end %>
5
+ <% end %>
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI::Dialog::Trigger — click-to-open wrapper.
4
+ #
5
+ # By default renders a UI::Button so variant/size styling works out of the
6
+ # box. Pass as: :a (or any other tag name) to fall back to a plain tag.
7
+ class UI::Dialog::Trigger < UI::Base
8
+ def initialize(as: :button, variant: :default, size: :default,
9
+ class: nil, **html_attrs)
10
+ @as = as.to_sym
11
+ @variant = variant
12
+ @size = size
13
+ @user_class = binding.local_variable_get(:class)
14
+ @html_attrs = html_attrs
15
+ end
16
+
17
+ def set_dialog_context(dialog)
18
+ @dialog = dialog
19
+ end
20
+
21
+ # ── Options passed to UI::Button (when @as == :button) ───────────────────
22
+
23
+ def button_options
24
+ opts = @html_attrs.deep_dup
25
+ opts[:variant] = @variant
26
+ opts[:size] = @size
27
+ opts[:type] = :button
28
+ opts[:class] = cn(@user_class)
29
+ opts[:data] = merged_data
30
+ opts[:aria] = merged_aria
31
+ opts
32
+ end
33
+
34
+ # ── Options passed to tag.public_send (when @as != :button) ──────────────
35
+
36
+ def html_options
37
+ opts = @html_attrs.dup
38
+ opts[:class] = cn(@user_class)
39
+
40
+ user_data = (opts.delete(:data) || {}).dup
41
+ user_action = user_data.delete(:action) || user_data.delete("action")
42
+ opts[:data] = user_data.merge(
43
+ "ui--dialog-target": "trigger",
44
+ action: [ user_action, "click->ui--dialog#open" ].compact.join(" ")
45
+ )
46
+
47
+ user_aria = (opts.delete(:aria) || {}).dup
48
+ opts[:aria] = user_aria.merge(
49
+ haspopup: "dialog",
50
+ expanded: "false",
51
+ controls: @dialog.id
52
+ )
53
+
54
+ opts
55
+ end
56
+
57
+ private
58
+
59
+ def merged_data
60
+ user_data = (@html_attrs[:data] || {}).deep_dup
61
+ user_action = user_data.delete(:action) || user_data.delete("action")
62
+ user_data.merge(
63
+ "ui--dialog-target": "trigger",
64
+ action: [ user_action, "click->ui--dialog#open" ].compact.join(" ")
65
+ )
66
+ end
67
+
68
+ def merged_aria
69
+ (@html_attrs[:aria] || {}).deep_dup.merge(
70
+ haspopup: "dialog",
71
+ expanded: "false",
72
+ controls: @dialog.id
73
+ )
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ <%= tag.div(class: root_classes,
2
+ data: { controller: "ui--dialog" }.merge(@html_attrs[:data] || {}),
3
+ **@html_attrs.except(:data)) do %><% if trigger? %><%= trigger %><% end %><% if body? %><%= body %><% end %><% end %>
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI::Dialog — modal dialog backed by the native <dialog> element + showModal().
4
+ #
5
+ # Why native <dialog>:
6
+ # - Browser owns the modal mode, focus trap, ESC handling, and inert
7
+ # background. We do not reimplement any of that in JS.
8
+ # - The Stimulus controller only opens, closes, and dismisses on backdrop
9
+ # click. Everything else is delegated to the platform.
10
+ #
11
+ # Compound:
12
+ # UI::Dialog
13
+ # UI::Dialog::Trigger — wraps any element with a click action
14
+ # UI::Dialog::Content — the <dialog> box; exposes with_title /
15
+ # with_description / with_footer block helpers
16
+ # (no further sub-component classes).
17
+ class UI::Dialog < UI::Base
18
+ renders_one :trigger, ->(**opts) {
19
+ UI::Dialog::Trigger.new(**opts).tap { |t| t.set_dialog_context(self) }
20
+ }
21
+ renders_one :body, ->(**opts) {
22
+ UI::Dialog::Content.new(**opts).tap { |c| c.set_dialog_context(self) }
23
+ }
24
+ alias_method :with_content, :with_body
25
+
26
+ def initialize(class: nil, **html_attrs)
27
+ @user_class = binding.local_variable_get(:class)
28
+ @html_attrs = html_attrs
29
+ @id = @html_attrs[:id] || "ui-dialog-#{SecureRandom.hex(4)}"
30
+ @html_attrs.delete(:id)
31
+ end
32
+
33
+ attr_reader :id
34
+
35
+ def title_id; "#{@id}-title"; end
36
+ def description_id; "#{@id}-description"; end
37
+
38
+ def root_classes
39
+ cn("contents", @user_class)
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ <% if label.present? %>
2
+ <%= tag.div class: wrapper_classes, **@html_attrs do %>
3
+ <%= tag.hr class: rule_classes %>
4
+ <%= tag.span label, class: label_classes %>
5
+ <%= tag.hr class: rule_classes %>
6
+ <% end %>
7
+ <% else %>
8
+ <%= tag.hr class: classes, **@html_attrs %>
9
+ <% end %>
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Divider < UI::Base
4
+ variants(
5
+ base: "shrink-0 border-border",
6
+ variants: {
7
+ orientation: {
8
+ horizontal: "border-t w-full",
9
+ vertical: "border-l self-stretch h-full"
10
+ }
11
+ },
12
+ defaults: { orientation: :horizontal }
13
+ )
14
+
15
+ def initialize(orientation: :horizontal, label: nil, class: nil, **html_attrs)
16
+ @orientation = orientation.to_sym
17
+ @label_text = label
18
+ @user_class = binding.local_variable_get(:class)
19
+ @html_attrs = html_attrs
20
+ end
21
+
22
+ def label; @label_text; end
23
+
24
+ def classes
25
+ cn(variant_classes(orientation: @orientation), @user_class)
26
+ end
27
+
28
+ # Beside a label each rule takes half the leftover space; `w-full` on both
29
+ # pushed the label and the second rule out of the row.
30
+ def rule_classes
31
+ cn(classes, "w-auto flex-1")
32
+ end
33
+
34
+ def wrapper_classes
35
+ cn("flex items-center gap-3", @user_class) if label.present?
36
+ end
37
+
38
+ def label_classes
39
+ "text-xs text-muted-foreground whitespace-nowrap"
40
+ end
41
+ end
@@ -0,0 +1 @@
1
+ <%= tag.i data: (@html_attrs[:data] || {}).merge(lucide: @name), class: classes, aria: aria_attributes, **@html_attrs.except(:data) %>