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,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::CheckboxTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_input_type_checkbox_with_appearance_none
10
+ render_inline(UI::Checkbox.new(name: "test"))
11
+ input = page.find("input[type='checkbox']")
12
+ classes = input[:class].split
13
+ assert_includes classes, "appearance-none"
14
+ assert_includes classes, "peer"
15
+ end
16
+
17
+ def test_checked_adds_checked_attribute
18
+ render_inline(UI::Checkbox.new(name: "test", checked: true))
19
+ input = page.find("input[type='checkbox']")
20
+ assert input["checked"].present?, "expected checked attribute"
21
+ end
22
+
23
+ def test_value_defaults_to_one
24
+ render_inline(UI::Checkbox.new(name: "test"))
25
+ input = page.find("input[type='checkbox']")
26
+ assert_equal "1", input[:value]
27
+ end
28
+
29
+ def test_value_can_be_customized
30
+ render_inline(UI::Checkbox.new(name: "test", value: "yes"))
31
+ input = page.find("input[type='checkbox']")
32
+ assert_equal "yes", input[:value]
33
+ end
34
+
35
+ def test_disabled_sets_disabled_attribute
36
+ render_inline(UI::Checkbox.new(name: "test", disabled: true))
37
+ input = page.find("input[disabled]")
38
+ assert input.disabled?
39
+ end
40
+
41
+ def test_disabled_wrapper_has_cursor_and_opacity_classes
42
+ render_inline(UI::Checkbox.new(name: "test", disabled: true, label: "Disabled"))
43
+ label_el = page.find("label")
44
+ classes = label_el[:class].split
45
+ assert_includes classes, "cursor-not-allowed"
46
+ assert_includes classes, "opacity-60"
47
+ end
48
+
49
+ def test_error_sets_aria_invalid_and_border_destructive
50
+ render_inline(UI::Checkbox.new(name: "test", error: true))
51
+ input = page.find("input[type='checkbox']")
52
+ assert_equal "true", input["aria-invalid"]
53
+ classes = input[:class].split
54
+ assert_includes classes, "border-destructive"
55
+ end
56
+
57
+ def test_required_sets_required_attribute
58
+ render_inline(UI::Checkbox.new(name: "test", required: true))
59
+ input = page.find("input[required]")
60
+ assert input["required"].present?
61
+ end
62
+
63
+ def test_label_renders_label_element_with_for_id
64
+ render_inline(UI::Checkbox.new(name: "terms", label: "Accept terms"))
65
+ label_el = page.find("label")
66
+ input_el = page.find("input[type='checkbox']")
67
+ assert label_el["for"].present?, "label should have for attribute"
68
+ assert_equal label_el["for"], input_el[:id]
69
+ assert_match(/Accept terms/, label_el.text)
70
+ end
71
+
72
+ def test_without_label_no_label_element_renders
73
+ render_inline(UI::Checkbox.new(name: "test"))
74
+ refute_selector "label"
75
+ end
76
+
77
+ def test_check_overlay_span_present_with_peer_checked_class
78
+ render_inline(UI::Checkbox.new(name: "test"))
79
+ wrapper = page.find(".relative.inline-flex")
80
+ overlay = wrapper.find("span.pointer-events-none")
81
+ classes = overlay[:class].split
82
+ assert_includes classes, "peer-checked:opacity-100"
83
+ assert_includes classes, "opacity-0"
84
+ end
85
+
86
+ def test_check_icon_inside_overlay
87
+ render_inline(UI::Checkbox.new(name: "test"))
88
+ overlay = page.find("span.pointer-events-none")
89
+ icon = overlay.find("i", visible: :all)
90
+ assert_equal "check", icon["data-lucide"]
91
+ end
92
+
93
+ def test_user_class_override_via_tailwind_merge
94
+ render_inline(UI::Checkbox.new(name: "test", class: "border-red-500"))
95
+ input = page.find("input[type='checkbox']")
96
+ classes = input[:class].split
97
+ refute_includes classes, "border-input"
98
+ assert_includes classes, "border-red-500"
99
+ end
100
+
101
+ def test_name_and_id_passthrough
102
+ render_inline(UI::Checkbox.new(name: "user[agree]", id: "agree-cb"))
103
+ input = page.find("input#agree-cb[name='user[agree]']")
104
+ assert input.present?
105
+ end
106
+
107
+ def test_data_attributes_passthrough
108
+ render_inline(UI::Checkbox.new(name: "test", data: { test_id: "my-cb" }))
109
+ assert_selector "input[data-test-id='my-cb']"
110
+ end
111
+
112
+ def test_size_sm_renders_smaller_box
113
+ render_inline(UI::Checkbox.new(name: "test", size: :sm))
114
+ classes = page.find("input[type='checkbox']")[:class].split
115
+ assert_includes classes, "w-3.5"
116
+ assert_includes classes, "h-3.5"
117
+ end
118
+
119
+ def test_size_lg_renders_larger_box
120
+ render_inline(UI::Checkbox.new(name: "test", size: :lg))
121
+ classes = page.find("input[type='checkbox']")[:class].split
122
+ assert_includes classes, "w-5"
123
+ assert_includes classes, "h-5"
124
+ end
125
+
126
+ def test_auto_generated_id_when_label_present
127
+ render_inline(UI::Checkbox.new(name: "test", label: "Label"))
128
+ input = page.find("input[type='checkbox']")
129
+ assert input[:id].present?, "id should be auto-generated when label is provided"
130
+ assert input[:id].start_with?("ui-cb-"), "id should start with ui-cb-"
131
+ end
132
+
133
+ def test_disabled_and_checked_combined
134
+ render_inline(UI::Checkbox.new(name: "test", disabled: true, checked: true))
135
+ input = page.find("input[disabled]")
136
+ assert input["checked"].present?
137
+ end
138
+
139
+ def test_label_text_span_uses_correct_classes
140
+ render_inline(UI::Checkbox.new(name: "test", label: "Terms"))
141
+ label_el = page.find("label")
142
+ span = label_el.all("span").find { |s| s.text.strip == "Terms" }
143
+ assert span, "label text span should exist"
144
+ classes = span[:class].split
145
+ assert_includes classes, "text-sm"
146
+ assert_includes classes, "leading-none"
147
+ end
148
+
149
+ def test_required_label_shows_styled_asterisk_span
150
+ render_inline(UI::Checkbox.new(name: "test", label: "Agree", required: true))
151
+ label_el = page.find("label")
152
+ assert_match(/Agree/, label_el.text)
153
+ asterisk = label_el.find("span.text-destructive")
154
+ assert_match(/\*/, asterisk.text)
155
+ classes = asterisk[:class].split
156
+ assert_includes classes, "text-destructive"
157
+ assert_includes classes, "ml-0.5"
158
+ end
159
+
160
+ def test_non_required_label_has_no_destructive_span
161
+ render_inline(UI::Checkbox.new(name: "test", label: "Agree", required: false))
162
+ refute_selector "label span.text-destructive"
163
+ end
164
+
165
+ def test_disabled_box_has_no_redundant_opacity_class
166
+ render_inline(UI::Checkbox.new(name: "test", disabled: true, label: "x"))
167
+ classes = page.find("input[type='checkbox']")[:class].split
168
+ # Wrapper owns the dimming via opacity-60; box should not double-dim.
169
+ refute_includes classes, "disabled:opacity-50"
170
+ refute_includes classes, "disabled:opacity-50!"
171
+ refute_includes classes, "disabled:cursor-not-allowed"
172
+ refute_includes classes, "disabled:cursor-not-allowed!"
173
+ end
174
+
175
+ # Radii scale with the box so the corners stay visually consistent.
176
+ def test_size_sm_radius
177
+ render_inline(UI::Checkbox.new(name: "test", size: :sm))
178
+ assert_includes page.find("input[type='checkbox']")[:class].split, "rounded-[3px]"
179
+ end
180
+
181
+ def test_size_default_radius
182
+ render_inline(UI::Checkbox.new(name: "test", size: :default))
183
+ assert_includes page.find("input[type='checkbox']")[:class].split, "rounded-[4px]"
184
+ end
185
+
186
+ def test_size_lg_radius
187
+ render_inline(UI::Checkbox.new(name: "test", size: :lg))
188
+ assert_includes page.find("input[type='checkbox']")[:class].split, "rounded-[5px]"
189
+ end
190
+
191
+ # --- form-builder binding + automatic labels (Rails convention) ---
192
+
193
+ # Stands in for an app model: the builder needs only a name and attributes.
194
+ class User
195
+ include ActiveModel::Model
196
+ attr_accessor :email, :password, :remember_me
197
+ end
198
+
199
+ def form_builder_for(record)
200
+ ActionView::Helpers::FormBuilder.new(:user, record, vc_test_controller.view_context, {})
201
+ end
202
+
203
+ def test_form_binding_scopes_the_field_name
204
+ render_inline(UI::Checkbox.new(form: form_builder_for(User.new), field: :remember_me))
205
+ assert_selector "input[type='checkbox'][name='user[remember_me]']", visible: :all
206
+ end
207
+
208
+ def test_label_is_derived_from_the_attribute_name
209
+ render_inline(UI::Checkbox.new(form: form_builder_for(User.new), field: :remember_me))
210
+ assert_text "Remember me"
211
+ end
212
+
213
+ def test_derived_label_is_associated_with_the_input
214
+ render_inline(UI::Checkbox.new(form: form_builder_for(User.new), field: :remember_me))
215
+ assert_equal page.find("label")[:for],
216
+ page.find("input[type='checkbox']", visible: :all)[:id]
217
+ end
218
+
219
+ # Rails' check_box emits the companion hidden field so an unchecked box still
220
+ # submits a value; the plain `name:` path has no such helper.
221
+ def test_form_binding_emits_the_unchecked_companion_field
222
+ render_inline(UI::Checkbox.new(form: form_builder_for(User.new), field: :remember_me))
223
+ hidden = page.find("input[type='hidden'][name='user[remember_me]']", visible: :all)
224
+ assert_equal "0", hidden[:value]
225
+ end
226
+
227
+ def test_explicit_label_overrides_the_derived_one
228
+ render_inline(UI::Checkbox.new(form: form_builder_for(User.new), field: :remember_me, label: "Keep me signed in"))
229
+ assert_text "Keep me signed in"
230
+ end
231
+
232
+ def test_label_false_opts_out
233
+ render_inline(UI::Checkbox.new(form: form_builder_for(User.new), field: :remember_me, label: false))
234
+ assert_no_selector "label"
235
+ end
236
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::DataGridTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ URL = "/customers.json"
10
+
11
+ def render_grid(**options)
12
+ render_inline(UI::DataGrid.new(url: URL, **options)) do |grid|
13
+ grid.column(:id, "ID", width: 90, type: :number)
14
+ grid.column(:name, "Name")
15
+ grid.column(:owner, "Owner")
16
+ grid.column(:notes, "Notes", sortable: false, filterable: false)
17
+ end
18
+ end
19
+
20
+ def grid = page.find("div[data-controller='ui--data-grid']")
21
+ def columns = JSON.parse(grid["data-ui--data-grid-columns-value"])
22
+
23
+ def test_declares_the_stimulus_controller
24
+ render_grid
25
+ assert_selector "div[data-controller='ui--data-grid']"
26
+ end
27
+
28
+ # Rows are never embedded: the grid fetches each page from this URL, sending
29
+ # the sort and filter it wants applied.
30
+ def test_points_at_the_url_it_fetches_pages_from
31
+ render_grid
32
+ assert_equal URL, grid["data-ui--data-grid-url-value"]
33
+ assert_nil grid["data-ui--data-grid-rows-value"]
34
+ end
35
+
36
+ def test_columns_carry_the_field_header_type_and_width
37
+ render_grid
38
+ assert_equal %w[id name owner notes], columns.map { |c| c["field"] }
39
+ assert_equal [ "ID", "Name", "Owner", "Notes" ], columns.map { |c| c["headerName"] }
40
+ assert_equal 90, columns.first["width"]
41
+ assert_equal "number", columns.first["type"]
42
+ end
43
+
44
+ # Only the declared width is sent, so AG Grid keeps sizing the rest itself.
45
+ def test_omits_width_when_not_given
46
+ render_grid
47
+ refute columns.last.key?("width")
48
+ end
49
+
50
+ # A column the server has no SQL expression for must not offer the control:
51
+ # a sort arrow that does nothing is worse than no sort arrow.
52
+ def test_columns_declare_whether_the_server_can_sort_or_filter_them
53
+ render_grid
54
+ assert columns.first["sortable"]
55
+ assert columns.first["filterable"]
56
+ refute columns.last["sortable"]
57
+ refute columns.last["filterable"]
58
+ end
59
+
60
+ def test_empty_message_reaches_the_grid
61
+ render_grid(empty_message: "No customers yet")
62
+ assert_equal "No customers yet", grid["data-ui--data-grid-empty-message-value"]
63
+ end
64
+
65
+ def test_page_size_defaults_to_a_screenful_and_can_be_set
66
+ render_grid
67
+ assert_equal "15", grid["data-ui--data-grid-page-size-value"]
68
+ render_grid(page_size: 50)
69
+ assert_equal "50", grid["data-ui--data-grid-page-size-value"]
70
+ end
71
+
72
+ # The height is reserved before the grid paints, so the page does not jump
73
+ # when its chunk arrives.
74
+ def test_reserves_height_for_a_full_page_of_rows
75
+ render_grid(page_size: 10)
76
+ assert_equal "height: 448px", grid[:style]
77
+ end
78
+
79
+ def test_user_class_merges
80
+ render_grid(class: "mt-8")
81
+ assert_includes grid[:class].split, "mt-8"
82
+ end
83
+
84
+ def test_html_attrs_pass_through
85
+ render_grid(id: "customers")
86
+ assert_selector "div#customers"
87
+ end
88
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::DatePickerTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_picker(**options)
10
+ render_inline(UI::DatePicker.new(**{ name: "holiday[date]" }.merge(options)))
11
+ end
12
+
13
+ def typed = page.find("input[type='text']")
14
+ def hidden = page.find("input[type='hidden']", visible: :all)
15
+ def root = page.find("div[data-controller='ui--date-picker']")
16
+
17
+ # --- the two fields ------------------------------------------------------
18
+
19
+ # The typed field carries no name, so a half-typed date can never reach the
20
+ # server: only the hidden ISO field is submitted.
21
+ def test_only_the_hidden_field_is_submitted
22
+ render_picker
23
+ assert_nil typed[:name]
24
+ assert_equal "holiday[date]", hidden[:name]
25
+ end
26
+
27
+ def test_the_typed_field_shows_the_locale_format_and_the_hidden_one_iso
28
+ render_picker(value: Date.new(2012, 5, 2))
29
+ assert_equal "02/05/2012", typed[:value]
30
+ assert_equal "2012-05-02", hidden[:value]
31
+ end
32
+
33
+ def test_both_fields_are_empty_without_a_value
34
+ render_picker
35
+ assert_equal "", typed[:value].to_s
36
+ assert_equal "", hidden[:value].to_s
37
+ end
38
+
39
+ # --- the format is not hardcoded ----------------------------------------
40
+ #
41
+ # date.formats.typed is the authority: the placeholder, the parse order and
42
+ # the separator are all derived from it, so they cannot drift apart.
43
+
44
+ def test_the_placeholder_is_derived_from_the_locale_format
45
+ render_picker
46
+ assert_equal "DD/MM/YYYY", typed[:placeholder]
47
+ end
48
+
49
+ def test_the_parse_order_and_separator_are_derived_from_the_locale_format
50
+ render_picker
51
+ assert_equal "DMY", root["data-ui--date-picker-order-value"]
52
+ assert_equal "/", root["data-ui--date-picker-separator-value"]
53
+ end
54
+
55
+ # A locale that writes dates the other way round changes all three together
56
+ # and needs no change here.
57
+ def test_a_different_locale_format_changes_the_placeholder_and_the_parser
58
+ with_format("%m-%d-%Y") do
59
+ render_picker(value: Date.new(2012, 5, 2))
60
+ assert_equal "MM-DD-YYYY", typed[:placeholder]
61
+ assert_equal "MDY", root["data-ui--date-picker-order-value"]
62
+ assert_equal "-", root["data-ui--date-picker-separator-value"]
63
+ assert_equal "05-02-2012", typed[:value]
64
+ end
65
+ end
66
+
67
+ def with_format(format)
68
+ original = I18n.backend.translate(:en, "date.formats.typed")
69
+ I18n.backend.store_translations(:en, date: { formats: { typed: format } })
70
+ yield
71
+ ensure
72
+ I18n.backend.store_translations(:en, date: { formats: { typed: original } })
73
+ end
74
+
75
+ # --- form binding --------------------------------------------------------
76
+
77
+ Holiday = Struct.new(:date) do
78
+ def self.human_attribute_name(name, *) = name.to_s.humanize
79
+ def to_model = self
80
+ def model_name = ActiveModel::Name.new(self.class, nil, "Holiday")
81
+ def persisted? = false
82
+ end
83
+
84
+ def form_for(record = Holiday.new(Date.new(2026, 8, 15)))
85
+ ActionView::Helpers::FormBuilder.new(:holiday, record, vc_test_controller.view_context, {})
86
+ end
87
+
88
+ def test_form_binding_names_the_hidden_field_and_reads_the_value
89
+ render_inline(UI::DatePicker.new(form: form_for, field: :date))
90
+ assert_equal "holiday[date]", hidden[:name]
91
+ assert_equal "2026-08-15", hidden[:value]
92
+ assert_equal "15/08/2026", typed[:value]
93
+ end
94
+
95
+ def test_form_binding_derives_the_label
96
+ render_inline(UI::DatePicker.new(form: form_for, field: :date))
97
+ assert_selector "label", text: "Date"
98
+ assert_equal page.find("label")[:for], typed[:id]
99
+ end
100
+
101
+ # --- state ---------------------------------------------------------------
102
+
103
+ def test_required_marks_the_typed_field
104
+ render_picker(required: true)
105
+ assert_selector "input[type='text'][required]"
106
+ end
107
+
108
+ def test_required_marks_the_label_with_an_asterisk
109
+ render_picker(required: true, label: "Date of birth")
110
+ assert_selector "label span.text-destructive", text: "*"
111
+ end
112
+
113
+ def test_min_and_max_reach_the_calendar
114
+ render_picker(min: "2026-04-01", max: "2027-03-31")
115
+ calendar = page.find("calendar-date", visible: :all)
116
+ assert_equal "2026-04-01", calendar[:min]
117
+ assert_equal "2027-03-31", calendar[:max]
118
+ end
119
+
120
+ # The calendar chunk is only fetched when the button is pressed, so a form
121
+ # nobody opens a calendar on downloads nothing.
122
+ def test_the_calendar_panel_starts_hidden_and_carries_the_module_url
123
+ render_picker
124
+ assert_selector "div[role='dialog'][hidden]", visible: :all
125
+ assert_match(/cally/, root["data-ui--date-picker-module-url-value"])
126
+ end
127
+
128
+ # The hint teaches the format on a data-entry form; in a filter toolbar the
129
+ # value is already on screen and three wrapped lines of hint per field push
130
+ # the page heading down.
131
+ def test_the_hint_can_be_turned_off
132
+ render_picker
133
+ assert_selector "p", text: "DD/MM/YYYY"
134
+ assert_equal "#{typed[:id]}-hint", typed["aria-describedby"]
135
+
136
+ render_picker(hint: false)
137
+ assert_no_selector "p"
138
+ assert_nil typed["aria-describedby"]
139
+ end
140
+
141
+ def test_the_toggle_is_labelled_and_marked_closed
142
+ render_picker
143
+ button = page.find("button[aria-haspopup='dialog']")
144
+ assert_equal "false", button["aria-expanded"]
145
+ assert_equal I18n.t("ui.date_picker.open"), button["aria-label"]
146
+ end
147
+
148
+ # --- the calendar header ---------------------------------------------------
149
+
150
+ # Month and year sit in Cally's heading slot, between the arrows, rather
151
+ # than under a second, duplicate heading.
152
+ def test_month_and_year_selects_fill_the_calendar_heading
153
+ render_picker
154
+ heading = page.find("calendar-date [slot='heading']", visible: :all)
155
+ assert heading.has_css?("calendar-select-month", visible: :all)
156
+ assert heading.has_css?("calendar-select-year", visible: :all)
157
+ end
158
+
159
+ # Cally wraps the arrows in its own buttons, so ours must not be buttons.
160
+ def test_arrows_are_labelled_icons_not_nested_buttons
161
+ render_picker
162
+ [ [ "previous", "ui.date_picker.previous" ], [ "next", "ui.date_picker.next" ] ].each do |slot, key|
163
+ arrow = page.find("calendar-date [slot='#{slot}']", visible: :all)
164
+ refute_equal "button", arrow.tag_name
165
+ assert_includes arrow.text(:all), I18n.t(key)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Dialog::ContentTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_dialog(content_opts = {}, &block)
10
+ render_inline(UI::Dialog.new(id: "profile-dialog")) do |dlg|
11
+ dlg.with_trigger { "Open" }
12
+ dlg.with_content(**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_dialog { |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_dialog { |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_dialog { |c| c.with_title("Hi") }
31
+ assert_equal "profile-dialog", page.find("dialog", visible: :all)[:id]
32
+ end
33
+
34
+ def test_panel_present_inside_dialog
35
+ render_dialog { |c| c.with_title("Hi") }
36
+ assert_selector "[data-ui--dialog-part='panel']", visible: :all
37
+ end
38
+
39
+ # ── Title / description ──────────────────────────────────────────────────
40
+
41
+ def test_title_text_arg_renders_h2
42
+ render_dialog { |c| c.with_title("Are you sure?") }
43
+ title = page.find("dialog h2", visible: :all)
44
+ assert_equal "Are you sure?", title.text.strip
45
+ classes = title[:class].split
46
+ assert_includes classes, "text-lg"
47
+ assert_includes classes, "font-semibold"
48
+ end
49
+
50
+ def test_title_block_renders_h2_with_block_content
51
+ render_dialog do |c|
52
+ c.with_title { "Block title" }
53
+ end
54
+ title = page.find("dialog h2", visible: :all)
55
+ assert_equal "Block title", title.text.strip
56
+ end
57
+
58
+ def test_title_links_via_aria_labelledby
59
+ render_dialog { |c| c.with_title("Hi") }
60
+ dialog = page.find("dialog", visible: :all)
61
+ title = page.find("dialog h2", visible: :all)
62
+ assert_equal "profile-dialog-title", title[:id]
63
+ assert_equal title[:id], dialog["aria-labelledby"]
64
+ end
65
+
66
+ def test_description_text_arg_renders_p
67
+ render_dialog do |c|
68
+ c.with_title("Hi")
69
+ c.with_description("This action cannot be undone.")
70
+ end
71
+ desc = page.find("dialog p", visible: :all)
72
+ assert_equal "This action cannot be undone.", desc.text.strip
73
+ assert_includes desc[:class].split, "text-muted-foreground"
74
+ end
75
+
76
+ def test_description_links_via_aria_describedby
77
+ render_dialog do |c|
78
+ c.with_title("Hi")
79
+ c.with_description("Body")
80
+ end
81
+ dialog = page.find("dialog", visible: :all)
82
+ desc = page.find("dialog p", visible: :all)
83
+ assert_equal "profile-dialog-description", desc[:id]
84
+ assert_equal desc[:id], dialog["aria-describedby"]
85
+ end
86
+
87
+ def test_no_title_means_no_aria_labelledby
88
+ render_dialog { |c| c.with_description("Body") }
89
+ refute page.find("dialog", visible: :all)["aria-labelledby"]
90
+ end
91
+
92
+ # ── Body ─────────────────────────────────────────────────────────────────
93
+
94
+ def test_body_content_renders_inside_dialog
95
+ render_dialog do |c|
96
+ c.with_title("Hi")
97
+ "raw body text"
98
+ end
99
+ assert_selector "dialog", text: "raw body text", visible: :all
100
+ end
101
+
102
+ # ── Footer ───────────────────────────────────────────────────────────────
103
+
104
+ def test_footer_block_renders_in_footer_element
105
+ render_dialog do |c|
106
+ c.with_title("Hi")
107
+ c.with_footer do
108
+ '<button type="button">Confirm</button>'.html_safe
109
+ end
110
+ end
111
+ assert_selector "dialog footer", visible: :all
112
+ assert_selector "dialog footer button", text: "Confirm", visible: :all
113
+ end
114
+
115
+ def test_no_footer_means_no_footer_element
116
+ render_dialog { |c| c.with_title("Hi") }
117
+ assert_no_selector "dialog footer", visible: :all
118
+ end
119
+
120
+ # ── Close button ─────────────────────────────────────────────────────────
121
+
122
+ def test_close_button_default_present
123
+ render_dialog { |c| c.with_title("Hi") }
124
+ btn = page.find("dialog button[aria-label='Close']", visible: :all)
125
+ assert_match(/click->ui--dialog#close/, btn["data-action"])
126
+ end
127
+
128
+ def test_close_button_can_be_disabled
129
+ render_dialog(show_close: false) { |c| c.with_title("Hi") }
130
+ assert_no_selector "dialog button[aria-label='Close']", visible: :all
131
+ end
132
+
133
+ # ── Sizes ────────────────────────────────────────────────────────────────
134
+
135
+ def test_size_default
136
+ render_dialog { |c| c.with_title("Hi") }
137
+ panel = page.find("[data-ui--dialog-part='panel']", visible: :all)
138
+ assert_includes panel[:class].split, "max-w-lg"
139
+ end
140
+
141
+ def test_other_sizes_apply_max_width_token
142
+ { sm: "max-w-md", lg: "max-w-2xl", xl: "max-w-4xl" }.each do |size, expected|
143
+ render_dialog(size: size) { |c| c.with_title("Hi") }
144
+ panel = page.find("[data-ui--dialog-part='panel']", visible: :all)
145
+ assert_includes panel[:class].split, expected, "size #{size}"
146
+ end
147
+ end
148
+
149
+ # ── User class merge on panel ────────────────────────────────────────────
150
+
151
+ def test_user_class_merges_into_panel
152
+ render_dialog(class: "border-red-500") { |c| c.with_title("Hi") }
153
+ panel = page.find("[data-ui--dialog-part='panel']", visible: :all)
154
+ assert_includes panel[:class].split, "border-red-500"
155
+ end
156
+ end