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,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::BaseTest < ViewComponent::TestCase
6
+ # Component tests do not need DB fixtures. Test_helper applies `fixtures :all`
7
+ # globally; admin_users fixtures are pre-existing-broken (duplicate empty emails).
8
+ # Skip fixture setup for this test class.
9
+ def setup_fixtures(*); end
10
+ def teardown_fixtures(*); end
11
+
12
+ class Probe < UI::Base
13
+ def call
14
+ tag.div(class: cn(*@parts)) { "x" }
15
+ end
16
+
17
+ def initialize(*parts)
18
+ @parts = parts
19
+ end
20
+ end
21
+
22
+ def test_cn_concatenates_string_classes
23
+ render_inline Probe.new("p-2", "rounded-md")
24
+ assert_selector "div.p-2.rounded-md"
25
+ end
26
+
27
+ def test_cn_drops_nil_and_blank_inputs
28
+ render_inline Probe.new("p-2", nil, "", "rounded-md")
29
+ assert_selector "div.p-2.rounded-md"
30
+ end
31
+
32
+ def test_cn_supports_conditional_hash
33
+ render_inline Probe.new("base", { "active-class" => true, "hidden-class" => false })
34
+ div = page.find("div")
35
+ assert_includes div[:class], "base"
36
+ assert_includes div[:class], "active-class"
37
+ refute_includes div[:class], "hidden-class"
38
+ end
39
+
40
+ def test_cn_resolves_tailwind_conflicts_user_wins
41
+ render_inline Probe.new("bg-primary text-primary-foreground", "bg-red-500")
42
+ div = page.find("div")
43
+ refute_includes div[:class], "bg-primary"
44
+ assert_includes div[:class], "bg-red-500"
45
+ assert_includes div[:class], "text-primary-foreground"
46
+ end
47
+
48
+ class VariantProbe < UI::Base
49
+ variants(
50
+ base: "btn",
51
+ variants: {
52
+ variant: { default: "bg-primary", outline: "border" },
53
+ size: { sm: "h-8", lg: "h-10" }
54
+ },
55
+ defaults: { variant: :default, size: :sm },
56
+ compound: [
57
+ { when: { variant: :outline, size: :lg }, class: "border-2" }
58
+ ]
59
+ )
60
+
61
+ def initialize(variant: nil, size: nil)
62
+ @variant = variant
63
+ @size = size
64
+ end
65
+
66
+ def call
67
+ tag.button(class: variant_classes(variant: @variant, size: @size)) { "x" }
68
+ end
69
+ end
70
+
71
+ def test_variants_uses_defaults_when_no_props_passed
72
+ render_inline VariantProbe.new
73
+ btn = page.find("button")
74
+ assert_includes btn[:class], "btn"
75
+ assert_includes btn[:class], "bg-primary"
76
+ assert_includes btn[:class], "h-8"
77
+ end
78
+
79
+ def test_variants_overrides_defaults_with_passed_props
80
+ render_inline VariantProbe.new(variant: :outline, size: :lg)
81
+ btn = page.find("button")
82
+ assert_includes btn[:class], "border"
83
+ assert_includes btn[:class], "h-10"
84
+ refute_includes btn[:class], "bg-primary"
85
+ end
86
+
87
+ def test_variants_applies_compound_rule_when_all_conditions_match
88
+ render_inline VariantProbe.new(variant: :outline, size: :lg)
89
+ btn = page.find("button")
90
+ assert_includes btn[:class], "border-2"
91
+ end
92
+
93
+ def test_variants_skips_compound_rule_when_any_condition_misses
94
+ render_inline VariantProbe.new(variant: :outline, size: :sm)
95
+ btn = page.find("button")
96
+ refute_includes btn[:class], "border-2"
97
+ end
98
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Breadcrumb::EllipsisTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_li
10
+ render_inline(UI::Breadcrumb::Ellipsis.new)
11
+ assert_selector "li"
12
+ end
13
+
14
+ def test_renders_ellipsis_icon
15
+ render_inline(UI::Breadcrumb::Ellipsis.new)
16
+ assert_selector "li i[data-lucide='ellipsis']"
17
+ end
18
+
19
+ def test_static_ellipsis_is_fully_decorative
20
+ render_inline(UI::Breadcrumb::Ellipsis.new)
21
+ assert_selector "li > span[role='presentation']"
22
+ assert_selector "li > span[aria-hidden='true']"
23
+ refute_selector "li .sr-only"
24
+ end
25
+
26
+ def test_user_class_override
27
+ render_inline(UI::Breadcrumb::Ellipsis.new(class: "my-ellipsis"))
28
+ classes = page.find("li > span")[:class].split
29
+ assert_includes classes, "my-ellipsis"
30
+ end
31
+
32
+ def test_renders_popover_when_content_present
33
+ render_inline(UI::Breadcrumb::Ellipsis.new) do
34
+ "<a href=\"/docs\">Documentation</a>".html_safe
35
+ end
36
+ assert_selector "li button[aria-label='More breadcrumb pages']"
37
+ assert_selector "li i[data-lucide='ellipsis']"
38
+ end
39
+
40
+ def test_popover_content_includes_provided_html
41
+ render_inline(UI::Breadcrumb::Ellipsis.new) do
42
+ "<li>Docs</li><li>Components</li>".html_safe
43
+ end
44
+ assert_selector "li button[aria-label='More breadcrumb pages']"
45
+ end
46
+
47
+ def test_html_attrs_passthrough
48
+ render_inline(UI::Breadcrumb::Ellipsis.new(id: "my-ellipsis", data: { test_id: "ell" }))
49
+ assert_selector "li#my-ellipsis[data-test-id='ell']"
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Breadcrumb::ItemTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_li
10
+ render_inline(UI::Breadcrumb::Item.new) { "Item" }
11
+ assert_selector "li", text: "Item"
12
+ end
13
+
14
+ def test_default_classes
15
+ render_inline(UI::Breadcrumb::Item.new) { "x" }
16
+ classes = page.find("li")[:class].split
17
+ assert_includes classes, "inline-flex"
18
+ assert_includes classes, "items-center"
19
+ assert_includes classes, "gap-1.5"
20
+ end
21
+
22
+ def test_user_class_override
23
+ render_inline(UI::Breadcrumb::Item.new(class: "my-item")) { "x" }
24
+ classes = page.find("li")[:class].split
25
+ assert_includes classes, "my-item"
26
+ end
27
+
28
+ def test_html_attrs_passthrough
29
+ render_inline(UI::Breadcrumb::Item.new(id: "my-item", data: { test_id: "item" })) { "x" }
30
+ assert_selector "li#my-item[data-test-id='item']"
31
+ end
32
+
33
+ def test_renders_with_link
34
+ render_inline(UI::Breadcrumb::Item.new) do
35
+ "<a href=\"/\">Home</a>".html_safe
36
+ end
37
+ assert_selector "li a[href='/']", text: "Home"
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Breadcrumb::ListTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_ol
10
+ render_inline(UI::Breadcrumb::List.new) { "Items" }
11
+ assert_selector "ol", text: "Items"
12
+ end
13
+
14
+ def test_default_classes
15
+ render_inline(UI::Breadcrumb::List.new) { "x" }
16
+ classes = page.find("ol")[:class].split
17
+ assert_includes classes, "flex"
18
+ assert_includes classes, "flex-wrap"
19
+ assert_includes classes, "items-center"
20
+ assert_includes classes, "gap-1.5"
21
+ assert_includes classes, "break-words"
22
+ end
23
+
24
+ def test_user_class_override
25
+ render_inline(UI::Breadcrumb::List.new(class: "my-list")) { "x" }
26
+ classes = page.find("ol")[:class].split
27
+ assert_includes classes, "my-list"
28
+ end
29
+
30
+ def test_html_attrs_passthrough
31
+ render_inline(UI::Breadcrumb::List.new(id: "my-list", data: { test_id: "list" })) { "x" }
32
+ assert_selector "ol#my-list[data-test-id='list']"
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::Breadcrumb::SeparatorTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def test_renders_li_with_presentation_role
10
+ render_inline(UI::Breadcrumb::Separator.new)
11
+ assert_selector "li[role='presentation']"
12
+ end
13
+
14
+ def test_renders_slash
15
+ render_inline(UI::Breadcrumb::Separator.new)
16
+ assert_selector "li span", text: "/"
17
+ end
18
+
19
+ def test_span_is_aria_hidden
20
+ render_inline(UI::Breadcrumb::Separator.new)
21
+ span = page.find("li > span")
22
+ assert_equal "true", span["aria-hidden"]
23
+ end
24
+
25
+ def test_default_classes
26
+ render_inline(UI::Breadcrumb::Separator.new)
27
+ classes = page.find("li > span")[:class].split
28
+ assert_includes classes, "text-muted-foreground/70"
29
+ end
30
+
31
+ def test_user_class_override
32
+ render_inline(UI::Breadcrumb::Separator.new(class: "my-sep"))
33
+ classes = page.find("li > span")[:class].split
34
+ assert_includes classes, "my-sep"
35
+ end
36
+ end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class UI::BreadcrumbTest < ViewComponent::TestCase
6
+ def setup_fixtures(*); end
7
+ def teardown_fixtures(*); end
8
+
9
+ def render_breadcrumb(items, size: :sm, max_items: nil, collapse_at: :middle)
10
+ render_inline(UI::Breadcrumb.new(size: size, max_items: max_items, collapse_at: collapse_at)) do |breadcrumb|
11
+ items.each do |item|
12
+ breadcrumb.with_item(**item)
13
+ end
14
+ end
15
+ end
16
+
17
+ def long_items
18
+ [
19
+ { href: "/a", text: "A" },
20
+ { href: "/b", text: "B" },
21
+ { href: "/c", text: "C" },
22
+ { href: "/d", text: "D" },
23
+ { href: "/e", text: "E" },
24
+ { href: "/f", text: "F" }
25
+ ]
26
+ end
27
+
28
+ def simple_items
29
+ [
30
+ { href: "/groups", text: "Workspace" },
31
+ { href: "/groups/website", text: "Website" },
32
+ { href: "/issues", text: "Issues" }
33
+ ]
34
+ end
35
+
36
+ # Structure
37
+
38
+ def test_renders_nav_landmark
39
+ render_breadcrumb(simple_items)
40
+ nav = page.find("nav[aria-label='Breadcrumb']")
41
+ assert nav.present?
42
+ end
43
+
44
+ def test_renders_ordered_list
45
+ render_breadcrumb(simple_items)
46
+ assert_selector "nav ol"
47
+ end
48
+
49
+ def test_renders_all_items_as_links
50
+ render_breadcrumb(simple_items)
51
+ links = page.all("nav ol li a")
52
+ assert_equal 3, links.size
53
+ end
54
+
55
+ def test_each_item_is_a_link
56
+ render_breadcrumb(simple_items)
57
+ links = page.all("nav ol a")
58
+ assert_equal 3, links.size
59
+ assert_equal "Workspace", links[0].text.strip
60
+ assert_equal "Website", links[1].text.strip
61
+ end
62
+
63
+ def test_links_have_href
64
+ render_breadcrumb(simple_items)
65
+ links = page.all("nav ol a")
66
+ assert_equal "/groups", links[0][:href]
67
+ assert_equal "/groups/website", links[1][:href]
68
+ assert_equal "/issues", links[2][:href]
69
+ end
70
+
71
+ def test_single_item_no_separator
72
+ render_breadcrumb([ { href: "/", text: "Home" } ])
73
+ refute_selector "span[aria-hidden='true']"
74
+ end
75
+
76
+ # Separators
77
+
78
+ def test_separator_between_items
79
+ render_breadcrumb(simple_items)
80
+ separators = page.all("span[aria-hidden='true']")
81
+ assert_equal 2, separators.size
82
+ end
83
+
84
+ def test_separator_is_slash
85
+ render_breadcrumb(simple_items)
86
+ sep = page.all("span[aria-hidden='true']").first
87
+ assert_includes sep.text, "/"
88
+ end
89
+
90
+ def test_separator_is_aria_hidden
91
+ render_breadcrumb(simple_items)
92
+ sep = page.all("span[aria-hidden='true']").first
93
+ assert_equal "true", sep["aria-hidden"]
94
+ end
95
+
96
+ # aria-current
97
+
98
+ def test_last_item_has_aria_current_page
99
+ render_breadcrumb(simple_items)
100
+ last_link = page.all("nav ol a").last
101
+ assert_equal "page", last_link["aria-current"]
102
+ end
103
+
104
+ def test_non_last_items_do_not_have_aria_current
105
+ render_breadcrumb(simple_items)
106
+ first_link = page.all("nav ol a").first
107
+ refute first_link["aria-current"].present?
108
+ end
109
+
110
+ # Sizes
111
+
112
+ def test_sm_is_default
113
+ render_breadcrumb(simple_items)
114
+ classes = page.find("nav")[:class].split
115
+ assert_includes classes, "text-xs"
116
+ end
117
+
118
+ def test_md_size
119
+ render_breadcrumb(simple_items, size: :md)
120
+ classes = page.find("nav")[:class].split
121
+ assert_includes classes, "text-sm"
122
+ end
123
+
124
+ # Avatar
125
+
126
+ def test_item_with_avatar
127
+ items = [
128
+ { href: "/groups/website", text: "Website", avatar_src: "/avatar.png" },
129
+ { href: "/issues", text: "Issues" }
130
+ ]
131
+ render_breadcrumb(items)
132
+ assert_selector "nav ol img[src='/avatar.png']"
133
+ end
134
+
135
+ # Truncation
136
+
137
+ def test_non_last_items_truncated
138
+ render_breadcrumb(simple_items)
139
+ first_link = page.all("nav ol a").first
140
+ classes = first_link[:class].split
141
+ assert_includes classes, "max-w-32"
142
+ assert_includes classes, "truncate"
143
+ end
144
+
145
+ def test_last_item_not_truncated
146
+ render_breadcrumb(simple_items)
147
+ last_link = page.all("nav ol a").last
148
+ classes = last_link[:class].split
149
+ refute_includes classes, "max-w-32"
150
+ end
151
+
152
+ # Data attributes for Stimulus decoupling
153
+
154
+ def test_items_have_breadcrumb_data_attrs
155
+ render_breadcrumb(simple_items)
156
+ first_li = page.all("nav ol li").first
157
+ assert_equal "/groups", first_li["data-breadcrumb-href"]
158
+ assert_equal "Workspace", first_li["data-breadcrumb-text"]
159
+ end
160
+
161
+ def test_last_item_has_data_last_attribute
162
+ render_breadcrumb(simple_items)
163
+ last_li = page.all("nav ol li").last
164
+ assert_equal "true", last_li["data-last"]
165
+ end
166
+
167
+ def test_non_last_items_do_not_have_data_last
168
+ render_breadcrumb(simple_items)
169
+ first_li = page.all("nav ol li").first
170
+ refute first_li["data-last"].present?
171
+ end
172
+
173
+ # Separator icon customization
174
+
175
+ def test_custom_separator_icon
176
+ skip "separator_icon option changes slash character — tested manually in previews"
177
+ end
178
+
179
+ # User class merge
180
+
181
+ def test_user_class_merge
182
+ render_inline(UI::Breadcrumb.new(class: "my-nav")) do |breadcrumb|
183
+ breadcrumb.with_item(href: "/", text: "Home")
184
+ end
185
+ classes = page.find("nav")[:class].split
186
+ assert_includes classes, "my-nav"
187
+ end
188
+
189
+ # HTML attrs passthrough
190
+
191
+ def test_html_attrs_passthrough
192
+ render_inline(UI::Breadcrumb.new(data: { test_id: "bc" })) do |breadcrumb|
193
+ breadcrumb.with_item(href: "/", text: "Home")
194
+ end
195
+ assert_selector "nav[data-test-id='bc']"
196
+ end
197
+
198
+ # Auto-ellipsis — collapse_at: :middle (default)
199
+
200
+ def test_middle_collapse_shows_first_and_last
201
+ render_breadcrumb(long_items, max_items: 3, collapse_at: :middle)
202
+ links = page.all("nav ol a")
203
+ texts = links.map { |a| a.text.strip }
204
+ # budget=2, head=1, tail=1 → A + E + F
205
+ assert_equal [ "A", "E", "F" ], texts
206
+ end
207
+
208
+ def test_middle_collapse_renders_auto_ellipsis
209
+ render_breadcrumb(long_items, max_items: 3, collapse_at: :middle)
210
+ assert_selector "nav ol button[aria-label='More breadcrumb pages']"
211
+ end
212
+
213
+ def test_middle_collapse_hidden_items_in_dropdown
214
+ render_breadcrumb(long_items, max_items: 3, collapse_at: :middle)
215
+ html = page.native.inner_html
216
+ # Hidden items (C, D, E) should be inside the collapsed popover list
217
+ assert_includes html, "Collapsed breadcrumb pages"
218
+ assert_includes html, 'href="/c"'
219
+ assert_includes html, 'href="/d"'
220
+ assert_includes html, 'href="/e"'
221
+ end
222
+
223
+ def test_middle_collapse_has_separators_around_ellipsis
224
+ render_breadcrumb(long_items, max_items: 3, collapse_at: :middle)
225
+ separators = page.all("nav ol span[aria-hidden='true']")
226
+ # A > B > ... > F → 3 separators
227
+ assert_equal 3, separators.size
228
+ end
229
+
230
+ def test_middle_collapse_renders_auto_ellipsis
231
+ render_breadcrumb(long_items, max_items: 3, collapse_at: :middle)
232
+ assert_selector "nav ol button[aria-label='More breadcrumb pages']"
233
+ end
234
+
235
+ # Auto-ellipsis — collapse_at: :start
236
+
237
+ def test_start_collapse_shows_last_items
238
+ render_breadcrumb(long_items, max_items: 3, collapse_at: :start)
239
+ links = page.all("nav ol a")
240
+ texts = links.map { |a| a.text.strip }
241
+ assert_equal [ "D", "E", "F" ], texts
242
+ end
243
+
244
+ def test_start_collapse_renders_auto_ellipsis_first
245
+ render_breadcrumb(long_items, max_items: 3, collapse_at: :start)
246
+ # Ellipsis should be the first element after the first separator
247
+ first_button = page.all("nav ol button").first
248
+ assert_equal "More breadcrumb pages", first_button["aria-label"]
249
+ end
250
+
251
+ # Auto-ellipsis — collapse_at: :end
252
+
253
+ def test_end_collapse_shows_first_items
254
+ render_breadcrumb(long_items, max_items: 3, collapse_at: :end)
255
+ links = page.all("nav ol a")
256
+ texts = links.map { |a| a.text.strip }
257
+ # budget=2 prefix + current → A, B + F
258
+ assert_equal [ "A", "B", "F" ], texts
259
+ end
260
+
261
+ def test_end_collapse_renders_auto_ellipsis_last
262
+ render_breadcrumb(long_items, max_items: 3, collapse_at: :end)
263
+ buttons = page.all("nav ol button")
264
+ assert_equal 1, buttons.size
265
+ assert_equal "More breadcrumb pages", buttons.last["aria-label"]
266
+ end
267
+
268
+ # Auto-ellipsis — no truncation needed
269
+
270
+ def test_no_truncation_when_items_within_max
271
+ render_breadcrumb(simple_items, max_items: 5, collapse_at: :middle)
272
+ links = page.all("nav ol a")
273
+ assert_equal 3, links.size
274
+ refute_selector "nav ol button"
275
+ end
276
+ end