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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Menu::Item < UI::Base
4
+ variants(
5
+ base:
6
+ "relative flex cursor-default select-none items-center gap-2 rounded w-full px-2 py-1 text-sm whitespace-nowrap border-0 bg-transparent outline-none transition-colors " \
7
+ "hover:bg-accent hover:text-accent-foreground " \
8
+ "focus:bg-accent focus:text-accent-foreground focus:ring-2 focus:ring-ring focus:ring-offset-1 " \
9
+ "aria-checked:bg-accent aria-checked:text-accent-foreground aria-checked:font-medium " \
10
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50 transition-all",
11
+ variants: {
12
+ variant: {
13
+ default: "",
14
+ destructive: "text-destructive hover:bg-destructive/10 hover:text-destructive focus:bg-destructive/10 focus:text-destructive"
15
+ },
16
+ inset: {
17
+ true => "pl-8",
18
+ false => ""
19
+ }
20
+ },
21
+ defaults: { variant: :default, inset: false }
22
+ )
23
+
24
+ def initialize(label = nil, href: nil, disabled: false, variant: :default, inset: false, class: nil, **html_attrs, &block)
25
+ @label = label
26
+ @href = href
27
+ @disabled = disabled
28
+ @variant = variant
29
+ @inset = inset
30
+ @user_class = binding.local_variable_get(:class)
31
+ @html_attrs = html_attrs
32
+ @content_block = block
33
+ end
34
+
35
+ def classes
36
+ cn(
37
+ variant_classes(variant: @variant, inset: @inset),
38
+ @user_class
39
+ )
40
+ end
41
+
42
+ def html_options
43
+ opts = @html_attrs.dup
44
+ opts[:class] = classes
45
+ opts[:role] ||= :menuitem
46
+ opts[:tabindex] = -1
47
+ opts[:"data-ui--menu-target"] = "item"
48
+ opts[:disabled] = true if @disabled
49
+ opts[:"data-disabled"] = true if @disabled
50
+ opts
51
+ end
52
+
53
+ def tag_name
54
+ @href ? :a : :button
55
+ end
56
+
57
+ def href_attr
58
+ @href ? { href: @href } : {}
59
+ end
60
+
61
+ def render_label
62
+ if @content_block
63
+ captured = view_context.capture { view_context.instance_exec(&@content_block) }
64
+ return captured if captured.present?
65
+ end
66
+ content.to_s.presence || @label.to_s
67
+ end
68
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div(**html_options) do %><%= render_label %><% end %>
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Menu::Label < UI::Base
4
+ def initialize(label = nil, inset: false, class: nil, **html_attrs, &block)
5
+ @label = label
6
+ @inset = inset
7
+ @user_class = binding.local_variable_get(:class)
8
+ @html_attrs = html_attrs
9
+ @content_block = block
10
+ end
11
+
12
+ def classes
13
+ cn(
14
+ "px-2 py-1.5 text-sm font-semibold text-muted-foreground",
15
+ { "pl-8" => @inset },
16
+ @user_class
17
+ )
18
+ end
19
+
20
+ def html_options
21
+ opts = @html_attrs.dup
22
+ opts[:class] = classes
23
+ opts
24
+ end
25
+
26
+ def render_label
27
+ if @content_block
28
+ captured = view_context.capture { view_context.instance_exec(&@content_block) }
29
+ return captured if captured.present?
30
+ end
31
+ content.to_s.presence || @label.to_s
32
+ end
33
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div(**html_options) do %><% end %>
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Menu::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("-mx-1 h-px bg-muted", @user_class)
11
+ end
12
+
13
+ def html_options
14
+ opts = @html_attrs.dup
15
+ opts[:class] = classes
16
+ opts[:role] = :separator
17
+ opts[:"aria-orientation"] = :horizontal
18
+ opts
19
+ end
20
+ end
@@ -0,0 +1 @@
1
+ <%= render(UI::Button.new(**button_options)) do %><%= content %><% end %>
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Menu::Trigger < UI::Base
4
+ def initialize(variant: :outline, size: :default, 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_menu_context(menu)
12
+ @menu = menu
13
+ @id = @html_attrs[:id] || "ui-menu-trigger-#{menu.id}"
14
+ end
15
+
16
+ def button_options
17
+ @html_attrs.merge(
18
+ id: @id,
19
+ class: @user_class,
20
+ variant: @variant,
21
+ size: @size,
22
+ "aria-haspopup" => true,
23
+ "aria-expanded" => false,
24
+ "aria-controls" => "ui-menu-content-#{@menu.id}",
25
+ "data-ui--menu-target" => "trigger",
26
+ "data-action" => "click->ui--menu#toggle keydown->ui--menu#triggerKeydown"
27
+ )
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ <%= tag.div(class: root_classes,
2
+ data: { controller: "ui--menu" }.merge(@html_attrs[:data] || {}),
3
+ **@html_attrs.except(:data)) do %><%= trigger %><%= menu_content %><% end %>
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Menu < UI::Base
4
+ renders_one :trigger, ->(**opts) {
5
+ UI::Menu::Trigger.new(**opts).tap { |t| t.set_menu_context(self) }
6
+ }
7
+ renders_one :menu_content, ->(**opts) {
8
+ UI::Menu::Content.new(**opts).tap { |c| c.set_menu_context(self) }
9
+ }
10
+ alias_method :with_content, :with_menu_content
11
+
12
+ def initialize(class: nil, **html_attrs)
13
+ @user_class = binding.local_variable_get(:class)
14
+ @html_attrs = html_attrs
15
+ @id = @html_attrs[:id] || "ui-menu-#{SecureRandom.hex(4)}"
16
+ end
17
+
18
+ attr_reader :id
19
+
20
+ def root_classes
21
+ cn("relative inline-block", @user_class)
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ <%# The chevron is decorative: appearance-none removes the native arrow, so it
2
+ is drawn back with a positioned icon over the control. %>
3
+ <% control = capture do %>
4
+ <div class="relative w-full">
5
+ <% if form_field? %><%= @form.select(@field, normalized_options, { include_blank: prompt.presence, selected: @selected }, form_select_options) %><% else %><%= tag.select(**select_options) do %><% if prompt.present? %><option value=""><%= prompt %></option><% end %><% normalized_options.each do |label, value| %><option value="<%= value %>" <%= "selected" if selected?(value) %>><%= label %></option><% end %><% end %><% end %>
6
+ <span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground"><%= render(UI::Icon.new(name: "chevron-down", size: :sm, aria: { hidden: true })) %></span>
7
+ </div>
8
+ <% end %>
9
+ <% if render_label? %>
10
+ <div class="flex flex-col gap-2 w-full">
11
+ <%= render(label_component) { label_text } %>
12
+ <%= control %>
13
+ </div>
14
+ <% else %>
15
+ <%= control %>
16
+ <% end %>
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ # UI::NativeSelect — the browser's own <select>, styled closed.
6
+ #
7
+ # UI::Select is the styled dropdown. Reach for this one when the platform
8
+ # control is the better fit: on a phone it opens the system picker, which is
9
+ # easier to use with a thumb than any custom list, and it needs no JavaScript.
10
+ # The open list is drawn by the operating system, so it can't be themed.
11
+ #
12
+ # Usage:
13
+ # <%= render(UI::NativeSelect.new(form: f, field: :team_id, label: "Team",
14
+ # options: @teams.map { [_1.name, _1.id] },
15
+ # prompt: "Choose a team")) %>
16
+ class UI::NativeSelect < UI::Base
17
+ variants(
18
+ base: "flex w-full appearance-none rounded-md border border-input bg-background " \
19
+ "text-foreground " \
20
+ "#{UI::Base::FOCUS_RING} " \
21
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted!",
22
+ variants: {
23
+ size: {
24
+ sm: "h-8 pl-2.5 pr-8 py-1 text-xs",
25
+ default: "h-9 pl-3 pr-9 py-2 text-sm",
26
+ lg: "h-10 pl-4 pr-10 py-2.5 text-base"
27
+ }
28
+ },
29
+ defaults: { size: :default }
30
+ )
31
+
32
+ def initialize(name: nil, options: [], selected: nil, prompt: nil,
33
+ label: nil, size: :default,
34
+ required: false, error: false, disabled: false,
35
+ form: nil, field: nil,
36
+ class: nil, **html_attrs)
37
+ @name = name
38
+ @options = options
39
+ @selected = selected
40
+ @prompt = prompt
41
+ @label = label
42
+ @size = size
43
+ @required = required
44
+ @error = error
45
+ @disabled = disabled
46
+ @form = form
47
+ @field = field
48
+ @user_class = binding.local_variable_get(:class)
49
+ @html_attrs = html_attrs
50
+ @select_id = @html_attrs[:id] || (render_label? ? "ui-native-select-#{SecureRandom.hex(4)}" : nil)
51
+ @html_attrs[:id] = @select_id if @select_id
52
+ end
53
+
54
+ attr_reader :options, :prompt
55
+
56
+ def classes
57
+ cn(variant_classes(size: @size),
58
+ { "border-destructive focus:ring-destructive" => @error },
59
+ @user_class)
60
+ end
61
+
62
+ def form_field? = @form.present? && @field.present?
63
+
64
+ # Same convention as UI::Input: bound to a form object the attribute name
65
+ # supplies the label; `label:` overrides, `label: false` opts out.
66
+ def render_label?
67
+ return false if @label == false
68
+
69
+ @label.present? || form_field?
70
+ end
71
+
72
+ def label_text
73
+ return nil unless render_label?
74
+ return @label if @label.is_a?(String) && @label.present?
75
+ return nil unless form_field?
76
+
77
+ object = @form.object
78
+ if object.respond_to?(:class) && object.class.respond_to?(:human_attribute_name)
79
+ object.class.human_attribute_name(@field)
80
+ else
81
+ @field.to_s.humanize
82
+ end
83
+ end
84
+
85
+ def label_component
86
+ UI::Label.new(for: @select_id, required: @required, disabled: @disabled)
87
+ end
88
+
89
+ def select_options
90
+ opts = @html_attrs.dup
91
+ opts[:class] = classes
92
+ opts[:name] = @name if @name
93
+ opts[:disabled] = true if @disabled
94
+ opts[:required] = true if @required
95
+ opts[:"aria-invalid"] = "true" if @error
96
+ opts
97
+ end
98
+
99
+ # The form builder derives the name and id itself, so they are dropped here
100
+ # to avoid overriding the scoped name with a bare one.
101
+ def form_select_options = select_options.except(:name)
102
+
103
+ # Accepts [["Label", value], ...] or a plain list of scalars.
104
+ def normalized_options
105
+ options.map { |option| option.is_a?(Array) ? option : [ option, option ] }
106
+ end
107
+
108
+ def selected?(value) = @selected.present? && @selected.to_s == value.to_s
109
+ end
@@ -0,0 +1,34 @@
1
+ <%# Laid out by its own width (a container query), so it fits a narrow card on
2
+ a wide screen as well as a phone. %>
3
+ <div class="@container w-full">
4
+ <div class="flex w-full flex-col items-center gap-2 @lg:flex-row @lg:justify-between">
5
+ <p class="text-sm text-muted-foreground whitespace-nowrap"><%= summary %></p>
6
+ <%= tag.nav aria: { label: label }, class: classes, **@html_attrs do %>
7
+ <ul class="flex flex-row flex-wrap items-center justify-center gap-1">
8
+ <li>
9
+ <% if previous_page %>
10
+ <%= render(UI::Link.new(href: url_for_page(previous_page), button_variant: true, class: "gap-1 px-2.5", aria: { label: t("ui.pagination.previous", default: "Go to previous page") })) do %><%= render(UI::Icon.new(name: "chevron-left", size: :sm)) %><span class="hidden @md:inline"><%= t("ui.pagination.previous_label", default: "Previous") %></span><% end %>
11
+ <% else %>
12
+ <span class="inline-flex items-center gap-1 px-2.5 py-2 text-sm text-muted-foreground opacity-50"><%= render(UI::Icon.new(name: "chevron-left", size: :sm)) %><span class="hidden @md:inline"><%= t("ui.pagination.previous_label", default: "Previous") %></span></span>
13
+ <% end %>
14
+ </li>
15
+ <% series.each do |page| %>
16
+ <li>
17
+ <% if page == UI::Pagination::GAP %>
18
+ <span class="flex size-9 items-center justify-center text-muted-foreground" aria-hidden="true">&hellip;<span class="sr-only"><%= t("ui.pagination.more", default: "More pages") %></span></span>
19
+ <% else %>
20
+ <%= render(UI::Link.new(href: url_for_page(page), button_variant: true, class: current?(page) ? "size-9 border border-input bg-background" : "size-9", aria: current?(page) ? { current: "page" } : {})) { page.to_s } %>
21
+ <% end %>
22
+ </li>
23
+ <% end %>
24
+ <li>
25
+ <% if next_page %>
26
+ <%= render(UI::Link.new(href: url_for_page(next_page), button_variant: true, class: "gap-1 px-2.5", aria: { label: t("ui.pagination.next", default: "Go to next page") })) do %><span class="hidden @md:inline"><%= t("ui.pagination.next_label", default: "Next") %></span><%= render(UI::Icon.new(name: "chevron-right", size: :sm)) %><% end %>
27
+ <% else %>
28
+ <span class="inline-flex items-center gap-1 px-2.5 py-2 text-sm text-muted-foreground opacity-50"><span class="hidden @md:inline"><%= t("ui.pagination.next_label", default: "Next") %></span><%= render(UI::Icon.new(name: "chevron-right", size: :sm)) %></span>
29
+ <% end %>
30
+ </li>
31
+ </ul>
32
+ <% end %>
33
+ </div>
34
+ </div>
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI::Pagination — page links for a long list.
4
+ #
5
+ # Follows shadcn's structure (nav > ul > li > a, ghost links with the current
6
+ # page outlined) and takes a Pagy::Offset, so controllers stay conventional.
7
+ #
8
+ # The series is computed here rather than read from Pagy, whose own #series is
9
+ # protected in v43 and whose gap marker differs by version.
10
+ #
11
+ # Usage:
12
+ # <%= render(UI::Pagination.new(pagy: @pagy) { |page| section_path(@section, page: page) }) %>
13
+ class UI::Pagination < UI::Base
14
+ GAP = :gap
15
+
16
+ # How many pages either side of the current one to show before collapsing.
17
+ WINDOW = 2
18
+
19
+ def initialize(pagy:, label: nil, class: nil, **html_attrs, &url_builder)
20
+ @pagy = pagy
21
+ @label = label
22
+ @url_builder = url_builder
23
+ @user_class = binding.local_variable_get(:class)
24
+ @html_attrs = html_attrs
25
+ end
26
+
27
+ attr_reader :pagy
28
+
29
+ # One page of results needs no controls.
30
+ def render? = pagy.present? && pagy.pages > 1
31
+
32
+ def classes = cn("mx-auto flex w-full justify-center", @user_class)
33
+
34
+ def label = @label || I18n.t("ui.pagination.label", default: "Pagination")
35
+
36
+ def url_for_page(page) = @url_builder ? @url_builder.call(page) : "?page=#{page}"
37
+
38
+ def current?(page) = page == pagy.page
39
+
40
+ # First and last are always shown so the ends stay reachable; the rest is a
41
+ # window around the current page, with gaps collapsed.
42
+ def series
43
+ return (1..pagy.pages).to_a if pagy.pages <= (WINDOW * 2) + 5
44
+
45
+ window = ((pagy.page - WINDOW)..(pagy.page + WINDOW)).select { |p| p.between?(1, pagy.pages) }
46
+ pages = ([ 1 ] + window + [ pagy.pages ]).uniq.sort
47
+
48
+ pages.each_with_object([]) do |page, out|
49
+ out << GAP if out.last.is_a?(Integer) && page > out.last + 1
50
+ out << page
51
+ end
52
+ end
53
+
54
+ def previous_page = pagy.previous
55
+ def next_page = pagy.next
56
+
57
+ # "Showing 26–50 of 240" — more use to an office than page numbers alone.
58
+ def summary
59
+ I18n.t("ui.pagination.summary", from: pagy.from, to: pagy.to, count: pagy.count,
60
+ default: "Showing %{from}–%{to} of %{count}")
61
+ end
62
+ end
@@ -0,0 +1,20 @@
1
+ <%# frozen_string_literal: true %>
2
+ <%# The Stimulus targets live on wrapper spans, not the icons themselves:
3
+ UI::Icon sets its own `data: { lucide: ... }`, and a second `data:` hash
4
+ passed through html_attrs replaces it wholesale, leaving a blank icon.
5
+ Visibility uses an inline style rather than a `hidden` class so no
6
+ utility-layer ordering can leave both icons showing at once. %>
7
+ <% group = capture do %><%= render(group_component) do |g| %>
8
+ <% g.with_field(**field_options) %>
9
+ <% g.with_addon(align: "inline-end") do |addon| %>
10
+ <% addon.with_icon_button(**toggle_button_options) do %><span class="inline-flex" data-ui--password-visibility-target="iconShow"><%= render(UI::Icon.new(name: "eye", size: :sm)) %></span><span class="inline-flex" style="display: none" data-ui--password-visibility-target="iconHide"><%= render(UI::Icon.new(name: "eye-off", size: :sm)) %></span><% end %>
11
+ <% end %>
12
+ <% end %><% end %>
13
+ <% if render_label? %>
14
+ <div class="flex flex-col gap-2 w-full">
15
+ <%= render(label_component) { label_text } %>
16
+ <%= group %>
17
+ </div>
18
+ <% else %>
19
+ <%= group %>
20
+ <% end %>
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ # UI::PasswordInput — password field with a show/hide toggle.
6
+ #
7
+ # Composes UI::InputGroup so the border, focus ring and disabled styling stay
8
+ # identical to every other grouped input; the toggle is an inline-end addon
9
+ # driven by the `ui--password-visibility` Stimulus controller.
10
+ #
11
+ # Velvet::PasswordInput hardcoded a placeholder (shadowing the caller's) and
12
+ # accepted `rounded:`/`full_width:`/`variant:`/`size:`/`mb:` that nothing read.
13
+ # Those are gone. Its toggle was also dead — no controller was ever registered.
14
+ #
15
+ # Usage:
16
+ # <%= render(UI::PasswordInput.new(form: f, field: :password, required: true)) %>
17
+ # <%= render(UI::PasswordInput.new(name: "user[password]")) %>
18
+ class UI::PasswordInput < UI::Base
19
+ def initialize(form: nil, field: nil, label: nil, required: false,
20
+ disabled: false, error: false, class: nil, **html_attrs)
21
+ @form = form
22
+ @field = field
23
+ @label = label
24
+ # The <input> is wrapped by the group's border, so the label has to sit
25
+ # outside it; give the field a stable id for the label to point at.
26
+ @input_id = html_attrs[:id] || "ui-password-#{SecureRandom.hex(4)}"
27
+ @required = required
28
+ @disabled = disabled
29
+ @error = error
30
+ @user_class = binding.local_variable_get(:class)
31
+ @html_attrs = html_attrs
32
+ end
33
+
34
+ def group_component
35
+ UI::InputGroup.new(
36
+ class: @user_class,
37
+ data: { controller: "ui--password-visibility" }
38
+ )
39
+ end
40
+
41
+ # Same convention as UI::Input: bound to a form object the attribute name
42
+ # supplies the label text; `label:` overrides it, `label: false` opts out.
43
+ def render_label?
44
+ return false if @label == false
45
+
46
+ @label.present? || form_field?
47
+ end
48
+
49
+ def form_field?
50
+ @form.present? && @field.present?
51
+ end
52
+
53
+ def label_text
54
+ return @label if @label.is_a?(String) && @label.present?
55
+ return nil unless form_field?
56
+
57
+ object = @form.object
58
+ if object.respond_to?(:class) && object.class.respond_to?(:human_attribute_name)
59
+ object.class.human_attribute_name(@field)
60
+ else
61
+ @field.to_s.humanize
62
+ end
63
+ end
64
+
65
+ def label_component
66
+ UI::Label.new(for: @input_id, required: @required, disabled: @disabled)
67
+ end
68
+
69
+ def field_options
70
+ {
71
+ type: :password,
72
+ id: @input_id,
73
+ form: @form,
74
+ field: @field,
75
+ label: false,
76
+ required: @required,
77
+ disabled: @disabled,
78
+ error: @error,
79
+ data: { "ui--password-visibility-target" => "input" },
80
+ **@html_attrs
81
+ }
82
+ end
83
+
84
+ def toggle_button_options
85
+ {
86
+ disabled: @disabled,
87
+ aria: { label: I18n.t("ui.password_input.show", default: "Show password"), pressed: "false" },
88
+ data: {
89
+ action: "ui--password-visibility#toggle",
90
+ "ui--password-visibility-target" => "toggle"
91
+ }
92
+ }
93
+ end
94
+ end
@@ -0,0 +1,12 @@
1
+ <%= tag.div(**html_options) do %>
2
+ <% if show_close? %>
3
+ <%= render(UI::Button.new(variant: :ghost, size: :icon_sm, class: "absolute right-1 top-1", data: { action: "click->ui--popover#close" }, aria: { label: "Close" })) do %>
4
+ <%= render(UI::Icon.new(name: :x, size: :sm)) %>
5
+ <% end %>
6
+ <% end %>
7
+ <% if title? %>
8
+ <%= tag.h3(id: title_id, class: "font-semibold leading-none tracking-tight mb-2") do %><%= title %><% end %>
9
+ <% end %>
10
+ <%= content %>
11
+ <%= tag.div(class: arrow_classes, data: { "ui--popover-target": "arrow", side: @side }) %>
12
+ <% end %>
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Popover::Content < UI::Base
4
+ variants(
5
+ base: "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none " \
6
+ "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 " \
7
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 " \
8
+ "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
9
+ variants: {
10
+ side: {
11
+ top: "",
12
+ right: "",
13
+ bottom: "",
14
+ left: ""
15
+ },
16
+ align: {
17
+ start: "",
18
+ center: "",
19
+ end: ""
20
+ }
21
+ },
22
+ defaults: { side: :top, align: :center }
23
+ )
24
+
25
+ renders_one :title
26
+
27
+ def initialize(side: :top, align: :center, show_close: false, class: nil, **html_attrs)
28
+ @side = side
29
+ @align = align
30
+ @show_close = show_close
31
+ @user_class = binding.local_variable_get(:class)
32
+ @html_attrs = html_attrs
33
+ end
34
+
35
+ def set_popover_context(popover)
36
+ @popover = popover
37
+ @id = @html_attrs[:id] || "ui-popover-content-#{popover.id}"
38
+ end
39
+
40
+ def classes
41
+ cn(
42
+ variant_classes(side: @side, align: @align),
43
+ @user_class
44
+ )
45
+ end
46
+
47
+ # A square turned 45°, half inside the popover. Only its two outer edges
48
+ # get a border, so it reads as the popover's outline stepping out to a
49
+ # point; the inner half covers the popover's own border. Keyed on
50
+ # data-side, which the controller updates when the popover flips.
51
+ def arrow_classes
52
+ "absolute size-2 rotate-45 bg-popover border-border " \
53
+ "data-[side=top]:border-r data-[side=top]:border-b " \
54
+ "data-[side=bottom]:border-l data-[side=bottom]:border-t " \
55
+ "data-[side=left]:border-t data-[side=left]:border-r " \
56
+ "data-[side=right]:border-b data-[side=right]:border-l"
57
+ end
58
+
59
+ def html_options
60
+ opts = @html_attrs.dup
61
+ opts[:id] = @id
62
+ opts[:class] = classes
63
+ opts[:role] = :dialog
64
+ opts[:"aria-modal"] = false
65
+ opts[:"aria-labelledby"] = "ui-popover-title-#{@popover.id}" if title?
66
+ opts[:"data-ui--popover-target"] = "content"
67
+ opts[:hidden] = true
68
+ opts[:"data-side"] = @side
69
+ opts[:"data-align"] = @align
70
+ opts[:tabindex] = -1
71
+ opts
72
+ end
73
+
74
+ def show_close?
75
+ @show_close
76
+ end
77
+
78
+ def title_id
79
+ "ui-popover-title-#{@popover.id}"
80
+ end
81
+
82
+ attr_reader :side, :align
83
+ end
@@ -0,0 +1 @@
1
+ <%= render(UI::Button.new(**button_options)) do %><%= content %><% end %>
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Popover::Trigger < UI::Base
4
+ def initialize(variant: :outline, size: :default, 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_popover_context(popover)
12
+ @popover = popover
13
+ @id = @html_attrs[:id] || "ui-popover-trigger-#{popover.id}"
14
+ end
15
+
16
+ def button_options
17
+ @html_attrs.merge(
18
+ id: @id,
19
+ class: @user_class,
20
+ variant: @variant || :default,
21
+ size: @size,
22
+ "aria-haspopup" => true,
23
+ "aria-expanded" => false,
24
+ "aria-controls" => "ui-popover-content-#{@popover.id}",
25
+ "data-ui--popover-target" => "trigger",
26
+ "data-action" => "click->ui--popover#toggle"
27
+ )
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ <%= tag.span(class: root_classes,
2
+ data: { controller: "ui--popover" }.merge(@html_attrs[:data] || {}),
3
+ **@html_attrs.except(:data)) do %><%= trigger %><%= popover_content %><% end %>