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,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::InputTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ # ── Default render ───────────────────────────────────────────────────────────
10
+
11
+ def test_renders_input_tag
12
+ render_inline(UI::Input.new)
13
+ assert_selector "input"
14
+ refute_selector "div"
15
+ end
16
+
17
+ def test_default_type_is_text
18
+ render_inline(UI::Input.new)
19
+ assert_selector "input[type='text']"
20
+ end
21
+
22
+ def test_default_size_classes_present
23
+ render_inline(UI::Input.new)
24
+ inp = page.find("input")
25
+ assert_includes inp[:class], "h-9"
26
+ end
27
+
28
+ def test_default_has_border_and_background
29
+ render_inline(UI::Input.new)
30
+ inp = page.find("input")
31
+ assert_includes inp[:class], "border-input"
32
+ assert_includes inp[:class], "bg-background"
33
+ end
34
+
35
+ # ── Type variants ────────────────────────────────────────────────────────────
36
+
37
+ def test_type_email
38
+ render_inline(UI::Input.new(type: :email))
39
+ assert_selector "input[type='email']"
40
+ end
41
+
42
+ def test_type_password
43
+ render_inline(UI::Input.new(type: :password))
44
+ assert_selector "input[type='password']"
45
+ end
46
+
47
+ def test_type_number
48
+ render_inline(UI::Input.new(type: :number))
49
+ assert_selector "input[type='number']"
50
+ end
51
+
52
+ def test_type_search
53
+ render_inline(UI::Input.new(type: :search))
54
+ assert_selector "input[type='search']"
55
+ end
56
+
57
+ # ── Size variants ────────────────────────────────────────────────────────────
58
+
59
+ def test_size_sm
60
+ render_inline(UI::Input.new(size: :sm))
61
+ inp = page.find("input")
62
+ classes = inp[:class].split
63
+ assert_includes classes, "h-8"
64
+ assert_includes classes, "text-xs"
65
+ end
66
+
67
+ def test_size_default
68
+ render_inline(UI::Input.new(size: :default))
69
+ assert_selector "input.h-9"
70
+ end
71
+
72
+ def test_size_lg
73
+ render_inline(UI::Input.new(size: :lg))
74
+ inp = page.find("input")
75
+ assert_includes inp[:class], "h-10"
76
+ end
77
+
78
+ # ── Error / invalid state ────────────────────────────────────────────────────
79
+
80
+ def test_error_state_sets_aria_invalid
81
+ render_inline(UI::Input.new(error: true, variant: :error))
82
+ assert_selector "input[aria-invalid='true']"
83
+ end
84
+
85
+ def test_error_state_adds_destructive_ring_class
86
+ render_inline(UI::Input.new(variant: :error))
87
+ inp = page.find("input")
88
+ assert_includes inp[:class], "border-destructive"
89
+ end
90
+
91
+ def test_no_error_does_not_set_aria_invalid
92
+ render_inline(UI::Input.new)
93
+ refute_selector "input[aria-invalid]"
94
+ end
95
+
96
+ # ── Disabled state ───────────────────────────────────────────────────────────
97
+
98
+ def test_disabled_sets_html_disabled
99
+ render_inline(UI::Input.new(disabled: true))
100
+ assert_selector "input[disabled]"
101
+ end
102
+
103
+ def test_disabled_adds_cursor_class
104
+ render_inline(UI::Input.new(disabled: true))
105
+ inp = page.find("input[disabled]")
106
+ assert_includes inp[:class], "disabled:cursor-not-allowed"
107
+ end
108
+
109
+ # ── Placeholder ──────────────────────────────────────────────────────────────
110
+
111
+ def test_placeholder_passes_through
112
+ render_inline(UI::Input.new(placeholder: "Enter email…"))
113
+ assert_selector "input[placeholder='Enter email…']"
114
+ end
115
+
116
+ # ── Name / id / value ────────────────────────────────────────────────────────
117
+
118
+ def test_name_and_id_pass_through
119
+ render_inline(UI::Input.new(name: "user[email]", id: "user_email"))
120
+ assert_selector "input#user_email[name='user[email]']"
121
+ end
122
+
123
+ def test_value_passes_through
124
+ render_inline(UI::Input.new(value: "hello@example.com"))
125
+ assert_selector "input[value='hello@example.com']"
126
+ end
127
+
128
+ # ── User-class merge ─────────────────────────────────────────────────────────
129
+
130
+ def test_user_class_merges_via_tailwind_merge
131
+ render_inline(UI::Input.new(class: "bg-red-100"))
132
+ inp = page.find("input")
133
+ classes = inp[:class].split
134
+ refute_includes classes, "bg-background"
135
+ assert_includes classes, "bg-red-100"
136
+ end
137
+
138
+ # ── Arbitrary html_attrs ─────────────────────────────────────────────────────
139
+
140
+ def test_html_attrs_pass_through
141
+ render_inline(UI::Input.new(id: "q", data: { test_id: "search" }))
142
+ assert_selector "input#q[data-test-id='search']"
143
+ end
144
+
145
+ def test_autocomplete_passes_through
146
+ render_inline(UI::Input.new(autocomplete: "email"))
147
+ assert_selector "input[autocomplete='email']"
148
+ end
149
+
150
+ # ── Label ────────────────────────────────────────────────────────────────────
151
+
152
+ def test_label_renders_label_element_as_sibling_of_input
153
+ render_inline(UI::Input.new(label: "Email"))
154
+ assert_selector "label", text: "Email"
155
+ # input must be a sibling, not a descendant
156
+ assert_selector "div > label + input"
157
+ refute_selector "label > input"
158
+ end
159
+
160
+ def test_label_for_attribute_links_to_input_id
161
+ render_inline(UI::Input.new(label: "Email"))
162
+ label = page.find("label")
163
+ input = page.find("input")
164
+ assert_equal input[:id], label[:for]
165
+ assert input[:id].to_s.start_with?("ui-input-")
166
+ end
167
+
168
+ def test_explicit_id_is_preserved_and_linked
169
+ render_inline(UI::Input.new(label: "Email", id: "user_email"))
170
+ assert_selector "label[for='user_email']"
171
+ assert_selector "input#user_email"
172
+ end
173
+
174
+ def test_label_with_size_variant
175
+ render_inline(UI::Input.new(label: "Email", label_size: :sm))
176
+ label = page.find("label")
177
+ assert_includes label[:class], "text-sm"
178
+ end
179
+
180
+ def test_required_label_shows_styled_asterisk_span
181
+ render_inline(UI::Input.new(label: "Email", required: true))
182
+ assert_selector "label", text: "Email *"
183
+ asterisk = page.find("label span.text-destructive")
184
+ assert_match(/\*/, asterisk.text)
185
+ end
186
+
187
+ def test_required_sets_html_required_on_input
188
+ render_inline(UI::Input.new(label: "Email", required: true))
189
+ assert_selector "input[required]"
190
+ end
191
+
192
+ def test_required_without_label_still_sets_html_required
193
+ render_inline(UI::Input.new(required: true))
194
+ assert_selector "input[required]"
195
+ end
196
+
197
+ def test_non_required_label_no_asterisk
198
+ render_inline(UI::Input.new(label: "Email"))
199
+ assert_selector "label", text: "Email"
200
+ refute_selector "label span.text-destructive"
201
+ refute_selector "input[required]"
202
+ end
203
+
204
+ def test_disabled_input_with_label_propagates_disabled_to_label
205
+ render_inline(UI::Input.new(label: "Email", disabled: true))
206
+ label = page.find("label")
207
+ assert_includes label[:class], "text-muted-foreground"
208
+ end
209
+
210
+ # --- form-builder binding + automatic labels (Rails convention) ---
211
+
212
+ # Stands in for an app model: the builder needs only a name and attributes.
213
+ class User
214
+ include ActiveModel::Model
215
+ attr_accessor :email, :password, :remember_me
216
+ end
217
+
218
+ def form_builder_for(record)
219
+ ActionView::Helpers::FormBuilder.new(:user, record, vc_test_controller.view_context, {})
220
+ end
221
+
222
+ def test_form_binding_scopes_the_field_name
223
+ render_inline(UI::Input.new(form: form_builder_for(User.new), field: :email, type: :email))
224
+ assert_selector "input[name='user[email]'][type='email']"
225
+ end
226
+
227
+ def test_label_is_derived_from_the_attribute_name
228
+ render_inline(UI::Input.new(form: form_builder_for(User.new), field: :email, type: :email))
229
+ assert_selector "label", text: "Email"
230
+ end
231
+
232
+ def test_derived_label_is_associated_with_the_input
233
+ render_inline(UI::Input.new(form: form_builder_for(User.new), field: :email, type: :email))
234
+ assert_equal page.find("label")[:for], page.find("input")[:id]
235
+ end
236
+
237
+ def test_explicit_label_overrides_the_derived_one
238
+ render_inline(UI::Input.new(form: form_builder_for(User.new), field: :email, label: "Work email"))
239
+ assert_selector "label", text: "Work email"
240
+ end
241
+
242
+ def test_label_false_opts_out
243
+ render_inline(UI::Input.new(form: form_builder_for(User.new), field: :email, label: false))
244
+ assert_no_selector "label"
245
+ end
246
+
247
+ def test_no_label_without_a_form_binding
248
+ render_inline(UI::Input.new(name: "user[email]"))
249
+ assert_no_selector "label"
250
+ end
251
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::LabelTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_default
10
+ render_inline(UI::Label.new) { "Email" }
11
+ assert_selector "label", text: "Email"
12
+ end
13
+
14
+ def test_renders_label_tag
15
+ render_inline(UI::Label.new) { "Name" }
16
+ assert_selector "label"
17
+ refute_selector "div"
18
+ end
19
+
20
+ def test_default_size_classes
21
+ render_inline(UI::Label.new) { "Label" }
22
+ lbl = page.find("label")
23
+ assert_includes lbl[:class], "text-sm"
24
+ end
25
+
26
+ def test_size_xs
27
+ render_inline(UI::Label.new(size: :xs)) { "Tiny" }
28
+ assert_selector "label.text-xs"
29
+ end
30
+
31
+ def test_size_sm
32
+ render_inline(UI::Label.new(size: :sm)) { "Small" }
33
+ assert_selector "label.text-sm"
34
+ end
35
+
36
+ def test_size_lg
37
+ render_inline(UI::Label.new(size: :lg)) { "Large" }
38
+ assert_selector "label.text-base"
39
+ end
40
+
41
+ def test_weight_normal
42
+ render_inline(UI::Label.new(weight: :normal)) { "Light" }
43
+ lbl = page.find("label")
44
+ assert_includes lbl[:class], "font-normal"
45
+ end
46
+
47
+ def test_weight_medium_is_default
48
+ render_inline(UI::Label.new) { "Default" }
49
+ lbl = page.find("label")
50
+ assert_includes lbl[:class], "font-medium"
51
+ end
52
+
53
+ def test_weight_semibold
54
+ render_inline(UI::Label.new(weight: :semibold)) { "Bold" }
55
+ lbl = page.find("label")
56
+ assert_includes lbl[:class], "font-semibold"
57
+ end
58
+
59
+ def test_for_attribute_passes_through
60
+ render_inline(UI::Label.new(for: "email_field")) { "Email" }
61
+ assert_selector "label[for='email_field']", text: "Email"
62
+ end
63
+
64
+ def test_required_appends_asterisk
65
+ render_inline(UI::Label.new(required: true)) { "Name" }
66
+ lbl = page.find("label")
67
+ assert_match(/Name/, lbl.text)
68
+ assert_selector "label span", text: "*"
69
+ end
70
+
71
+ def test_required_false_does_not_append_asterisk
72
+ render_inline(UI::Label.new(required: false)) { "Name" }
73
+ refute_selector "label span"
74
+ end
75
+
76
+ def test_disabled_applies_muted_class
77
+ render_inline(UI::Label.new(disabled: true)) { "Off" }
78
+ lbl = page.find("label")
79
+ classes = lbl[:class].split
80
+ assert_includes classes, "text-muted-foreground"
81
+ assert_includes classes, "cursor-not-allowed"
82
+ end
83
+
84
+ def test_user_class_overrides_default_via_tailwind_merge
85
+ render_inline(UI::Label.new(class: "text-lg")) { "Override" }
86
+ lbl = page.find("label")
87
+ classes = lbl[:class].split
88
+ refute_includes classes, "text-sm"
89
+ assert_includes classes, "text-lg"
90
+ end
91
+
92
+ def test_html_attrs_pass_through
93
+ render_inline(UI::Label.new(id: "my-label", data: { test_id: "lbl" })) { "Test" }
94
+ assert_selector "label#my-label[data-test-id='lbl']", text: "Test"
95
+ end
96
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::LinkTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_default_link_variant
10
+ render_inline(UI::Link.new(href: "/docs")) { "Learn more" }
11
+ link = page.find("a", text: "Learn more")
12
+ classes = link[:class].split
13
+ assert_equal "/docs", link[:href]
14
+ assert_includes classes, "text-primary"
15
+ assert_includes classes, "underline-offset-4"
16
+ refute_includes classes, "px-3"
17
+ refute_includes classes, "px-4"
18
+ refute_includes classes, "px-8"
19
+ refute_includes classes, "py-2"
20
+ refute_includes classes, "h-8"
21
+ refute_includes classes, "h-9"
22
+ refute_includes classes, "h-10"
23
+ end
24
+
25
+ def test_renders_button_style_default_variant
26
+ render_inline(UI::Link.new(href: "/signup", variant: :default, button_variant: true)) { "Sign up" }
27
+ link = page.find("a", text: "Sign up")
28
+ classes = link[:class].split
29
+ assert_includes classes, "bg-primary"
30
+ assert_includes classes, "text-primary-foreground"
31
+ end
32
+
33
+ def test_renders_outline_variant
34
+ render_inline(UI::Link.new(variant: :outline, button_variant: true)) { "Outline" }
35
+ link = page.find("a", text: "Outline")
36
+ classes = link[:class].split
37
+ assert_includes classes, "border"
38
+ assert_includes classes, "bg-background"
39
+ end
40
+
41
+ def test_renders_destructive_variant
42
+ render_inline(UI::Link.new(variant: :destructive, button_variant: true)) { "Delete" }
43
+ link = page.find("a", text: "Delete")
44
+ classes = link[:class].split
45
+ assert_includes classes, "bg-destructive"
46
+ assert_includes classes, "focus:ring-destructive"
47
+ end
48
+
49
+ def test_size_sm
50
+ render_inline(UI::Link.new(size: :sm, button_variant: true)) { "Small" }
51
+ classes = page.find("a", text: "Small")[:class].split
52
+ assert_includes classes, "h-8"
53
+ assert_includes classes, "text-xs"
54
+ end
55
+
56
+ def test_size_lg
57
+ render_inline(UI::Link.new(size: :lg, button_variant: true)) { "Large" }
58
+ classes = page.find("a", text: "Large")[:class].split
59
+ assert_includes classes, "h-10"
60
+ assert_includes classes, "px-8"
61
+ end
62
+
63
+ def test_user_class_overrides_default
64
+ render_inline(UI::Link.new(class: "bg-red-500", variant: :default, button_variant: true)) { "Custom" }
65
+ classes = page.find("a", text: "Custom")[:class].split
66
+ refute_includes classes, "bg-primary"
67
+ assert_includes classes, "bg-red-500"
68
+ end
69
+
70
+ def test_html_attrs_pass_through
71
+ render_inline(UI::Link.new(id: "docs-link", data: { test_id: "docs" }, target: "_blank")) { "Docs" }
72
+ assert_selector "a#docs-link[data-test-id='docs'][target='_blank']", text: "Docs"
73
+ end
74
+
75
+ def test_leading_icon_renders_before_content
76
+ render_inline(UI::Link.new(href: "/next")) do |link|
77
+ link.with_leading_icon(name: "arrow-right")
78
+ "Continue"
79
+ end
80
+
81
+ anchor = page.find("a", text: "Continue")
82
+ assert_equal "arrow-right", anchor.find("i", visible: :all)["data-lucide"]
83
+ assert_match(/<i[^>]*data-lucide="arrow-right".*Continue/m, anchor.native.to_html)
84
+ end
85
+
86
+ def test_trailing_icon_renders_after_content
87
+ render_inline(UI::Link.new(href: "/next")) do |link|
88
+ link.with_trailing_icon(name: "arrow-right")
89
+ "Continue"
90
+ end
91
+
92
+ anchor = page.find("a", text: "Continue")
93
+ assert_match(/Continue.*<i[^>]*data-lucide="arrow-right"/m, anchor.native.to_html)
94
+ end
95
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Menu::ContentTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_content
10
+ result = render_inline(UI::Menu.new) do |menu|
11
+ menu.with_trigger { "Trigger" }
12
+ menu.with_content do |content|
13
+ content.with_item("Menu content")
14
+ end
15
+ end
16
+ assert result.to_html.include?("Menu content")
17
+ end
18
+
19
+ def test_has_role_menu
20
+ render_inline(UI::Menu.new) do |menu|
21
+ menu.with_trigger { "Trigger" }
22
+ menu.with_content do |content|
23
+ content.with_item("Item")
24
+ end
25
+ end
26
+ assert_selector "[role='menu']", visible: false
27
+ end
28
+
29
+ def test_has_vertical_orientation
30
+ render_inline(UI::Menu.new) do |menu|
31
+ menu.with_trigger { "Trigger" }
32
+ menu.with_content do |content|
33
+ content.with_item("Item")
34
+ end
35
+ end
36
+ content = page.find("[role='menu']", visible: false)
37
+ assert_equal "vertical", content[:"aria-orientation"]
38
+ end
39
+
40
+ def test_is_initially_hidden
41
+ render_inline(UI::Menu.new) do |menu|
42
+ menu.with_trigger { "Trigger" }
43
+ menu.with_content do |content|
44
+ content.with_item("Item")
45
+ end
46
+ end
47
+ content = page.find("[role='menu']", visible: false)
48
+ assert content[:hidden].present?
49
+ end
50
+
51
+ def test_has_data_target
52
+ render_inline(UI::Menu.new) do |menu|
53
+ menu.with_trigger { "Trigger" }
54
+ menu.with_content do |content|
55
+ content.with_item("Item")
56
+ end
57
+ end
58
+ content = page.find("[role='menu']", visible: false)
59
+ assert_equal "content", content[:"data-ui--menu-target"]
60
+ end
61
+
62
+ def test_default_side_bottom
63
+ render_inline(UI::Menu.new) do |menu|
64
+ menu.with_trigger { "Trigger" }
65
+ menu.with_content do |content|
66
+ content.with_item("Item")
67
+ end
68
+ end
69
+ content = page.find("[role='menu']", visible: false)
70
+ assert_equal "bottom", content[:"data-side"]
71
+ end
72
+
73
+ def test_custom_side
74
+ render_inline(UI::Menu.new) do |menu|
75
+ menu.with_trigger { "Trigger" }
76
+ menu.with_content(side: :top) do |content|
77
+ content.with_item("Item")
78
+ end
79
+ end
80
+ content = page.find("[role='menu']", visible: false)
81
+ assert_equal "top", content[:"data-side"]
82
+ end
83
+
84
+ def test_custom_align
85
+ render_inline(UI::Menu.new) do |menu|
86
+ menu.with_trigger { "Trigger" }
87
+ menu.with_content(align: :end) do |content|
88
+ content.with_item("Item")
89
+ end
90
+ end
91
+ content = page.find("[role='menu']", visible: false)
92
+ assert_equal "end", content[:"data-align"]
93
+ end
94
+
95
+ def test_has_animation_classes
96
+ render_inline(UI::Menu.new) do |menu|
97
+ menu.with_trigger { "Trigger" }
98
+ menu.with_content do |content|
99
+ content.with_item("Item")
100
+ end
101
+ end
102
+ content = page.find("[role='menu']", visible: false)
103
+ assert content[:class].include?("animate-in")
104
+ assert content[:class].include?("fade-in-0")
105
+ end
106
+
107
+ def test_has_min_width
108
+ render_inline(UI::Menu.new) do |menu|
109
+ menu.with_trigger { "Trigger" }
110
+ menu.with_content do |content|
111
+ content.with_item("Item")
112
+ end
113
+ end
114
+ content = page.find("[role='menu']", visible: false)
115
+ assert content[:class].include?("min-w-[8rem]")
116
+ end
117
+
118
+ def test_has_popover_styling
119
+ render_inline(UI::Menu.new) do |menu|
120
+ menu.with_trigger { "Trigger" }
121
+ menu.with_content do |content|
122
+ content.with_item("Item")
123
+ end
124
+ end
125
+ content = page.find("[role='menu']", visible: false)
126
+ assert content[:class].include?("bg-popover")
127
+ assert content[:class].include?("text-popover-foreground")
128
+ assert content[:class].include?("rounded-md")
129
+ assert content[:class].include?("shadow-md")
130
+ end
131
+
132
+ def test_custom_class_merge
133
+ render_inline(UI::Menu.new) do |menu|
134
+ menu.with_trigger { "Trigger" }
135
+ menu.with_content(class: "custom-content") do |content|
136
+ content.with_item("Item")
137
+ end
138
+ end
139
+ content = page.find("[role='menu']", visible: false)
140
+ assert_includes content[:class].split, "custom-content"
141
+ end
142
+
143
+ def test_collects_items
144
+ result = render_inline(UI::Menu.new) do |menu|
145
+ menu.with_trigger { "Trigger" }
146
+ menu.with_content do |content|
147
+ content.with_label("Account")
148
+ content.with_item("Profile")
149
+ content.with_item("Settings")
150
+ content.with_divider
151
+ content.with_item("Logout")
152
+ end
153
+ end
154
+
155
+ assert result.to_html.include?("Account")
156
+ assert result.to_html.include?("Profile")
157
+ assert result.to_html.include?("Settings")
158
+ assert result.to_html.include?("Logout")
159
+ end
160
+
161
+ def test_renders_separator
162
+ result = render_inline(UI::Menu.new) do |menu|
163
+ menu.with_trigger { "Trigger" }
164
+ menu.with_content do |content|
165
+ content.with_item("Item 1")
166
+ content.with_divider
167
+ content.with_item("Item 2")
168
+ end
169
+ end
170
+
171
+ assert_selector "[role='separator']", visible: false
172
+ end
173
+
174
+ def test_renders_label
175
+ result = render_inline(UI::Menu.new) do |menu|
176
+ menu.with_trigger { "Trigger" }
177
+ menu.with_content do |content|
178
+ content.with_label("My Label")
179
+ content.with_item("Item")
180
+ end
181
+ end
182
+
183
+ assert_text "My Label"
184
+ assert_selector "div", text: "My Label", visible: false
185
+ end
186
+
187
+ def test_item_as_link
188
+ result = render_inline(UI::Menu.new) do |menu|
189
+ menu.with_trigger { "Trigger" }
190
+ menu.with_content do |content|
191
+ content.with_item("Profile", href: "/profile")
192
+ end
193
+ end
194
+
195
+ assert_selector "a[href='/profile']", text: "Profile", visible: false
196
+ end
197
+
198
+ def test_item_with_variant
199
+ result = render_inline(UI::Menu.new) do |menu|
200
+ menu.with_trigger { "Trigger" }
201
+ menu.with_content do |content|
202
+ content.with_item("Delete", variant: :destructive)
203
+ end
204
+ end
205
+
206
+ item = page.find("[role='menuitem']", text: "Delete", visible: false)
207
+ assert_includes item[:class].split, "text-destructive"
208
+ end
209
+
210
+ def test_item_with_inset
211
+ result = render_inline(UI::Menu.new) do |menu|
212
+ menu.with_trigger { "Trigger" }
213
+ menu.with_content do |content|
214
+ content.with_item("Item", inset: true)
215
+ end
216
+ end
217
+
218
+ item = page.find("[role='menuitem']", text: "Item", visible: false)
219
+ assert_includes item[:class].split, "pl-8"
220
+ end
221
+
222
+ def test_disabled_item
223
+ result = render_inline(UI::Menu.new) do |menu|
224
+ menu.with_trigger { "Trigger" }
225
+ menu.with_content do |content|
226
+ content.with_item("Disabled", disabled: true)
227
+ end
228
+ end
229
+
230
+ item = page.find("[role='menuitem']", text: "Disabled", visible: false)
231
+ assert_equal "true", item[:"data-disabled"]
232
+ end
233
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Menu::GroupTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_content
10
+ render_inline(UI::Menu::Group.new) { "Group content" }
11
+ assert_text "Group content"
12
+ end
13
+
14
+ def test_has_role_group
15
+ render_inline(UI::Menu::Group.new) { "Group" }
16
+ assert_selector "[role='group']"
17
+ end
18
+
19
+ def test_custom_class_merge
20
+ render_inline(UI::Menu::Group.new(class: "custom-group")) { "Group" }
21
+ group = page.find("[role='group']")
22
+ assert_includes group[:class].split, "custom-group"
23
+ end
24
+ end