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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Menu::ItemTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_as_button_by_default
10
+ render_inline(UI::Menu::Item.new) { "Item text" }
11
+ assert_selector "button", text: "Item text"
12
+ end
13
+
14
+ def test_renders_as_link_with_href
15
+ render_inline(UI::Menu::Item.new(href: "/profile")) { "Profile" }
16
+ assert_selector "a[href='/profile']", text: "Profile"
17
+ end
18
+
19
+ def test_renders_label_param
20
+ render_inline(UI::Menu::Item.new("Label text"))
21
+ assert_selector "button", text: "Label text"
22
+ end
23
+
24
+ def test_renders_block_over_label
25
+ render_inline(UI::Menu::Item.new("Label text")) { "Block text" }
26
+ assert_selector "button", text: "Block text"
27
+ refute_text "Label text"
28
+ end
29
+
30
+ def test_has_role_menuitem
31
+ render_inline(UI::Menu::Item.new) { "Item" }
32
+ assert_selector "[role='menuitem']"
33
+ end
34
+
35
+ def test_has_tabindex_minus_one
36
+ render_inline(UI::Menu::Item.new) { "Item" }
37
+ item = page.find("[role='menuitem']")
38
+ assert_equal "-1", item[:tabindex]
39
+ end
40
+
41
+ def test_has_data_target
42
+ render_inline(UI::Menu::Item.new) { "Item" }
43
+ assert_selector "[data-ui--menu-target='item']"
44
+ end
45
+
46
+ def test_disabled_state
47
+ render_inline(UI::Menu::Item.new(disabled: true)) { "Disabled" }
48
+ item = page.find("[role='menuitem']")
49
+ assert item[:disabled].present?
50
+ assert_equal "true", item[:"data-disabled"]
51
+ end
52
+
53
+ def test_default_variant
54
+ render_inline(UI::Menu::Item.new) { "Item" }
55
+ item = page.find("[role='menuitem']")
56
+ refute_includes item[:class].split, "text-destructive"
57
+ end
58
+
59
+ def test_destructive_variant
60
+ render_inline(UI::Menu::Item.new(variant: :destructive)) { "Delete" }
61
+ item = page.find("[role='menuitem']")
62
+ assert_includes item[:class].split, "text-destructive"
63
+ end
64
+
65
+ def test_inset_padding
66
+ render_inline(UI::Menu::Item.new(inset: true)) { "Item" }
67
+ item = page.find("[role='menuitem']")
68
+ assert_includes item[:class].split, "pl-8"
69
+ end
70
+
71
+ def test_custom_class_merge
72
+ render_inline(UI::Menu::Item.new(class: "custom-item")) { "Item" }
73
+ item = page.find("[role='menuitem']")
74
+ assert_includes item[:class].split, "custom-item"
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Menu::LabelTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_content
10
+ render_inline(UI::Menu::Label.new) { "My Label" }
11
+ assert_text "My Label"
12
+ end
13
+
14
+ def test_renders_label_param
15
+ render_inline(UI::Menu::Label.new("Label text"))
16
+ assert_text "Label text"
17
+ end
18
+
19
+ def test_renders_block_over_label
20
+ render_inline(UI::Menu::Label.new("Label text")) { "Block text" }
21
+ assert_text "Block text"
22
+ refute_text "Label text"
23
+ end
24
+
25
+ def test_has_styling
26
+ render_inline(UI::Menu::Label.new) { "Label" }
27
+ label = page.find("div", text: "Label")
28
+ assert label[:class].include?("font-semibold")
29
+ assert label[:class].include?("text-muted-foreground")
30
+ end
31
+
32
+ def test_inset_padding
33
+ render_inline(UI::Menu::Label.new(inset: true)) { "Label" }
34
+ label = page.find("div", text: "Label")
35
+ assert_includes label[:class].split, "pl-8"
36
+ end
37
+
38
+ def test_custom_class_merge
39
+ render_inline(UI::Menu::Label.new(class: "custom-label")) { "Label" }
40
+ label = page.find("div", text: "Label")
41
+ assert_includes label[:class].split, "custom-label"
42
+ end
43
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Menu::SeparatorTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_separator
10
+ render_inline(UI::Menu::Separator.new)
11
+ assert_selector "[role='separator']"
12
+ end
13
+
14
+ def test_has_horizontal_orientation
15
+ render_inline(UI::Menu::Separator.new)
16
+ sep = page.find("[role='separator']")
17
+ assert_equal "horizontal", sep[:"aria-orientation"]
18
+ end
19
+
20
+ def test_has_styling
21
+ render_inline(UI::Menu::Separator.new)
22
+ sep = page.find("[role='separator']")
23
+ assert sep[:class].include?("bg-muted")
24
+ assert sep[:class].include?("h-px")
25
+ end
26
+
27
+ def test_custom_class_merge
28
+ render_inline(UI::Menu::Separator.new(class: "custom-sep"))
29
+ sep = page.find("[role='separator']")
30
+ assert_includes sep[:class].split, "custom-sep"
31
+ end
32
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Menu::TriggerTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_content
10
+ render_inline(UI::Menu.new) do |menu|
11
+ menu.with_trigger { "Trigger text" }
12
+ menu.with_content do |content|
13
+ content.with_item("Item")
14
+ end
15
+ end
16
+ assert_text "Trigger text"
17
+ end
18
+
19
+ def test_has_aria_attributes
20
+ render_inline(UI::Menu.new(id: "test-id")) do |menu|
21
+ menu.with_trigger { "Trigger" }
22
+ menu.with_content do |content|
23
+ content.with_item("Item")
24
+ end
25
+ end
26
+ trigger = page.find("button[data-ui--menu-target='trigger']")
27
+ assert_equal "true", trigger[:"aria-haspopup"]
28
+ assert_equal "false", trigger[:"aria-expanded"]
29
+ assert trigger[:"aria-controls"].start_with?("ui-menu-content-")
30
+ end
31
+
32
+ def test_has_data_target
33
+ render_inline(UI::Menu.new) do |menu|
34
+ menu.with_trigger { "Trigger" }
35
+ menu.with_content do |content|
36
+ content.with_item("Item")
37
+ end
38
+ end
39
+ assert_selector "button[data-ui--menu-target='trigger']"
40
+ end
41
+
42
+ def test_has_toggle_action
43
+ render_inline(UI::Menu.new) do |menu|
44
+ menu.with_trigger { "Trigger" }
45
+ menu.with_content do |content|
46
+ content.with_item("Item")
47
+ end
48
+ end
49
+ trigger = page.find("button[data-ui--menu-target='trigger']")
50
+ assert trigger[:"data-action"].include?("click->ui--menu#toggle")
51
+ end
52
+
53
+ def test_has_keydown_action
54
+ render_inline(UI::Menu.new) do |menu|
55
+ menu.with_trigger { "Trigger" }
56
+ menu.with_content do |content|
57
+ content.with_item("Item")
58
+ end
59
+ end
60
+ trigger = page.find("button[data-ui--menu-target='trigger']")
61
+ assert trigger[:"data-action"].include?("keydown->ui--menu#triggerKeydown")
62
+ end
63
+
64
+ def test_renders_as_button_with_default_variant
65
+ render_inline(UI::Menu.new) do |menu|
66
+ menu.with_trigger { "Trigger" }
67
+ menu.with_content do |content|
68
+ content.with_item("Item")
69
+ end
70
+ end
71
+ trigger = page.find("button[data-ui--menu-target='trigger']")
72
+ assert trigger[:class].include?("border")
73
+ assert trigger[:class].include?("bg-background")
74
+ end
75
+
76
+ def test_custom_variant
77
+ render_inline(UI::Menu.new) do |menu|
78
+ menu.with_trigger(variant: :default) { "Trigger" }
79
+ menu.with_content do |content|
80
+ content.with_item("Item")
81
+ end
82
+ end
83
+ trigger = page.find("button[data-ui--menu-target='trigger']")
84
+ assert trigger[:class].include?("bg-primary")
85
+ end
86
+
87
+ def test_custom_size
88
+ render_inline(UI::Menu.new) do |menu|
89
+ menu.with_trigger(size: :sm) { "Small" }
90
+ menu.with_content do |content|
91
+ content.with_item("Item")
92
+ end
93
+ end
94
+ trigger = page.find("button[data-ui--menu-target='trigger']", text: "Small")
95
+ assert trigger[:class].include?("h-8")
96
+ assert trigger[:class].include?("text-xs")
97
+ end
98
+
99
+ def test_custom_class_merge
100
+ render_inline(UI::Menu.new) do |menu|
101
+ menu.with_trigger(class: "custom-trigger") { "Trigger" }
102
+ menu.with_content do |content|
103
+ content.with_item("Item")
104
+ end
105
+ end
106
+ trigger = page.find("button[data-ui--menu-target='trigger']")
107
+ assert_includes trigger[:class].split, "custom-trigger"
108
+ end
109
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::MenuTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_menu
10
+ render_inline(UI::Menu.new) do |menu|
11
+ menu.with_trigger { "Open Menu" }
12
+ menu.with_content do |content|
13
+ content.with_label("My Account")
14
+ content.with_item("Profile")
15
+ content.with_item("Settings")
16
+ content.with_divider
17
+ content.with_item("Logout", variant: :destructive)
18
+ end
19
+ end
20
+ end
21
+
22
+ def test_root_renders_trigger_and_content
23
+ result = render_menu
24
+ assert_text "Open Menu"
25
+ assert_text "Profile"
26
+ assert_text "Settings"
27
+ assert_text "Logout"
28
+ end
29
+
30
+ def test_trigger_has_aria_attributes
31
+ render_menu
32
+ trigger = page.find("[data-ui--menu-target='trigger']")
33
+ assert_equal "true", trigger[:"aria-haspopup"]
34
+ assert_equal "false", trigger[:"aria-expanded"]
35
+ assert trigger[:"aria-controls"].present?
36
+ assert trigger[:"aria-controls"].start_with?("ui-menu-content-")
37
+ end
38
+
39
+ def test_trigger_has_toggle_action
40
+ render_menu
41
+ trigger = page.find("[data-ui--menu-target='trigger']")
42
+ assert trigger[:"data-action"].include?("click->ui--menu#toggle")
43
+ assert trigger[:"data-action"].include?("keydown->ui--menu#triggerKeydown")
44
+ end
45
+
46
+ def test_content_has_role_menu
47
+ render_menu
48
+ assert_selector "[role='menu']", visible: false
49
+ end
50
+
51
+ def test_content_has_vertical_orientation
52
+ render_menu
53
+ content = page.find("[role='menu']", visible: false)
54
+ assert_equal "vertical", content[:"aria-orientation"]
55
+ end
56
+
57
+ def test_content_is_initially_hidden
58
+ render_menu
59
+ content = page.find("[role='menu']", visible: false)
60
+ assert content[:hidden].present?
61
+ end
62
+
63
+ def test_default_side_is_bottom
64
+ render_menu
65
+ content = page.find("[role='menu']", visible: false)
66
+ assert_equal "bottom", content[:"data-side"]
67
+ end
68
+
69
+ def test_default_align_is_start
70
+ render_menu
71
+ content = page.find("[role='menu']", visible: false)
72
+ assert_equal "start", content[:"data-align"]
73
+ end
74
+
75
+ def test_custom_side
76
+ render_inline(UI::Menu.new) do |menu|
77
+ menu.with_trigger { "Open" }
78
+ menu.with_content(side: :top) do |content|
79
+ content.with_item("Item")
80
+ end
81
+ end
82
+ content = page.find("[role='menu']", visible: false)
83
+ assert_equal "top", content[:"data-side"]
84
+ end
85
+
86
+ def test_custom_id_preserved
87
+ render_inline(UI::Menu.new(id: "my-menu")) do |menu|
88
+ menu.with_trigger { "Open" }
89
+ menu.with_content do |content|
90
+ content.with_item("Item")
91
+ end
92
+ end
93
+ assert_selector "#my-menu"
94
+ end
95
+
96
+ def test_user_class_merge
97
+ render_inline(UI::Menu.new(class: "my-custom-class")) do |menu|
98
+ menu.with_trigger { "Open" }
99
+ menu.with_content do |content|
100
+ content.with_item("Item")
101
+ end
102
+ end
103
+ root = page.find("[data-controller='ui--menu']")
104
+ classes = root[:class].split
105
+ assert_includes classes, "my-custom-class"
106
+ assert_includes classes, "relative"
107
+ end
108
+
109
+ def test_html_attrs_passthrough
110
+ render_inline(UI::Menu.new(data: { test_id: "menu-x" })) do |menu|
111
+ menu.with_trigger { "Open" }
112
+ menu.with_content do |content|
113
+ content.with_item("Item")
114
+ end
115
+ end
116
+ assert_selector "[data-controller='ui--menu'][data-test-id='menu-x']"
117
+ end
118
+
119
+ def test_content_with_collected_items
120
+ render_inline(UI::Menu.new) do |menu|
121
+ menu.with_trigger { "Open" }
122
+ menu.with_content do |content|
123
+ content.with_label("Account")
124
+ content.with_item("Profile", href: "/profile")
125
+ content.with_item("Settings")
126
+ content.with_divider
127
+ content.with_item("Logout", variant: :destructive)
128
+ end
129
+ end
130
+
131
+ assert_selector "[role='menu']", visible: false
132
+ assert_text "Account"
133
+ assert_selector "a[href='/profile']", text: "Profile", visible: false
134
+ assert_selector "[role='menuitem']", text: "Settings", visible: false
135
+ assert_selector "[role='separator']", visible: false
136
+ assert_selector "[role='menuitem']", text: "Logout", visible: false
137
+ end
138
+
139
+ def test_item_with_block_content
140
+ render_inline(UI::Menu.new) do |menu|
141
+ menu.with_trigger { "Open" }
142
+ menu.with_content do |content|
143
+ content.with_item do
144
+ safe_join([ tag.div("Icon", class: "icon"), "Profile" ])
145
+ end
146
+ end
147
+ end
148
+
149
+ assert_text "Icon"
150
+ assert_text "Profile"
151
+ end
152
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::NativeSelectTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ TEAMS = [ [ "Design", 1 ], [ "Engineering", 2 ] ].freeze
10
+
11
+ def test_renders_a_native_select_with_options
12
+ render_inline(UI::NativeSelect.new(name: "member[team_id]", options: TEAMS))
13
+ assert_selector "select[name='member[team_id]']"
14
+ assert_selector "select option", count: 2
15
+ assert_selector "select option[value='2']", text: "Engineering"
16
+ end
17
+
18
+ # Native rather than a custom listbox: it gets the platform picker on a
19
+ # phone, keyboard support and screen-reader behaviour for free.
20
+ def test_accepts_a_plain_array_of_strings
21
+ render_inline(UI::NativeSelect.new(name: "board", options: %w[cbse icse]))
22
+ assert_selector "option[value='cbse']", text: "cbse"
23
+ end
24
+
25
+ def test_marks_the_selected_option
26
+ render_inline(UI::NativeSelect.new(name: "s", options: TEAMS, selected: 2))
27
+ assert_equal "selected", page.find("option[value='2']")[:selected]
28
+ assert_nil page.find("option[value='1']")[:selected]
29
+ end
30
+
31
+ # Without a prompt a select silently submits its first option, so a required
32
+ # choice looks already-made.
33
+ def test_prompt_renders_a_blank_first_option
34
+ render_inline(UI::NativeSelect.new(name: "s", options: TEAMS, prompt: "Choose a team"))
35
+ first = page.all("option").first
36
+ assert_equal "", first[:value]
37
+ assert_equal "Choose a team", first.text
38
+ end
39
+
40
+ def test_label_is_associated_with_the_select
41
+ render_inline(UI::NativeSelect.new(name: "s", options: TEAMS, label: "Team"))
42
+ assert_equal page.find("label")[:for], page.find("select")[:id]
43
+ assert_selector "label", text: "Team"
44
+ end
45
+
46
+ def test_no_label_element_without_one
47
+ render_inline(UI::NativeSelect.new(name: "s", options: TEAMS))
48
+ assert_no_selector "label"
49
+ end
50
+
51
+ def test_required_and_disabled_pass_through
52
+ render_inline(UI::NativeSelect.new(name: "s", options: TEAMS, required: true, disabled: true))
53
+ select = page.find("select", visible: :all)
54
+ assert select[:required].present?
55
+ assert select[:disabled].present?
56
+ end
57
+
58
+ def test_error_state_marks_the_select_invalid
59
+ render_inline(UI::NativeSelect.new(name: "s", options: TEAMS, error: true))
60
+ assert_equal "true", page.find("select")[:"aria-invalid"]
61
+ assert_includes page.find("select")[:class].split, "border-destructive"
62
+ end
63
+
64
+ def test_sizes_change_the_control_height
65
+ render_inline(UI::NativeSelect.new(name: "s", options: TEAMS, size: :sm))
66
+ assert_includes page.find("select")[:class].split, "h-8"
67
+ end
68
+
69
+ def test_user_class_overrides_default_via_tailwind_merge
70
+ render_inline(UI::NativeSelect.new(name: "s", options: TEAMS, class: "h-20"))
71
+ classes = page.find("select")[:class].split
72
+ assert_includes classes, "h-20"
73
+ refute_includes classes, "h-9"
74
+ end
75
+
76
+ def test_html_attrs_pass_through
77
+ render_inline(UI::NativeSelect.new(name: "s", options: TEAMS, id: "sec", data: { test_id: "x" }))
78
+ assert_selector "select#sec[data-test-id='x']"
79
+ end
80
+
81
+ # --- form binding ---
82
+ #
83
+ # The label alone is not enough: a bound select must also render a scoped
84
+ # name, or the form submits nothing for the field.
85
+
86
+ # A plain object rather than a tenant-scoped model: the component only needs
87
+ # something the form builder can name fields against.
88
+ Member = Struct.new(:team_id) do
89
+ def self.human_attribute_name(name, *) = name.to_s.humanize
90
+ def to_model = self
91
+ def model_name = ActiveModel::Name.new(self.class, nil, "Member")
92
+ def persisted? = false
93
+ end
94
+
95
+ def form_builder_for(record = Member.new)
96
+ ActionView::Helpers::FormBuilder.new(:member, record, vc_test_controller.view_context, {})
97
+ end
98
+
99
+ def test_form_binding_scopes_the_field_name
100
+ render_inline(UI::NativeSelect.new(form: form_builder_for, field: :team_id,
101
+ options: TEAMS))
102
+ assert_selector "select[name='member[team_id]']"
103
+ end
104
+
105
+ def test_form_binding_derives_the_label_and_associates_it
106
+ render_inline(UI::NativeSelect.new(form: form_builder_for, field: :team_id,
107
+ options: TEAMS))
108
+ assert_selector "label"
109
+ assert_equal page.find("label")[:for], page.find("select")[:id]
110
+ end
111
+
112
+ def test_form_binding_still_renders_the_prompt_and_selection
113
+ render_inline(UI::NativeSelect.new(form: form_builder_for, field: :team_id,
114
+ options: TEAMS, prompt: "Choose", selected: 2))
115
+ assert_equal "", page.all("option").first[:value]
116
+ assert_equal "selected", page.find("option[value='2']")[:selected]
117
+ end
118
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::PaginationTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def pagy(count:, page: 1, limit: 25)
10
+ Pagy::Offset.new(count: count, page: page, limit: limit)
11
+ end
12
+
13
+ def render_pagination(**options, &block)
14
+ render_inline(UI::Pagination.new(**options), &block)
15
+ end
16
+
17
+ # Controls on a single page are noise.
18
+ def test_renders_nothing_for_a_single_page
19
+ render_pagination(pagy: pagy(count: 10))
20
+ assert_no_selector "nav"
21
+ end
22
+
23
+ def test_renders_a_labelled_nav_with_page_links
24
+ render_pagination(pagy: pagy(count: 240))
25
+ assert_selector "nav[aria-label]"
26
+ assert_selector "nav ul li"
27
+ assert_selector "a", text: "2"
28
+ end
29
+
30
+ # An office wants to know where it is in the list, not just which page.
31
+ def test_shows_a_record_range_summary
32
+ render_pagination(pagy: pagy(count: 240, page: 2))
33
+ assert_text "Showing 26–50 of 240"
34
+ end
35
+
36
+ def test_marks_the_current_page
37
+ render_pagination(pagy: pagy(count: 240, page: 3))
38
+ current = page.find("a[aria-current='page']")
39
+ assert_equal "3", current.text.strip
40
+ assert_equal 1, page.all("a[aria-current='page']").size
41
+ end
42
+
43
+ # Previous on page one, and next on the last page, would go nowhere.
44
+ def test_previous_is_inert_on_the_first_page
45
+ render_pagination(pagy: pagy(count: 240, page: 1))
46
+ assert_no_selector "a[aria-label='Go to previous page']"
47
+ assert_selector "a[aria-label='Go to next page']"
48
+ end
49
+
50
+ def test_next_is_inert_on_the_last_page
51
+ render_pagination(pagy: pagy(count: 240, page: 10))
52
+ assert_selector "a[aria-label='Go to previous page']"
53
+ assert_no_selector "a[aria-label='Go to next page']"
54
+ end
55
+
56
+ # A hundred pages of links is unusable, so the middle collapses.
57
+ def test_collapses_long_ranges_around_the_current_page
58
+ render_pagination(pagy: pagy(count: 2_500, page: 50))
59
+ numbers = page.all("nav a").map(&:text).select { |t| t.match?(/\A\d+\z/) }
60
+ assert_includes numbers, "1"
61
+ assert_includes numbers, "100"
62
+ assert_includes numbers, "50"
63
+ refute_includes numbers, "25", "the middle of a long range should collapse"
64
+ assert_selector "span[aria-hidden='true']", minimum: 1
65
+ end
66
+
67
+ def test_shows_every_page_when_the_range_is_short
68
+ render_pagination(pagy: pagy(count: 100))
69
+ numbers = page.all("nav a").map(&:text).select { |t| t.match?(/\A\d+\z/) }
70
+ assert_equal %w[1 2 3 4], numbers
71
+ end
72
+
73
+ # The caller owns the routes, so the component takes a block.
74
+ def test_builds_urls_from_the_given_block
75
+ render_inline(UI::Pagination.new(pagy: pagy(count: 240)) { |p| "/invoices?page=#{p}" })
76
+ assert_selector "a[href='/invoices?page=2']"
77
+ end
78
+
79
+ # Laid out by the width it is given, not the screen's: in a narrow card on
80
+ # a wide screen, the screen-width layout squeezed the summary and the
81
+ # links into one row and they overlapped.
82
+ def test_lays_out_by_its_container_not_the_screen
83
+ render_inline(UI::Pagination.new(pagy: pagy(count: 240, page: 5)) { |p| "/invoices?page=#{p}" })
84
+ with_class = ->(name) { "contains(concat(' ', normalize-space(@class), ' '), ' #{name} ')" }
85
+ container = page.find(:xpath, "//div[#{with_class.("@container")}]")
86
+ row = container.find(:xpath, "./div")
87
+ assert_includes row[:class].split, "@lg:flex-row"
88
+ refute(row[:class].split.any? { _1.start_with?("sm:") }, "no screen breakpoints")
89
+ assert_includes page.find("ul")[:class].split, "flex-wrap"
90
+ assert_equal 2, page.all(:xpath, "//span[#{with_class.("@md:inline")}]", visible: :all).size
91
+ end
92
+ end