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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9f176f98bff0013d64b619054845556e1323b6c285e374297238d9e13896c444
4
+ data.tar.gz: 37703180175c2b19f0630f92801e5831843531a97b1e1dfe3cd9f47f5995e6ad
5
+ SHA512:
6
+ metadata.gz: e4432fd886028e08a31f840bebc3e9128fa84f19da9ab4d8bb1f40e78f623580279228c248bd21f98e6d9e1fe56adaa951cdadc508958386b0db4cdd652d53c6
7
+ data.tar.gz: b2be00dac1a8d5e8682b0ae02dae3670511644c92703397ad153a8934b5843087c9dfb0c88c4ca58ed81b36dbb5b441ed6021d69db2a3a45060712bac14884e9
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0.beta — 2026-10-26
4
+
5
+ - First release of the component library.
6
+ - `velvetkit:install` and `velvetkit:add` generators, with a configurable namespace.
7
+ - 39 components, their Stimulus controllers, tests and Lookbook previews.
8
+ - `Select` is a styled dropdown (listbox, keyboard, type-ahead), with
9
+ `searchable:` and `multiple:`; it posts through a hidden native select, so
10
+ forms work unchanged. The browser's own control is now `NativeSelect`.
11
+ - `velvetkit:add --all`, and support for cssbundling-rails Tailwind entry points.
12
+ - Docs site for velvetkit.com in `site/`, built from the registry.
13
+ - `velvetkit:ai`, run by `install` and kept current by `add`: `docs/velvetkit.md`,
14
+ a Claude Code skill and an AGENTS.md section, so agents use the kit.
15
+ - Primary colour is velvet, `#4D1A7F`, lifted in dark mode to stay readable.
16
+ - Fix: `Icon` dropped its icon name when given `data:` attributes.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Rain Root
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # VelvetKit
2
+
3
+ Accessible UI components for Rails, built on [ViewComponent](https://viewcomponent.org),
4
+ Tailwind CSS 4 and Stimulus. Website: **https://velvetkit.com**
5
+
6
+ VelvetKit doesn't load a component library at runtime. A generator copies the
7
+ components you ask for, plus the ones they depend on, into your app. After
8
+ that they're your code, under whatever module name you choose, and you can
9
+ change them however you like.
10
+
11
+ ```bash
12
+ bin/rails generate velvetkit:add dialog
13
+ # create app/components/ui/base.rb
14
+ # create app/components/ui/icon.rb
15
+ # create app/components/ui/spinner.rb
16
+ # create app/components/ui/button.rb
17
+ # create app/components/ui/dialog.rb …
18
+ # create app/javascript/controllers/ui/dialog_controller.ts
19
+ ```
20
+
21
+ ## Requirements
22
+
23
+ - Rails 7.1+, Ruby 3.2+
24
+ - Tailwind CSS 4
25
+ - A JavaScript bundler that compiles TypeScript (Bun or esbuild through
26
+ `jsbundling-rails`). The Stimulus controllers are `.ts` files.
27
+
28
+ ## Install
29
+
30
+ ```ruby
31
+ # Gemfile
32
+ gem "velvetkit", group: :development
33
+ ```
34
+
35
+ ```bash
36
+ bundle install
37
+ bin/rails generate velvetkit:install # components under UI::
38
+ bin/rails generate velvetkit:install --namespace=Kit # or under Kit::
39
+ ```
40
+
41
+ `install` does the following:
42
+
43
+ - writes `config/velvetkit.yml` with the namespace
44
+ - adds `view_component` and `tailwind_merge` to the Gemfile
45
+ - adds an acronym inflection when the namespace needs one (`UI`, not `Kit`)
46
+ - copies the base component, design tokens (`app/assets/tailwind/velvetkit.css`),
47
+ translations (`config/locales/velvetkit.en.yml`) and Lucide icon setup
48
+ (`app/javascript/velvetkit_icons.ts`), and imports the CSS and icons from
49
+ your entry points
50
+ - installs `@hotwired/stimulus`, `lucide` and `tw-animate-css` with your
51
+ package manager, which it picks from your lockfile
52
+
53
+ ## Add components
54
+
55
+ ```bash
56
+ bin/rails generate velvetkit:add # list what's available
57
+ bin/rails generate velvetkit:add date_picker menu # add these and what they use
58
+ ```
59
+
60
+ `add` does the following for each component:
61
+
62
+ - copies the component, its templates and sub-components
63
+ - copies its Stimulus controllers and any lazily-loaded entry points (AG Grid
64
+ for `data_grid`, Cally for `date_picker`)
65
+ - copies its Minitest tests and Lookbook previews. A preview that shows
66
+ components you haven't added is skipped, and `add` tells you which.
67
+ - registers its controllers in `app/javascript/controllers/index.js` when
68
+ that file lists controllers one by one
69
+ - installs any npm packages it needs, and adds any gems it needs (`pagy` for
70
+ `pagination`) to the Gemfile
71
+
72
+ Options: `--namespace=Kit`, `--skip-tests`, `--skip-previews`,
73
+ `--skip-packages` (list the packages instead of installing them), and the usual
74
+ `--force`/`--skip` for files that already exist.
75
+
76
+ ## AI agents
77
+
78
+ `install` also writes three files so coding agents use your components
79
+ instead of inventing new ones:
80
+
81
+ - `docs/velvetkit.md`: the rules for building UI with the kit, then every
82
+ installed component's arguments, slots, notes and working examples, all in
83
+ your namespace. `add` rewrites it whenever you add components.
84
+ - `.claude/skills/velvetkit/SKILL.md`: a Claude Code skill pointing at it.
85
+ - A short section in `AGENTS.md` pointing at it, for Codex, Cursor and others.
86
+ Your own notes in that file are kept.
87
+
88
+ Run `bin/rails generate velvetkit:ai` to regenerate them, or install with
89
+ `--skip-ai` to leave them out. velvetkit.com also serves
90
+ [llms.txt](https://velvetkit.com/llms.txt), and every component page has a
91
+ Markdown version.
92
+
93
+ ## Namespacing
94
+
95
+ Every component is written once, against `UI`. The generators rewrite each file
96
+ as they copy it, so with `--namespace=Kit`:
97
+
98
+ | In the registry | In your app |
99
+ | ---------------------------- | ----------------------------- |
100
+ | `UI::Button` | `Kit::Button` |
101
+ | `app/components/ui/` | `app/components/kit/` |
102
+ | `data-controller="ui--menu"` | `data-controller="kit--menu"` |
103
+ | `t("ui.dialog.close_label")` | `t("kit.dialog.close_label")` |
104
+ | `"ui-dialog-#{id}"` | `"kit-dialog-#{id}"` |
105
+
106
+ The namespace must be a single constant name, like `UI`, `Kit` or `VelvetKit`.
107
+
108
+ ## Theming
109
+
110
+ Components style themselves from the CSS variables in `velvetkit.css`
111
+ (`--primary`, `--radius`, `--sidebar`, …), with a `.dark` set alongside. To
112
+ retheme, change those values. Don't change the components.
113
+
114
+ For dark mode without a flash on load, add `theme_toggle` and render its
115
+ partial in your layout's `<head>`:
116
+
117
+ ```erb
118
+ <%= render "layouts/theme_script" %>
119
+ ```
120
+
121
+ ## Developing VelvetKit
122
+
123
+ ```bash
124
+ bundle install
125
+ bundle exec rake test:gem # namespace rewriting, registry, generators
126
+ bundle exec rake test:components # every component, from the registry, as UI::
127
+ bundle exec rake test:namespaced # every component copied out as Kit::, then tested
128
+ ```
129
+
130
+ `registry/` holds the components, laid out like a Rails app.
131
+ `registry/components.yml` holds only what the files can't say themselves
132
+ (descriptions, package and gem versions). Dependencies, Stimulus controllers
133
+ and npm imports are read from the source, so they can't drift.
134
+
135
+ ## The docs site (velvetkit.com)
136
+
137
+ `site/` is a Rails app that serves https://velvetkit.com. It installs the kit
138
+ from `registry/` the way any app would, with `bin/sync-kit` (it runs
139
+ `velvetkit:install` and `velvetkit:add --all`). The copied files are
140
+ gitignored, so the registry is the only source.
141
+
142
+ The pages build themselves from the registry:
143
+
144
+ - each component page renders that component's Lookbook preview templates
145
+ live, next to their source
146
+ - the "Comes with" list comes from the same dependency scan `add` uses
147
+ - the arguments and slots are read from the component's Ruby
148
+
149
+ To document a component, write or improve its preview. The site picks it up.
150
+
151
+ ```bash
152
+ cd site
153
+ bun install && bundle install
154
+ bin/dev # sync the kit, then run Rails, JS and CSS watchers
155
+ bin/rails test # every page renders, every live example, SEO and AI endpoints
156
+ bin/ux-check # real clicks, keys and clipboard in headless Chrome (site on :3000)
157
+ ```
158
+
159
+ ### Deploying
160
+
161
+ The image is built from the `Dockerfile` at the repository root. It needs
162
+ the gem and the registry as well as `site/`.
163
+
164
+ **Dokploy:** create an Application from this repository with build type
165
+ **Dockerfile**. The defaults fit: Dockerfile path `Dockerfile`, context `.`.
166
+ Then:
167
+
168
+ - Environment:
169
+ - `SECRET_KEY_BASE`, required: generate one with `openssl rand -hex 64`.
170
+ - `SITE_URL`, if the site isn't served at https://velvetkit.com.
171
+ - Domains: `velvetkit.com` on container port **80**, with HTTPS on. Traefik
172
+ terminates TLS and the app trusts it (`assume_ssl`).
173
+ - Health check: the image declares one against `GET /up`.
174
+
175
+ **Kamal:** fill in the TODOs in `site/config/deploy.yml`, then run
176
+ `bin/kamal deploy` from `site/`. It uses the same Dockerfile.
177
+
178
+ **Anywhere else:**
179
+
180
+ ```bash
181
+ docker build -t velvetkit-site .
182
+ docker run -p 8080:80 -e SECRET_KEY_BASE=$(openssl rand -hex 64) -e FORCE_SSL=false velvetkit-site
183
+ ```
184
+
185
+ ### SEO and AI
186
+
187
+ Every page has its own title, description, canonical URL and Open Graph
188
+ and Twitter cards, using `public/og.png`. The home page carries JSON-LD for
189
+ `SoftwareApplication` and `WebSite`; docs pages carry `TechArticle` and
190
+ `BreadcrumbList`. `/sitemap.xml` and `/robots.txt` are built from the
191
+ routes, and robots explicitly welcomes AI crawlers and points them at
192
+ `/llms.txt`. Component pages link their Markdown version with
193
+ `rel="alternate"`.
194
+
195
+ ## License
196
+
197
+ MIT
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "json"
5
+ require_relative "../helpers"
6
+
7
+ module Velvetkit
8
+ module Generators
9
+ class AddGenerator < Rails::Generators::Base
10
+ include Helpers
11
+
12
+ desc <<~DESC
13
+ Copy VelvetKit components into the app, with every component they use.
14
+
15
+ Run with no components to list what is available.
16
+
17
+ bin/rails generate velvetkit:add dialog date_picker
18
+ DESC
19
+
20
+ argument :components, type: :array, default: [], banner: "component component ..."
21
+
22
+ class_option :namespace, type: :string,
23
+ desc: "Module for the components (default: #{CONFIG_FILE}, else UI)"
24
+ class_option :all, type: :boolean, default: false, desc: "Add every component"
25
+ class_option :skip_tests, type: :boolean, default: false, desc: "Don't copy component tests"
26
+ class_option :skip_previews, type: :boolean, default: false, desc: "Don't copy Lookbook previews"
27
+ class_option :skip_packages, type: :boolean, default: false,
28
+ desc: "List the npm packages to add instead of installing them"
29
+
30
+ def list_components
31
+ return if requested.any?
32
+
33
+ say "Usage: bin/rails generate velvetkit:add COMPONENT [COMPONENT ...]\n\n"
34
+ registry.names.each { |name| say format(" %-16s %s", name, registry.description(name)) }
35
+ end
36
+
37
+ def copy_components
38
+ resolved.each do |name|
39
+ files = registry.component_files(name) + registry.javascript_files(name)
40
+ files += registry.test_files(name) unless options[:skip_tests]
41
+ files.uniq.each { |file| copy_from_registry(file) }
42
+ end
43
+ end
44
+
45
+ def copy_previews
46
+ return if options[:skip_previews]
47
+
48
+ resolved.each do |name|
49
+ needs = registry.preview_requires(name, resolved)
50
+ if needs.any?
51
+ say_status :skip, "#{name} preview, it also shows #{needs.join(', ')}", :yellow
52
+ else
53
+ registry.preview_files(name).each { |file| copy_from_registry(file) }
54
+ end
55
+ end
56
+ end
57
+
58
+ # Apps that list controllers in a manifest (jsbundling with
59
+ # stimulus:manifest:update) need each one registered; apps that load the
60
+ # directory (importmap, eagerLoadControllersFrom) need nothing.
61
+ def register_stimulus_controllers
62
+ manifest = File.join(destination_root, "app/javascript/controllers/index.js")
63
+ return unless File.exist?(manifest) && !File.read(manifest).match?(/(eager|lazy)LoadControllersFrom/)
64
+
65
+ controllers.each do |file|
66
+ path = kit_namespace.rewrite_path(file).delete_prefix("app/javascript/controllers/").delete_suffix("_controller.ts")
67
+ identifier = path.tr("_", "-").gsub("/", "--")
68
+ next if File.read(manifest).match?(/["']#{Regexp.escape(identifier)}["']/)
69
+
70
+ constant = "#{path.camelize.gsub('::', '__')}Controller"
71
+ append_to_file "app/javascript/controllers/index.js", <<~JS
72
+ \nimport #{constant} from "./#{path}_controller"
73
+ application.register("#{identifier}", #{constant})
74
+ JS
75
+ end
76
+ end
77
+
78
+ def add_gems
79
+ gemfile = File.read(File.join(destination_root, "Gemfile"))
80
+ resolved.flat_map { |name| registry.gems(name).to_a }.uniq.each do |name, version|
81
+ gem name, version unless gemfile.match?(/gem ["']#{name}["']/)
82
+ end
83
+ end
84
+
85
+ def add_packages
86
+ install_packages(resolved.flat_map { |name| registry.packages(name) }.uniq)
87
+ end
88
+
89
+ # Keep the agents' reference listing what is installed now.
90
+ def refresh_ai_reference
91
+ return if resolved.empty? || !File.exist?(File.join(destination_root, "docs/velvetkit.md"))
92
+
93
+ invoke "velvetkit:ai", [], namespace: kit_namespace.name
94
+ end
95
+
96
+ private
97
+
98
+ def resolved
99
+ @resolved ||= requested.empty? ? [] : registry.resolve(requested)
100
+ end
101
+
102
+ def requested
103
+ options[:all] ? registry.names : components
104
+ end
105
+
106
+ def controllers
107
+ resolved.flat_map { |name| registry.stimulus_controllers(name) }.uniq
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require_relative "../helpers"
5
+
6
+ module Velvetkit
7
+ module Generators
8
+ # Tells AI coding agents how to build UI in this app with its components.
9
+ #
10
+ # Writes docs/velvetkit.md (the rules, then a reference for each installed
11
+ # component, in the app's namespace) and points agents at it: a Claude
12
+ # Code skill, and a section in AGENTS.md for Codex, Cursor and the rest.
13
+ # `add` runs this again whenever the reference exists, so it stays current.
14
+ class AiGenerator < Rails::Generators::Base
15
+ include Helpers
16
+
17
+ REFERENCE = "docs/velvetkit.md"
18
+ SKILL = ".claude/skills/velvetkit/SKILL.md"
19
+ MARKER = "<!-- velvetkit -->"
20
+
21
+ desc <<~DESC
22
+ Write docs/velvetkit.md for AI agents, describing the components
23
+ installed in this app, and point Claude Code and AGENTS.md at it.
24
+ DESC
25
+
26
+ class_option :namespace, type: :string,
27
+ desc: "Module the components are under (default: #{CONFIG_FILE}, else UI)"
28
+
29
+ def write_reference
30
+ create_file REFERENCE, Velvetkit.agent_guide(installed, namespace: kit_namespace), force: true
31
+ end
32
+
33
+ def write_claude_skill
34
+ create_file SKILL, <<~MARKDOWN, force: true
35
+ ---
36
+ name: velvetkit
37
+ description: Build Rails UI with this app's #{kit_namespace.name}:: components (VelvetKit ViewComponents). Use when writing or changing views, forms, layouts, pages or ViewComponents.
38
+ ---
39
+
40
+ This app's UI is built from VelvetKit components under `#{kit_namespace.name}::`.
41
+ Before writing or changing any view, read `#{REFERENCE}`: it has the rules
42
+ and every installed component's arguments, slots and working examples.
43
+
44
+ Need a component that isn't installed? Run `bin/rails generate velvetkit:add <name>`
45
+ instead of writing one.
46
+ MARKDOWN
47
+ end
48
+
49
+ def point_agents_md_at_reference
50
+ section = <<~MARKDOWN
51
+ #{MARKER}
52
+ ## UI components
53
+
54
+ Build UI from the app's VelvetKit components (`#{kit_namespace.name}::Button`,
55
+ `#{kit_namespace.name}::Dialog`, …). Read `#{REFERENCE}` before writing views: it lists
56
+ the rules and every installed component with working examples.
57
+ MARKDOWN
58
+
59
+ agents = File.join(destination_root, "AGENTS.md")
60
+ if !File.exist?(agents)
61
+ create_file "AGENTS.md", "# Agent notes\n\n#{section}"
62
+ elsif !File.read(agents).include?(MARKER)
63
+ append_to_file "AGENTS.md", "\n#{section}"
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Components present in the app, matched against the registry.
70
+ def installed
71
+ folder = File.join(destination_root, "app/components", kit_namespace.path)
72
+ present = Dir.glob("*.rb", base: folder).map { |file| File.basename(file, ".rb") } +
73
+ Dir.glob("*/", base: folder).map { |dir| dir.chomp("/") }
74
+ registry.names & present
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "velvetkit"
4
+
5
+ module Velvetkit
6
+ module Generators
7
+ # What the install and add generators share: where the registry is, which
8
+ # namespace the app uses, and how a file gets from one to the other.
9
+ module Helpers
10
+ LOCKFILES = {
11
+ "bun.lock" => "bun add",
12
+ "bun.lockb" => "bun add",
13
+ "yarn.lock" => "yarn add",
14
+ "pnpm-lock.yaml" => "pnpm add",
15
+ "package-lock.json" => "npm install"
16
+ }.freeze
17
+
18
+ private
19
+
20
+ def registry
21
+ Velvetkit.registry
22
+ end
23
+
24
+ # --namespace, else config/velvetkit.yml, else UI.
25
+ def kit_namespace
26
+ @kit_namespace ||= Namespace.new(options[:namespace] || configured_namespace || Namespace::CANONICAL)
27
+ end
28
+
29
+ def configured_namespace
30
+ config = File.join(destination_root, CONFIG_FILE)
31
+ YAML.load_file(config)&.fetch("namespace", nil) if File.exist?(config)
32
+ end
33
+
34
+ def copy_from_registry(file)
35
+ create_file kit_namespace.rewrite_path(file), kit_namespace.rewrite(registry.read(file))
36
+ end
37
+
38
+ def install_packages(packages)
39
+ package_json = File.join(destination_root, "package.json")
40
+ missing = packages - installed_packages(package_json)
41
+ return if missing.empty?
42
+
43
+ specs = missing.map { |package| "#{package}@#{registry.package_version(package)}" }
44
+ if options[:skip_packages] || !File.exist?(package_json)
45
+ say_status :packages, "add these to package.json: #{specs.join(' ')}", :yellow
46
+ else
47
+ run "#{package_manager} #{specs.join(' ')}"
48
+ end
49
+ end
50
+
51
+ def installed_packages(package_json)
52
+ return [] unless File.exist?(package_json)
53
+
54
+ json = JSON.parse(File.read(package_json))
55
+ json.fetch("dependencies", {}).keys + json.fetch("devDependencies", {}).keys
56
+ end
57
+
58
+ def package_manager
59
+ LOCKFILES.each { |lockfile, command| return command if File.exist?(File.join(destination_root, lockfile)) }
60
+ "npm install"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "json"
5
+ require_relative "../helpers"
6
+
7
+ module Velvetkit
8
+ module Generators
9
+ class InstallGenerator < Rails::Generators::Base
10
+ include Helpers
11
+
12
+ desc <<~DESC
13
+ Set the app up for VelvetKit: choose the namespace, and add the base
14
+ component, design tokens, translations and icons every component uses.
15
+
16
+ bin/rails generate velvetkit:install --namespace=Kit
17
+ DESC
18
+
19
+ class_option :namespace, type: :string, default: Namespace::CANONICAL,
20
+ desc: "Module to put the components under"
21
+ class_option :skip_tests, type: :boolean, default: false, desc: "Don't copy the base component's test"
22
+ class_option :skip_packages, type: :boolean, default: false,
23
+ desc: "List the npm packages to add instead of installing them"
24
+ class_option :skip_ai, type: :boolean, default: false,
25
+ desc: "Don't write docs/velvetkit.md and the pointers to it for AI agents"
26
+
27
+ def write_config
28
+ create_file CONFIG_FILE, <<~YAML
29
+ # VelvetKit — https://velvetkit.com
30
+ # `bin/rails generate velvetkit:add` puts components under this module.
31
+ namespace: #{kit_namespace.name}
32
+ YAML
33
+ end
34
+
35
+ def add_gems
36
+ gemfile = File.read(File.join(destination_root, "Gemfile"))
37
+ gem "view_component" unless gemfile.match?(/gem ["']view_component["']/)
38
+ gem "tailwind_merge" unless gemfile.match?(/gem ["']tailwind_merge["']/)
39
+ end
40
+
41
+ # Zeitwerk maps app/components/ui to Ui unless told UI is an acronym.
42
+ def add_inflection
43
+ return unless kit_namespace.acronym?
44
+
45
+ inflections = "config/initializers/inflections.rb"
46
+ rule = %(inflect.acronym "#{kit_namespace.name}")
47
+ block = <<~RUBY
48
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
49
+ #{rule}
50
+ end
51
+ RUBY
52
+ return create_file(inflections, block) unless exists?(inflections)
53
+ return if read(inflections).include?(rule)
54
+
55
+ append_to_file inflections, "\n#{block}"
56
+ end
57
+
58
+ def copy_foundation
59
+ Registry::FOUNDATION.each { |file| copy_from_registry(file) }
60
+ end
61
+
62
+ # tailwindcss-rails and cssbundling-rails keep the entry point in
63
+ # different places; the tokens always live in app/assets/tailwind.
64
+ TAILWIND_ENTRIES = {
65
+ "app/assets/tailwind/application.css" => "./velvetkit.css",
66
+ "app/assets/stylesheets/application.tailwind.css" => "../tailwind/velvetkit.css"
67
+ }.freeze
68
+
69
+ def import_stylesheet
70
+ entry, path = TAILWIND_ENTRIES.find { |file, _| exists?(file) }
71
+ return say_status(:css, "import velvetkit.css from your Tailwind entry point", :yellow) unless entry
72
+ return if read(entry).include?("velvetkit.css")
73
+
74
+ inject_into_file entry, %(@import "#{path}";\n), after: /@import ["']tailwindcss["'];?\n/
75
+ end
76
+
77
+ def import_icons
78
+ entry = %w[app/javascript/application.js app/javascript/application.ts].find { |file| exists?(file) }
79
+ return say_status(:js, "import ./velvetkit_icons from your JavaScript entry point", :yellow) unless entry
80
+ return if read(entry).include?("velvetkit_icons")
81
+
82
+ append_to_file entry, %(import "./velvetkit_icons"\n)
83
+ end
84
+
85
+ def add_base_component
86
+ invoke "velvetkit:add", [ "base" ], namespace: kit_namespace.name, skip_tests: options[:skip_tests], skip_packages: true
87
+ end
88
+
89
+ def add_packages
90
+ install_packages(Registry::FOUNDATION_PACKAGES)
91
+ end
92
+
93
+ def write_ai_reference
94
+ invoke "velvetkit:ai", [], namespace: kit_namespace.name unless options[:skip_ai]
95
+ end
96
+
97
+ private
98
+
99
+ def exists?(file)
100
+ File.exist?(File.join(destination_root, file))
101
+ end
102
+
103
+ def read(file)
104
+ File.read(File.join(destination_root, file))
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string"
4
+ require "active_support/core_ext/object/blank"
5
+
6
+ module Velvetkit
7
+ # The Ruby module the components live under in the host app.
8
+ #
9
+ # The registry is written once, against `UI`. Every file is rewritten on the
10
+ # way into the host, so `Kit` turns `UI::Button` into `Kit::Button`,
11
+ # `app/components/ui/` into `app/components/kit/`, the `ui--dialog` Stimulus
12
+ # identifier into `kit--dialog`, and the `ui.` I18n scope into `kit.`.
13
+ class Namespace
14
+ CANONICAL = "UI"
15
+ FORMAT = /\A[A-Z][A-Za-z0-9]*\z/
16
+
17
+ attr_reader :name
18
+
19
+ def initialize(name)
20
+ @name = name.to_s
21
+ return if FORMAT.match?(@name)
22
+
23
+ raise Error, "Namespace must be a single Ruby constant name like UI or Kit, got #{name.inspect}"
24
+ end
25
+
26
+ # True when Rails cannot get from the directory name back to the constant
27
+ # without an inflection: `ui`.camelize is `Ui`, not `UI`.
28
+ def acronym?
29
+ name.underscore.camelize != name
30
+ end
31
+
32
+ # Directory name and I18n scope: UI → ui, VelvetKit → velvet_kit.
33
+ def path
34
+ acronym? ? name.downcase : name.underscore
35
+ end
36
+
37
+ # Prefix for Stimulus identifiers and DOM ids: VelvetKit → velvet-kit.
38
+ def dom
39
+ path.dasherize
40
+ end
41
+
42
+ def rewrite_path(file)
43
+ file.gsub(%r{(?<=\A|/)ui(?=/)}, path)
44
+ end
45
+
46
+ def rewrite(source)
47
+ source
48
+ .gsub(/\bUI(?=::)/, name) # UI::Button
49
+ .gsub(/\bmodule UI\b/, "module #{name}") # module UI; end
50
+ .gsub("ui--", "#{dom}--") # Stimulus identifiers
51
+ .gsub(/(?<=["'`])ui-(?=[a-z`])/, "#{dom}-") # DOM ids, storage keys
52
+ .gsub(/(?<=["'`])ui\.(?=[a-z`])/, "#{path}.") # I18n keys
53
+ .gsub(/^(\s+)ui:$/, "\\1#{path}:") # locale file scope
54
+ .gsub(%r{(?<=/)ui(?=/)}, path) # controllers/ui/… paths
55
+ end
56
+ end
57
+ end