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,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Velvetkit
4
+ # What it takes to use one component: what it is, how to add it, its
5
+ # arguments and slots, its notes and working examples.
6
+ #
7
+ # Read from the registry, so it can't drift from what `add` copies, and
8
+ # written in the app's namespace, so an AI agent reading it writes
9
+ # `Kit::Button` in an app that has Kit::Button. The docs site renders it as
10
+ # HTML; `velvetkit:ai` and llms.txt hand it to agents as Markdown.
11
+ class Reference
12
+ Example = Data.define(:key, :title, :source)
13
+
14
+ attr_reader :name, :namespace
15
+
16
+ def initialize(name, namespace: Namespace.new(Namespace::CANONICAL), registry: Velvetkit.registry)
17
+ @name = name
18
+ @namespace = namespace
19
+ @registry = registry
20
+ end
21
+
22
+ def title = name.humanize
23
+ def description = registry.description(name)
24
+ def add_command = "bin/rails generate velvetkit:add #{name}"
25
+
26
+ def class_name
27
+ constant = "#{namespace.name}::#{name.camelize}"
28
+ File.exist?(source_path) ? constant : "#{constant}::*"
29
+ end
30
+
31
+ # Other components `add` brings along.
32
+ def dependencies
33
+ registry.resolve([ name ]) - [ "base", name ]
34
+ end
35
+
36
+ def stimulus_controllers
37
+ registry.stimulus_controllers(name).map do |file|
38
+ "#{namespace.dom}--#{File.basename(file, "_controller.ts").dasherize}"
39
+ end
40
+ end
41
+
42
+ def packages = registry.packages(name) - [ "@hotwired/stimulus" ]
43
+ def gems = registry.gems(name).keys
44
+
45
+ # The comment above the class, where a component explains itself.
46
+ def notes
47
+ return unless File.exist?(source_path)
48
+
49
+ lines = File.readlines(source_path).drop_while { |line| !line.start_with?("#") || line.include?("frozen_string_literal") }
50
+ comment = lines.take_while { |line| line.start_with?("#") }.map { |line| line.delete_prefix("#").delete_prefix(" ") }
51
+ namespace.rewrite(comment.join).strip.presence
52
+ end
53
+
54
+ def signature
55
+ return unless File.exist?(source_path)
56
+
57
+ args = File.read(source_path)[/^ def initialize\((.*?)\)\n/m, 1]&.squish
58
+ args && "#{class_name}.new(#{args})"
59
+ end
60
+
61
+ def slots
62
+ files = Dir[File.join(registry.root, Registry::COMPONENTS, "#{name}{.rb,/**/*.rb}")]
63
+ files.flat_map do |file|
64
+ source = File.read(file)
65
+ declared = source.scan(/^\s*renders_(one|many) :(\w+)/).map { |kind, slot| "with_#{kind == "many" ? slot.singularize : slot}" }
66
+ declared + source.scan(/^\s*(?:alias_method :|def )(with_\w+)/).flatten
67
+ end.uniq
68
+ end
69
+
70
+ # Preview templates, in the order the preview class lists them.
71
+ # Playgrounds take Lookbook params, so they only work inside Lookbook.
72
+ def examples
73
+ labels = preview_labels
74
+ Dir[File.join(previews_root, "#{name}_preview", "[^_]*.html.erb")]
75
+ .map { |file| File.basename(file, ".html.erb") }
76
+ .reject { |key| key == "playground" }
77
+ .sort_by { |key| labels.keys.index(key) || labels.size }
78
+ .map { |key| Example.new(key, labels.fetch(key, key.humanize), namespace.rewrite(File.read(File.join(previews_root, "#{name}_preview", "#{key}.html.erb")))) }
79
+ end
80
+
81
+ def to_markdown
82
+ parts = [ "## #{title} (`#{class_name}`)", description ]
83
+ parts << "Add it: `#{add_command}`"
84
+ parts << "Also adds: #{dependencies.map { |d| "`#{d}`" }.join(", ")}" if dependencies.any?
85
+ parts << "Stimulus: #{stimulus_controllers.map { |c| "`#{c}`" }.join(", ")}" if stimulus_controllers.any?
86
+ parts << "Installs: #{(packages + gems).map { |p| "`#{p}`" }.join(", ")}" if (packages + gems).any?
87
+ parts << "### Arguments\n\n```ruby\n#{signature}\n```" if signature
88
+ parts << "Slots: #{slots.map { |s| "`#{s}`" }.join(", ")}" if slots.any?
89
+ parts << "### Notes\n\n#{notes}" if notes
90
+ examples.each { |example| parts << "### Example: #{example.title}\n\n```erb\n#{example.source.strip}\n```" }
91
+ parts.join("\n\n") + "\n"
92
+ end
93
+
94
+ private
95
+
96
+ attr_reader :registry
97
+
98
+ def source_path = File.join(registry.root, Registry::COMPONENTS, "#{name}.rb")
99
+ def previews_root = File.join(registry.root, Registry::PREVIEWS)
100
+
101
+ def preview_labels
102
+ preview = File.join(previews_root, "#{name}_preview.rb")
103
+ return {} unless File.exist?(preview)
104
+
105
+ File.read(preview).scan(/# @label (.+)\n(?:\s*#.*\n)*\s*def (\w+)/).to_h { |label, method| [ method, label.strip ] }
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "active_support/core_ext/string/inflections"
5
+
6
+ module Velvetkit
7
+ # The components the gem can copy into an app, and everything each one needs.
8
+ #
9
+ # Paths are relative to the registry root and written against the canonical
10
+ # `UI` namespace; Velvetkit::Namespace rewrites them on the way out.
11
+ class Registry
12
+ COMPONENTS = "app/components/ui"
13
+ CONTROLLERS = "app/javascript/controllers/ui"
14
+ TESTS = "test/components/ui"
15
+ PREVIEWS = "test/components/previews/ui"
16
+
17
+ # Installed by `velvetkit:install` rather than `velvetkit:add`.
18
+ FOUNDATION = %w[
19
+ config/locales/velvetkit.en.yml
20
+ app/assets/tailwind/velvetkit.css
21
+ app/javascript/velvetkit_icons.ts
22
+ ].freeze
23
+ FOUNDATION_PACKAGES = %w[@hotwired/stimulus lucide tw-animate-css].freeze
24
+
25
+ attr_reader :root
26
+
27
+ def initialize(root)
28
+ @root = root
29
+ @catalog = YAML.load_file(File.join(root, "components.yml"))
30
+ end
31
+
32
+ def names
33
+ @catalog.fetch("components").keys
34
+ end
35
+
36
+ def description(name)
37
+ fetch(name).fetch("description")
38
+ end
39
+
40
+ def package_version(package)
41
+ @catalog.fetch("packages").fetch(package) do
42
+ raise Error, "#{package} is imported but has no version in components.yml"
43
+ end
44
+ end
45
+
46
+ # The requested components plus every component they render, base first.
47
+ def resolve(requested)
48
+ requested.each { |name| fetch(name) }
49
+
50
+ resolved = []
51
+ visiting = Set.new
52
+ visit = lambda do |name|
53
+ # A component already placed, or one whose dependencies are being
54
+ # walked right now: two components may render each other.
55
+ next if resolved.include?(name) || !visiting.add?(name)
56
+
57
+ dependencies(name).each(&visit)
58
+ resolved << name
59
+ end
60
+ ([ "base" ] + requested).each(&visit)
61
+ resolved
62
+ end
63
+
64
+ def dependencies(name)
65
+ sources = component_files(name) + test_files(name)
66
+ (references(sources) - [ name ]).sort
67
+ end
68
+
69
+ # Ruby, templates and any extra files listed in the catalog.
70
+ def component_files(name)
71
+ files = glob("#{COMPONENTS}/#{name}.*", "#{COMPONENTS}/#{name}/**/*")
72
+ files + fetch(name).fetch("files", [])
73
+ end
74
+
75
+ def test_files(name)
76
+ glob("#{TESTS}/#{name}_test.rb", "#{TESTS}/#{name}/**/*")
77
+ end
78
+
79
+ def preview_files(name)
80
+ glob("#{PREVIEWS}/#{name}_preview.rb", "#{PREVIEWS}/#{name}_preview/**/*")
81
+ end
82
+
83
+ # Components a preview renders that are not in `installed`. A preview with
84
+ # any is skipped rather than shipped broken.
85
+ def preview_requires(name, installed)
86
+ references(preview_files(name)) - installed - [ name ]
87
+ end
88
+
89
+ # Stimulus controllers, their shared helpers and lazily-loaded entry points.
90
+ def javascript_files(name)
91
+ sources = component_files(name)
92
+ controllers = scan(sources, /\bui--([a-z]+(?:-[a-z]+)*)/).filter_map { |id| controller_for(id) }
93
+ entry_points = scan(sources, /asset_path\(["']([a-z_]+)\.js["']\)/).map { |entry| "app/javascript/#{entry}.ts" }
94
+ js = (controllers + entry_points).uniq
95
+ internals = scan(js, %r{from ['"]\./_internal/([a-z_]+)\.js['"]}).map { |f| "#{CONTROLLERS}/_internal/#{f}.ts" }
96
+ js + internals.uniq
97
+ end
98
+
99
+ def stimulus_controllers(name)
100
+ javascript_files(name).select { |f| f.start_with?(CONTROLLERS) && f.end_with?("_controller.ts") }
101
+ end
102
+
103
+ def packages(name)
104
+ scan(javascript_files(name), /(?:from|import)\s+['"]([^.'"][^'"]*)['"]/)
105
+ .map { |spec| spec.start_with?("@") ? spec.split("/").first(2).join("/") : spec.split("/").first }
106
+ .uniq.sort
107
+ end
108
+
109
+ # Gems the component needs, with the version to add to the Gemfile.
110
+ def gems(name)
111
+ fetch(name).fetch("gems", []).to_h { |gem| [ gem, @catalog.fetch("gems").fetch(gem) ] }
112
+ end
113
+
114
+ def read(file)
115
+ File.read(File.join(root, file))
116
+ end
117
+
118
+ private
119
+
120
+ def fetch(name)
121
+ @catalog.fetch("components").fetch(name.to_s) do
122
+ raise Error, "Unknown component #{name.inspect}. Available: #{names.join(', ')}"
123
+ end
124
+ end
125
+
126
+ # Components the files render. Comments don't count: a component's notes
127
+ # may point at another without needing it.
128
+ def references(files)
129
+ files.flat_map { |file| code(file).scan(/\bUI::([A-Z][A-Za-z0-9]*)/).flatten }
130
+ .map(&:underscore).select { |n| names.include?(n) }.uniq
131
+ end
132
+
133
+ def code(file)
134
+ read(file).gsub(/^\s*#.*$/, "").gsub(/<%#.*?%>/m, "")
135
+ end
136
+
137
+ # `ui--data-grid-url-value` names the data-grid controller; drop trailing
138
+ # words until what is left matches a controller file.
139
+ def controller_for(attribute)
140
+ words = attribute.split("-")
141
+ words.size.downto(1) do |n|
142
+ file = "#{CONTROLLERS}/#{words.first(n).join('_')}_controller.ts"
143
+ return file if exist?(file)
144
+ end
145
+ nil
146
+ end
147
+
148
+ def scan(files, pattern)
149
+ files.flat_map { |file| read(file).scan(pattern).flatten }.uniq
150
+ end
151
+
152
+ def glob(*patterns)
153
+ patterns.flat_map { |pattern| Dir.glob(pattern, base: root) }
154
+ .select { |f| File.file?(File.join(root, f)) }
155
+ .sort
156
+ end
157
+
158
+ def exist?(file)
159
+ File.exist?(File.join(root, file))
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Velvetkit
4
+ VERSION = "0.1.0.beta"
5
+ end
data/lib/velvetkit.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "velvetkit/version"
4
+ require "velvetkit/namespace"
5
+ require "velvetkit/registry"
6
+ require "velvetkit/reference"
7
+
8
+ # VelvetKit copies ViewComponent UI components into a Rails app, one at a time
9
+ # and under the namespace the app chooses. Nothing runs from the gem at
10
+ # runtime: once copied, the components are the app's own code.
11
+ module Velvetkit
12
+ class Error < StandardError; end
13
+
14
+ CONFIG_FILE = "config/velvetkit.yml"
15
+
16
+ def self.registry
17
+ @registry ||= Registry.new(File.expand_path("../registry", __dir__))
18
+ end
19
+
20
+ # Everything an AI agent needs to build UI in an app: the rules, then a
21
+ # reference for each component, all in the app's namespace.
22
+ def self.agent_guide(components, namespace: Namespace.new(Namespace::CANONICAL))
23
+ guide = File.read(File.join(registry.root, "docs/agents.md"))
24
+ .sub("{{available}}", (registry.names - [ "base" ]).join(", "))
25
+ references = (components - [ "base" ]).map { |name| Reference.new(name, namespace: namespace).to_markdown }
26
+ ([ namespace.rewrite(guide) ] + references).join("\n")
27
+ end
28
+ end
@@ -0,0 +1,342 @@
1
+ /* VelvetKit design tokens.
2
+ *
3
+ * Import this once from your Tailwind entry point, after Tailwind itself:
4
+ *
5
+ * @import "tailwindcss";
6
+ * @import "./velvetkit.css";
7
+ *
8
+ * Every component styles itself from these variables, so retheming the kit
9
+ * means changing the values below — never the components.
10
+ */
11
+ @import 'tw-animate-css';
12
+
13
+ @custom-variant dark (&:is(.dark *));
14
+
15
+ :root {
16
+ line-height: 1.5;
17
+ font-weight: 400;
18
+
19
+ color-scheme: light;
20
+
21
+ font-synthesis: none;
22
+ text-rendering: optimizeLegibility;
23
+ -webkit-font-smoothing: antialiased;
24
+ -moz-osx-font-smoothing: grayscale;
25
+ --radius: 0.65rem;
26
+ --background: oklch(1 0 0);
27
+ --foreground: oklch(0.141 0.005 285.823);
28
+ --card: oklch(1 0 0);
29
+ --card-foreground: oklch(0.141 0.005 285.823);
30
+ --popover: oklch(1 0 0);
31
+ --popover-foreground: oklch(0.141 0.005 285.823);
32
+ --primary: oklch(0.357 0.158 301.6); /* #4D1A7F, velvet */
33
+ --primary-bolder: oklch(0.42 0.17 301.6);
34
+ --primary-hovered: oklch(0.42 0.17 301.6);
35
+ --primary-foreground: oklch(0.985 0.008 301.6);
36
+ --secondary: oklch(0.967 0.001 286.375);
37
+ --secondary-foreground: oklch(0.21 0.006 285.885);
38
+ --muted: oklch(0.967 0.001 286.375);
39
+ --muted-foreground: oklch(0.53 0.016 285.938);
40
+ --accent: oklch(0.967 0.001 286.375);
41
+ --accent-foreground: oklch(0.21 0.006 285.885);
42
+ --accent-orange-subtle: oklch(0.8178 0.1249 59.29);
43
+ --accent-green-subtle: oklch(0.8968 0.1735 157.95);
44
+ --accent-yellow-subtle: oklch(0.9453 0.1627 104.19);
45
+ --accent-purple-subtle: oklch(0.634 0.1797 274.43);
46
+ --destructive: oklch(0.577 0.245 27.325);
47
+ --border: oklch(0.92 0.004 286.32);
48
+ --input: oklch(0.92 0.004 286.32);
49
+ --ring: var(--primary);
50
+ --chart-pie-active: oklch(0.9137 0.1511 150.96);
51
+ --chart-pie-inactive: oklch(0.3533 0.0096 164.42);
52
+ --chart-1: oklch(0.646 0.222 41.116);
53
+ --chart-2: oklch(0.6 0.118 184.704);
54
+ --chart-3: oklch(0.398 0.07 227.392);
55
+ --chart-4: oklch(0.828 0.189 84.429);
56
+ --chart-5: oklch(0.769 0.188 70.08);
57
+ --sidebar: oklch(0.985 0.001 286.32);
58
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
59
+ /* Accent for the active nav item; lighter in dark so it clears AA against
60
+ the dark panel, where --primary itself would only reach 3.08:1. */
61
+ --sidebar-primary: oklch(0.357 0.158 301.6);
62
+ --sidebar-primary-foreground: var(--primary-foreground);
63
+ --sidebar-accent: oklch(0.97 0.001 286.375);
64
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
65
+ --sidebar-border: oklch(0.922 0.004 286.32);
66
+ --sidebar-ring: var(--primary);
67
+ }
68
+
69
+ .dark {
70
+ /* Native scrollbars, date pickers and other UA widgets follow the theme. */
71
+ color-scheme: dark;
72
+
73
+ --background: oklch(0.141 0.005 285.823);
74
+ --foreground: oklch(0.985 0 0);
75
+ --card: oklch(0.21 0.006 285.885);
76
+ --card-foreground: oklch(0.985 0 0);
77
+ --popover: oklch(0.21 0.006 285.885);
78
+ --popover-foreground: oklch(0.985 0 0);
79
+ /* Velvet (hue 301.6), lifted from the light theme's #4D1A7F, which would
80
+ sit at 1.7:1 against this page. Here white text clears WCAG AA (5.2:1)
81
+ and the button still separates from the page (3.8:1). */
82
+ --primary: oklch(0.55 0.17 301.6);
83
+ --primary-foreground: oklch(0.985 0.008 301.6);
84
+ --secondary: oklch(0.274 0.006 286.033);
85
+ --secondary-foreground: oklch(0.985 0 0);
86
+ --muted: oklch(0.274 0.006 286.033);
87
+ --muted-foreground: oklch(0.705 0.015 286.067);
88
+ --accent: oklch(0.274 0.006 286.033);
89
+ --accent-foreground: oklch(0.985 0 0);
90
+ --destructive: oklch(0.704 0.191 22.216);
91
+ --border: oklch(1 0 0 / 10%);
92
+ --input: oklch(1 0 0 / 15%);
93
+ --ring: var(--primary);
94
+ --chart-pie-active: oklch(0.696 0.17 162.48);
95
+ --chart-pie-inactive: oklch(0.556 0 0);
96
+ --chart-1: oklch(0.488 0.243 264.376);
97
+ --chart-2: oklch(0.696 0.17 162.48);
98
+ --chart-3: oklch(0.769 0.188 70.08);
99
+ --chart-4: oklch(0.627 0.265 303.9);
100
+ --chart-5: oklch(0.645 0.246 16.439);
101
+ --sidebar: oklch(0.21 0.006 285.885);
102
+ --sidebar-foreground: oklch(0.985 0 0);
103
+ --sidebar-primary: oklch(0.7 0.15 301.6);
104
+ --sidebar-primary-foreground: var(--primary-foreground);
105
+ --sidebar-accent: oklch(0.274 0.006 286.033);
106
+ --sidebar-accent-foreground: oklch(0.985 0 0);
107
+ --sidebar-border: oklch(1 0 0 / 10%);
108
+ --sidebar-ring: var(--primary);
109
+ }
110
+
111
+ @theme inline {
112
+ --radius-sm: calc(var(--radius) - 4px);
113
+ --radius-md: calc(var(--radius) - 2px);
114
+ --radius-lg: var(--radius);
115
+ --radius-xl: calc(var(--radius) + 4px);
116
+ --color-background: var(--background);
117
+ --color-foreground: var(--foreground);
118
+ --color-card: var(--card);
119
+ --color-card-foreground: var(--card-foreground);
120
+ --color-popover: var(--popover);
121
+ --color-popover-foreground: var(--popover-foreground);
122
+ --color-primary: var(--primary);
123
+ --color-primary-foreground: var(--primary-foreground);
124
+ --color-secondary: var(--secondary);
125
+ --color-secondary-foreground: var(--secondary-foreground);
126
+ --color-muted: var(--muted);
127
+ --color-muted-foreground: var(--muted-foreground);
128
+ --color-accent: var(--accent);
129
+ --color-accent-foreground: var(--accent-foreground);
130
+ --color-destructive: var(--destructive);
131
+ --color-border: var(--border);
132
+ --color-input: var(--input);
133
+ --color-ring: var(--ring);
134
+ --chart-pie-active: oklch(0.696 0.17 162.48);
135
+ --chart-pie-inactive: oklch(0.556 0 0);
136
+ --color-chart-1: var(--chart-1);
137
+ --color-chart-2: var(--chart-2);
138
+ --color-chart-3: var(--chart-3);
139
+ --color-chart-4: var(--chart-4);
140
+ --color-chart-5: var(--chart-5);
141
+ --color-sidebar: var(--sidebar);
142
+ --color-sidebar-foreground: var(--sidebar-foreground);
143
+ --color-sidebar-primary: var(--sidebar-primary);
144
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
145
+ --color-sidebar-accent: var(--sidebar-accent);
146
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
147
+ --color-sidebar-border: var(--sidebar-border);
148
+ --color-sidebar-ring: var(--sidebar-ring);
149
+ }
150
+
151
+ @layer base {
152
+ * {
153
+ @apply border-border outline-ring/50;
154
+ /* A ring with an offset paints the gap in this colour. Tailwind's default
155
+ is white, which showed as a white line inside rings in dark mode. */
156
+ --tw-ring-offset-color: var(--background);
157
+ }
158
+ body {
159
+ @apply bg-background text-foreground;
160
+ }
161
+ }
162
+
163
+ ::selection {
164
+ background: var(--primary);
165
+ color: var(--primary-foreground);
166
+ }
167
+
168
+ /* Chrome paints autofilled fields with its own pale-blue background and
169
+ ignores `background-color`, which also left InputGroup two-tone: the fill
170
+ covered the input but not the addon. Clipping the UA background to the text
171
+ hides it without touching `box-shadow`, so Tailwind's focus ring (which is
172
+ a box-shadow) still works. The long transition defers the fill Chrome
173
+ re-applies on interaction. */
174
+ input:-webkit-autofill,
175
+ input:-webkit-autofill:hover,
176
+ input:-webkit-autofill:focus,
177
+ input:-webkit-autofill:active,
178
+ textarea:-webkit-autofill,
179
+ select:-webkit-autofill {
180
+ -webkit-background-clip: text;
181
+ background-clip: text;
182
+ -webkit-text-fill-color: var(--foreground);
183
+ caret-color: var(--foreground);
184
+ transition: background-color 100000s ease-in-out 0s;
185
+ }
186
+
187
+ /* The date picker calendar.
188
+ *
189
+ * Cally renders in a shadow root, so it is reachable only through the custom
190
+ * properties and ::part() names it publishes. All of them are pointed at the
191
+ * kit's tokens, which is what makes the calendar follow dark mode with no
192
+ * JavaScript.
193
+ *
194
+ * Layout: ‹ [May ▾] [2012 ▾] › in one header row. The selects sit in Cally's
195
+ * heading slot (see date_picker.html.erb), so its own heading and the month's
196
+ * heading would only repeat them.
197
+ */
198
+ calendar-date,
199
+ calendar-range {
200
+ --color-accent: var(--primary);
201
+ --color-text-on-accent: var(--primary-foreground);
202
+ font-family: inherit;
203
+ font-size: 0.875rem;
204
+ color: var(--popover-foreground);
205
+ }
206
+
207
+ calendar-date::part(header),
208
+ calendar-range::part(header) {
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: space-between;
212
+ gap: 0.25rem;
213
+ margin-bottom: 0.5rem;
214
+ }
215
+
216
+ /* The arrows: ghost icon buttons. */
217
+ calendar-date::part(button),
218
+ calendar-range::part(button) {
219
+ display: inline-grid;
220
+ place-items: center;
221
+ width: 2rem;
222
+ height: 2rem;
223
+ padding: 0;
224
+ border: 0;
225
+ border-radius: var(--radius-md);
226
+ background: transparent;
227
+ color: var(--muted-foreground);
228
+ cursor: pointer;
229
+ }
230
+
231
+ calendar-date::part(button):hover,
232
+ calendar-range::part(button):hover {
233
+ background: var(--accent);
234
+ color: var(--accent-foreground);
235
+ }
236
+
237
+ calendar-date::part(button):focus-visible,
238
+ calendar-range::part(button):focus-visible {
239
+ outline: 2px solid var(--ring);
240
+ outline-offset: 1px;
241
+ }
242
+
243
+ calendar-date::part(disabled),
244
+ calendar-range::part(disabled) {
245
+ opacity: 0.35;
246
+ cursor: default;
247
+ }
248
+
249
+ calendar-month::part(heading) {
250
+ display: none;
251
+ }
252
+
253
+ calendar-month::part(th) {
254
+ padding-bottom: 0.25rem;
255
+ font-size: 0.75rem;
256
+ font-weight: 500;
257
+ color: var(--muted-foreground);
258
+ }
259
+
260
+ /* Days. */
261
+ calendar-month::part(button) {
262
+ width: 2.25rem;
263
+ height: 2.25rem;
264
+ border: 0;
265
+ border-radius: var(--radius-md);
266
+ background: transparent;
267
+ color: var(--popover-foreground);
268
+ font-size: 0.875rem;
269
+ cursor: pointer;
270
+ }
271
+
272
+ calendar-month::part(button):hover {
273
+ background: var(--accent);
274
+ color: var(--accent-foreground);
275
+ }
276
+
277
+ calendar-month::part(button):focus-visible {
278
+ outline: 2px solid var(--ring);
279
+ outline-offset: 1px;
280
+ }
281
+
282
+ calendar-month::part(outside) {
283
+ color: var(--muted-foreground);
284
+ opacity: 0.6;
285
+ }
286
+
287
+ /* Today: a quiet tint rather than a hard box. */
288
+ calendar-month::part(today) {
289
+ outline: none;
290
+ background: var(--accent);
291
+ color: var(--primary);
292
+ font-weight: 600;
293
+ }
294
+
295
+ /* The chosen day, after today so it wins when they're the same. */
296
+ calendar-month::part(selected) {
297
+ background: var(--primary);
298
+ color: var(--primary-foreground);
299
+ font-weight: 600;
300
+ }
301
+
302
+ /* Month and year: the reason this library was chosen, since reaching a birth
303
+ year is one click rather than a hundred and seventy. Styled like the kit's
304
+ fields; the open list is the platform's own. */
305
+ calendar-select-month::part(select),
306
+ calendar-select-year::part(select) {
307
+ appearance: none;
308
+ height: 2rem;
309
+ padding: 0 1.75rem 0 0.625rem;
310
+ border: 1px solid var(--input);
311
+ border-radius: var(--radius-md);
312
+ background: var(--background)
313
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a8a93' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")
314
+ no-repeat right 0.5rem center / 0.875rem;
315
+ color: var(--foreground);
316
+ font: inherit;
317
+ font-size: 0.875rem;
318
+ font-weight: 500;
319
+ cursor: pointer;
320
+ }
321
+
322
+ calendar-select-month::part(select):hover,
323
+ calendar-select-year::part(select):hover {
324
+ background-color: var(--accent);
325
+ }
326
+
327
+ calendar-select-month::part(select):focus-visible,
328
+ calendar-select-year::part(select):focus-visible {
329
+ outline: 2px solid var(--ring);
330
+ outline-offset: 1px;
331
+ }
332
+
333
+ /* The "Month" and "Year" labels stay for screen readers only. */
334
+ calendar-select-month::part(label),
335
+ calendar-select-year::part(label) {
336
+ position: absolute;
337
+ width: 1px;
338
+ height: 1px;
339
+ overflow: hidden;
340
+ clip-path: inset(50%);
341
+ white-space: nowrap;
342
+ }
@@ -0,0 +1,5 @@
1
+ <%= tag.div(**html_options) do %>
2
+ <div class="overflow-hidden min-h-0">
3
+ <div class="pl-8 pb-4 text-sm"><%= content %></div>
4
+ </div>
5
+ <% end %>
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Accordion::Content < UI::Base
4
+ def initialize(trigger_id: nil, class: nil, **html_attrs)
5
+ @trigger_id = trigger_id
6
+ @user_class = binding.local_variable_get(:class)
7
+ @html_attrs = html_attrs
8
+ end
9
+
10
+ def wrapper_classes
11
+ cn(
12
+ "grid grid-rows-[0fr] transition-[grid-template-rows] duration-200",
13
+ "data-[state=open]:grid-rows-[1fr]",
14
+ @user_class
15
+ )
16
+ end
17
+
18
+ def content_id
19
+ @html_attrs[:id] || "ui-accordion-content-#{SecureRandom.hex(4)}"
20
+ end
21
+
22
+ def html_options
23
+ opts = @html_attrs.dup
24
+ opts[:id] = content_id
25
+ opts[:role] = "region"
26
+ opts[:class] = wrapper_classes
27
+ opts[:"aria-labelledby"] = @trigger_id if @trigger_id
28
+ opts[:data] = {
29
+ "ui--accordion-target": "content",
30
+ state: "closed"
31
+ }.merge(opts[:data] || {})
32
+ opts
33
+ end
34
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div(**html_options) do %><%= trigger if trigger? %><%= accordion_content if accordion_content? %><% end %>