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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Popover < UI::Base
4
+ renders_one :trigger, ->(**opts) {
5
+ UI::Popover::Trigger.new(**opts).tap { |t| t.set_popover_context(self) }
6
+ }
7
+ renders_one :popover_content, ->(**opts) {
8
+ UI::Popover::Content.new(**opts).tap { |c| c.set_popover_context(self) }
9
+ }
10
+ # Alias for more natural API: `with_content` instead of `with_popover_content`
11
+ alias_method :with_content, :with_popover_content
12
+
13
+ def initialize(class: nil, **html_attrs)
14
+ @user_class = binding.local_variable_get(:class)
15
+ @html_attrs = html_attrs
16
+ @id = @html_attrs[:id] || "ui-popover-#{SecureRandom.hex(4)}"
17
+ end
18
+
19
+ attr_reader :id
20
+
21
+ def root_classes
22
+ cn("inline-block", @user_class)
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ <% if label.present? %>
2
+ <%= tag.label for: id, class: wrapper_classes do %>
3
+ <%= tag.span class: "relative inline-flex" do %>
4
+ <%= tag.input(**input_options) %>
5
+ <%= tag.span class: dot_classes do %>
6
+ <%= tag.span class: dot_size_class + " rounded-full bg-primary-foreground" %>
7
+ <% end %>
8
+ <% end %>
9
+ <%= tag.span class: label_text_classes do %><%= label %><% if required? %><%= tag.span(" *", class: "text-destructive ml-0.5") %><% end %><% end %>
10
+ <% end %>
11
+ <% else %>
12
+ <%= tag.span class: "relative inline-flex" do %>
13
+ <%= tag.input(**input_options) %>
14
+ <%= tag.span class: dot_classes do %>
15
+ <%= tag.span class: dot_size_class + " rounded-full bg-primary-foreground" %>
16
+ <% end %>
17
+ <% end %>
18
+ <% end %>
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::RadioGroup::Item < UI::Base
4
+ variants(
5
+ base: "peer appearance-none shrink-0 rounded-full border border-input bg-background " \
6
+ "transition-all " \
7
+ "checked:bg-primary checked:border-primary " \
8
+ "focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1 " \
9
+ "ring-offset-background",
10
+ variants: {
11
+ size: {
12
+ sm: "w-3.5 h-3.5",
13
+ default: "w-4 h-4",
14
+ lg: "w-5 h-5"
15
+ }
16
+ },
17
+ defaults: { size: :default }
18
+ )
19
+
20
+ DOT_SIZE = { sm: "w-2 h-2", default: "w-2.5 h-2.5", lg: "w-3.5 h-3.5" }.freeze
21
+
22
+ def initialize(value:, label: nil, disabled: false, size: nil,
23
+ class: nil, **html_attrs)
24
+ @value = value
25
+ @label_text = label
26
+ @disabled = disabled
27
+ @size = size
28
+ @user_class = binding.local_variable_get(:class)
29
+ @html_attrs = html_attrs
30
+ @id = @html_attrs[:id] || "ui-rg-item-#{SecureRandom.hex(4)}"
31
+ @html_attrs[:id] = @id
32
+ end
33
+
34
+ attr_reader :value
35
+
36
+ def label; @label_text; end
37
+ def id; @id; end
38
+ def disabled?; @disabled; end
39
+ def required?; @group_required; end
40
+
41
+ def input_options
42
+ opts = @html_attrs.dup
43
+ opts[:type] = :radio
44
+ opts[:value] = @value
45
+ opts[:checked] = true if @group_value == @value.to_s
46
+ opts[:disabled] = true if @disabled || @group_disabled
47
+ opts[:required] = true if @group_required
48
+ opts[:name] = @group_name
49
+ opts[:class] = classes
50
+ opts
51
+ end
52
+
53
+ def classes
54
+ cn(variant_classes(size: @size || :default), @user_class)
55
+ end
56
+
57
+ def dot_classes
58
+ cn(
59
+ "pointer-events-none absolute inset-0 flex items-center justify-center",
60
+ "opacity-0 peer-checked:opacity-100 transition-opacity"
61
+ )
62
+ end
63
+
64
+ def dot_size_class
65
+ DOT_SIZE[@size || :default]
66
+ end
67
+
68
+ def wrapper_classes
69
+ cn(
70
+ "inline-flex items-center gap-2 select-none",
71
+ { "cursor-not-allowed opacity-60" => @disabled || @group_disabled }
72
+ )
73
+ end
74
+
75
+ def label_text_classes
76
+ cn(
77
+ "text-sm leading-none",
78
+ { "text-muted-foreground" => @disabled || @group_disabled }
79
+ )
80
+ end
81
+
82
+ def checked?
83
+ @group_value == @value.to_s
84
+ end
85
+
86
+ def set_group_context(group)
87
+ @group_name = group.name
88
+ @group_value = group.value
89
+ @group_required = group.required?
90
+ @group_disabled = group.disabled?
91
+ @size ||= group.size
92
+ end
93
+
94
+ def render?
95
+ @group_name.present?
96
+ end
97
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div role: "radiogroup", id: group_id, class: classes, **@html_attrs.except(:id) do %><% items.each do |item| %><%= render item %><% end %><% end %>
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::RadioGroup < UI::Base
4
+ renders_many :items, ->(value:, **opts) {
5
+ item = UI::RadioGroup::Item.new(value: value, **opts)
6
+ item.set_group_context(self)
7
+ item
8
+ }
9
+
10
+ def initialize(name: nil, value: nil, size: :default,
11
+ required: false, disabled: false,
12
+ class: nil, **html_attrs)
13
+ @name = name
14
+ @value = value
15
+ @size = size
16
+ @required = required
17
+ @disabled = disabled
18
+ @user_class = binding.local_variable_get(:class)
19
+ @html_attrs = html_attrs
20
+ end
21
+
22
+ def classes
23
+ cn("flex flex-col gap-2", @user_class)
24
+ end
25
+
26
+ def group_id
27
+ @html_attrs[:id] || "ui-rg-#{SecureRandom.hex(4)}"
28
+ end
29
+
30
+ def required?; @required; end
31
+ def disabled?; @disabled; end
32
+ def name; @name; end
33
+ def value; @value; end
34
+ def size; @size; end
35
+ end
@@ -0,0 +1,60 @@
1
+ <%= tag.div(class: "relative w-full", data: root_data) do %>
2
+ <% if render_label? %>
3
+ <div class="mb-2"><%= render(label_component) { label_text } %></div>
4
+ <% end %>
5
+
6
+ <%= tag.button(**trigger_options) do %>
7
+ <span data-ui--select-target="value" class="<%= placeholder_shown? ? "text-muted-foreground" : "" %> truncate"><%= current_label %></span>
8
+ <%= render(UI::Icon.new(name: "chevrons-up-down", size: :sm, class: "shrink-0 text-muted-foreground")) %>
9
+ <% end %>
10
+
11
+ <%= tag.div(hidden: true, data: { "ui--select-target": "panel", state: "closed" },
12
+ class: "z-50 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md " \
13
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95") do %>
14
+ <% if searchable? %>
15
+ <div class="flex items-center gap-2 border-b px-3 py-2">
16
+ <%= render(UI::Icon.new(name: "search", size: :sm, class: "shrink-0 text-muted-foreground")) %>
17
+ <%= tag.input(type: "search", id: search_id, autocomplete: "off",
18
+ placeholder: t("ui.select.search", default: "Search"),
19
+ class: "w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground [&::-webkit-search-cancel-button]:hidden",
20
+ aria: { label: t("ui.select.search_label", default: "Search the options"), controls: listbox_id, autocomplete: "list", expanded: "true" },
21
+ role: "combobox",
22
+ data: { "ui--select-target": "search",
23
+ action: "input->ui--select#filter keydown->ui--select#keydown" }) %>
24
+ </div>
25
+ <% end %>
26
+
27
+ <%= tag.div(id: listbox_id, role: "listbox", tabindex: -1,
28
+ aria: { labelledby: (render_label? ? trigger_id : nil), multiselectable: (multiple? ? "true" : nil) },
29
+ data: { "ui--select-target": "listbox", action: "mousedown->ui--select#keepFocus" },
30
+ class: "max-h-64 overflow-y-auto p-1") do %>
31
+ <% listbox_options.each_with_index do |(label, value), index| %>
32
+ <%= tag.div(id: option_id(index), role: "option", aria: { selected: selected?(value).to_s },
33
+ data: { value: value, "ui--select-target": "option",
34
+ action: "click->ui--select#choose mousemove->ui--select#hover" },
35
+ class: "relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-8 pl-2 #{size == :sm ? "text-xs" : "text-sm"} " \
36
+ "outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground aria-selected:font-medium " \
37
+ "#{value.to_s.empty? ? "text-muted-foreground" : ""}") do %>
38
+ <span class="truncate"><%= label %></span>
39
+ <span class="absolute right-2 flex size-4 items-center justify-center text-primary dark:text-foreground" data-ui--select-target="check" <%= "hidden" unless selected?(value) %>>
40
+ <%= render(UI::Icon.new(name: "check", size: :sm)) %>
41
+ </span>
42
+ <% end %>
43
+ <% end %>
44
+ <% end %>
45
+
46
+ <% if searchable? %>
47
+ <p hidden data-ui--select-target="empty" class="px-3 py-3 text-sm text-muted-foreground"><%= t("ui.select.no_matches", default: "No matches") %></p>
48
+ <% end %>
49
+ <% end %>
50
+
51
+ <% if form_field? %>
52
+ <%= @form.select(@field, native_choices, { selected: current_values }, native_options) %>
53
+ <% else %>
54
+ <%= tag.select(**native_options) do %>
55
+ <% native_choices.each do |label, value| %>
56
+ <%= tag.option(label, value: value, selected: selected?(value)) %>
57
+ <% end %>
58
+ <% end %>
59
+ <% end %>
60
+ <% end %>
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ # UI::Select — a dropdown for choosing one value, or several, styled to match the kit.
6
+ #
7
+ # The visible part is a WAI-ARIA "select-only combobox": a button with
8
+ # role=combobox and a floating role=listbox. Focus stays on the button while
9
+ # the arrow keys move through the options, as the pattern specifies.
10
+ #
11
+ # The value lives in a visually hidden native <select>. That keeps everything a
12
+ # form already relies on: the field submits under its name, `required` blocks
13
+ # submission, form reset works, the form builder binds it, and anything
14
+ # listening for `change` on it still hears one. The Stimulus controller only
15
+ # mirrors the choice into it.
16
+ #
17
+ # `searchable: true` adds a search box that filters the list; `multiple: true`
18
+ # lets people pick several, posting `name[]` like a native multiple select.
19
+ #
20
+ # UI::NativeSelect is the browser's own control, for when a phone's system
21
+ # picker is the better fit.
22
+ #
23
+ # Usage:
24
+ # <%= render(UI::Select.new(form: f, field: :plan, label: "Plan",
25
+ # options: [["Free", "free"], ["Pro", "pro"]],
26
+ # prompt: "Choose a plan")) %>
27
+ class UI::Select < UI::Base
28
+ variants(
29
+ base: "flex w-full items-center justify-between gap-2 rounded-md border border-input bg-background " \
30
+ "text-left text-foreground shadow-xs " \
31
+ "#{UI::Base::FOCUS_RING} " \
32
+ "aria-expanded:ring-2 aria-expanded:ring-primary aria-expanded:ring-offset-1 " \
33
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted",
34
+ variants: {
35
+ size: {
36
+ sm: "h-8 px-2.5 text-xs",
37
+ default: "h-9 px-3 text-sm",
38
+ lg: "h-10 px-4 text-base"
39
+ }
40
+ },
41
+ defaults: { size: :default }
42
+ )
43
+
44
+ def initialize(name: nil, options: [], selected: nil, prompt: nil,
45
+ label: nil, size: :default, searchable: false, multiple: false,
46
+ required: false, error: false, disabled: false,
47
+ form: nil, field: nil,
48
+ class: nil, **html_attrs)
49
+ @name = name
50
+ @options = options
51
+ @selected = selected
52
+ @prompt = prompt
53
+ @label = label
54
+ @size = size
55
+ @searchable = searchable
56
+ @multiple = multiple
57
+ @required = required
58
+ @error = error
59
+ @disabled = disabled
60
+ @form = form
61
+ @field = field
62
+ @user_class = binding.local_variable_get(:class)
63
+ @html_attrs = html_attrs
64
+ @uid = SecureRandom.hex(4)
65
+ end
66
+
67
+ attr_reader :prompt, :size
68
+
69
+ def searchable? = @searchable
70
+ def multiple? = @multiple
71
+
72
+ def trigger_id = "ui-select-#{@uid}"
73
+ def listbox_id = "ui-select-#{@uid}-listbox"
74
+ def search_id = "ui-select-#{@uid}-search"
75
+ def option_id(index) = "ui-select-#{@uid}-option-#{index}"
76
+
77
+ def trigger_classes
78
+ cn(variant_classes(size: @size),
79
+ { "border-destructive focus:ring-destructive" => @error },
80
+ @user_class)
81
+ end
82
+
83
+ def trigger_options
84
+ {
85
+ type: "button",
86
+ id: trigger_id,
87
+ class: trigger_classes,
88
+ role: "combobox",
89
+ disabled: @disabled,
90
+ aria: { haspopup: "listbox", expanded: "false", controls: listbox_id, invalid: (@error ? "true" : nil) },
91
+ data: { "ui--select-target": "trigger",
92
+ action: "click->ui--select#toggle keydown->ui--select#keydown" }
93
+ }
94
+ end
95
+
96
+ def form_field? = @form.present? && @field.present?
97
+
98
+ # Same convention as UI::Input: bound to a form object the attribute name
99
+ # supplies the label; `label:` overrides, `label: false` opts out.
100
+ def render_label?
101
+ return false if @label == false
102
+
103
+ @label.present? || form_field?
104
+ end
105
+
106
+ def label_text
107
+ return @label if @label.is_a?(String) && @label.present?
108
+ return nil unless form_field?
109
+
110
+ object = @form.object
111
+ if object.respond_to?(:class) && object.class.respond_to?(:human_attribute_name)
112
+ object.class.human_attribute_name(@field)
113
+ else
114
+ @field.to_s.humanize
115
+ end
116
+ end
117
+
118
+ def label_component
119
+ UI::Label.new(for: trigger_id, required: @required, disabled: @disabled)
120
+ end
121
+
122
+ # Accepts [["Label", value], ...] or a plain list of scalars. A prompt
123
+ # becomes a first, empty option, as `include_blank` makes it natively;
124
+ # a multiple select has no "nothing" option, since nothing is unticking.
125
+ def listbox_options
126
+ list = @options.map { |option| option.is_a?(Array) ? option : [ option, option ] }
127
+ prompt.present? && !multiple? ? [ [ prompt, "" ] ] + list : list
128
+ end
129
+
130
+ # What the field holds now, as strings: the explicit selection, else the
131
+ # bound attribute, else (single only) whatever a native select shows first.
132
+ def current_values
133
+ value = @selected
134
+ value = @form.object.public_send(@field) if value.nil? && form_field? && @form.object.respond_to?(@field)
135
+ return Array(value).map(&:to_s) unless value.nil?
136
+
137
+ multiple? ? [] : [ listbox_options.first&.last.to_s ]
138
+ end
139
+
140
+ def selected?(value) = current_values.include?(value.to_s)
141
+
142
+ def chosen_labels = listbox_options.select { |_, value| selected?(value) && value.to_s != "" }.map(&:first)
143
+
144
+ # A multiple select names up to two choices, then counts them.
145
+ def current_label
146
+ return chosen_labels.first || prompt unless multiple?
147
+ return prompt || t("ui.select.placeholder", default: "Choose…") if chosen_labels.empty?
148
+
149
+ chosen_labels.size > 2 ? summary.sub("%{count}", chosen_labels.size.to_s) : chosen_labels.join(", ")
150
+ end
151
+
152
+ def placeholder_shown? = chosen_labels.empty?
153
+
154
+ # "%{count} selected", left for the controller to fill in as ticks change.
155
+ def summary = t("ui.select.selected", default: "%{count} selected", count: "%{count}")
156
+
157
+ def root_data
158
+ { controller: "ui--select", "ui--select-multiple-value": multiple?.to_s,
159
+ "ui--select-summary-value": summary,
160
+ "ui--select-placeholder-value": prompt || t("ui.select.placeholder", default: "Choose…") }
161
+ end
162
+
163
+ # The hidden native select. Everything the caller passed beyond our own
164
+ # arguments lands here, so ids and change listeners keep working.
165
+ def native_options
166
+ attrs = @html_attrs.dup
167
+ attrs[:class] = "sr-only"
168
+ attrs[:tabindex] = -1
169
+ attrs[:aria] = (attrs[:aria] || {}).merge(hidden: true)
170
+ attrs[:data] = (attrs[:data] || {}).merge("ui--select-target": "native")
171
+ attrs[:required] = true if @required
172
+ attrs[:disabled] = true if @disabled
173
+ attrs[:multiple] = true if multiple?
174
+ attrs[:name] = multiple? ? "#{@name}[]" : @name if @name && !form_field?
175
+ attrs
176
+ end
177
+
178
+ def native_choices
179
+ listbox_options.map { |label, value| [ label, value ] }
180
+ end
181
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Sheet::Close < UI::Dialog::Close
4
+ end
@@ -0,0 +1,24 @@
1
+ <% body = content %>
2
+ <%= tag.dialog(**html_options) do %>
3
+ <%= tag.div(class: panel_classes, data: { state: "closed", "ui--dialog-part": "panel" }) do %>
4
+ <% if show_close? %>
5
+ <%= render(UI::Sheet::Close.new) %>
6
+ <% end %>
7
+ <% if title? || description? %>
8
+ <header class="flex flex-col space-y-1.5 text-center sm:text-left">
9
+ <% if title? %>
10
+ <%= tag.h2(title_html, id: title_id, class: "text-lg font-semibold leading-none tracking-tight") %>
11
+ <% end %>
12
+ <% if description? %>
13
+ <%= tag.p(description_html, id: description_id, class: "text-sm text-muted-foreground") %>
14
+ <% end %>
15
+ </header>
16
+ <% end %>
17
+ <% if body.present? %>
18
+ <div class="overflow-y-auto flex-1 min-h-0 <%= title? || description? ? 'mt-4' : '' %>"><%= body %></div>
19
+ <% end %>
20
+ <% if footer? %>
21
+ <footer class="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"><%= footer_html %></footer>
22
+ <% end %>
23
+ <% end %>
24
+ <% end %>
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI::Sheet::Content — the native <dialog> element with a fixed-position panel
4
+ # anchored to a viewport edge. Inline block helpers for title, description, and
5
+ # footer (no nested ViewComponent sub-classes).
6
+ class UI::Sheet::Content < UI::Base
7
+ PANEL_BASE =
8
+ "fixed z-50 flex flex-col overflow-hidden border bg-background p-6 shadow-lg " \
9
+ "data-[state=open]:animate-in data-[state=closed]:animate-out"
10
+
11
+ variants(
12
+ base: PANEL_BASE,
13
+ variants: {
14
+ side: {
15
+ right: "inset-y-0 right-0 h-full w-full border-l sm:max-w-md data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right",
16
+ left: "inset-y-0 left-0 h-full w-full border-r sm:max-w-md data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left",
17
+ top: "inset-x-0 top-0 max-h-[80vh] w-full border-b data-[state=open]:slide-in-from-top data-[state=closed]:slide-out-to-top",
18
+ bottom: "inset-x-0 bottom-0 max-h-[80vh] w-full border-t data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom"
19
+ }
20
+ },
21
+ defaults: { side: :right }
22
+ )
23
+
24
+ def initialize(side: :right, show_close: true, class: nil, **html_attrs)
25
+ @side = side
26
+ @show_close = show_close
27
+ @user_class = binding.local_variable_get(:class)
28
+ @html_attrs = html_attrs
29
+ end
30
+
31
+ def set_sheet_context(sheet)
32
+ @sheet = sheet
33
+ end
34
+
35
+ # ── Inline slot helpers ───────────────────────────────────────────────────
36
+
37
+ def with_title(text = nil, &block)
38
+ @title_text = text
39
+ @title_block = block
40
+ self
41
+ end
42
+
43
+ def with_description(text = nil, &block)
44
+ @description_text = text
45
+ @description_block = block
46
+ self
47
+ end
48
+
49
+ def with_footer(&block)
50
+ @footer_block = block
51
+ self
52
+ end
53
+
54
+ def title?; @title_text.present? || @title_block.present?; end
55
+ def description?; @description_text.present? || @description_block.present?; end
56
+ def footer?; @footer_block.present?; end
57
+ def show_close?; @show_close; end
58
+
59
+ def title_html
60
+ @title_block ? view_context.capture(&@title_block) : @title_text
61
+ end
62
+
63
+ def description_html
64
+ @description_block ? view_context.capture(&@description_block) : @description_text
65
+ end
66
+
67
+ def footer_html
68
+ view_context.capture(&@footer_block) if @footer_block
69
+ end
70
+
71
+ # ── Class strings ─────────────────────────────────────────────────────────
72
+
73
+ def panel_classes
74
+ cn(variant_classes(side: @side), @user_class)
75
+ end
76
+
77
+ def dialog_classes
78
+ "fixed inset-0 m-0 p-0 max-w-none max-h-none w-full h-full " \
79
+ "bg-transparent border-0 overflow-visible " \
80
+ "open:flex open:items-center open:justify-center " \
81
+ "backdrop:bg-black/60 backdrop:backdrop-blur-[1px]"
82
+ end
83
+
84
+ def html_options
85
+ opts = @html_attrs.dup
86
+ opts[:id] = @sheet.id
87
+ opts[:class] = dialog_classes
88
+ opts[:"aria-labelledby"] = @sheet.title_id if title?
89
+ opts[:"aria-describedby"] = @sheet.description_id if description?
90
+ opts[:"data-ui--dialog-target"] = "content"
91
+ opts[:"data-state"] = "closed"
92
+ opts
93
+ end
94
+
95
+ def title_id; @sheet.title_id; end
96
+ def description_id; @sheet.description_id; end
97
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Sheet::Trigger < UI::Dialog::Trigger
4
+ end
@@ -0,0 +1,3 @@
1
+ <%= tag.div(class: root_classes,
2
+ data: { controller: "ui--dialog" }.merge(@html_attrs[:data] || {}),
3
+ **@html_attrs.except(:data)) do %><% if trigger? %><%= trigger %><% end %><% if body? %><%= body %><% end %><% end %>
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # UI::Sheet — side-anchored modal panel backed by the native <dialog> element.
4
+ #
5
+ # Reuses the ui--dialog controller and follows the same compound API as
6
+ # UI::Dialog, but anchors the panel to an edge of the viewport instead of
7
+ # centering it.
8
+ #
9
+ # Compound:
10
+ # UI::Sheet
11
+ # UI::Sheet::Trigger — click-to-open wrapper (inherits from Dialog::Trigger)
12
+ # UI::Sheet::Content — the <dialog> + fixed panel; inline title / description / footer
13
+ class UI::Sheet < UI::Base
14
+ renders_one :trigger, ->(**opts) {
15
+ UI::Sheet::Trigger.new(**opts).tap { |t| t.set_dialog_context(self) }
16
+ }
17
+ renders_one :body, ->(**opts) {
18
+ UI::Sheet::Content.new(**opts).tap { |c| c.set_sheet_context(self) }
19
+ }
20
+ alias_method :with_content, :with_body
21
+
22
+ def initialize(class: nil, **html_attrs)
23
+ @user_class = binding.local_variable_get(:class)
24
+ @html_attrs = html_attrs
25
+ @id = @html_attrs[:id] || "ui-sheet-#{SecureRandom.hex(4)}"
26
+ @html_attrs.delete(:id)
27
+ end
28
+
29
+ attr_reader :id
30
+
31
+ def title_id; "#{@id}-title"; end
32
+ def description_id; "#{@id}-description"; end
33
+
34
+ def root_classes
35
+ cn("contents", @user_class)
36
+ end
37
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div(**@system_args) do %><% sidebar_groups.each do |group| %><%= group %><% end %><% end %>
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Sidebar::Content < UI::Base
4
+ renders_many :sidebar_groups, UI::Sidebar::Group
5
+
6
+ def initialize(**system_args)
7
+ @system_args = system_args
8
+
9
+ @system_args[:data] ||= {}
10
+ @system_args[:data][:slot] = "sidebar-content"
11
+ @system_args[:data][:sidebar] = "content"
12
+
13
+ @system_args[:class] = class_names(
14
+ "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
15
+ @system_args[:class]
16
+ )
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ <%= tag.div **@system_args do %>
2
+ <%= content %>
3
+ <% end %>
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Sidebar::Footer < UI::Base
4
+ def initialize(**system_args)
5
+ @system_args = system_args
6
+
7
+ @system_args[:data] ||= {}
8
+ @system_args[:data][:slot] = "sidebar-footer"
9
+ @system_args[:data][:sidebar] = "sidebar"
10
+
11
+ @system_args[:class] = class_names(
12
+ "flex flex-col gap-2 p-2",
13
+ @system_args[:class]
14
+ )
15
+ end
16
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div(**@system_args) do %><%= label if label %><%= menu if menu %><%= content %><% end %>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UI::Sidebar::Group < UI::Base
4
+ renders_one :label, UI::Sidebar::GroupLabel
5
+ renders_one :menu, UI::Sidebar::Menu
6
+
7
+ def initialize(**system_args)
8
+ @system_args = system_args
9
+
10
+ @system_args[:data] ||= {}
11
+ @system_args[:data][:slot] = "sidebar-group"
12
+ @system_args[:data][:sidebar] = "group"
13
+
14
+ @system_args[:class] = class_names(
15
+ "relative flex w-full min-w-0 flex-col p-2",
16
+ @system_args[:class]
17
+ )
18
+ end
19
+ end
@@ -0,0 +1 @@
1
+ <%= tag.div(**@system_args) do %><%= @label if @label %><%= content %><% end %>