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,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::SelectTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ TEAMS = [ [ "Design", 1 ], [ "Engineering", 2 ], [ "Marketing", 3 ] ].freeze
10
+
11
+ Member = Struct.new(:team_id) do
12
+ def to_model = self
13
+ def model_name = ActiveModel::Name.new(self.class, nil, "Member")
14
+ def persisted? = false
15
+ end
16
+
17
+ def form_builder_for(record = Member.new)
18
+ ActionView::Helpers::FormBuilder.new(:member, record, vc_test_controller.view_context, {})
19
+ end
20
+
21
+ def trigger = page.find("button[role='combobox']")
22
+ def listbox = page.find("[role='listbox']", visible: :all)
23
+ def native = page.find("select", visible: :all)
24
+
25
+ # --- the visible control (WAI-ARIA select-only combobox) ---------------
26
+
27
+ def test_trigger_is_a_collapsed_combobox_controlling_the_listbox
28
+ render_inline(UI::Select.new(name: "section", options: TEAMS))
29
+ assert_equal "button", trigger[:type]
30
+ assert_equal "listbox", trigger["aria-haspopup"]
31
+ assert_equal "false", trigger["aria-expanded"]
32
+ assert_equal listbox[:id], trigger["aria-controls"]
33
+ assert page.find("[data-ui--select-target='panel']", visible: :all)[:hidden], "the panel starts closed"
34
+ end
35
+
36
+ def test_each_option_is_an_aria_option_carrying_its_value
37
+ render_inline(UI::Select.new(name: "section", options: TEAMS))
38
+ options = page.all("[role='option']", visible: :all)
39
+ assert_equal [ "1", "2", "3" ], options.map { _1["data-value"] }
40
+ assert_equal [ "Design", "Engineering", "Marketing" ], options.map { _1.text(:all).strip }
41
+ end
42
+
43
+ def test_selected_option_is_marked_and_shown_on_the_trigger
44
+ render_inline(UI::Select.new(name: "section", options: TEAMS, selected: 2))
45
+ assert_equal "true", page.find("[role='option'][data-value='2']", visible: :all)["aria-selected"]
46
+ assert_equal "false", page.find("[role='option'][data-value='1']", visible: :all)["aria-selected"]
47
+ assert_includes trigger.text, "Engineering"
48
+ end
49
+
50
+ def test_prompt_shows_when_nothing_is_selected_and_clears_the_value
51
+ render_inline(UI::Select.new(name: "section", options: TEAMS, prompt: "Choose a team"))
52
+ assert_includes trigger.text, "Choose a team"
53
+ assert_selector "[role='option'][data-value='']", text: "Choose a team", visible: :all
54
+ end
55
+
56
+ def test_without_a_prompt_the_first_option_shows_as_a_native_select_would
57
+ render_inline(UI::Select.new(name: "section", options: TEAMS))
58
+ assert_includes trigger.text, "Design"
59
+ end
60
+
61
+ def test_plain_values_are_their_own_labels
62
+ render_inline(UI::Select.new(name: "role", options: %w[Admin Editor], selected: "Editor"))
63
+ assert_includes trigger.text, "Editor"
64
+ assert_selector "[role='option'][data-value='Admin']", visible: :all
65
+ end
66
+
67
+ # --- the value, carried by a hidden native select ------------------------
68
+
69
+ def test_a_hidden_native_select_carries_the_name_value_and_options
70
+ render_inline(UI::Select.new(name: "section", options: TEAMS, selected: 3))
71
+ assert_equal "section", native[:name]
72
+ assert_equal "-1", native[:tabindex]
73
+ assert_equal "true", native["aria-hidden"]
74
+ assert_includes native[:class], "sr-only"
75
+ assert_selector "select option[value='3'][selected]", visible: :all
76
+ end
77
+
78
+ def test_required_and_disabled_reach_the_native_select_and_the_trigger
79
+ render_inline(UI::Select.new(name: "section", options: TEAMS, required: true, disabled: true))
80
+ assert native[:required]
81
+ assert native[:disabled]
82
+ assert trigger[:disabled]
83
+ end
84
+
85
+ def test_extra_attributes_stay_on_the_native_select_so_change_listeners_keep_working
86
+ render_inline(UI::Select.new(name: "section", options: TEAMS, id: "section-picker", data: { action: "change->filters#apply" }))
87
+ assert_equal "section-picker", native[:id]
88
+ assert_equal "change->filters#apply", native["data-action"]
89
+ end
90
+
91
+ # --- label, states, sizes ---------------------------------------------
92
+
93
+ def test_label_is_associated_with_the_trigger
94
+ render_inline(UI::Select.new(name: "section", options: TEAMS, label: "Team"))
95
+ assert_equal trigger[:id], page.find("label")[:for]
96
+ end
97
+
98
+ def test_error_marks_the_trigger_invalid
99
+ render_inline(UI::Select.new(name: "section", options: TEAMS, error: true))
100
+ assert_equal "true", trigger["aria-invalid"]
101
+ assert_includes trigger[:class], "border-destructive"
102
+ end
103
+
104
+ def test_sizes
105
+ { sm: "h-8", default: "h-9", lg: "h-10" }.each do |size, height|
106
+ render_inline(UI::Select.new(name: "s", options: TEAMS, size: size))
107
+ assert_includes trigger[:class].split, height
108
+ end
109
+ end
110
+
111
+ def test_user_class_goes_on_the_trigger
112
+ render_inline(UI::Select.new(name: "s", options: TEAMS, class: "w-48"))
113
+ assert_includes trigger[:class].split, "w-48"
114
+ end
115
+
116
+ def test_wires_the_stimulus_controller
117
+ render_inline(UI::Select.new(name: "s", options: TEAMS))
118
+ assert_selector "[data-controller='ui--select']"
119
+ assert_equal "trigger", trigger["data-ui--select-target"]
120
+ assert_equal "listbox", listbox["data-ui--select-target"]
121
+ assert_equal "native", native["data-ui--select-target"]
122
+ end
123
+
124
+ # --- form builder --------------------------------------------------------
125
+
126
+ def test_form_binding_scopes_the_name_and_reads_the_value
127
+ render_inline(UI::Select.new(form: form_builder_for(Member.new(2)), field: :team_id, options: TEAMS))
128
+ assert_equal "member[team_id]", native[:name]
129
+ assert_includes trigger.text, "Engineering"
130
+ assert_equal "true", page.find("[role='option'][data-value='2']", visible: :all)["aria-selected"]
131
+ end
132
+
133
+ def test_form_binding_derives_the_label
134
+ render_inline(UI::Select.new(form: form_builder_for, field: :team_id, options: TEAMS))
135
+ assert_selector "label", text: "Team"
136
+ end
137
+ # --- searchable ------------------------------------------------------------
138
+
139
+ def test_searchable_adds_a_search_box_that_controls_the_listbox
140
+ render_inline(UI::Select.new(name: "s", options: TEAMS, searchable: true))
141
+ search = page.find("input[type='search']", visible: :all)
142
+ assert_equal "search", search["data-ui--select-target"]
143
+ assert_equal listbox[:id], search["aria-controls"]
144
+ assert_equal "Search", search[:placeholder]
145
+ assert_selector "[data-ui--select-target='empty']", text: "No matches", visible: :all
146
+ end
147
+
148
+ def test_not_searchable_by_default
149
+ render_inline(UI::Select.new(name: "s", options: TEAMS))
150
+ assert_no_selector "input[type='search']", visible: :all
151
+ end
152
+
153
+ # --- multiple --------------------------------------------------------------
154
+
155
+ def test_multiple_marks_every_chosen_option_and_posts_an_array
156
+ render_inline(UI::Select.new(name: "tags", options: TEAMS, multiple: true, selected: [ 1, 3 ]))
157
+ assert_equal "true", listbox["aria-multiselectable"]
158
+ assert native[:multiple]
159
+ assert_equal "tags[]", native[:name]
160
+ assert_equal %w[true false true], page.all("[role='option']", visible: :all).map { _1["aria-selected"] }
161
+ assert_includes trigger.text, "Design, Marketing"
162
+ end
163
+
164
+ def test_multiple_summarises_more_than_two_choices
165
+ render_inline(UI::Select.new(name: "tags", options: TEAMS, multiple: true, selected: [ 1, 2, 3 ]))
166
+ assert_includes trigger.text, "3 selected"
167
+ end
168
+
169
+ def test_multiple_with_nothing_chosen_shows_the_prompt_and_offers_no_blank_option
170
+ render_inline(UI::Select.new(name: "tags", options: TEAMS, multiple: true, prompt: "Pick sections"))
171
+ assert_includes trigger.text, "Pick sections"
172
+ assert_no_selector "[role='option'][data-value='']", visible: :all
173
+ end
174
+
175
+ def test_multiple_hands_the_summary_wording_to_the_controller
176
+ render_inline(UI::Select.new(name: "tags", options: TEAMS, multiple: true))
177
+ root = page.find("[data-controller='ui--select']")
178
+ assert_equal "true", root["data-ui--select-multiple-value"]
179
+ assert_equal "%{count} selected", root["data-ui--select-summary-value"]
180
+ end
181
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Sheet::ContentTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_sheet(side: :right, **content_opts, &block)
10
+ render_inline(UI::Sheet.new(id: "profile-sheet")) do |sheet|
11
+ sheet.with_trigger { "Open" }
12
+ sheet.with_content(side: side, **content_opts) { |c| block.call(c) }
13
+ end
14
+ end
15
+
16
+ # ── Native dialog wrapper ────────────────────────────────────────────────
17
+
18
+ def test_renders_native_dialog_element
19
+ render_sheet { |c| c.with_title("Hi") }
20
+ assert_selector "dialog[data-ui--dialog-target='content']", visible: :all
21
+ end
22
+
23
+ def test_dialog_has_data_state_closed_by_default
24
+ render_sheet { |c| c.with_title("Hi") }
25
+ assert_equal "closed", page.find("dialog", visible: :all)["data-state"]
26
+ assert_nil page.find("dialog", visible: :all)[:open]
27
+ end
28
+
29
+ def test_dialog_id_matches_root
30
+ render_sheet { |c| c.with_title("Hi") }
31
+ assert_equal "profile-sheet", page.find("dialog", visible: :all)[:id]
32
+ end
33
+
34
+ def test_panel_present_inside_dialog
35
+ render_sheet { |c| c.with_title("Hi") }
36
+ assert_selector "[data-ui--dialog-part='panel']", visible: :all
37
+ end
38
+
39
+ # ── Side variants ────────────────────────────────────────────────────────
40
+
41
+ def test_default_side_is_right
42
+ render_sheet { |c| c.with_title("Hi") }
43
+ panel = page.find("[data-ui--dialog-part='panel']", visible: :all)
44
+ assert_includes panel[:class], "right-0"
45
+ assert_includes panel[:class], "h-full"
46
+ assert_includes panel[:class], "max-w-md"
47
+ end
48
+
49
+ def test_left_side_positions_sheet_on_left
50
+ render_sheet(side: :left) { |c| c.with_title("Hi") }
51
+ panel = page.find("[data-ui--dialog-part='panel']", visible: :all)
52
+ assert_includes panel[:class], "left-0"
53
+ end
54
+
55
+ def test_top_side_positions_sheet_on_top
56
+ render_sheet(side: :top) { |c| c.with_title("Hi") }
57
+ panel = page.find("[data-ui--dialog-part='panel']", visible: :all)
58
+ assert_includes panel[:class], "top-0"
59
+ assert_includes panel[:class], "inset-x-0"
60
+ assert_includes panel[:class], "max-h-[80vh]"
61
+ end
62
+
63
+ def test_bottom_side_positions_sheet_on_bottom
64
+ render_sheet(side: :bottom) { |c| c.with_title("Hi") }
65
+ panel = page.find("[data-ui--dialog-part='panel']", visible: :all)
66
+ assert_includes panel[:class], "bottom-0"
67
+ end
68
+
69
+ # ── Title / description ──────────────────────────────────────────────────
70
+
71
+ def test_title_text_arg_renders_h2
72
+ render_sheet { |c| c.with_title("Are you sure?") }
73
+ title = page.find("dialog h2", visible: :all)
74
+ assert_equal "Are you sure?", title.text.strip
75
+ end
76
+
77
+ def test_description_text_arg_renders_p
78
+ render_sheet do |c|
79
+ c.with_title("Hi")
80
+ c.with_description("This action cannot be undone.")
81
+ end
82
+ desc = page.find("dialog p", visible: :all)
83
+ assert_equal "This action cannot be undone.", desc.text.strip
84
+ end
85
+
86
+ def test_description_links_via_aria_describedby
87
+ render_sheet do |c|
88
+ c.with_title("Hi")
89
+ c.with_description("Body")
90
+ end
91
+ dialog = page.find("dialog", visible: :all)
92
+ desc = page.find("dialog p", visible: :all)
93
+ assert_equal "profile-sheet-description", desc[:id]
94
+ assert_equal desc[:id], dialog["aria-describedby"]
95
+ end
96
+
97
+ # ── Footer ───────────────────────────────────────────────────────────────
98
+
99
+ def test_footer_block_renders_in_footer_element
100
+ render_sheet do |c|
101
+ c.with_title("Hi")
102
+ c.with_footer do
103
+ '<button type="button">Confirm</button>'.html_safe
104
+ end
105
+ end
106
+ assert_selector "dialog footer", visible: :all
107
+ assert_selector "dialog footer button", text: "Confirm", visible: :all
108
+ end
109
+
110
+ def test_no_footer_means_no_footer_element
111
+ render_sheet { |c| c.with_title("Hi") }
112
+ assert_no_selector "dialog footer", visible: :all
113
+ end
114
+
115
+ # ── Close button ─────────────────────────────────────────────────────────
116
+
117
+ def test_close_button_default_present
118
+ render_sheet { |c| c.with_title("Hi") }
119
+ btn = page.find("dialog button[aria-label='Close']", visible: :all)
120
+ assert_match(/click->ui--dialog#close/, btn["data-action"])
121
+ end
122
+
123
+ def test_close_button_can_be_disabled
124
+ render_sheet(show_close: false) { |c| c.with_title("Hi") }
125
+ assert_no_selector "dialog button[aria-label='Close']", visible: :all
126
+ end
127
+
128
+ # ── User class merge on panel ────────────────────────────────────────────
129
+
130
+ def test_user_class_merges_into_panel
131
+ render_sheet(class: "border-red-500") { |c| c.with_title("Hi") }
132
+ panel = page.find("[data-ui--dialog-part='panel']", visible: :all)
133
+ assert_includes panel[:class].split, "border-red-500"
134
+ end
135
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Sheet::TriggerTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_trigger_reuses_dialog_behavior
10
+ render_inline(UI::Sheet.new(id: "profile-sheet")) do |sheet|
11
+ sheet.with_trigger { "Open" }
12
+ sheet.with_content { |c| c.with_title("Hi") }
13
+ end
14
+
15
+ trigger = page.find("[data-ui--dialog-target='trigger']")
16
+ assert_equal "dialog", trigger["aria-haspopup"]
17
+ assert_equal "profile-sheet", trigger["aria-controls"]
18
+ assert trigger["data-action"].include?("click->ui--dialog#open")
19
+ assert_equal "Open", trigger.text.strip
20
+ # Should render as UI::Button by default
21
+ assert_includes trigger[:class].split, "bg-primary"
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::SheetTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_sheet(**opts)
10
+ render_inline(UI::Sheet.new(**opts)) do |sheet|
11
+ sheet.with_trigger { "Open sheet" }
12
+ sheet.with_content do |content|
13
+ content.with_title { "Edit profile" }
14
+ content.with_description { "Make changes to your profile here." }
15
+ "Sheet body"
16
+ end
17
+ end
18
+ end
19
+
20
+ def test_root_carries_dialog_controller
21
+ render_sheet
22
+ assert_selector "[data-controller~='ui--dialog']"
23
+ end
24
+
25
+ def test_dialog_element_has_generated_id
26
+ render_sheet
27
+ dialog = page.find("dialog", visible: :all)
28
+ assert dialog[:id].present?
29
+ assert dialog[:id].start_with?("ui-sheet-")
30
+ end
31
+
32
+ def test_custom_id_lands_on_dialog_element
33
+ render_sheet(id: "profile-sheet")
34
+ assert_selector "dialog#profile-sheet", visible: :all
35
+ end
36
+
37
+ def test_user_class_merge
38
+ render_sheet(class: "my-sheet-root")
39
+ classes = page.find("[data-controller~='ui--dialog']")[:class].split
40
+ assert_includes classes, "my-sheet-root"
41
+ assert_includes classes, "contents"
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Sidebar::MenuButtonTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_active_button_sets_data_active
10
+ render_inline(UI::Sidebar::MenuButton.new("#", active: true)) { "Dashboard" }
11
+ link = page.find("a", text: "Dashboard")
12
+ assert_equal "true", link[:"data-active"]
13
+ assert_includes link[:class], "data-[active=true]:bg-sidebar-primary/10"
14
+ end
15
+
16
+ # Active is a primary tint while hover stays the neutral sidebar accent, so
17
+ # hovering an inactive item can't be mistaken for the current page.
18
+ def test_active_styling_is_distinct_from_hover
19
+ render_inline(UI::Sidebar::MenuButton.new("#", active: true)) { "Dashboard" }
20
+ classes = page.find("a", text: "Dashboard")[:class]
21
+ assert_includes classes, "data-[active=true]:text-sidebar-primary"
22
+ assert_includes classes, "data-[active=true]:font-semibold"
23
+ assert_includes classes, "hover:bg-sidebar-accent"
24
+ end
25
+
26
+ def test_allows_aria_current
27
+ render_inline(UI::Sidebar::MenuButton.new("#", current: "page")) { "Dashboard" }
28
+ link = page.find("a", text: "Dashboard")
29
+ assert_equal "page", link[:"aria-current"]
30
+ end
31
+
32
+ def test_active_sets_aria_current_page_by_default
33
+ render_inline(UI::Sidebar::MenuButton.new("#", active: true)) { "Dashboard" }
34
+ link = page.find("a", text: "Dashboard")
35
+ assert_equal "page", link[:"aria-current"]
36
+ end
37
+
38
+ def test_active_does_not_override_explicit_current
39
+ render_inline(UI::Sidebar::MenuButton.new("#", active: true, current: "step")) { "Dashboard" }
40
+ link = page.find("a", text: "Dashboard")
41
+ assert_equal "step", link[:"aria-current"]
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Sidebar::TriggerTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_trigger_wires_toggle_action
10
+ render_inline(UI::Sidebar::Trigger.new(sidebar_id: "main-sidebar"))
11
+ button = page.find("button")
12
+ assert_equal "main-sidebar", button[:"aria-controls"]
13
+ assert_equal "true", button[:"aria-expanded"]
14
+ assert_equal "Toggle sidebar", button[:"aria-label"]
15
+ assert button[:"data-action"].include?("ui--sidebar#toggle")
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Sidebar::WrapperTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_controller_and_css_vars
10
+ render_inline(UI::Sidebar::Wrapper.new) { "x" }
11
+ wrapper = page.find("[data-controller~='ui--sidebar']")
12
+ assert_includes wrapper[:class], "group/sidebar-wrapper"
13
+ assert_includes wrapper[:style], "--sidebar-width:"
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::SidebarTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_sidebar(**opts)
10
+ render_inline(UI::Sidebar.new(**opts)) do |sidebar|
11
+ sidebar.with_sidebar_header { "Header" }
12
+ sidebar.with_sidebar_content do |content|
13
+ content.with_sidebar_group do |group|
14
+ group.with_label("Applications")
15
+ group.with_menu do |menu|
16
+ menu.with_menu_item do |item|
17
+ item.with_menu_button("#", active: true) { "Dashboard" }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def test_renders_aside_with_sidebar_target
26
+ render_sidebar
27
+ aside = page.find("aside[data-ui--sidebar-target='sidebar']")
28
+ assert_equal "expanded", aside[:"data-state"]
29
+ assert_equal "none", aside[:"data-collapsible"]
30
+ end
31
+
32
+ def test_custom_id_preserved
33
+ render_sidebar(id: "main-sidebar")
34
+ assert_selector "aside#main-sidebar"
35
+ end
36
+
37
+ def test_user_class_merge
38
+ render_sidebar(class: "custom-sidebar")
39
+ classes = page.find("aside")[:class].split
40
+ assert_includes classes, "custom-sidebar"
41
+ assert_includes classes, "peer"
42
+ end
43
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::SpinnerTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_loader_icon_with_animate_spin
10
+ render_inline UI::Spinner.new
11
+ icon = page.find("i", visible: :all)
12
+ assert_equal "loader", icon["data-lucide"]
13
+ assert_includes icon[:class].split, "animate-spin"
14
+ end
15
+
16
+ def test_default_size_classes
17
+ render_inline UI::Spinner.new
18
+ classes = page.find("i", visible: :all)[:class].split
19
+ assert_includes classes, "w-5"
20
+ assert_includes classes, "h-5"
21
+ end
22
+
23
+ def test_size_xs
24
+ render_inline UI::Spinner.new(size: :xs)
25
+ classes = page.find("i", visible: :all)[:class].split
26
+ assert_includes classes, "w-3"
27
+ assert_includes classes, "h-3"
28
+ end
29
+
30
+ def test_size_sm
31
+ render_inline UI::Spinner.new(size: :sm)
32
+ classes = page.find("i", visible: :all)[:class].split
33
+ assert_includes classes, "w-4"
34
+ assert_includes classes, "h-4"
35
+ end
36
+
37
+ def test_size_lg
38
+ render_inline UI::Spinner.new(size: :lg)
39
+ classes = page.find("i", visible: :all)[:class].split
40
+ assert_includes classes, "w-6"
41
+ assert_includes classes, "h-6"
42
+ end
43
+
44
+ def test_size_xl
45
+ render_inline UI::Spinner.new(size: :xl)
46
+ classes = page.find("i", visible: :all)[:class].split
47
+ assert_includes classes, "w-8"
48
+ assert_includes classes, "h-8"
49
+ end
50
+
51
+ def test_user_class_merges
52
+ render_inline UI::Spinner.new(class: "text-primary")
53
+ classes = page.find("i", visible: :all)[:class].split
54
+ assert_includes classes, "text-primary"
55
+ assert_includes classes, "animate-spin"
56
+ end
57
+
58
+ def test_html_attrs_pass_through
59
+ render_inline UI::Spinner.new(aria: { label: "Loading" })
60
+ icon = page.find("i", visible: :all)
61
+ assert_equal "Loading", icon["aria-label"]
62
+ end
63
+ end