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,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::PasswordInputTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_a_masked_input_inside_an_input_group
10
+ render_inline(UI::PasswordInput.new(name: "user[password]"))
11
+ assert_selector "[data-slot='input-group']"
12
+ assert_selector "input[type='password'][name='user[password]']"
13
+ end
14
+
15
+ def test_wires_the_stimulus_controller_and_input_target
16
+ render_inline(UI::PasswordInput.new(name: "user[password]"))
17
+ assert_selector "[data-controller='ui--password-visibility']"
18
+ assert_selector "input[data-ui--password-visibility-target='input']"
19
+ end
20
+
21
+ def test_toggle_button_wires_action_and_accessible_name
22
+ render_inline(UI::PasswordInput.new(name: "user[password]"))
23
+ button = page.find("button[data-ui--password-visibility-target='toggle']")
24
+ assert_includes button[:"data-action"], "ui--password-visibility#toggle"
25
+ assert_equal "Show password", button[:"aria-label"]
26
+ assert_equal "false", button[:"aria-pressed"]
27
+ end
28
+
29
+ def test_renders_both_toggle_icons_with_hide_hidden_initially
30
+ render_inline(UI::PasswordInput.new(name: "user[password]"))
31
+ assert_selector "span[data-ui--password-visibility-target='iconShow']"
32
+ hide = page.find("span[data-ui--password-visibility-target='iconHide']", visible: :all)
33
+ assert_includes hide[:style].to_s, "display: none"
34
+ end
35
+
36
+ def test_required_and_disabled_pass_through
37
+ render_inline(UI::PasswordInput.new(name: "user[password]", required: true, disabled: true))
38
+ input = page.find("input[type='password']", visible: :all)
39
+ assert input[:required].present?
40
+ assert input[:disabled].present?
41
+ end
42
+
43
+ # Velvet hardcoded a placeholder in its template, shadowing the caller's.
44
+ def test_caller_placeholder_is_not_overridden
45
+ render_inline(UI::PasswordInput.new(name: "user[password]", placeholder: "Your secret"))
46
+ assert_equal "Your secret", page.find("input[type='password']")[:placeholder]
47
+ end
48
+
49
+ def test_no_placeholder_by_default
50
+ render_inline(UI::PasswordInput.new(name: "user[password]"))
51
+ assert_nil page.find("input[type='password']")[:placeholder].presence
52
+ end
53
+
54
+ def test_error_state_marks_input_invalid
55
+ render_inline(UI::PasswordInput.new(name: "user[password]", error: true))
56
+ assert_equal "true", page.find("input[type='password']")[:"aria-invalid"]
57
+ end
58
+
59
+ def test_user_class_reaches_the_group_wrapper
60
+ render_inline(UI::PasswordInput.new(name: "user[password]", class: "mt-4"))
61
+ assert_includes page.find("[data-slot='input-group']")[:class].split, "mt-4"
62
+ end
63
+
64
+ # --- form-builder binding + automatic labels (Rails convention) ---
65
+
66
+ # Stands in for an app model: the builder needs only a name and attributes.
67
+ class User
68
+ include ActiveModel::Model
69
+ attr_accessor :email, :password, :remember_me
70
+ end
71
+
72
+ def form_builder_for(record)
73
+ ActionView::Helpers::FormBuilder.new(:user, record, vc_test_controller.view_context, {})
74
+ end
75
+
76
+ def test_form_binding_scopes_the_field_name
77
+ render_inline(UI::PasswordInput.new(form: form_builder_for(User.new), field: :password))
78
+ assert_selector "input[name='user[password]'][type='password']"
79
+ end
80
+
81
+ def test_label_is_derived_from_the_attribute_name
82
+ render_inline(UI::PasswordInput.new(form: form_builder_for(User.new), field: :password))
83
+ assert_selector "label", text: "Password"
84
+ end
85
+
86
+ # The input sits inside the group's border, so the label must render outside
87
+ # it while still pointing at the field.
88
+ def test_derived_label_sits_outside_the_group_and_is_associated
89
+ render_inline(UI::PasswordInput.new(form: form_builder_for(User.new), field: :password))
90
+ assert_equal page.find("label")[:for], page.find("input[type='password']")[:id]
91
+ assert_no_selector "[data-slot='input-group'] label"
92
+ end
93
+
94
+ def test_label_false_opts_out
95
+ render_inline(UI::PasswordInput.new(form: form_builder_for(User.new), field: :password, label: false))
96
+ assert_no_selector "label"
97
+ end
98
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Popover::ContentTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_content
10
+ result = render_inline(UI::Popover.new) do |popover|
11
+ popover.with_trigger { "Trigger" }
12
+ popover.with_content { "Popover content" }
13
+ end
14
+ assert result.to_html.include?("Popover content")
15
+ end
16
+
17
+ def test_has_role_dialog
18
+ render_inline(UI::Popover.new) do |popover|
19
+ popover.with_trigger { "Trigger" }
20
+ popover.with_content { "Content" }
21
+ end
22
+ assert_selector "[role='dialog']", visible: false
23
+ end
24
+
25
+ def test_has_aria_modal_false
26
+ render_inline(UI::Popover.new) do |popover|
27
+ popover.with_trigger { "Trigger" }
28
+ popover.with_content { "Content" }
29
+ end
30
+ content = page.find("[role='dialog']", visible: false)
31
+ assert_equal "false", content[:"aria-modal"]
32
+ end
33
+
34
+ def test_is_initially_hidden
35
+ render_inline(UI::Popover.new) do |popover|
36
+ popover.with_trigger { "Trigger" }
37
+ popover.with_content { "Content" }
38
+ end
39
+ content = page.find("[role='dialog']", visible: false)
40
+ assert content[:hidden].present?
41
+ end
42
+
43
+ def test_has_data_target
44
+ render_inline(UI::Popover.new) do |popover|
45
+ popover.with_trigger { "Trigger" }
46
+ popover.with_content { "Content" }
47
+ end
48
+ content = page.find("[role='dialog']", visible: false)
49
+ assert_equal "content", content[:"data-ui--popover-target"]
50
+ end
51
+
52
+ def test_default_side_top
53
+ render_inline(UI::Popover.new) do |popover|
54
+ popover.with_trigger { "Trigger" }
55
+ popover.with_content { "Content" }
56
+ end
57
+ content = page.find("[role='dialog']", visible: false)
58
+ assert_equal "top", content[:"data-side"]
59
+ end
60
+
61
+ def test_custom_side
62
+ render_inline(UI::Popover.new) do |popover|
63
+ popover.with_trigger { "Trigger" }
64
+ popover.with_content(side: :right) { "Content" }
65
+ end
66
+ content = page.find("[role='dialog']", visible: false)
67
+ assert_equal "right", content[:"data-side"]
68
+ end
69
+
70
+ def test_custom_align
71
+ render_inline(UI::Popover.new) do |popover|
72
+ popover.with_trigger { "Trigger" }
73
+ popover.with_content(align: :end) { "Content" }
74
+ end
75
+ content = page.find("[role='dialog']", visible: false)
76
+ assert_equal "end", content[:"data-align"]
77
+ end
78
+
79
+ def test_has_animation_classes
80
+ render_inline(UI::Popover.new) do |popover|
81
+ popover.with_trigger { "Trigger" }
82
+ popover.with_content { "Content" }
83
+ end
84
+ content = page.find("[role='dialog']", visible: false)
85
+ assert content[:class].include?("animate-in")
86
+ assert content[:class].include?("fade-in-0")
87
+ end
88
+
89
+ def test_has_arrow_element
90
+ render_inline(UI::Popover.new) do |popover|
91
+ popover.with_trigger { "Trigger" }
92
+ popover.with_content { "Content" }
93
+ end
94
+ assert_selector "[data-ui--popover-target='arrow']", visible: false
95
+ end
96
+
97
+ def test_has_default_width
98
+ render_inline(UI::Popover.new) do |popover|
99
+ popover.with_trigger { "Trigger" }
100
+ popover.with_content { "Content" }
101
+ end
102
+ content = page.find("[role='dialog']", visible: false)
103
+ assert content[:class].include?("w-72")
104
+ end
105
+
106
+ def test_has_popover_styling
107
+ render_inline(UI::Popover.new) do |popover|
108
+ popover.with_trigger { "Trigger" }
109
+ popover.with_content { "Content" }
110
+ end
111
+ content = page.find("[role='dialog']", visible: false)
112
+ assert content[:class].include?("bg-popover")
113
+ assert content[:class].include?("text-popover-foreground")
114
+ assert content[:class].include?("rounded-md")
115
+ assert content[:class].include?("shadow-md")
116
+ end
117
+
118
+ def test_custom_class_merge
119
+ render_inline(UI::Popover.new) do |popover|
120
+ popover.with_trigger { "Trigger" }
121
+ popover.with_content(class: "custom-content") { "Content" }
122
+ end
123
+ content = page.find("[role='dialog']", visible: false)
124
+ assert_includes content[:class].split, "custom-content"
125
+ end
126
+
127
+ def test_renders_title
128
+ result = render_inline(UI::Popover.new) do |popover|
129
+ popover.with_trigger { "Trigger" }
130
+ popover.with_content do |content|
131
+ content.with_title { "My Title" }
132
+ "Body content"
133
+ end
134
+ end
135
+ assert result.to_html.include?("My Title")
136
+ end
137
+
138
+ def test_title_has_heading_classes
139
+ render_inline(UI::Popover.new) do |popover|
140
+ popover.with_trigger { "Trigger" }
141
+ popover.with_content do |content|
142
+ content.with_title { "My Title" }
143
+ "Body"
144
+ end
145
+ end
146
+ title = page.find("h3", text: "My Title", visible: false)
147
+ assert title[:class].include?("font-semibold")
148
+ end
149
+
150
+ def test_title_sets_aria_labelledby
151
+ render_inline(UI::Popover.new(id: "test-popover")) do |popover|
152
+ popover.with_trigger { "Trigger" }
153
+ popover.with_content do |content|
154
+ content.with_title { "My Title" }
155
+ "Body"
156
+ end
157
+ end
158
+ content = page.find("[role='dialog']", visible: false)
159
+ assert_equal "ui-popover-title-test-popover", content[:"aria-labelledby"]
160
+ end
161
+
162
+ def test_show_close_button
163
+ render_inline(UI::Popover.new) do |popover|
164
+ popover.with_trigger { "Trigger" }
165
+ popover.with_content(show_close: true) { "Content" }
166
+ end
167
+ close_button = page.find("button[aria-label='Close']", visible: false)
168
+ assert close_button.present?
169
+ end
170
+
171
+ def test_close_button_has_action
172
+ render_inline(UI::Popover.new) do |popover|
173
+ popover.with_trigger { "Trigger" }
174
+ popover.with_content(show_close: true) { "Content" }
175
+ end
176
+ close_button = page.find("button[aria-label='Close']", visible: false)
177
+ assert close_button[:"data-action"].include?("click->ui--popover#close")
178
+ end
179
+
180
+ def test_close_button_uses_ghost_variant
181
+ render_inline(UI::Popover.new) do |popover|
182
+ popover.with_trigger { "Trigger" }
183
+ popover.with_content(show_close: true) { "Content" }
184
+ end
185
+ close_button = page.find("button[aria-label='Close']", visible: false)
186
+ assert close_button[:class].include?("hover:bg-accent")
187
+ end
188
+
189
+ def test_arrow_has_data_side_attribute
190
+ # Arrow has data-side attribute that Floating UI can update dynamically
191
+ render_inline(UI::Popover.new) do |popover|
192
+ popover.with_trigger { "Trigger" }
193
+ popover.with_content(side: :top) { "Content" }
194
+ end
195
+ arrow = page.find("[data-ui--popover-target='arrow']", visible: false)
196
+ assert_equal "top", arrow[:"data-side"]
197
+ end
198
+
199
+ # The arrow is a square turned 45°. Only the two edges that make its outer
200
+ # point carry a border; the inner two sit over the popover and stay bare,
201
+ # or they show as a line inside the popover.
202
+ def test_arrow_borders_only_its_outer_point_on_every_side
203
+ render_inline(UI::Popover.new) do |popover|
204
+ popover.with_trigger { "Trigger" }
205
+ popover.with_content { "Content" }
206
+ end
207
+ classes = page.find("[data-ui--popover-target='arrow']", visible: false)[:class].split
208
+ { top: %w[r b], bottom: %w[l t], left: %w[t r], right: %w[b l] }.each do |side, edges|
209
+ shown = classes.filter_map { |c| c[/\Adata-\[side=#{side}\]:border-([trbl])\z/, 1] }
210
+ assert_equal edges.sort, shown.sort, "side=#{side}"
211
+ end
212
+ refute(classes.any? { |c| c.match?(/\Aborder-[trbl]\z/) }, "no border outside a side variant")
213
+ end
214
+
215
+ def test_arrow_side_matches_content_side
216
+ # Arrow's data-side matches the content's data-side
217
+ render_inline(UI::Popover.new) do |popover|
218
+ popover.with_trigger { "Trigger" }
219
+ popover.with_content(side: :bottom) { "Content" }
220
+ end
221
+ content = page.find("[role='dialog']", visible: false)
222
+ arrow = page.find("[data-ui--popover-target='arrow']", visible: false)
223
+ assert_equal content[:"data-side"], arrow[:"data-side"]
224
+ assert_equal "bottom", arrow[:"data-side"]
225
+ end
226
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Popover::TriggerTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_content
10
+ render_inline(UI::Popover.new) do |popover|
11
+ popover.with_trigger { "Trigger text" }
12
+ popover.with_content { "Content" }
13
+ end
14
+ assert_text "Trigger text"
15
+ end
16
+
17
+ def test_has_aria_attributes
18
+ render_inline(UI::Popover.new(id: "test-id")) do |popover|
19
+ popover.with_trigger { "Trigger" }
20
+ popover.with_content { "Content" }
21
+ end
22
+ trigger = page.find("button[data-ui--popover-target='trigger']")
23
+ assert_equal "true", trigger[:"aria-haspopup"]
24
+ assert_equal "false", trigger[:"aria-expanded"]
25
+ assert_equal "ui-popover-content-test-id", trigger[:"aria-controls"]
26
+ end
27
+
28
+ def test_has_data_target
29
+ render_inline(UI::Popover.new) do |popover|
30
+ popover.with_trigger { "Trigger" }
31
+ popover.with_content { "Content" }
32
+ end
33
+ assert_selector "button[data-ui--popover-target='trigger']"
34
+ end
35
+
36
+ def test_has_toggle_action
37
+ render_inline(UI::Popover.new) do |popover|
38
+ popover.with_trigger { "Trigger" }
39
+ popover.with_content { "Content" }
40
+ end
41
+ trigger = page.find("button[data-ui--popover-target='trigger']")
42
+ assert trigger[:"data-action"].include?("click->ui--popover#toggle")
43
+ end
44
+
45
+ def test_renders_as_button_with_default_variant
46
+ render_inline(UI::Popover.new) do |popover|
47
+ popover.with_trigger { "Trigger" }
48
+ popover.with_content { "Content" }
49
+ end
50
+ trigger = page.find("button[data-ui--popover-target='trigger']")
51
+ assert trigger[:class].include?("border")
52
+ assert trigger[:class].include?("bg-background")
53
+ end
54
+
55
+ def test_custom_variant
56
+ render_inline(UI::Popover.new) do |popover|
57
+ popover.with_trigger(variant: :default) { "Trigger" }
58
+ popover.with_content { "Content" }
59
+ end
60
+ trigger = page.find("button[data-ui--popover-target='trigger']")
61
+ assert trigger[:class].include?("bg-primary")
62
+ assert trigger[:class].include?("text-primary-foreground")
63
+ end
64
+
65
+ def test_custom_size
66
+ render_inline(UI::Popover.new) do |popover|
67
+ popover.with_trigger(size: :sm) { "Small" }
68
+ popover.with_content { "Content" }
69
+ end
70
+ trigger = page.find("button[data-ui--popover-target='trigger']", text: "Small")
71
+ assert trigger[:class].include?("h-8")
72
+ assert trigger[:class].include?("text-xs")
73
+ end
74
+
75
+ def test_custom_class_merge
76
+ render_inline(UI::Popover.new) do |popover|
77
+ popover.with_trigger(class: "custom-trigger") { "Trigger" }
78
+ popover.with_content { "Content" }
79
+ end
80
+ trigger = page.find("button[data-ui--popover-target='trigger']")
81
+ assert_includes trigger[:class].split, "custom-trigger"
82
+ end
83
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::PopoverTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_popover
10
+ render_inline(UI::Popover.new) do |popover|
11
+ popover.with_trigger { "Open Popover" }
12
+ popover.with_content { "Popover content" }
13
+ end
14
+ end
15
+
16
+ def test_root_renders_trigger_and_content
17
+ result = render_popover
18
+ assert_text "Open Popover"
19
+ assert result.to_html.include?("Popover content")
20
+ end
21
+
22
+ def test_trigger_has_aria_attributes
23
+ render_popover
24
+ trigger = page.find("[data-ui--popover-target='trigger']")
25
+ assert_equal "true", trigger[:"aria-haspopup"]
26
+ assert_equal "false", trigger[:"aria-expanded"]
27
+ assert trigger[:"aria-controls"].present?
28
+ assert trigger[:"aria-controls"].start_with?("ui-popover-content-")
29
+ end
30
+
31
+ def test_trigger_has_click_action
32
+ render_popover
33
+ trigger = page.find("[data-ui--popover-target='trigger']")
34
+ assert trigger[:"data-action"].include?("click->ui--popover#toggle")
35
+ end
36
+
37
+ def test_content_has_role_dialog
38
+ render_popover
39
+ assert_selector "[role='dialog']", visible: false
40
+ end
41
+
42
+ def test_content_is_initially_hidden
43
+ render_popover
44
+ content = page.find("[role='dialog']", visible: false)
45
+ assert content[:hidden].present?
46
+ end
47
+
48
+ def test_content_has_data_targets
49
+ render_popover
50
+ content = page.find("[role='dialog']", visible: false)
51
+ assert_equal "content", content[:"data-ui--popover-target"]
52
+ end
53
+
54
+ def test_default_side_is_top
55
+ render_popover
56
+ content = page.find("[role='dialog']", visible: false)
57
+ assert_equal "top", content[:"data-side"]
58
+ end
59
+
60
+ def test_custom_side
61
+ render_inline(UI::Popover.new) do |popover|
62
+ popover.with_trigger { "Open" }
63
+ popover.with_content(side: :bottom) { "Content" }
64
+ end
65
+ content = page.find("[role='dialog']", visible: false)
66
+ assert_equal "bottom", content[:"data-side"]
67
+ end
68
+
69
+ def test_default_width_280px
70
+ render_popover
71
+ content = page.find("[role='dialog']", visible: false)
72
+ assert content[:class].include?("w-72") # 72 * 4px = 288px (Tailwind w-72 is 18rem/288px)
73
+ end
74
+
75
+ def test_custom_id_preserved
76
+ render_inline(UI::Popover.new(id: "my-popover")) do |popover|
77
+ popover.with_trigger { "Open" }
78
+ popover.with_content { "Content" }
79
+ end
80
+ assert_selector "#my-popover"
81
+ end
82
+
83
+ def test_user_class_merge
84
+ render_inline(UI::Popover.new(class: "my-custom-class")) do |popover|
85
+ popover.with_trigger { "Open" }
86
+ popover.with_content { "Content" }
87
+ end
88
+ root = page.find("[data-controller='ui--popover']")
89
+ classes = root[:class].split
90
+ assert_includes classes, "my-custom-class"
91
+ assert_includes classes, "inline-block"
92
+ end
93
+
94
+ def test_html_attrs_passthrough
95
+ render_inline(UI::Popover.new(data: { test_id: "popover-x" })) do |popover|
96
+ popover.with_trigger { "Open" }
97
+ popover.with_content { "Content" }
98
+ end
99
+ assert_selector "[data-controller='ui--popover'][data-test-id='popover-x']"
100
+ end
101
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::RadioGroup::ItemTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_render_without_group_context_returns_nothing
10
+ item = UI::RadioGroup::Item.new(value: "free", label: "Free")
11
+ render_inline(item)
12
+ refute page.text.present?
13
+ end
14
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::RadioGroupTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_radiogroup_role
10
+ render_inline(UI::RadioGroup.new(name: "plan")) do |rg|
11
+ rg.with_item(value: "free") { "Free" }
12
+ end
13
+ assert_selector "[role='radiogroup']"
14
+ end
15
+
16
+ def test_auto_generates_id
17
+ render_inline(UI::RadioGroup.new(name: "plan")) do |rg|
18
+ rg.with_item(value: "free")
19
+ end
20
+ rg = page.find("[role='radiogroup']")
21
+ assert rg[:id].present?
22
+ assert rg[:id].start_with?("ui-rg-")
23
+ end
24
+
25
+ def test_user_class_passthrough
26
+ render_inline(UI::RadioGroup.new(name: "plan", class: "gap-4")) { "" }
27
+ classes = page.find("[role='radiogroup']")[:class].split
28
+ assert_includes classes, "gap-4"
29
+ assert_includes classes, "flex"
30
+ assert_includes classes, "flex-col"
31
+ end
32
+
33
+ def test_html_attrs_passthrough
34
+ render_inline(UI::RadioGroup.new(name: "plan", data: { test_id: "rg-x" })) { "" }
35
+ assert_selector "[role='radiogroup'][data-test-id='rg-x']"
36
+ end
37
+
38
+ def test_renders_items
39
+ render_inline(UI::RadioGroup.new(name: "plan")) do |rg|
40
+ rg.with_item(value: "free", label: "Free")
41
+ rg.with_item(value: "pro", label: "Pro")
42
+ end
43
+ inputs = page.all("input[type='radio']")
44
+ assert_equal 2, inputs.size
45
+ end
46
+
47
+ def test_items_share_same_name
48
+ render_inline(UI::RadioGroup.new(name: "plan")) do |rg|
49
+ rg.with_item(value: "free")
50
+ rg.with_item(value: "pro")
51
+ end
52
+ inputs = page.all("input[type='radio']")
53
+ assert_equal "plan", inputs.first[:name]
54
+ assert_equal "plan", inputs.last[:name]
55
+ end
56
+
57
+ def test_checked_item_reflects_group_value
58
+ render_inline(UI::RadioGroup.new(name: "plan", value: "pro")) do |rg|
59
+ rg.with_item(value: "free", label: "Free")
60
+ rg.with_item(value: "pro", label: "Pro")
61
+ end
62
+ inputs = page.all("input[type='radio']")
63
+ refute inputs.first["checked"].present?
64
+ assert inputs.last["checked"].present?
65
+ end
66
+
67
+ def test_item_label_renders_with_for_id
68
+ render_inline(UI::RadioGroup.new(name: "plan")) do |rg|
69
+ rg.with_item(value: "free", label: "Free plan")
70
+ end
71
+ label_el = page.find("label")
72
+ input_el = page.find("input[type='radio']")
73
+ assert_equal label_el["for"], input_el[:id]
74
+ assert_match(/Free plan/, label_el.text)
75
+ end
76
+
77
+ def test_item_without_label_no_label_element
78
+ render_inline(UI::RadioGroup.new(name: "plan")) do |rg|
79
+ rg.with_item(value: "free")
80
+ end
81
+ refute_selector "label"
82
+ end
83
+
84
+ def test_disabled_group_disables_all_items
85
+ render_inline(UI::RadioGroup.new(name: "plan", disabled: true)) do |rg|
86
+ rg.with_item(value: "free", label: "Free")
87
+ rg.with_item(value: "pro", label: "Pro")
88
+ end
89
+ inputs = page.all("input[type='radio']")
90
+ inputs.each { |i| assert i.disabled? }
91
+ end
92
+
93
+ def test_required_group_sets_required_on_items
94
+ render_inline(UI::RadioGroup.new(name: "plan", required: true)) do |rg|
95
+ rg.with_item(value: "free", label: "Free")
96
+ rg.with_item(value: "pro", label: "Pro")
97
+ end
98
+ inputs = page.all("input[type='radio']")
99
+ inputs.each { |i| assert i["required"].present? }
100
+ end
101
+
102
+ def test_required_label_shows_asterisk
103
+ render_inline(UI::RadioGroup.new(name: "plan", required: true)) do |rg|
104
+ rg.with_item(value: "free", label: "Free")
105
+ end
106
+ label_el = page.find("label")
107
+ assert_match(/Free \*/, label_el.text)
108
+ end
109
+
110
+ def test_disabled_item_wrapper_dims
111
+ render_inline(UI::RadioGroup.new(name: "plan")) do |rg|
112
+ rg.with_item(value: "free", label: "Free", disabled: true)
113
+ end
114
+ label_el = page.find("label")
115
+ classes = label_el[:class].split
116
+ assert_includes classes, "cursor-not-allowed"
117
+ assert_includes classes, "opacity-60"
118
+ end
119
+
120
+ def test_item_dot_appears_when_checked
121
+ render_inline(UI::RadioGroup.new(name: "plan", value: "pro")) do |rg|
122
+ rg.with_item(value: "pro", label: "Pro")
123
+ end
124
+ input = page.find("input[type='radio'][checked]")
125
+ dot = page.find("span.rounded-full.bg-primary-foreground", visible: :all)
126
+ classes = dot[:class].split
127
+ assert_includes classes, "bg-primary-foreground"
128
+ end
129
+
130
+ def test_item_box_has_rounded_full_and_checked_states
131
+ render_inline(UI::RadioGroup.new(name: "plan")) do |rg|
132
+ rg.with_item(value: "free")
133
+ end
134
+ classes = page.find("input[type='radio']")[:class].split
135
+ assert_includes classes, "rounded-full"
136
+ assert_includes classes, "peer"
137
+ assert_includes classes, "appearance-none"
138
+ end
139
+ end