uikit_rails 0.1.1

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 (378) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +301 -0
  3. data/Rakefile +12 -0
  4. data/app/controllers/uikit_rails/styleguide_controller.rb +72 -0
  5. data/app/views/layouts/uikit_rails/application.html.erb +412 -0
  6. data/app/views/uikit_rails/styleguide/index.html.erb +27 -0
  7. data/app/views/uikit_rails/styleguide/show.html.erb +56 -0
  8. data/config/routes.rb +6 -0
  9. data/lib/uikit_rails/engine.rb +13 -0
  10. data/lib/uikit_rails/generators/add_generator.rb +110 -0
  11. data/lib/uikit_rails/generators/install_generator.rb +62 -0
  12. data/lib/uikit_rails/generators/templates/base_component.rb.tt +32 -0
  13. data/lib/uikit_rails/generators/templates/uikit_rails.css.tt +69 -0
  14. data/lib/uikit_rails/templates/components/accordion/USAGE +30 -0
  15. data/lib/uikit_rails/templates/components/accordion/accordion.css +74 -0
  16. data/lib/uikit_rails/templates/components/accordion/component.html.erb +13 -0
  17. data/lib/uikit_rails/templates/components/accordion/component.rb +35 -0
  18. data/lib/uikit_rails/templates/components/accordion/preview.yml +81 -0
  19. data/lib/uikit_rails/templates/components/alert/USAGE +25 -0
  20. data/lib/uikit_rails/templates/components/alert/alert.css +60 -0
  21. data/lib/uikit_rails/templates/components/alert/component.html.erb +9 -0
  22. data/lib/uikit_rails/templates/components/alert/component.rb +27 -0
  23. data/lib/uikit_rails/templates/components/alert/preview.yml +53 -0
  24. data/lib/uikit_rails/templates/components/alert_dialog/USAGE +38 -0
  25. data/lib/uikit_rails/templates/components/alert_dialog/alert_dialog.css +108 -0
  26. data/lib/uikit_rails/templates/components/alert_dialog/component.html.erb +27 -0
  27. data/lib/uikit_rails/templates/components/alert_dialog/component.rb +23 -0
  28. data/lib/uikit_rails/templates/components/alert_dialog/preview.yml +94 -0
  29. data/lib/uikit_rails/templates/components/avatar/USAGE +20 -0
  30. data/lib/uikit_rails/templates/components/avatar/avatar.css +53 -0
  31. data/lib/uikit_rails/templates/components/avatar/component.html.erb +7 -0
  32. data/lib/uikit_rails/templates/components/avatar/component.rb +31 -0
  33. data/lib/uikit_rails/templates/components/avatar/preview.yml +42 -0
  34. data/lib/uikit_rails/templates/components/badge/USAGE +13 -0
  35. data/lib/uikit_rails/templates/components/badge/badge.css +61 -0
  36. data/lib/uikit_rails/templates/components/badge/component.rb +28 -0
  37. data/lib/uikit_rails/templates/components/badge/preview.yml +38 -0
  38. data/lib/uikit_rails/templates/components/breadcrumb/USAGE +23 -0
  39. data/lib/uikit_rails/templates/components/breadcrumb/breadcrumb.css +55 -0
  40. data/lib/uikit_rails/templates/components/breadcrumb/component.html.erb +14 -0
  41. data/lib/uikit_rails/templates/components/breadcrumb/component.rb +40 -0
  42. data/lib/uikit_rails/templates/components/breadcrumb/preview.yml +42 -0
  43. data/lib/uikit_rails/templates/components/button/USAGE +21 -0
  44. data/lib/uikit_rails/templates/components/button/button.css +125 -0
  45. data/lib/uikit_rails/templates/components/button/component.rb +44 -0
  46. data/lib/uikit_rails/templates/components/button/preview.yml +106 -0
  47. data/lib/uikit_rails/templates/components/card/USAGE +33 -0
  48. data/lib/uikit_rails/templates/components/card/card.css +63 -0
  49. data/lib/uikit_rails/templates/components/card/component.html.erb +16 -0
  50. data/lib/uikit_rails/templates/components/card/component.rb +26 -0
  51. data/lib/uikit_rails/templates/components/card/preview.yml +57 -0
  52. data/lib/uikit_rails/templates/components/checkbox/USAGE +19 -0
  53. data/lib/uikit_rails/templates/components/checkbox/checkbox.css +67 -0
  54. data/lib/uikit_rails/templates/components/checkbox/component.html.erb +6 -0
  55. data/lib/uikit_rails/templates/components/checkbox/component.rb +26 -0
  56. data/lib/uikit_rails/templates/components/checkbox/preview.yml +43 -0
  57. data/lib/uikit_rails/templates/components/collapsible/USAGE +31 -0
  58. data/lib/uikit_rails/templates/components/collapsible/collapsible.css +55 -0
  59. data/lib/uikit_rails/templates/components/collapsible/component.html.erb +8 -0
  60. data/lib/uikit_rails/templates/components/collapsible/component.rb +18 -0
  61. data/lib/uikit_rails/templates/components/collapsible/preview.yml +65 -0
  62. data/lib/uikit_rails/templates/components/dialog/USAGE +41 -0
  63. data/lib/uikit_rails/templates/components/dialog/component.html.erb +23 -0
  64. data/lib/uikit_rails/templates/components/dialog/component.rb +20 -0
  65. data/lib/uikit_rails/templates/components/dialog/dialog.css +133 -0
  66. data/lib/uikit_rails/templates/components/dialog/preview.yml +77 -0
  67. data/lib/uikit_rails/templates/components/dropdown/USAGE +40 -0
  68. data/lib/uikit_rails/templates/components/dropdown/component.html.erb +14 -0
  69. data/lib/uikit_rails/templates/components/dropdown/component.rb +62 -0
  70. data/lib/uikit_rails/templates/components/dropdown/dropdown.css +104 -0
  71. data/lib/uikit_rails/templates/components/dropdown/preview.yml +75 -0
  72. data/lib/uikit_rails/templates/components/form/USAGE +51 -0
  73. data/lib/uikit_rails/templates/components/form/builder.rb +233 -0
  74. data/lib/uikit_rails/templates/components/form/form.css +48 -0
  75. data/lib/uikit_rails/templates/components/form/preview.yml +95 -0
  76. data/lib/uikit_rails/templates/components/input/USAGE +21 -0
  77. data/lib/uikit_rails/templates/components/input/component.rb +25 -0
  78. data/lib/uikit_rails/templates/components/input/input.css +43 -0
  79. data/lib/uikit_rails/templates/components/input/preview.yml +58 -0
  80. data/lib/uikit_rails/templates/components/label/USAGE +16 -0
  81. data/lib/uikit_rails/templates/components/label/component.rb +25 -0
  82. data/lib/uikit_rails/templates/components/label/label.css +25 -0
  83. data/lib/uikit_rails/templates/components/label/preview.yml +34 -0
  84. data/lib/uikit_rails/templates/components/pagination/USAGE +45 -0
  85. data/lib/uikit_rails/templates/components/pagination/component.html.erb +7 -0
  86. data/lib/uikit_rails/templates/components/pagination/component.rb +90 -0
  87. data/lib/uikit_rails/templates/components/pagination/pagination.css +89 -0
  88. data/lib/uikit_rails/templates/components/pagination/preview.yml +61 -0
  89. data/lib/uikit_rails/templates/components/popover/USAGE +44 -0
  90. data/lib/uikit_rails/templates/components/popover/component.html.erb +8 -0
  91. data/lib/uikit_rails/templates/components/popover/component.rb +19 -0
  92. data/lib/uikit_rails/templates/components/popover/popover.css +94 -0
  93. data/lib/uikit_rails/templates/components/popover/preview.yml +102 -0
  94. data/lib/uikit_rails/templates/components/progress/USAGE +15 -0
  95. data/lib/uikit_rails/templates/components/progress/component.html.erb +3 -0
  96. data/lib/uikit_rails/templates/components/progress/component.rb +38 -0
  97. data/lib/uikit_rails/templates/components/progress/preview.yml +44 -0
  98. data/lib/uikit_rails/templates/components/progress/progress.css +20 -0
  99. data/lib/uikit_rails/templates/components/select/USAGE +19 -0
  100. data/lib/uikit_rails/templates/components/select/component.rb +38 -0
  101. data/lib/uikit_rails/templates/components/select/preview.yml +61 -0
  102. data/lib/uikit_rails/templates/components/select/select.css +46 -0
  103. data/lib/uikit_rails/templates/components/separator/USAGE +15 -0
  104. data/lib/uikit_rails/templates/components/separator/component.rb +34 -0
  105. data/lib/uikit_rails/templates/components/separator/preview.yml +32 -0
  106. data/lib/uikit_rails/templates/components/separator/separator.css +21 -0
  107. data/lib/uikit_rails/templates/components/sheet/USAGE +44 -0
  108. data/lib/uikit_rails/templates/components/sheet/component.html.erb +23 -0
  109. data/lib/uikit_rails/templates/components/sheet/component.rb +23 -0
  110. data/lib/uikit_rails/templates/components/sheet/preview.yml +105 -0
  111. data/lib/uikit_rails/templates/components/sheet/sheet.css +193 -0
  112. data/lib/uikit_rails/templates/components/skeleton/USAGE +19 -0
  113. data/lib/uikit_rails/templates/components/skeleton/component.rb +38 -0
  114. data/lib/uikit_rails/templates/components/skeleton/preview.yml +44 -0
  115. data/lib/uikit_rails/templates/components/skeleton/skeleton.css +25 -0
  116. data/lib/uikit_rails/templates/components/switch/USAGE +19 -0
  117. data/lib/uikit_rails/templates/components/switch/component.html.erb +19 -0
  118. data/lib/uikit_rails/templates/components/switch/component.rb +23 -0
  119. data/lib/uikit_rails/templates/components/switch/preview.yml +43 -0
  120. data/lib/uikit_rails/templates/components/switch/switch.css +81 -0
  121. data/lib/uikit_rails/templates/components/table/USAGE +40 -0
  122. data/lib/uikit_rails/templates/components/table/component.html.erb +14 -0
  123. data/lib/uikit_rails/templates/components/table/component.rb +25 -0
  124. data/lib/uikit_rails/templates/components/table/preview.yml +109 -0
  125. data/lib/uikit_rails/templates/components/table/table.css +86 -0
  126. data/lib/uikit_rails/templates/components/tabs/USAGE +24 -0
  127. data/lib/uikit_rails/templates/components/tabs/component.html.erb +10 -0
  128. data/lib/uikit_rails/templates/components/tabs/component.rb +35 -0
  129. data/lib/uikit_rails/templates/components/tabs/preview.yml +60 -0
  130. data/lib/uikit_rails/templates/components/tabs/tabs.css +72 -0
  131. data/lib/uikit_rails/templates/components/textarea/USAGE +19 -0
  132. data/lib/uikit_rails/templates/components/textarea/component.rb +25 -0
  133. data/lib/uikit_rails/templates/components/textarea/preview.yml +47 -0
  134. data/lib/uikit_rails/templates/components/textarea/textarea.css +39 -0
  135. data/lib/uikit_rails/templates/components/toggle/USAGE +25 -0
  136. data/lib/uikit_rails/templates/components/toggle/component.rb +39 -0
  137. data/lib/uikit_rails/templates/components/toggle/preview.yml +81 -0
  138. data/lib/uikit_rails/templates/components/toggle/toggle.css +89 -0
  139. data/lib/uikit_rails/templates/components/tooltip/USAGE +23 -0
  140. data/lib/uikit_rails/templates/components/tooltip/component.html.erb +8 -0
  141. data/lib/uikit_rails/templates/components/tooltip/component.rb +19 -0
  142. data/lib/uikit_rails/templates/components/tooltip/preview.yml +52 -0
  143. data/lib/uikit_rails/templates/components/tooltip/tooltip.css +78 -0
  144. data/lib/uikit_rails/templates/stimulus/accordion_controller.js +19 -0
  145. data/lib/uikit_rails/templates/stimulus/alert_dialog_controller.js +25 -0
  146. data/lib/uikit_rails/templates/stimulus/collapsible_controller.js +9 -0
  147. data/lib/uikit_rails/templates/stimulus/dialog_controller.js +19 -0
  148. data/lib/uikit_rails/templates/stimulus/dropdown_controller.js +47 -0
  149. data/lib/uikit_rails/templates/stimulus/popover_controller.js +47 -0
  150. data/lib/uikit_rails/templates/stimulus/sheet_controller.js +19 -0
  151. data/lib/uikit_rails/templates/stimulus/tabs_controller.js +24 -0
  152. data/lib/uikit_rails/templates/stimulus/tooltip_controller.js +13 -0
  153. data/lib/uikit_rails/version.rb +5 -0
  154. data/lib/uikit_rails.rb +59 -0
  155. data/sig/uikit_rails.rbs +4 -0
  156. data/test_app/.dockerignore +51 -0
  157. data/test_app/.gitattributes +9 -0
  158. data/test_app/.github/dependabot.yml +12 -0
  159. data/test_app/.github/workflows/ci.yml +124 -0
  160. data/test_app/.gitignore +35 -0
  161. data/test_app/.kamal/hooks/docker-setup.sample +3 -0
  162. data/test_app/.kamal/hooks/post-app-boot.sample +3 -0
  163. data/test_app/.kamal/hooks/post-deploy.sample +14 -0
  164. data/test_app/.kamal/hooks/post-proxy-reboot.sample +3 -0
  165. data/test_app/.kamal/hooks/pre-app-boot.sample +3 -0
  166. data/test_app/.kamal/hooks/pre-build.sample +51 -0
  167. data/test_app/.kamal/hooks/pre-connect.sample +47 -0
  168. data/test_app/.kamal/hooks/pre-deploy.sample +122 -0
  169. data/test_app/.kamal/hooks/pre-proxy-reboot.sample +3 -0
  170. data/test_app/.kamal/secrets +20 -0
  171. data/test_app/.rubocop.yml +8 -0
  172. data/test_app/.ruby-version +1 -0
  173. data/test_app/Dockerfile +77 -0
  174. data/test_app/Gemfile +68 -0
  175. data/test_app/Gemfile.lock +587 -0
  176. data/test_app/README.md +24 -0
  177. data/test_app/Rakefile +6 -0
  178. data/test_app/app/assets/images/.keep +0 -0
  179. data/test_app/app/assets/stylesheets/application.css +10 -0
  180. data/test_app/app/assets/stylesheets/ui/accordion.css +74 -0
  181. data/test_app/app/assets/stylesheets/ui/alert.css +60 -0
  182. data/test_app/app/assets/stylesheets/ui/alert_dialog.css +108 -0
  183. data/test_app/app/assets/stylesheets/ui/avatar.css +53 -0
  184. data/test_app/app/assets/stylesheets/ui/badge.css +61 -0
  185. data/test_app/app/assets/stylesheets/ui/breadcrumb.css +55 -0
  186. data/test_app/app/assets/stylesheets/ui/button.css +125 -0
  187. data/test_app/app/assets/stylesheets/ui/card.css +63 -0
  188. data/test_app/app/assets/stylesheets/ui/checkbox.css +67 -0
  189. data/test_app/app/assets/stylesheets/ui/collapsible.css +55 -0
  190. data/test_app/app/assets/stylesheets/ui/dialog.css +133 -0
  191. data/test_app/app/assets/stylesheets/ui/dropdown.css +104 -0
  192. data/test_app/app/assets/stylesheets/ui/form.css +48 -0
  193. data/test_app/app/assets/stylesheets/ui/input.css +43 -0
  194. data/test_app/app/assets/stylesheets/ui/label.css +25 -0
  195. data/test_app/app/assets/stylesheets/ui/pagination.css +89 -0
  196. data/test_app/app/assets/stylesheets/ui/popover.css +94 -0
  197. data/test_app/app/assets/stylesheets/ui/progress.css +20 -0
  198. data/test_app/app/assets/stylesheets/ui/select.css +46 -0
  199. data/test_app/app/assets/stylesheets/ui/separator.css +21 -0
  200. data/test_app/app/assets/stylesheets/ui/sheet.css +193 -0
  201. data/test_app/app/assets/stylesheets/ui/skeleton.css +25 -0
  202. data/test_app/app/assets/stylesheets/ui/switch.css +81 -0
  203. data/test_app/app/assets/stylesheets/ui/table.css +86 -0
  204. data/test_app/app/assets/stylesheets/ui/tabs.css +72 -0
  205. data/test_app/app/assets/stylesheets/ui/textarea.css +39 -0
  206. data/test_app/app/assets/stylesheets/ui/toggle.css +89 -0
  207. data/test_app/app/assets/stylesheets/ui/tooltip.css +78 -0
  208. data/test_app/app/assets/stylesheets/uikit_rails.css +69 -0
  209. data/test_app/app/components/ui/accordion/component.html.erb +13 -0
  210. data/test_app/app/components/ui/accordion/component.rb +35 -0
  211. data/test_app/app/components/ui/accordion/preview.yml +81 -0
  212. data/test_app/app/components/ui/alert/component.html.erb +9 -0
  213. data/test_app/app/components/ui/alert/component.rb +27 -0
  214. data/test_app/app/components/ui/alert/preview.yml +53 -0
  215. data/test_app/app/components/ui/alert_dialog/component.html.erb +27 -0
  216. data/test_app/app/components/ui/alert_dialog/component.rb +23 -0
  217. data/test_app/app/components/ui/alert_dialog/preview.yml +94 -0
  218. data/test_app/app/components/ui/avatar/component.html.erb +7 -0
  219. data/test_app/app/components/ui/avatar/component.rb +31 -0
  220. data/test_app/app/components/ui/avatar/preview.yml +42 -0
  221. data/test_app/app/components/ui/badge/component.rb +28 -0
  222. data/test_app/app/components/ui/badge/preview.yml +38 -0
  223. data/test_app/app/components/ui/base_component.rb +32 -0
  224. data/test_app/app/components/ui/breadcrumb/component.html.erb +14 -0
  225. data/test_app/app/components/ui/breadcrumb/component.rb +40 -0
  226. data/test_app/app/components/ui/breadcrumb/preview.yml +42 -0
  227. data/test_app/app/components/ui/button/component.rb +44 -0
  228. data/test_app/app/components/ui/button/preview.yml +106 -0
  229. data/test_app/app/components/ui/card/component.html.erb +16 -0
  230. data/test_app/app/components/ui/card/component.rb +26 -0
  231. data/test_app/app/components/ui/card/preview.yml +57 -0
  232. data/test_app/app/components/ui/checkbox/component.html.erb +6 -0
  233. data/test_app/app/components/ui/checkbox/component.rb +26 -0
  234. data/test_app/app/components/ui/checkbox/preview.yml +43 -0
  235. data/test_app/app/components/ui/collapsible/component.html.erb +8 -0
  236. data/test_app/app/components/ui/collapsible/component.rb +18 -0
  237. data/test_app/app/components/ui/collapsible/preview.yml +65 -0
  238. data/test_app/app/components/ui/dialog/component.html.erb +23 -0
  239. data/test_app/app/components/ui/dialog/component.rb +20 -0
  240. data/test_app/app/components/ui/dialog/preview.yml +77 -0
  241. data/test_app/app/components/ui/dropdown/component.html.erb +14 -0
  242. data/test_app/app/components/ui/dropdown/component.rb +62 -0
  243. data/test_app/app/components/ui/dropdown/preview.yml +75 -0
  244. data/test_app/app/components/ui/form/USAGE +51 -0
  245. data/test_app/app/components/ui/form/builder.rb +233 -0
  246. data/test_app/app/components/ui/form/component.rb +258 -0
  247. data/test_app/app/components/ui/form/form.css +48 -0
  248. data/test_app/app/components/ui/form/preview.yml +95 -0
  249. data/test_app/app/components/ui/input/component.rb +25 -0
  250. data/test_app/app/components/ui/input/preview.yml +58 -0
  251. data/test_app/app/components/ui/label/component.rb +25 -0
  252. data/test_app/app/components/ui/label/preview.yml +34 -0
  253. data/test_app/app/components/ui/pagination/component.html.erb +7 -0
  254. data/test_app/app/components/ui/pagination/component.rb +90 -0
  255. data/test_app/app/components/ui/pagination/preview.yml +61 -0
  256. data/test_app/app/components/ui/popover/component.html.erb +8 -0
  257. data/test_app/app/components/ui/popover/component.rb +19 -0
  258. data/test_app/app/components/ui/popover/preview.yml +102 -0
  259. data/test_app/app/components/ui/progress/component.html.erb +3 -0
  260. data/test_app/app/components/ui/progress/component.rb +38 -0
  261. data/test_app/app/components/ui/progress/preview.yml +44 -0
  262. data/test_app/app/components/ui/select/component.rb +38 -0
  263. data/test_app/app/components/ui/select/preview.yml +61 -0
  264. data/test_app/app/components/ui/separator/component.rb +34 -0
  265. data/test_app/app/components/ui/separator/preview.yml +32 -0
  266. data/test_app/app/components/ui/sheet/component.html.erb +23 -0
  267. data/test_app/app/components/ui/sheet/component.rb +23 -0
  268. data/test_app/app/components/ui/sheet/preview.yml +105 -0
  269. data/test_app/app/components/ui/skeleton/component.rb +38 -0
  270. data/test_app/app/components/ui/skeleton/preview.yml +44 -0
  271. data/test_app/app/components/ui/switch/component.html.erb +19 -0
  272. data/test_app/app/components/ui/switch/component.rb +23 -0
  273. data/test_app/app/components/ui/switch/preview.yml +43 -0
  274. data/test_app/app/components/ui/table/component.html.erb +14 -0
  275. data/test_app/app/components/ui/table/component.rb +25 -0
  276. data/test_app/app/components/ui/table/preview.yml +109 -0
  277. data/test_app/app/components/ui/tabs/component.html.erb +10 -0
  278. data/test_app/app/components/ui/tabs/component.rb +35 -0
  279. data/test_app/app/components/ui/tabs/preview.yml +60 -0
  280. data/test_app/app/components/ui/textarea/component.rb +25 -0
  281. data/test_app/app/components/ui/textarea/preview.yml +47 -0
  282. data/test_app/app/components/ui/toggle/component.rb +39 -0
  283. data/test_app/app/components/ui/toggle/preview.yml +81 -0
  284. data/test_app/app/components/ui/tooltip/component.html.erb +8 -0
  285. data/test_app/app/components/ui/tooltip/component.rb +19 -0
  286. data/test_app/app/components/ui/tooltip/preview.yml +52 -0
  287. data/test_app/app/controllers/application_controller.rb +7 -0
  288. data/test_app/app/controllers/concerns/.keep +0 -0
  289. data/test_app/app/helpers/application_helper.rb +2 -0
  290. data/test_app/app/javascript/application.js +3 -0
  291. data/test_app/app/javascript/controllers/application.js +9 -0
  292. data/test_app/app/javascript/controllers/hello_controller.js +7 -0
  293. data/test_app/app/javascript/controllers/index.js +4 -0
  294. data/test_app/app/javascript/controllers/ui/accordion_controller.js +19 -0
  295. data/test_app/app/javascript/controllers/ui/alert_dialog_controller.js +25 -0
  296. data/test_app/app/javascript/controllers/ui/collapsible_controller.js +9 -0
  297. data/test_app/app/javascript/controllers/ui/dialog_controller.js +19 -0
  298. data/test_app/app/javascript/controllers/ui/dropdown_controller.js +47 -0
  299. data/test_app/app/javascript/controllers/ui/popover_controller.js +47 -0
  300. data/test_app/app/javascript/controllers/ui/sheet_controller.js +19 -0
  301. data/test_app/app/javascript/controllers/ui/tabs_controller.js +24 -0
  302. data/test_app/app/javascript/controllers/ui/tooltip_controller.js +13 -0
  303. data/test_app/app/jobs/application_job.rb +7 -0
  304. data/test_app/app/mailers/application_mailer.rb +4 -0
  305. data/test_app/app/models/application_record.rb +3 -0
  306. data/test_app/app/models/concerns/.keep +0 -0
  307. data/test_app/app/views/layouts/application.html.erb +29 -0
  308. data/test_app/app/views/layouts/mailer.html.erb +13 -0
  309. data/test_app/app/views/layouts/mailer.text.erb +1 -0
  310. data/test_app/app/views/pwa/manifest.json.erb +22 -0
  311. data/test_app/app/views/pwa/service-worker.js +26 -0
  312. data/test_app/bin/brakeman +7 -0
  313. data/test_app/bin/bundler-audit +6 -0
  314. data/test_app/bin/ci +6 -0
  315. data/test_app/bin/dev +2 -0
  316. data/test_app/bin/docker-entrypoint +8 -0
  317. data/test_app/bin/importmap +4 -0
  318. data/test_app/bin/jobs +6 -0
  319. data/test_app/bin/kamal +16 -0
  320. data/test_app/bin/rails +4 -0
  321. data/test_app/bin/rake +4 -0
  322. data/test_app/bin/rubocop +8 -0
  323. data/test_app/bin/setup +35 -0
  324. data/test_app/bin/thrust +5 -0
  325. data/test_app/config/application.rb +27 -0
  326. data/test_app/config/boot.rb +4 -0
  327. data/test_app/config/bundler-audit.yml +5 -0
  328. data/test_app/config/cable.yml +17 -0
  329. data/test_app/config/cache.yml +16 -0
  330. data/test_app/config/ci.rb +24 -0
  331. data/test_app/config/credentials.yml.enc +1 -0
  332. data/test_app/config/database.yml +40 -0
  333. data/test_app/config/deploy.yml +119 -0
  334. data/test_app/config/environment.rb +5 -0
  335. data/test_app/config/environments/development.rb +78 -0
  336. data/test_app/config/environments/production.rb +90 -0
  337. data/test_app/config/environments/test.rb +53 -0
  338. data/test_app/config/importmap.rb +7 -0
  339. data/test_app/config/initializers/assets.rb +7 -0
  340. data/test_app/config/initializers/content_security_policy.rb +29 -0
  341. data/test_app/config/initializers/filter_parameter_logging.rb +8 -0
  342. data/test_app/config/initializers/inflections.rb +16 -0
  343. data/test_app/config/locales/en.yml +31 -0
  344. data/test_app/config/puma.rb +42 -0
  345. data/test_app/config/queue.yml +18 -0
  346. data/test_app/config/recurring.yml +15 -0
  347. data/test_app/config/routes.rb +13 -0
  348. data/test_app/config/storage.yml +27 -0
  349. data/test_app/config.ru +6 -0
  350. data/test_app/db/cable_schema.rb +11 -0
  351. data/test_app/db/cache_schema.rb +12 -0
  352. data/test_app/db/queue_schema.rb +129 -0
  353. data/test_app/db/seeds.rb +9 -0
  354. data/test_app/lib/tasks/.keep +0 -0
  355. data/test_app/log/.keep +0 -0
  356. data/test_app/public/400.html +135 -0
  357. data/test_app/public/404.html +135 -0
  358. data/test_app/public/406-unsupported-browser.html +135 -0
  359. data/test_app/public/422.html +135 -0
  360. data/test_app/public/500.html +135 -0
  361. data/test_app/public/icon.png +0 -0
  362. data/test_app/public/icon.svg +3 -0
  363. data/test_app/public/robots.txt +1 -0
  364. data/test_app/script/.keep +0 -0
  365. data/test_app/storage/.keep +0 -0
  366. data/test_app/test/controllers/.keep +0 -0
  367. data/test_app/test/fixtures/files/.keep +0 -0
  368. data/test_app/test/helpers/.keep +0 -0
  369. data/test_app/test/integration/.keep +0 -0
  370. data/test_app/test/mailers/.keep +0 -0
  371. data/test_app/test/models/.keep +0 -0
  372. data/test_app/test/test_helper.rb +15 -0
  373. data/test_app/tmp/.keep +0 -0
  374. data/test_app/tmp/pids/.keep +0 -0
  375. data/test_app/tmp/storage/.keep +0 -0
  376. data/test_app/vendor/.keep +0 -0
  377. data/test_app/vendor/javascript/.keep +0 -0
  378. metadata +448 -0
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ui
4
+ module Dropdown
5
+ # Dropdown menu triggered by a button.
6
+ class Component < Ui::BaseComponent
7
+ renders_one :trigger
8
+ renders_many :items, types: {
9
+ item: { renders: ->(**attrs, &block) { Item.new(**attrs, &block) }, as: :item },
10
+ separator: { renders: -> { SeparatorItem.new }, as: :separator },
11
+ label: { renders: ->(**attrs, &block) { LabelItem.new(**attrs, &block) }, as: :label }
12
+ }
13
+
14
+ attr_reader :align, :html_attrs
15
+
16
+ def initialize(align: :start, **html_attrs)
17
+ @align = align.to_sym
18
+ @html_attrs = html_attrs
19
+ super()
20
+ end
21
+
22
+ def align_class
23
+ "ui-dropdown__menu--#{align}"
24
+ end
25
+
26
+ class Item < Ui::BaseComponent
27
+ attr_reader :href, :html_attrs
28
+
29
+ def initialize(href: nil, **html_attrs)
30
+ @href = href
31
+ @html_attrs = html_attrs
32
+ super()
33
+ end
34
+
35
+ def call
36
+ tag_name = href ? :a : :div
37
+ attrs = merge_attrs({ class: "ui-dropdown__item", href: href, role: "menuitem" }.compact, html_attrs)
38
+ content_tag(tag_name, content, **attrs)
39
+ end
40
+ end
41
+
42
+ class SeparatorItem < Ui::BaseComponent
43
+ def call
44
+ tag.div(class: "ui-dropdown__separator", role: "separator")
45
+ end
46
+ end
47
+
48
+ class LabelItem < Ui::BaseComponent
49
+ attr_reader :html_attrs
50
+
51
+ def initialize(**html_attrs)
52
+ @html_attrs = html_attrs
53
+ super()
54
+ end
55
+
56
+ def call
57
+ content_tag(:div, content, class: "ui-dropdown__label", **html_attrs)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ description: A dropdown menu triggered by a button. Supports items, separators, and labels. Uses Stimulus for toggling with outside-click and escape-key dismissal.
2
+
3
+ sections:
4
+ - title: Basic Dropdown
5
+ examples:
6
+ - title: Simple menu
7
+ code: |
8
+ <%= render Ui::Dropdown::Component.new do |dropdown| %>
9
+ <% dropdown.with_trigger do %>
10
+ <%= render Ui::Button::Component.new(variant: :outline) do %>Options<% end %>
11
+ <% end %>
12
+ <% dropdown.with_item do %>Profile<% end %>
13
+ <% dropdown.with_item do %>Settings<% end %>
14
+ <% dropdown.with_separator %>
15
+ <% dropdown.with_item do %>Log out<% end %>
16
+ <% end %>
17
+
18
+ - title: With Labels and Links
19
+ examples:
20
+ - title: Grouped items with label
21
+ code: |
22
+ <%= render Ui::Dropdown::Component.new do |dropdown| %>
23
+ <% dropdown.with_trigger do %>
24
+ <%= render Ui::Button::Component.new(variant: :outline) do %>My Account<% end %>
25
+ <% end %>
26
+ <% dropdown.with_label do %>Account<% end %>
27
+ <% dropdown.with_item(href: "/profile") do %>Profile<% end %>
28
+ <% dropdown.with_item(href: "/billing") do %>Billing<% end %>
29
+ <% dropdown.with_separator %>
30
+ <% dropdown.with_label do %>Team<% end %>
31
+ <% dropdown.with_item(href: "/team") do %>Members<% end %>
32
+ <% dropdown.with_item(href: "/team/settings") do %>Team Settings<% end %>
33
+ <% dropdown.with_separator %>
34
+ <% dropdown.with_item do %>Log out<% end %>
35
+ <% end %>
36
+
37
+ - title: Alignment
38
+ examples:
39
+ - title: Right-aligned menu
40
+ code: |
41
+ <div style="display:flex;justify-content:flex-end;">
42
+ <%= render Ui::Dropdown::Component.new(align: :end) do |dropdown| %>
43
+ <% dropdown.with_trigger do %>
44
+ <%= render Ui::Button::Component.new(variant: :secondary) do %>Actions &#9662;<% end %>
45
+ <% end %>
46
+ <% dropdown.with_item do %>Edit<% end %>
47
+ <% dropdown.with_item do %>Duplicate<% end %>
48
+ <% dropdown.with_separator %>
49
+ <% dropdown.with_item do %>Archive<% end %>
50
+ <% dropdown.with_item do %>Delete<% end %>
51
+ <% end %>
52
+ </div>
53
+
54
+ - title: Left-aligned menu (default)
55
+ code: |
56
+ <%= render Ui::Dropdown::Component.new(align: :start) do |dropdown| %>
57
+ <% dropdown.with_trigger do %>
58
+ <%= render Ui::Button::Component.new(variant: :ghost) do %>Menu &#9662;<% end %>
59
+ <% end %>
60
+ <% dropdown.with_item do %>New File<% end %>
61
+ <% dropdown.with_item do %>Open<% end %>
62
+ <% dropdown.with_item do %>Save<% end %>
63
+ <% end %>
64
+
65
+ - title: Custom Attributes
66
+ examples:
67
+ - title: Dropdown with custom data attributes
68
+ code: |
69
+ <%= render Ui::Dropdown::Component.new(class: "my-dropdown", data: { turbo: false }) do |dropdown| %>
70
+ <% dropdown.with_trigger do %>
71
+ <%= render Ui::Button::Component.new do %>More<% end %>
72
+ <% end %>
73
+ <% dropdown.with_item(data: { action: "click->my-controller#export" }) do %>Export<% end %>
74
+ <% dropdown.with_item(data: { action: "click->my-controller#print" }) do %>Print<% end %>
75
+ <% end %>
@@ -0,0 +1,51 @@
1
+ Prerequisites (install with form or before):
2
+
3
+ rails generate uikit_rails:add input textarea select checkbox label button form
4
+
5
+ Same components as standalone usage, e.g.:
6
+
7
+ <%= render Ui::Input::Component.new(...) %>
8
+
9
+ Basic form:
10
+
11
+ <%= form_with model: @user, builder: Ui::Form::Builder, class: "ui-form" do |f| %>
12
+ <%= f.field :name %>
13
+ <%= f.field :email, as: :email_field, placeholder: "you@example.com" %>
14
+ <%= f.submit "Save" %>
15
+ <% end %>
16
+
17
+ Field helper (label, control, errors):
18
+
19
+ <%= f.field :name %>
20
+ <%= f.field :email, as: :email_field %>
21
+ <%= f.field :password, as: :password_field %>
22
+ <%= f.field :bio, as: :text_area, rows: 4 %>
23
+ <%= f.field :role, as: :select, collection: %w[admin editor viewer] %>
24
+ <%= f.field :terms, as: :check_box, label: "I agree to the terms" %>
25
+
26
+ With description:
27
+
28
+ <%= f.field :email, as: :email_field, description: "We'll never share your email." %>
29
+
30
+ Individual helpers:
31
+
32
+ <%= f.label :email %>
33
+ <%= f.email_field :email, placeholder: "you@example.com" %>
34
+ <%= f.text_area :bio %>
35
+ <%= f.select :role, %w[admin editor viewer] %>
36
+ <%= f.check_box :terms, label: "I agree" %>
37
+ <%= f.submit "Save" %>
38
+
39
+ Grouped select (optgroup) uses the default Rails select helper.
40
+
41
+ App-wide default (ApplicationController):
42
+
43
+ class ApplicationController < ActionController::Base
44
+ default_form_builder Ui::Form::Builder
45
+ end
46
+
47
+ Or in config/initializers/uikit_rails.rb:
48
+
49
+ Rails.application.config.to_prepare do
50
+ ApplicationController.default_form_builder Ui::Form::Builder
51
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ui
4
+ module Form
5
+ # Form builder that renders Ui::Input, Ui::Textarea, Ui::Select, Ui::Checkbox,
6
+ # Ui::Label, and Ui::Button via render_in so forms match standalone components.
7
+ class Builder < ActionView::Helpers::FormBuilder
8
+ TEXT_FIELD_HELPERS = {
9
+ text_field: "text",
10
+ email_field: "email",
11
+ password_field: "password",
12
+ number_field: "number",
13
+ phone_field: "tel",
14
+ url_field: "url",
15
+ search_field: "search",
16
+ date_field: "date",
17
+ time_field: "time",
18
+ datetime_local_field: "datetime-local",
19
+ month_field: "month",
20
+ week_field: "week",
21
+ color_field: "color"
22
+ }.freeze
23
+
24
+ TEXT_FIELD_HELPERS.each do |method_name, input_type|
25
+ define_method(method_name) do |attribute, options = {}|
26
+ render_input_component(attribute, input_type, options)
27
+ end
28
+ end
29
+
30
+ def text_area(attribute, options = {})
31
+ opts = send(:objectify_options, options.deep_dup)
32
+ opts.delete(:object)
33
+ value = opts.delete(:value)
34
+
35
+ id = attr_id(attribute, opts)
36
+ name = attr_name(attribute, opts)
37
+ html_attrs = component_html_attrs(opts)
38
+
39
+ comp = Ui::Textarea::Component.new(**html_attrs.merge(name: name, id: id))
40
+ comp.with_content(value.to_s)
41
+ render_component_instance(comp)
42
+ end
43
+
44
+ def select(attribute, choices = nil, options = {}, html_options = {}, &block)
45
+ return super if block_given?
46
+ return super if choices.is_a?(Hash)
47
+
48
+ html_opts = html_options.deep_dup
49
+ sel_opts = options.deep_dup
50
+ prompt = sel_opts.delete(:prompt)
51
+
52
+ merged = send(:objectify_options, html_opts.deep_dup)
53
+ merged.delete(:object)
54
+ merged[:multiple] = true if html_opts[:multiple]
55
+ selected = merged.delete(:value)
56
+
57
+ id = attr_id(attribute, merged)
58
+ name = attr_name(attribute, merged)
59
+ html_attrs = component_html_attrs(merged)
60
+ pairs = normalize_select_choices(choices)
61
+
62
+ comp = Ui::Select::Component.new(
63
+ options: pairs,
64
+ prompt: prompt,
65
+ selected: selected,
66
+ **html_attrs.merge(name: name, id: id)
67
+ )
68
+ render_component_instance(comp)
69
+ end
70
+
71
+ def label(attribute, text = nil, options = {}, &block)
72
+ opts = send(:objectify_options, options.deep_dup)
73
+ opts.delete(:object)
74
+ id_for = opts.delete(:for) || attr_id(attribute, opts)
75
+ html_attrs = component_html_attrs(opts)
76
+
77
+ content =
78
+ if block_given?
79
+ @template.capture(&block)
80
+ else
81
+ text || attribute.to_s.humanize
82
+ end
83
+
84
+ comp = Ui::Label::Component.new(for: id_for, **html_attrs)
85
+ comp.with_content(content)
86
+ render_component_instance(comp)
87
+ end
88
+
89
+ def check_box(attribute, options = {}, checked_value = "1", unchecked_value = "0")
90
+ label_text = options.delete(:label)
91
+ opts = send(:objectify_options, options.deep_dup)
92
+ opts.delete(:object)
93
+
94
+ id = attr_id(attribute, opts)
95
+ name = attr_name(attribute, opts)
96
+ html_attrs = component_html_attrs(opts)
97
+ checked = checkbox_checked?(attribute, checked_value)
98
+ hidden = hidden_field(attribute, value: unchecked_value, id: nil)
99
+
100
+ box_attrs = html_attrs.merge(name: name, id: id, value: checked_value)
101
+ box_attrs[:checked] = true if checked
102
+
103
+ comp = Ui::Checkbox::Component.new(label: label_text, **box_attrs)
104
+ hidden + render_component_instance(comp)
105
+ end
106
+
107
+ def submit(value = nil, options = {})
108
+ variant = options.delete(:variant) || :default
109
+ size = options.delete(:size) || :md
110
+ opts = send(:objectify_options, options.deep_dup)
111
+ opts.delete(:object)
112
+ html_attrs = component_html_attrs(opts)
113
+ html_attrs[:name] ||= "commit"
114
+ html_attrs[:type] = "submit"
115
+ label = value.presence || default_submit_label
116
+
117
+ comp = Ui::Button::Component.new(variant: variant, size: size, tag: :button, **html_attrs)
118
+ comp.with_content(label)
119
+ render_component_instance(comp)
120
+ end
121
+
122
+ # Renders label, control, optional description, and model errors.
123
+ def field(attribute, as: :text_field, label: nil, description: nil, **options)
124
+ label_text = label || attribute.to_s.humanize
125
+ parts = []
126
+
127
+ if as == :check_box
128
+ options[:label] = label_text
129
+ parts << check_box(attribute, options)
130
+ else
131
+ parts << self.label(attribute, label_text)
132
+ parts << build_control(attribute, as, options)
133
+ end
134
+
135
+ parts << @template.content_tag(:p, description, class: "ui-form-description") if description
136
+ parts << error_message(attribute)
137
+
138
+ @template.content_tag(:div, parts.compact.join.html_safe, class: "ui-form-field")
139
+ end
140
+
141
+ private
142
+
143
+ def render_input_component(attribute, input_type, options)
144
+ opts = send(:objectify_options, options.deep_dup)
145
+ opts.delete(:object)
146
+ id = attr_id(attribute, opts)
147
+ name = attr_name(attribute, opts)
148
+ html_attrs = component_html_attrs(opts)
149
+
150
+ comp = Ui::Input::Component.new(**html_attrs.merge(name: name, id: id, type: input_type))
151
+ render_component_instance(comp)
152
+ end
153
+
154
+ def build_control(attribute, as, options)
155
+ case as
156
+ when :select
157
+ collection = options.delete(:collection) || []
158
+ prompt = options.delete(:prompt)
159
+ select(attribute, collection, { prompt: prompt }, options)
160
+ when :text_area
161
+ text_area(attribute, options)
162
+ else
163
+ send(as, attribute, options)
164
+ end
165
+ end
166
+
167
+ def error_message(attribute)
168
+ return unless object.respond_to?(:errors) && object.errors[attribute].any?
169
+
170
+ messages = object.errors.full_messages_for(attribute).join(", ")
171
+ @template.content_tag(:p, messages, class: "ui-form-error")
172
+ end
173
+
174
+ def render_component_instance(component)
175
+ component.render_in(@template)
176
+ end
177
+
178
+ def attr_id(attribute, opts)
179
+ opts[:id].presence || @template.field_id(object_name, attribute)
180
+ end
181
+
182
+ def attr_name(attribute, opts)
183
+ if opts[:multiple]
184
+ @template.field_name(object_name, attribute, multiple: true)
185
+ elsif opts[:name].present?
186
+ opts[:name]
187
+ else
188
+ @template.field_name(object_name, attribute)
189
+ end
190
+ end
191
+
192
+ def component_html_attrs(opts)
193
+ opts.except(:id, :name, :value, :type, :object, :skip_default_ids)
194
+ end
195
+
196
+ def normalize_select_choices(choices)
197
+ return [] if choices.nil?
198
+
199
+ Array(choices).map { |c| c.is_a?(Array) ? c : [c, c] }
200
+ end
201
+
202
+ def checkbox_checked?(attribute, checked_value)
203
+ return false unless object.respond_to?(attribute)
204
+
205
+ val = object.public_send(attribute)
206
+ case val
207
+ when TrueClass, FalseClass
208
+ val
209
+ when Array
210
+ val.map(&:to_s).include?(checked_value.to_s)
211
+ else
212
+ val.to_s == checked_value.to_s
213
+ end
214
+ end
215
+
216
+ def default_submit_label
217
+ unless object.respond_to?(:persisted?)
218
+ return I18n.t("helpers.submit.default", default: "Save")
219
+ end
220
+
221
+ if object.persisted?
222
+ I18n.t("helpers.submit.update", model: model_name_from_object, default: "Update")
223
+ else
224
+ I18n.t("helpers.submit.create", model: model_name_from_object, default: "Create")
225
+ end
226
+ end
227
+
228
+ def model_name_from_object
229
+ object.respond_to?(:model_name) ? object.model_name.human : ""
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ui
4
+ module Form
5
+ # Custom form builder that renders the same ViewComponents as standalone usage
6
+ # (Ui::Input, Ui::Textarea, Ui::Select, Ui::Checkbox, Ui::Label, Ui::Button)
7
+ # so forms stay visually and structurally consistent with the rest of your UI.
8
+ #
9
+ # form_with model: @user, builder: Ui::Form::Builder, class: "ui-form" do |f|
10
+ # f.field :email, as: :email_field, placeholder: "you@example.com"
11
+ # f.submit "Save"
12
+ # end
13
+ #
14
+ # Add these components first so the classes exist:
15
+ # rails generate uikit_rails:add input textarea select checkbox label button form
16
+ #
17
+ class Builder < ActionView::Helpers::FormBuilder
18
+ TEXT_FIELD_HELPERS = {
19
+ text_field: "text",
20
+ email_field: "email",
21
+ password_field: "password",
22
+ number_field: "number",
23
+ phone_field: "tel",
24
+ url_field: "url",
25
+ search_field: "search",
26
+ date_field: "date",
27
+ time_field: "time",
28
+ datetime_local_field: "datetime-local",
29
+ month_field: "month",
30
+ week_field: "week",
31
+ color_field: "color"
32
+ }.freeze
33
+
34
+ TEXT_FIELD_HELPERS.each do |method_name, input_type|
35
+ define_method(method_name) do |attribute, options = {}|
36
+ render_input_component(attribute, input_type, options)
37
+ end
38
+ end
39
+
40
+ def text_area(attribute, options = {})
41
+ opts = send(:objectify_options, options.deep_dup)
42
+ opts.delete(:object)
43
+ value = opts.delete(:value)
44
+
45
+ id = attr_id(attribute, opts)
46
+ name = attr_name(attribute, opts)
47
+ html_attrs = component_html_attrs(opts)
48
+
49
+ comp = Ui::Textarea::Component.new(**html_attrs.merge(name: name, id: id))
50
+ comp.with_content(value.to_s)
51
+ render_component_instance(comp)
52
+ end
53
+
54
+ def select(attribute, choices = nil, options = {}, html_options = {}, &block)
55
+ return super if block_given?
56
+ return super if choices.is_a?(Hash) # grouped options — fall back to Rails helper
57
+
58
+ html_opts = html_options.deep_dup
59
+ sel_opts = options.deep_dup
60
+
61
+ prompt = sel_opts.delete(:prompt)
62
+
63
+ merged = send(:objectify_options, html_opts.deep_dup)
64
+ merged.delete(:object)
65
+ merged[:multiple] = true if html_opts[:multiple]
66
+ selected = merged.delete(:value)
67
+
68
+ id = attr_id(attribute, merged)
69
+ name = attr_name(attribute, merged)
70
+ html_attrs = component_html_attrs(merged)
71
+
72
+ pairs = normalize_select_choices(choices)
73
+
74
+ comp = Ui::Select::Component.new(
75
+ options: pairs,
76
+ prompt: prompt,
77
+ selected: selected,
78
+ **html_attrs.merge(name: name, id: id)
79
+ )
80
+ render_component_instance(comp)
81
+ end
82
+
83
+ def label(attribute, text = nil, options = {}, &block)
84
+ opts = send(:objectify_options, options.deep_dup)
85
+ opts.delete(:object)
86
+
87
+ id_for = opts.delete(:for) || attr_id(attribute, opts)
88
+ html_attrs = component_html_attrs(opts)
89
+
90
+ content = if block_given?
91
+ @template.capture(&block)
92
+ else
93
+ text || attribute.to_s.humanize
94
+ end
95
+
96
+ comp = Ui::Label::Component.new(for: id_for, **html_attrs)
97
+ comp.with_content(content)
98
+ render_component_instance(comp)
99
+ end
100
+
101
+ def check_box(attribute, options = {}, checked_value = "1", unchecked_value = "0")
102
+ label_text = options.delete(:label)
103
+ opts = send(:objectify_options, options.deep_dup)
104
+ opts.delete(:object)
105
+
106
+ id = attr_id(attribute, opts)
107
+ name = attr_name(attribute, opts)
108
+ html_attrs = component_html_attrs(opts)
109
+
110
+ checked = checkbox_checked?(attribute, checked_value)
111
+
112
+ hidden = hidden_field(attribute, value: unchecked_value, id: nil)
113
+
114
+ box_attrs = html_attrs.merge(name: name, id: id, value: checked_value)
115
+ box_attrs[:checked] = true if checked
116
+
117
+ comp = Ui::Checkbox::Component.new(label: label_text, **box_attrs)
118
+
119
+ hidden + render_component_instance(comp)
120
+ end
121
+
122
+ def submit(value = nil, options = {})
123
+ variant = options.delete(:variant) || :default
124
+ size = options.delete(:size) || :md
125
+
126
+ opts = send(:objectify_options, options.deep_dup)
127
+ opts.delete(:object)
128
+ html_attrs = component_html_attrs(opts)
129
+
130
+ html_attrs[:name] ||= "commit"
131
+ html_attrs[:type] = "submit"
132
+
133
+ label = value.presence || default_submit_label
134
+
135
+ comp = Ui::Button::Component.new(variant: variant, size: size, tag: :button, **html_attrs)
136
+ comp.with_content(label)
137
+ render_component_instance(comp)
138
+ end
139
+
140
+ # Renders a complete form field: label + control + error messages.
141
+ #
142
+ # f.field :email, as: :email_field, placeholder: "you@example.com"
143
+ # f.field :bio, as: :text_area, rows: 4
144
+ # f.field :role, as: :select, collection: %w[admin editor viewer]
145
+ # f.field :terms, as: :check_box, label: "I agree"
146
+ #
147
+ def field(attribute, as: :text_field, label: nil, description: nil, **options)
148
+ label_text = label || attribute.to_s.humanize
149
+ parts = []
150
+
151
+ if as == :check_box
152
+ options[:label] = label_text
153
+ parts << check_box(attribute, options)
154
+ else
155
+ parts << self.label(attribute, label_text)
156
+ parts << build_control(attribute, as, options)
157
+ end
158
+
159
+ parts << @template.content_tag(:p, description, class: "ui-form-description") if description
160
+ parts << error_message(attribute)
161
+
162
+ @template.content_tag(:div, parts.compact.join.html_safe, class: "ui-form-field")
163
+ end
164
+
165
+ private
166
+
167
+ def render_input_component(attribute, input_type, options)
168
+ opts = send(:objectify_options, options.deep_dup)
169
+ opts.delete(:object)
170
+
171
+ id = attr_id(attribute, opts)
172
+ name = attr_name(attribute, opts)
173
+ html_attrs = component_html_attrs(opts)
174
+
175
+ comp = Ui::Input::Component.new(**html_attrs.merge(name: name, id: id, type: input_type))
176
+ render_component_instance(comp)
177
+ end
178
+
179
+ def build_control(attribute, as, options)
180
+ case as
181
+ when :select
182
+ collection = options.delete(:collection) || []
183
+ prompt = options.delete(:prompt)
184
+ select(attribute, collection, { prompt: prompt }, options)
185
+ when :text_area
186
+ text_area(attribute, options)
187
+ else
188
+ send(as, attribute, options)
189
+ end
190
+ end
191
+
192
+ def error_message(attribute)
193
+ return unless object.respond_to?(:errors) && object.errors[attribute].any?
194
+
195
+ messages = object.errors.full_messages_for(attribute).join(", ")
196
+ @template.content_tag(:p, messages, class: "ui-form-error")
197
+ end
198
+
199
+ def render_component_instance(component)
200
+ component.render_in(@template)
201
+ end
202
+
203
+ def attr_id(attribute, opts)
204
+ opts[:id].presence || @template.field_id(object_name, attribute)
205
+ end
206
+
207
+ def attr_name(attribute, opts)
208
+ if opts[:multiple]
209
+ @template.field_name(object_name, attribute, multiple: true)
210
+ elsif opts[:name].present?
211
+ opts[:name]
212
+ else
213
+ @template.field_name(object_name, attribute)
214
+ end
215
+ end
216
+
217
+ def component_html_attrs(opts)
218
+ opts.except(:id, :name, :value, :type, :object, :skip_default_ids)
219
+ end
220
+
221
+ def normalize_select_choices(choices)
222
+ return [] if choices.nil?
223
+
224
+ Array(choices).map { |c| c.is_a?(Array) ? c : [c, c] }
225
+ end
226
+
227
+ def checkbox_checked?(attribute, checked_value)
228
+ return false unless object.respond_to?(attribute)
229
+
230
+ val = object.public_send(attribute)
231
+ case val
232
+ when TrueClass, FalseClass
233
+ val
234
+ when Array
235
+ val.map(&:to_s).include?(checked_value.to_s)
236
+ else
237
+ val.to_s == checked_value.to_s
238
+ end
239
+ end
240
+
241
+ def default_submit_label
242
+ unless object.respond_to?(:persisted?)
243
+ return I18n.t("helpers.submit.default", default: "Save")
244
+ end
245
+
246
+ if object.persisted?
247
+ I18n.t("helpers.submit.update", model: model_name_from_object, default: "Update")
248
+ else
249
+ I18n.t("helpers.submit.create", model: model_name_from_object, default: "Create")
250
+ end
251
+ end
252
+
253
+ def model_name_from_object
254
+ object.respond_to?(:model_name) ? object.model_name.human : ""
255
+ end
256
+ end
257
+ end
258
+ end