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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Breadcrumb::ItemSlot < UI::Base
4
+ def initialize(href:, text:, avatar_src: nil, current: false, **html_attrs)
5
+ @href = href
6
+ @text = text
7
+ @avatar_src = avatar_src
8
+ @is_current = current
9
+ @html_attrs = html_attrs
10
+ end
11
+
12
+ def set_context(index, total, size)
13
+ @index = index
14
+ @total = total
15
+ @size = size
16
+ end
17
+
18
+ def effective_current?
19
+ @is_current || (@index == @total - 1)
20
+ end
21
+
22
+ def size_text_class
23
+ UI::Breadcrumb::SIZES[@size]
24
+ end
25
+
26
+ def link_classes
27
+ cn(
28
+ size_text_class,
29
+ effective_current? ? "text-foreground font-normal justify-start rounded" : "text-muted-foreground font-normal justify-start rounded"
30
+ )
31
+ end
32
+
33
+ def link_options
34
+ opts = @html_attrs.dup
35
+ opts[:aria] = { current: "page" } if effective_current?
36
+ opts[:data] = { breadcrumb_text: @text, breadcrumb_href: @href, breadcrumb_avatar: @avatar_src }.compact
37
+ opts
38
+ end
39
+
40
+ def data_attrs
41
+ attrs = { breadcrumb_text: @text, breadcrumb_href: @href, breadcrumb_avatar: @avatar_src }.compact
42
+ attrs[:last] = "true" if effective_current?
43
+ attrs
44
+ end
45
+
46
+ attr_reader :href, :text, :avatar_src
47
+ end
@@ -0,0 +1 @@
1
+ <%= tag.ol class: classes, **@html_attrs do %><%= content %><% end %>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Breadcrumb::List < 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 classes
10
+ cn("flex flex-wrap items-center gap-1.5 break-words sm:gap-2.5", @user_class)
11
+ end
12
+ end
@@ -0,0 +1 @@
1
+ <%= tag.li role: "presentation" do %><%= tag.span class: classes, aria: { hidden: true } do %>/<% end %><% end %>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Breadcrumb::Separator < 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 classes
10
+ cn("text-muted-foreground/70", @user_class)
11
+ end
12
+ end
@@ -0,0 +1,30 @@
1
+ <%= tag.nav aria: { label: t("ui.breadcrumb.label", default: "Breadcrumb") }, class: classes, **@html_attrs do %>
2
+ <ol class="flex flex-wrap items-center gap-1.5 break-words sm:gap-2.5">
3
+ <% visible = visible_items %>
4
+ <% hidden = hidden_items %>
5
+ <% prefix = visible[0...-1] %>
6
+ <% current = visible.last %>
7
+ <%# START: ellipsis before visible items %>
8
+ <% if hidden.any? && @collapse_at == :start %>
9
+ <%= render(UI::Breadcrumb::AutoEllipsis.new(items: hidden, size: @size)) %>
10
+ <li role="presentation"><%= tag.span class: separator_classes, aria: { hidden: true } do %>/<% end %></li>
11
+ <% end %>
12
+ <%# Prefix items with separators %>
13
+ <% prefix.each do |item| %>
14
+ <%= item %>
15
+ <li role="presentation"><%= tag.span class: separator_classes, aria: { hidden: true } do %>/<% end %></li>
16
+ <% end %>
17
+ <%# MIDDLE / END: ellipsis before current page %>
18
+ <% if hidden.any? && %i[middle end].include?(@collapse_at) %>
19
+ <%= render(UI::Breadcrumb::AutoEllipsis.new(items: hidden, size: @size)) %>
20
+ <li role="presentation"><%= tag.span class: separator_classes, aria: { hidden: true } do %>/<% end %></li>
21
+ <% end %>
22
+ <%# Current page (always last, never truncated) %>
23
+ <%= current %>
24
+ <%# Legacy explicit ellipsis support (deprecated) %>
25
+ <% if ellipsis? && !has_items? %>
26
+ <%= ellipsis %>
27
+ <% end %>
28
+ <%= content %>
29
+ </ol>
30
+ <% end %>
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Breadcrumb < UI::Base
4
+ SEPARATOR_COLOR = "text-muted-foreground/70".freeze
5
+ SIZES = { sm: "text-xs", md: "text-sm" }.freeze
6
+ COLLAPSE_POSITIONS = %i[start middle end].freeze
7
+
8
+ renders_many :items, ->(href:, text:, avatar_src: nil, current: false, **opts) {
9
+ UI::Breadcrumb::ItemSlot.new(href: href, text: text, avatar_src: avatar_src, current: current, **opts)
10
+ }
11
+ # Deprecated: explicit ellipsis slot will be replaced by auto-ellipsis
12
+ renders_one :ellipsis, UI::Breadcrumb::Ellipsis
13
+
14
+ def initialize(size: :sm, separator_icon: :slash, max_items: nil, collapse_at: :middle, class: nil, **html_attrs)
15
+ @size = size
16
+ @separator_icon = separator_icon
17
+ @max_items = max_items
18
+ @collapse_at = collapse_at
19
+ @user_class = binding.local_variable_get(:class)
20
+ @html_attrs = html_attrs
21
+ end
22
+
23
+ def before_render
24
+ items.each_with_index do |item, idx|
25
+ item.set_context(idx, items.length, @size)
26
+ end
27
+ end
28
+
29
+ def classes
30
+ cn(SIZES[@size], @user_class)
31
+ end
32
+
33
+ def separator_classes
34
+ cn(
35
+ SEPARATOR_COLOR,
36
+ @size == :md ? "text-sm" : "text-xs"
37
+ )
38
+ end
39
+
40
+ def auto_collapse?
41
+ @max_items && items.length > @max_items
42
+ end
43
+
44
+ # The last item (current page) is always preserved.
45
+ # Remaining budget = max_items - 1 is distributed across the prefix.
46
+ def visible_items
47
+ return items unless auto_collapse?
48
+
49
+ current = [ items.last ]
50
+ budget = @max_items - 1
51
+ prefix = items[0...-1]
52
+
53
+ case @collapse_at
54
+ when :start
55
+ prefix.last([ budget, 0 ].max) + current
56
+ when :end
57
+ prefix.first([ budget, 0 ].max) + current
58
+ when :middle
59
+ head_count = [ (budget / 2.0).ceil, prefix.length ].min
60
+ tail_count = [ budget - head_count, prefix.length - head_count ].min
61
+ tail_count = [ tail_count, 0 ].max
62
+ prefix.first(head_count) + prefix.last(tail_count) + current
63
+ else
64
+ items
65
+ end
66
+ end
67
+
68
+ def hidden_items
69
+ return [] unless auto_collapse?
70
+
71
+ visible_set = visible_items.to_set
72
+ items.reject { |item| visible_set.include?(item) }
73
+ end
74
+
75
+ def show_separator_after?(idx, collection = items)
76
+ return false if idx == collection.length - 1
77
+ true
78
+ end
79
+
80
+ def has_items?
81
+ items.any?
82
+ end
83
+ end
@@ -0,0 +1 @@
1
+ <%= tag.button type: @type, class: classes, disabled: effective_disabled?, aria: { busy: @loading }, **@html_attrs do %><% if @loading %><span class="sr-only"><%= t("ui.button.loading", default: "Loading, please wait") %></span><%= render(loader_icon) %><% elsif leading_icon? %><%= leading_icon %><% end %><%= content %><% if trailing_icon? && !@loading %><%= trailing_icon %><% end %><% end %>
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Button < UI::Base
4
+ ICON_SIZE_FOR_BUTTON_SIZE = {
5
+ sm: :xs,
6
+ default: :sm,
7
+ lg: :default,
8
+ icon: :sm,
9
+ icon_sm: :xs
10
+ }.freeze
11
+
12
+ # The disabled-state utilities below carry Tailwind v4 `!` (important) so
13
+ # they beat the legacy unlayered UI CSS — unlayered rules otherwise
14
+ # outrank any layered Tailwind utility regardless of specificity.
15
+ variants(
16
+ base: "inline-flex items-center justify-center gap-2 whitespace-nowrap " \
17
+ "rounded-md text-sm font-medium " \
18
+ "#{UI::Base::FOCUS_RING} " \
19
+ "disabled:pointer-events-none " \
20
+ "disabled:bg-muted! disabled:text-muted-foreground!",
21
+ variants: {
22
+ variant: {
23
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
24
+ destructive: "bg-destructive text-white hover:bg-destructive/90 focus:ring-destructive",
25
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground " \
26
+ "disabled:border-input!",
27
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
28
+ ghost: "hover:bg-accent hover:text-accent-foreground disabled:bg-transparent!",
29
+ link: "text-primary underline-offset-4 hover:underline " \
30
+ "disabled:bg-transparent! disabled:no-underline!"
31
+ },
32
+ size: {
33
+ default: "h-9 px-4 py-2",
34
+ sm: "h-8 rounded-md px-3 text-xs",
35
+ lg: "h-10 rounded-md px-8",
36
+ icon: "h-9 w-9",
37
+ icon_sm: "h-7 w-7"
38
+ }
39
+ },
40
+ defaults: { variant: :default, size: :default }
41
+ )
42
+
43
+ renders_one :leading_icon, ->(name:, size: nil, **opts) {
44
+ UI::Icon.new(name: name, size: size || ICON_SIZE_FOR_BUTTON_SIZE.fetch(@size, :default), **opts)
45
+ }
46
+
47
+ renders_one :trailing_icon, ->(name:, size: nil, **opts) {
48
+ UI::Icon.new(name: name, size: size || ICON_SIZE_FOR_BUTTON_SIZE.fetch(@size, :default), **opts)
49
+ }
50
+
51
+ def initialize(variant: :default, size: :default, type: :button,
52
+ full_width: false, loading: false, disabled: false,
53
+ as_child: false, class: nil, **html_attrs)
54
+ @variant = variant
55
+ @size = size
56
+ @type = type
57
+ @full_width = full_width
58
+ @loading = loading
59
+ @disabled = disabled
60
+ @as_child = as_child
61
+ @user_class = binding.local_variable_get(:class)
62
+ @html_attrs = html_attrs
63
+ end
64
+
65
+ def effective_disabled?
66
+ @disabled || @loading
67
+ end
68
+
69
+ def classes
70
+ cn(
71
+ variant_classes(variant: @variant, size: @size),
72
+ "disabled:cursor-not-allowed",
73
+ { "w-full" => @full_width },
74
+ @user_class
75
+ )
76
+ end
77
+
78
+ def loader_icon
79
+ UI::Spinner.new(size: ICON_SIZE_FOR_BUTTON_SIZE.fetch(@size, :default))
80
+ end
81
+ end
@@ -0,0 +1 @@
1
+ <%= button_to @path, **button_options do %><% if @loading %><span class="sr-only"><%= t("ui.button.loading", default: "Loading, please wait") %></span><%= render(loader_icon) %><% elsif leading_icon? %><%= leading_icon %><% end %><%= content %><% if trailing_icon? && !@loading %><%= trailing_icon %><% end %><% end %>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI::ButtonTo — a UI::Button rendered inside a real form via Rails' `button_to`.
4
+ #
5
+ # Use when the action is a non-GET request (sign-in with a provider, destroy,
6
+ # any state change) rather than navigation. Inherits UI::Button's variants,
7
+ # sizes and icon slots so the two stay visually identical.
8
+ #
9
+ # Velvet::ButtonTo inherited Button's icon/loading slots but its template never
10
+ # rendered them; this one does.
11
+ #
12
+ # Usage:
13
+ # <%= render(UI::ButtonTo.new(sign_out_path, method: :delete, variant: :outline)) do %>
14
+ # Sign out
15
+ # <% end %>
16
+ class UI::ButtonTo < UI::Button
17
+ def initialize(path, form_class: nil, **args)
18
+ @path = path
19
+ @form_class = form_class
20
+ super(**args)
21
+ end
22
+
23
+ # `button_to` splits its options: everything here decorates the <button>,
24
+ # while form-level concerns (method, params, form_class) stay on the form.
25
+ def button_options
26
+ opts = @html_attrs.dup
27
+ opts[:class] = classes
28
+ opts[:disabled] = true if effective_disabled?
29
+ opts[:form_class] = @form_class if @form_class
30
+ opts[:"aria-busy"] = "true" if @loading
31
+ opts
32
+ end
33
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div class: classes, **@html_attrs do %><%= content %><% end %>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Card::Content < 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 classes
10
+ cn("p-6 pt-0 first:pt-6", @user_class)
11
+ end
12
+ end
@@ -0,0 +1 @@
1
+ <%= tag.p class: classes, **@html_attrs do %><%= content %><% end %>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Card::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 classes
10
+ cn("text-sm text-muted-foreground", @user_class)
11
+ end
12
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div class: classes, **@html_attrs do %><%= content %><% end %>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Card::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 classes
10
+ cn("flex items-center p-6 pt-0", @user_class)
11
+ end
12
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div class: classes, **@html_attrs do %><% if title? %><%= title %><% end %><% if description? %><%= description %><% end %><%= content %><% end %>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Card::Header < UI::Base
4
+ renders_one :title, UI::Card::Title
5
+ renders_one :description, UI::Card::Description
6
+
7
+ def initialize(class: nil, **html_attrs)
8
+ @user_class = binding.local_variable_get(:class)
9
+ @html_attrs = html_attrs
10
+ end
11
+
12
+ def classes
13
+ cn("flex flex-col gap-1.5 p-6", @user_class)
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ <%= tag.h3 class: classes, **@html_attrs do %><%= content %><% end %>
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Card::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 classes
10
+ cn("text-lg font-semibold leading-none tracking-tight", @user_class)
11
+ end
12
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div class: classes, **@html_attrs do %><%= header if header? %><%= card_content if card_content? %><%= footer if footer? %><%= content %><% end %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Card < UI::Base
4
+ renders_one :header, UI::Card::Header
5
+ renders_one :card_content, UI::Card::Content
6
+ renders_one :footer, UI::Card::Footer
7
+
8
+ alias_method :with_content, :with_card_content
9
+
10
+ def initialize(class: nil, **html_attrs)
11
+ @user_class = binding.local_variable_get(:class)
12
+ @html_attrs = html_attrs
13
+ end
14
+
15
+ def classes
16
+ cn("rounded-xl border bg-card text-card-foreground shadow", @user_class)
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ <% if label.present? %>
2
+ <%= tag.label for: @id, class: wrapper_classes do %>
3
+ <%= tag.span class: "relative inline-flex" do %>
4
+ <% if form_field? %><%= form_check_box %><% else %><%= tag.input(**input_options) %><% end %>
5
+ <%= tag.span class: overlay_classes do %>
6
+ <%= render(UI::Icon.new(name: "check", size: icon_size, class: "stroke-[3]", aria: { hidden: true })) %>
7
+ <% end %>
8
+ <% end %>
9
+ <%= tag.span class: label_text_classes do %><%= label %><% if required? %><%= tag.span(" *", class: "text-destructive ml-0.5") %><% end %><% end %>
10
+ <% end %>
11
+ <% else %>
12
+ <%= tag.span class: "relative inline-flex" do %>
13
+ <% if form_field? %><%= form_check_box %><% else %><%= tag.input(**input_options) %><% end %>
14
+ <%= tag.span class: overlay_classes do %>
15
+ <%= render(UI::Icon.new(name: "check", size: icon_size, class: "stroke-[3]", aria: { hidden: true })) %>
16
+ <% end %>
17
+ <% end %>
18
+ <% end %>
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Checkbox < UI::Base
4
+ ICON_SIZE_FOR_SIZE = { sm: :xs, default: :xs, lg: :sm }.freeze
5
+
6
+ variants(
7
+ base: "peer appearance-none shrink-0 border border-input bg-background " \
8
+ "transition-all " \
9
+ "checked:bg-primary checked:border-primary " \
10
+ "focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1 " \
11
+ "ring-offset-background",
12
+ variants: {
13
+ size: {
14
+ sm: "w-3.5 h-3.5 rounded-[3px]",
15
+ default: "w-4 h-4 rounded-[4px]",
16
+ lg: "w-5 h-5 rounded-[5px]"
17
+ }
18
+ },
19
+ defaults: { size: :default }
20
+ )
21
+
22
+ def initialize(name: nil, value: "1", unchecked_value: "0", checked: false,
23
+ label: nil, size: :default,
24
+ form: nil, field: nil,
25
+ required: false, error: false, disabled: false,
26
+ class: nil, **html_attrs)
27
+ @form = form
28
+ @field = field
29
+ @name = name
30
+ @value = value
31
+ @unchecked_value = unchecked_value
32
+ @checked = checked
33
+ @label_text = label
34
+ @size = size
35
+ @required = required
36
+ @error = error
37
+ @disabled = disabled
38
+ @user_class = binding.local_variable_get(:class)
39
+ @html_attrs = html_attrs
40
+ @id = @html_attrs[:id] || ("ui-cb-#{SecureRandom.hex(4)}" if label?)
41
+ @html_attrs[:id] = @id if @id
42
+ end
43
+
44
+ def form_field?
45
+ @form.present? && @field.present?
46
+ end
47
+
48
+ # Same convention as UI::Input: bound to a form object the attribute name
49
+ # supplies the label text (via human_attribute_name, so i18n applies);
50
+ # `label:` overrides it and `label: false` opts out.
51
+ def label?
52
+ return false if @label_text == false
53
+
54
+ @label_text.present? || form_field?
55
+ end
56
+
57
+ def label
58
+ return nil unless label?
59
+ return @label_text if @label_text.is_a?(String) && @label_text.present?
60
+ return nil unless form_field?
61
+
62
+ object = @form.object
63
+ if object.respond_to?(:class) && object.class.respond_to?(:human_attribute_name)
64
+ object.class.human_attribute_name(@field)
65
+ else
66
+ @field.to_s.humanize
67
+ end
68
+ end
69
+
70
+ # Rails' check_box also emits the companion hidden field, so an unchecked box
71
+ # still submits its unchecked value.
72
+ def form_check_box
73
+ @form.check_box(@field, form_input_options, @value, @unchecked_value)
74
+ end
75
+
76
+ def form_input_options
77
+ input_options.except(:type, :name, :value, :checked)
78
+ end
79
+ def required?; @required; end
80
+ def icon_size; ICON_SIZE_FOR_SIZE.fetch(@size); end
81
+
82
+ def input_options
83
+ opts = @html_attrs.dup
84
+ opts[:type] = :checkbox
85
+ opts[:name] = @name if @name
86
+ opts[:value] = @value
87
+ opts[:checked] = true if @checked
88
+ opts[:disabled] = true if @disabled
89
+ opts[:required] = true if @required
90
+ opts[:"aria-invalid"] = "true" if @error
91
+ opts[:class] = classes
92
+ # Add accessible name when no visual label is present
93
+ if !label? && opts[:"aria-label"].blank?
94
+ opts[:"aria-label"] = accessible_name
95
+ end
96
+ opts
97
+ end
98
+
99
+ def accessible_name
100
+ # Derive accessible name from name attribute or use default
101
+ return "Checkbox" if @name.blank?
102
+
103
+ # Convert "user[terms]" to "User terms"
104
+ @name.to_s.gsub(/[\[\]_]/, " ").gsub(/\s+/, " ").strip.titleize
105
+ end
106
+
107
+ def classes
108
+ cn(
109
+ variant_classes(size: @size),
110
+ { "border-destructive aria-invalid:focus:ring-destructive" => @error },
111
+ @user_class
112
+ )
113
+ end
114
+
115
+ def overlay_classes
116
+ "pointer-events-none absolute inset-0 flex items-center justify-center " \
117
+ "text-primary-foreground opacity-0 peer-checked:opacity-100 transition-opacity"
118
+ end
119
+
120
+ def wrapper_classes
121
+ cn(
122
+ "inline-flex items-center gap-2 select-none",
123
+ { "cursor-not-allowed opacity-60" => @disabled }
124
+ )
125
+ end
126
+
127
+ def label_text_classes
128
+ cn(
129
+ "text-sm leading-none",
130
+ { "text-muted-foreground" => @disabled }
131
+ )
132
+ end
133
+ end
@@ -0,0 +1,2 @@
1
+ <%= tag.div class: classes, style: "height: #{reserved_height}",
2
+ data: grid_options, **@html_attrs %>
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI::DataGrid — a sortable, filterable, server-paged table backed by AG Grid.
4
+ #
5
+ # The grid holds one page at a time and asks the server for the next, sending
6
+ # the sort and filter it wants applied, so paging, sorting and filtering all
7
+ # happen in the database. The server answers GET `url` with `startRow`,
8
+ # `endRow`, `sortModel` and `filterModel` (both JSON) as params, returning
9
+ # `{ "rows": [...], "total": n }`.
10
+ #
11
+ # Columns are declared in Ruby so the headers, their widths and which of them
12
+ # the server will accept a sort or filter for are decided in one place.
13
+ #
14
+ # AG Grid is ~365 KB gzipped, so the Stimulus controller fetches it at connect
15
+ # time: a page without a grid never downloads it.
16
+ #
17
+ # Usage:
18
+ # <%= render(UI::DataGrid.new(url: customers_path(format: :json)) do |grid|
19
+ # grid.column(:id, t(".id"), width: 90, type: :number)
20
+ # grid.column(:name, t(".name"))
21
+ # grid.column(:owner, t(".owner"), sortable: false, filterable: false)
22
+ # end) %>
23
+ class UI::DataGrid < UI::Base
24
+ Column = Struct.new(:field, :header, :width, :type, :sortable, :filterable,
25
+ keyword_init: true)
26
+
27
+ def initialize(url:, page_size: 15, empty_message: nil, class: nil, **html_attrs)
28
+ @url = url
29
+ @page_size = page_size
30
+ @empty_message = empty_message
31
+ @columns = []
32
+ @user_class = binding.local_variable_get(:class)
33
+ @html_attrs = html_attrs
34
+ end
35
+
36
+ # sortable/filterable default to true, and are turned off for a column the
37
+ # server has no SQL expression for — a value read through an association,
38
+ # or one translated for display. Offering the control and then ignoring it
39
+ # would be worse than not offering it.
40
+ def column(field, header, width: nil, type: :text, sortable: true, filterable: true)
41
+ @columns << Column.new(field: field.to_s, header: header, width: width, type: type,
42
+ sortable: sortable, filterable: filterable)
43
+ self
44
+ end
45
+
46
+ def before_render
47
+ content # declares the columns
48
+ end
49
+
50
+ # Rows navigate on click when the server sends a path with them, so the
51
+ # cursor has to say so.
52
+ def classes = cn("[&_.ag-row]:cursor-pointer", @user_class)
53
+
54
+ def grid_options
55
+ {
56
+ controller: "ui--data-grid",
57
+ "ui--data-grid-url-value": @url,
58
+ "ui--data-grid-columns-value": column_specs.to_json,
59
+ "ui--data-grid-page-size-value": @page_size,
60
+ "ui--data-grid-empty-message-value": @empty_message || t("ui.data_grid.empty"),
61
+ # Propshaft digests the filename, so the URL has to come from Ruby.
62
+ "ui--data-grid-module-url-value": helpers.asset_path("ag_grid.js")
63
+ }
64
+ end
65
+
66
+ # Reserves the grid's final height before its chunk arrives, so the page does
67
+ # not jump once it paints.
68
+ def reserved_height = "#{HEADER_HEIGHT + (ROW_HEIGHT * @page_size) + FOOTER_HEIGHT}px"
69
+
70
+ HEADER_HEIGHT = 36
71
+ ROW_HEIGHT = 36
72
+ FOOTER_HEIGHT = 52
73
+
74
+ private
75
+
76
+ def column_specs
77
+ @columns.map do |column|
78
+ spec = { field: column.field, headerName: column.header, type: column.type,
79
+ sortable: column.sortable, filterable: column.filterable }
80
+ spec[:width] = column.width if column.width
81
+ spec
82
+ end
83
+ end
84
+ end