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,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::SwitchTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ # ── Input element ────────────────────────────────────────────────────────────
10
+
11
+ def test_renders_input_with_role_switch
12
+ render_inline(UI::Switch.new(name: "notify"))
13
+ input = page.find("input[type='checkbox']", visible: :all)
14
+ assert_equal "switch", input[:role]
15
+ end
16
+
17
+ def test_input_is_visually_hidden_via_sr_only
18
+ render_inline(UI::Switch.new(name: "notify"))
19
+ classes = page.find("input[type='checkbox']", visible: :all)[:class].split
20
+ assert_includes classes, "peer"
21
+ assert_includes classes, "sr-only"
22
+ end
23
+
24
+ def test_input_does_not_carry_track_classes
25
+ # Native browser tick painted whenever <input type=checkbox> stays in flow
26
+ # — we keep it sr-only so no track/visual classes belong on the input.
27
+ render_inline(UI::Switch.new(name: "notify"))
28
+ classes = page.find("input[type='checkbox']", visible: :all)[:class].split
29
+ refute_includes classes, "rounded-full"
30
+ refute_includes classes, "bg-input"
31
+ refute_includes classes, "appearance-none"
32
+ end
33
+
34
+ def test_checked_sets_checked_attribute
35
+ render_inline(UI::Switch.new(name: "notify", checked: true))
36
+ input = page.find("input[type='checkbox']", visible: :all)
37
+ assert input["checked"].present?
38
+ end
39
+
40
+ def test_value_defaults_to_one
41
+ render_inline(UI::Switch.new(name: "notify"))
42
+ assert_equal "1", page.find("input[type='checkbox']", visible: :all)[:value]
43
+ end
44
+
45
+ def test_custom_value
46
+ render_inline(UI::Switch.new(name: "notify", value: "on"))
47
+ assert_equal "on", page.find("input[type='checkbox']", visible: :all)[:value]
48
+ end
49
+
50
+ def test_disabled_sets_disabled_attribute
51
+ render_inline(UI::Switch.new(name: "notify", disabled: true))
52
+ input = page.find("input[type='checkbox'][disabled]", visible: :all)
53
+ assert input.disabled?
54
+ end
55
+
56
+ def test_required_sets_required_attribute
57
+ render_inline(UI::Switch.new(name: "notify", required: true))
58
+ assert_selector "input[type='checkbox'][required]", visible: :all
59
+ end
60
+
61
+ def test_data_attributes_passthrough
62
+ render_inline(UI::Switch.new(name: "notify", data: { test_id: "sw-x" }))
63
+ assert_selector "input[type='checkbox'][data-test-id='sw-x']", visible: :all
64
+ end
65
+
66
+ def test_auto_generated_id_when_label_present
67
+ render_inline(UI::Switch.new(name: "notify", label: "Notify"))
68
+ input = page.find("input[type='checkbox']", visible: :all)
69
+ assert input[:id].present?
70
+ assert input[:id].start_with?("ui-sw-")
71
+ end
72
+
73
+ # ── Track (sibling span) ─────────────────────────────────────────────────────
74
+
75
+ def test_track_span_uses_peer_checked_for_state
76
+ render_inline(UI::Switch.new(name: "notify"))
77
+ track = page.all("span", visible: :all).find { |s|
78
+ s[:class].to_s.include?("peer-checked:bg-primary")
79
+ }
80
+ assert track, "track span with peer-checked:bg-primary should exist"
81
+ classes = track[:class].split
82
+ assert_includes classes, "rounded-full"
83
+ assert_includes classes, "bg-input"
84
+ assert_includes classes, "peer-checked:bg-primary"
85
+ end
86
+
87
+ def test_track_span_has_focus_ring_via_peer_focus_visible
88
+ render_inline(UI::Switch.new(name: "notify"))
89
+ track = page.all("span", visible: :all).find { |s|
90
+ s[:class].to_s.include?("peer-focus:ring-2")
91
+ }
92
+ assert track, "track span with peer-focus:ring-2 should exist"
93
+ classes = track[:class].split
94
+ assert_includes classes, "peer-focus:ring-primary"
95
+ assert_includes classes, "peer-focus:ring-offset-1"
96
+ end
97
+
98
+ # ── Thumb ────────────────────────────────────────────────────────────────────
99
+
100
+ def test_thumb_span_present_with_default_translate_class
101
+ render_inline(UI::Switch.new(name: "notify"))
102
+ thumb = page.all("span", visible: :all).find { |s|
103
+ s[:class].to_s.include?("peer-checked:translate-x-")
104
+ }
105
+ assert thumb, "thumb span should exist"
106
+ classes = thumb[:class].split
107
+ assert_includes classes, "rounded-full"
108
+ assert_includes classes, "bg-white"
109
+ assert_includes classes, "peer-checked:translate-x-5"
110
+ end
111
+
112
+ # ── Sizes ────────────────────────────────────────────────────────────────────
113
+
114
+ def test_size_sm_renders_smaller_track_and_thumb
115
+ render_inline(UI::Switch.new(name: "notify", size: :sm))
116
+ spans = page.all("span", visible: :all)
117
+ root = spans.find { |s| s[:class].to_s.include?("inline-flex shrink-0") }
118
+ assert_includes root[:class].split, "w-8"
119
+ assert_includes root[:class].split, "h-4"
120
+ thumb = spans.find { |s| s[:class].to_s.include?("peer-checked:translate-x-") }
121
+ assert_includes thumb[:class].split, "w-3"
122
+ assert_includes thumb[:class].split, "h-3"
123
+ assert_includes thumb[:class].split, "peer-checked:translate-x-4"
124
+ end
125
+
126
+ def test_size_lg_renders_larger_track_and_thumb
127
+ render_inline(UI::Switch.new(name: "notify", size: :lg))
128
+ spans = page.all("span", visible: :all)
129
+ root = spans.find { |s| s[:class].to_s.include?("inline-flex shrink-0") }
130
+ assert_includes root[:class].split, "w-12"
131
+ assert_includes root[:class].split, "h-6"
132
+ thumb = spans.find { |s| s[:class].to_s.include?("peer-checked:translate-x-") }
133
+ assert_includes thumb[:class].split, "w-5"
134
+ assert_includes thumb[:class].split, "h-5"
135
+ assert_includes thumb[:class].split, "peer-checked:translate-x-6"
136
+ end
137
+
138
+ # ── Wrapper / label ──────────────────────────────────────────────────────────
139
+
140
+ def test_label_renders_label_element_with_for_id
141
+ render_inline(UI::Switch.new(name: "notify", label: "Enable notifications"))
142
+ label_el = page.find("label")
143
+ input_el = page.find("input[type='checkbox']", visible: :all)
144
+ assert_equal label_el["for"], input_el[:id]
145
+ assert_match(/Enable notifications/, label_el.text)
146
+ end
147
+
148
+ # The checkbox is visually hidden, so without a <label> around it a click on
149
+ # the track reaches nothing and the switch can't be flipped by mouse.
150
+ def test_without_visible_label_the_switch_is_still_a_clickable_label
151
+ render_inline(UI::Switch.new(name: "notify", aria: { label: "Notifications" }))
152
+ label_el = page.find("label")
153
+ assert label_el.has_css?("input[type='checkbox']", visible: :all)
154
+ assert_empty label_el.text.strip
155
+ end
156
+
157
+ # White in both themes: in dark mode the background token is near black,
158
+ # which made the knob vanish into the track.
159
+ def test_thumb_is_white_in_both_themes
160
+ render_inline(UI::Switch.new(name: "notify"))
161
+ thumb = page.all("span", visible: :all).find { |s| s[:class].to_s.include?("rounded-full") && s[:class].to_s.include?("shadow") }
162
+ assert_includes thumb[:class].split, "bg-white"
163
+ end
164
+
165
+ def test_disabled_wrapper_dims
166
+ render_inline(UI::Switch.new(name: "notify", disabled: true, label: "Notifications"))
167
+ classes = page.find("label")[:class].split
168
+ assert_includes classes, "cursor-not-allowed"
169
+ assert_includes classes, "opacity-60"
170
+ end
171
+
172
+ def test_required_label_shows_styled_asterisk
173
+ render_inline(UI::Switch.new(name: "notify", label: "Sub", required: true))
174
+ asterisk = page.find("label span.text-destructive")
175
+ assert_match(/\*/, asterisk.text)
176
+ classes = asterisk[:class].split
177
+ assert_includes classes, "text-destructive"
178
+ assert_includes classes, "ml-0.5"
179
+ end
180
+
181
+ def test_non_required_label_has_no_destructive_span
182
+ render_inline(UI::Switch.new(name: "notify", label: "Sub"))
183
+ refute_selector "label span.text-destructive"
184
+ end
185
+
186
+ def test_label_text_renders_as_span_with_typography
187
+ render_inline(UI::Switch.new(name: "notify", label: "Toggle"))
188
+ span = page.find("label").all("span").find { |s| s.text.strip.start_with?("Toggle") }
189
+ assert span, "label text span should exist"
190
+ classes = span[:class].split
191
+ assert_includes classes, "text-sm"
192
+ assert_includes classes, "leading-none"
193
+ end
194
+
195
+ # ── User-class merge ────────────────────────────────────────────────────────
196
+
197
+ def test_user_class_lands_on_track_not_input
198
+ render_inline(UI::Switch.new(name: "notify", class: "ring-orange-500"))
199
+ track = page.all("span", visible: :all).find { |s|
200
+ s[:class].to_s.include?("peer-checked:bg-primary")
201
+ }
202
+ assert_includes track[:class].split, "ring-orange-500"
203
+ input_classes = page.find("input[type='checkbox']", visible: :all)[:class].split
204
+ refute_includes input_classes, "ring-orange-500"
205
+ end
206
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::TableTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_table
10
+ render_inline(UI::Table.new) do |table|
11
+ table.with_head do |head|
12
+ head.with_column { "Name" }
13
+ head.with_column(align: :end) { "Amount" }
14
+ end
15
+ table.with_row do |row|
16
+ row.with_cell { "Aditi Rao" }
17
+ row.with_cell(align: :end) { "12" }
18
+ end
19
+ table.with_row do |row|
20
+ row.with_cell { "Arjun Nair" }
21
+ row.with_cell(align: :end) { "13" }
22
+ end
23
+ end
24
+ end
25
+
26
+ def test_renders_a_semantic_table
27
+ render_table
28
+ assert_selector "table thead tr th", count: 2
29
+ assert_selector "table tbody tr", count: 2
30
+ assert_selector "table tbody tr:first-child td", text: "Aditi Rao"
31
+ end
32
+
33
+ # Screen readers announce a header cell differently from a data cell, so the
34
+ # distinction has to be real markup rather than styling.
35
+ def test_header_cells_are_th_with_a_scope
36
+ render_table
37
+ assert_equal "col", page.find("thead th", text: "Name")[:scope]
38
+ end
39
+
40
+ # A long table scrolls sideways on a phone rather than breaking the page.
41
+ def test_wraps_the_table_in_a_horizontal_scroll_container
42
+ render_table
43
+ assert_includes page.find("div", match: :first)[:class].split, "overflow-x-auto"
44
+ end
45
+
46
+ def test_alignment_applies_to_matching_head_and_body_cells
47
+ render_table
48
+ assert_includes page.find("thead th", text: "Amount")[:class].split, "text-right"
49
+ assert_includes page.find("tbody td", text: "12")[:class].split, "text-right"
50
+ end
51
+
52
+ def test_caption_is_rendered_when_given
53
+ render_inline(UI::Table.new(caption: "Invoices")) do |table|
54
+ table.with_row { |row| row.with_cell { "x" } }
55
+ end
56
+ assert_selector "table caption", text: "Invoices"
57
+ end
58
+
59
+ def test_no_caption_element_without_one
60
+ render_table
61
+ assert_no_selector "caption"
62
+ end
63
+
64
+ # An empty table should say so rather than render a bare header.
65
+ def test_empty_state_replaces_the_body_when_there_are_no_rows
66
+ render_inline(UI::Table.new) do |table|
67
+ table.with_head { |head| head.with_column { "Name" } }
68
+ table.with_empty_state { "No invoices yet" }
69
+ end
70
+ assert_text "No invoices yet"
71
+ assert_no_selector "tbody tr td", text: "Aditi"
72
+ end
73
+
74
+ def test_empty_state_is_ignored_once_rows_exist
75
+ render_inline(UI::Table.new) do |table|
76
+ table.with_head { |head| head.with_column { "Name" } }
77
+ table.with_empty_state { "No invoices yet" }
78
+ table.with_row { |row| row.with_cell { "Aditi Rao" } }
79
+ end
80
+ assert_no_text "No invoices yet"
81
+ assert_selector "tbody td", text: "Aditi Rao"
82
+ end
83
+
84
+ def test_rows_can_be_marked_selected
85
+ render_inline(UI::Table.new) do |table|
86
+ table.with_row(selected: true) { |row| row.with_cell { "Aditi" } }
87
+ end
88
+ assert_equal "true", page.find("tbody tr")[:"aria-selected"]
89
+ end
90
+
91
+ def test_user_class_overrides_default_via_tailwind_merge
92
+ render_inline(UI::Table.new(class: "text-base")) do |table|
93
+ table.with_row { |row| row.with_cell { "x" } }
94
+ end
95
+ classes = page.find("table")[:class].split
96
+ assert_includes classes, "text-base"
97
+ refute_includes classes, "text-sm"
98
+ end
99
+
100
+ def test_html_attrs_pass_through
101
+ render_inline(UI::Table.new(id: "invoices", data: { test_id: "t" })) do |table|
102
+ table.with_row { |row| row.with_cell { "x" } }
103
+ end
104
+ assert_selector "table#invoices[data-test-id='t']"
105
+ end
106
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Tabs::ContentTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_content(default_value: "a")
10
+ render_inline(UI::Tabs.new(default_value: default_value)) do |tabs|
11
+ tabs.with_list do |list|
12
+ list.with_trigger(value: "a") { "A" }
13
+ list.with_trigger(value: "b") { "B" }
14
+ end
15
+ tabs.with_panel(value: "a") { "panel-a" }
16
+ tabs.with_panel(value: "b") { "panel-b" }
17
+ end
18
+ end
19
+
20
+ def test_renders_tabpanel_role
21
+ render_content
22
+ assert_selector "[role='tabpanel']"
23
+ end
24
+
25
+ def test_aria_labelledby_matches_trigger_id
26
+ render_content
27
+ panel = page.find("[role='tabpanel'][data-value='a']")
28
+ trigger = page.find("[role='tab'][data-value='a']")
29
+ assert_equal trigger[:id], panel["aria-labelledby"]
30
+ end
31
+
32
+ def test_tabindex_zero
33
+ render_content
34
+ panel = page.find("[role='tabpanel']")
35
+ assert_equal "0", panel[:tabindex]
36
+ end
37
+
38
+ def test_data_state_active_when_active
39
+ render_content(default_value: "a")
40
+ panel = page.find("[role='tabpanel'][data-state='active']")
41
+ assert_equal "a", panel["data-value"]
42
+ end
43
+
44
+ def test_data_state_inactive_when_inactive
45
+ render_content(default_value: "a")
46
+ panel = page.find("[role='tabpanel'][data-state='inactive']", visible: :all)
47
+ assert_equal "b", panel["data-value"]
48
+ end
49
+
50
+ def test_data_ui_tabs_target_panel
51
+ render_content
52
+ panel = page.find("[role='tabpanel']")
53
+ assert_equal "panel", panel["data-ui--tabs-target"]
54
+ end
55
+
56
+ def test_hidden_when_inactive
57
+ render_content(default_value: "a")
58
+ panel = page.find("[role='tabpanel'][data-value='b']", visible: :all)
59
+ assert panel[:hidden].present?
60
+ end
61
+
62
+ def test_not_hidden_when_active
63
+ render_content(default_value: "a")
64
+ panel = page.find("[role='tabpanel'][data-value='a']")
65
+ refute panel[:hidden].present?
66
+ end
67
+
68
+ def test_user_class_merge
69
+ render_inline(UI::Tabs.new(default_value: "a")) do |tabs|
70
+ tabs.with_list do |list|
71
+ list.with_trigger(value: "a") { "A" }
72
+ end
73
+ tabs.with_panel(value: "a", class: "my-panel") { "panel-a" }
74
+ end
75
+ classes = page.find("[role='tabpanel']")[:class].split
76
+ assert_includes classes, "my-panel"
77
+ assert_includes classes, "mt-2"
78
+ end
79
+
80
+ def test_auto_generated_panel_id
81
+ render_content
82
+ panel = page.find("[role='tabpanel']")
83
+ assert panel[:id].present?
84
+ assert panel[:id].start_with?("ui-panel-")
85
+ end
86
+
87
+ def test_panel_id_matches_trigger_aria_controls
88
+ render_content
89
+ panel = page.find("[role='tabpanel'][data-value='a']")
90
+ trigger = page.find("[role='tab'][data-value='a']")
91
+ assert_equal panel[:id], trigger["aria-controls"]
92
+ end
93
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Tabs::ListTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_list(variant: :line)
10
+ render_inline(UI::Tabs.new(default_value: "a", variant: variant)) do |tabs|
11
+ tabs.with_list do |list|
12
+ list.with_trigger(value: "a") { "A" }
13
+ list.with_trigger(value: "b") { "B" }
14
+ end
15
+ tabs.with_panel(value: "a") { "panel-a" }
16
+ tabs.with_panel(value: "b") { "panel-b" }
17
+ end
18
+ end
19
+
20
+ def test_renders_tablist_role
21
+ render_list
22
+ assert_selector "[role='tablist']"
23
+ end
24
+
25
+ def test_line_variant_classes
26
+ render_list(variant: :line)
27
+ classes = page.find("[role='tablist']")[:class].split
28
+ assert_includes classes, "inline-flex"
29
+ assert_includes classes, "border-b"
30
+ assert_includes classes, "border-input"
31
+ end
32
+
33
+ def test_pill_variant_classes
34
+ render_list(variant: :pill)
35
+ classes = page.find("[role='tablist']")[:class].split
36
+ assert_includes classes, "bg-muted"
37
+ assert_includes classes, "p-1"
38
+ assert_includes classes, "rounded-md"
39
+ end
40
+
41
+ def test_solid_variant_classes
42
+ render_list(variant: :solid)
43
+ classes = page.find("[role='tablist']")[:class].split
44
+ assert_includes classes, "border"
45
+ assert_includes classes, "divide-x"
46
+ assert_includes classes, "bg-background"
47
+ end
48
+
49
+ def test_user_class_merge
50
+ render_inline(UI::Tabs.new(default_value: "a")) do |tabs|
51
+ tabs.with_list(class: "my-list-class") do |list|
52
+ list.with_trigger(value: "a") { "A" }
53
+ end
54
+ tabs.with_panel(value: "a") { "panel-a" }
55
+ end
56
+ classes = page.find("[role='tablist']")[:class].split
57
+ assert_includes classes, "my-list-class"
58
+ end
59
+
60
+ def test_triggers_rendered_inside_list
61
+ render_list
62
+ triggers = page.all("[role='tablist'] [role='tab']")
63
+ assert_equal 2, triggers.size
64
+ end
65
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Tabs::TriggerTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_trigger(variant: :line, size: :default, disabled: false)
10
+ render_inline(UI::Tabs.new(default_value: "a", variant: variant, size: size)) do |tabs|
11
+ tabs.with_list do |list|
12
+ list.with_trigger(value: "a", disabled: disabled) { "A" }
13
+ list.with_trigger(value: "b") { "B" }
14
+ end
15
+ tabs.with_panel(value: "a") { "panel-a" }
16
+ tabs.with_panel(value: "b") { "panel-b" }
17
+ end
18
+ end
19
+
20
+ def test_renders_button_with_role_tab
21
+ render_trigger
22
+ btn = page.find("[role='tab'][data-value='a']")
23
+ assert_equal "button", btn.tag_name
24
+ assert_equal "tab", btn[:role]
25
+ end
26
+
27
+ def test_type_button
28
+ render_trigger
29
+ btn = page.find("[role='tab'][data-value='a']")
30
+ assert_equal "button", btn[:type]
31
+ end
32
+
33
+ def test_aria_selected_true_when_active
34
+ render_trigger
35
+ active = page.find("[role='tab'][aria-selected='true']")
36
+ assert_equal "a", active["data-value"]
37
+ end
38
+
39
+ def test_aria_selected_false_when_inactive
40
+ render_trigger
41
+ inactive = page.find("[role='tab'][aria-selected='false']")
42
+ assert_equal "b", inactive["data-value"]
43
+ end
44
+
45
+ def test_aria_controls_matches_panel_id
46
+ render_trigger
47
+ trigger = page.find("[role='tab'][data-value='a']")
48
+ panel = page.find("[role='tabpanel'][data-value='a']")
49
+ assert_equal panel[:id], trigger["aria-controls"]
50
+ end
51
+
52
+ def test_tabindex_zero_when_active
53
+ render_trigger
54
+ active = page.find("[role='tab'][tabindex='0']")
55
+ assert_equal "a", active["data-value"]
56
+ end
57
+
58
+ def test_tabindex_minus_one_when_inactive
59
+ render_trigger
60
+ inactive = page.find("[role='tab'][tabindex='-1']")
61
+ assert_equal "b", inactive["data-value"]
62
+ end
63
+
64
+ def test_data_state_active_when_active
65
+ render_trigger
66
+ active = page.find("[role='tab'][data-state='active']")
67
+ assert_equal "a", active["data-value"]
68
+ end
69
+
70
+ def test_data_state_inactive_when_inactive
71
+ render_trigger
72
+ inactive = page.find("[role='tab'][data-state='inactive']")
73
+ assert_equal "b", inactive["data-value"]
74
+ end
75
+
76
+ def test_data_action_includes_click_and_keydown
77
+ render_trigger
78
+ btn = page.find("[role='tab'][data-value='a']")
79
+ assert_includes btn["data-action"], "click->ui--tabs#activate"
80
+ assert_includes btn["data-action"], "keydown->ui--tabs#keydown"
81
+ end
82
+
83
+ def test_data_ui_tabs_target_trigger
84
+ render_trigger
85
+ btn = page.find("[role='tab'][data-value='a']")
86
+ assert_equal "trigger", btn["data-ui--tabs-target"]
87
+ end
88
+
89
+ def test_disabled_sets_disabled_attribute
90
+ render_trigger(disabled: true)
91
+ btn = page.find("[role='tab'][disabled]")
92
+ assert btn.disabled?
93
+ assert_equal "a", btn["data-value"]
94
+ end
95
+
96
+ def test_leading_icon_slot
97
+ render_inline(UI::Tabs.new(default_value: "a")) do |tabs|
98
+ tabs.with_list do |list|
99
+ list.with_trigger(value: "a") do |trigger|
100
+ trigger.with_leading_icon(name: "user")
101
+ "Account"
102
+ end
103
+ end
104
+ tabs.with_panel(value: "a") { "panel-a" }
105
+ end
106
+ icon = page.find("[role='tab'][data-value='a'] i", visible: :all)
107
+ assert_equal "user", icon["data-lucide"]
108
+ end
109
+
110
+ def test_line_variant_size_sm
111
+ render_trigger(variant: :line, size: :sm)
112
+ btn = page.find("[role='tab'][data-value='a']")
113
+ classes = btn[:class].split
114
+ assert_includes classes, "h-8"
115
+ assert_includes classes, "text-xs"
116
+ end
117
+
118
+ def test_line_variant_size_lg
119
+ render_trigger(variant: :line, size: :lg)
120
+ btn = page.find("[role='tab'][data-value='a']")
121
+ classes = btn[:class].split
122
+ assert_includes classes, "h-11"
123
+ assert_includes classes, "text-base"
124
+ end
125
+
126
+ def test_pill_variant_size_default
127
+ render_trigger(variant: :pill, size: :default)
128
+ btn = page.find("[role='tab'][data-value='a']")
129
+ classes = btn[:class].split
130
+ assert_includes classes, "h-8"
131
+ assert_includes classes, "text-sm"
132
+ end
133
+
134
+ def test_solid_variant_size_default
135
+ render_trigger(variant: :solid, size: :default)
136
+ btn = page.find("[role='tab'][data-value='a']")
137
+ classes = btn[:class].split
138
+ assert_includes classes, "h-10"
139
+ assert_includes classes, "text-sm"
140
+ end
141
+
142
+ def test_user_class_merge
143
+ render_inline(UI::Tabs.new(default_value: "a")) do |tabs|
144
+ tabs.with_list do |list|
145
+ list.with_trigger(value: "a", class: "my-trigger") { "A" }
146
+ end
147
+ tabs.with_panel(value: "a") { "panel-a" }
148
+ end
149
+ classes = page.find("[role='tab']")[:class].split
150
+ assert_includes classes, "my-trigger"
151
+ end
152
+
153
+ def test_auto_generated_id
154
+ render_trigger
155
+ btn = page.find("[role='tab'][data-value='a']")
156
+ assert btn[:id].present?
157
+ assert btn[:id].start_with?("ui-tab-")
158
+ end
159
+ end