better_ui 0.6.0 → 0.7.0

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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +257 -212
  3. data/Rakefile +11 -2
  4. data/app/components/better_ui/action_messages_component/action_messages_component.html.erb +48 -0
  5. data/app/components/better_ui/action_messages_component.rb +544 -0
  6. data/app/components/better_ui/application_component.rb +66 -0
  7. data/app/components/better_ui/button_component/button_component.html.erb +31 -0
  8. data/app/components/better_ui/button_component.rb +307 -0
  9. data/app/components/better_ui/card_component/card_component.html.erb +17 -0
  10. data/app/components/better_ui/card_component.rb +460 -0
  11. data/app/components/better_ui/drawer/header_component/header_component.html.erb +24 -0
  12. data/app/components/better_ui/drawer/header_component.rb +238 -0
  13. data/app/components/better_ui/drawer/layout_component/layout_component.html.erb +44 -0
  14. data/app/components/better_ui/drawer/layout_component.rb +270 -0
  15. data/app/components/better_ui/drawer/nav_group_component/nav_group_component.html.erb +10 -0
  16. data/app/components/better_ui/drawer/nav_group_component.rb +155 -0
  17. data/app/components/better_ui/drawer/nav_item_component/nav_item_component.html.erb +13 -0
  18. data/app/components/better_ui/drawer/nav_item_component.rb +225 -0
  19. data/app/components/better_ui/drawer/sidebar_component/sidebar_component.html.erb +17 -0
  20. data/app/components/better_ui/drawer/sidebar_component.rb +263 -0
  21. data/app/components/better_ui/forms/base_component.rb +450 -0
  22. data/app/components/better_ui/forms/checkbox_component/checkbox_component.html.erb +28 -0
  23. data/app/components/better_ui/forms/checkbox_component.rb +419 -0
  24. data/app/components/better_ui/forms/checkbox_group_component/checkbox_group_component.html.erb +40 -0
  25. data/app/components/better_ui/forms/checkbox_group_component.rb +363 -0
  26. data/app/components/better_ui/forms/number_input_component/number_input_component.html.erb +40 -0
  27. data/app/components/better_ui/forms/number_input_component.rb +320 -0
  28. data/app/components/better_ui/forms/password_input_component/password_input_component.html.erb +71 -0
  29. data/app/components/better_ui/forms/password_input_component.rb +206 -0
  30. data/app/components/better_ui/forms/text_input_component/text_input_component.html.erb +40 -0
  31. data/app/components/better_ui/forms/text_input_component.rb +258 -0
  32. data/app/components/better_ui/forms/textarea_component/textarea_component.html.erb +40 -0
  33. data/app/components/better_ui/forms/textarea_component.rb +329 -0
  34. data/app/form_builders/better_ui/ui_form_builder.rb +467 -0
  35. data/app/helpers/better_ui/application_helper.rb +325 -58
  36. data/app/views/layouts/better_ui/application.html.erb +1 -1
  37. data/config/routes.rb +1 -0
  38. data/lib/better_ui/engine.rb +34 -5
  39. data/lib/better_ui/version.rb +1 -1
  40. data/lib/better_ui.rb +32 -5
  41. data/lib/generators/better_ui/install/USAGE +44 -0
  42. data/lib/generators/better_ui/install/install_generator.rb +87 -0
  43. data/lib/generators/better_ui/install/templates/better_ui_theme.css.tt +280 -0
  44. data/lib/tasks/better_ui_tasks.rake +39 -4
  45. metadata +52 -200
  46. data/app/components/better_ui/application/card/component.html.erb +0 -20
  47. data/app/components/better_ui/application/card/component.rb +0 -214
  48. data/app/components/better_ui/application/main/component.html.erb +0 -9
  49. data/app/components/better_ui/application/main/component.rb +0 -123
  50. data/app/components/better_ui/application/navbar/component.html.erb +0 -92
  51. data/app/components/better_ui/application/navbar/component.rb +0 -136
  52. data/app/components/better_ui/application/sidebar/component.html.erb +0 -249
  53. data/app/components/better_ui/application/sidebar/component.rb +0 -187
  54. data/app/components/better_ui/general/accordion/component.html.erb +0 -5
  55. data/app/components/better_ui/general/accordion/component.rb +0 -92
  56. data/app/components/better_ui/general/accordion/item_component.html.erb +0 -12
  57. data/app/components/better_ui/general/accordion/item_component.rb +0 -176
  58. data/app/components/better_ui/general/alert/component.html.erb +0 -32
  59. data/app/components/better_ui/general/alert/component.rb +0 -242
  60. data/app/components/better_ui/general/avatar/component.html.erb +0 -20
  61. data/app/components/better_ui/general/avatar/component.rb +0 -301
  62. data/app/components/better_ui/general/badge/component.html.erb +0 -23
  63. data/app/components/better_ui/general/badge/component.rb +0 -248
  64. data/app/components/better_ui/general/breadcrumb/component.html.erb +0 -15
  65. data/app/components/better_ui/general/breadcrumb/component.rb +0 -187
  66. data/app/components/better_ui/general/button/component.html.erb +0 -34
  67. data/app/components/better_ui/general/button/component.rb +0 -214
  68. data/app/components/better_ui/general/divider/component.html.erb +0 -10
  69. data/app/components/better_ui/general/divider/component.rb +0 -226
  70. data/app/components/better_ui/general/dropdown/component.html.erb +0 -28
  71. data/app/components/better_ui/general/dropdown/component.rb +0 -192
  72. data/app/components/better_ui/general/dropdown/divider_component.html.erb +0 -1
  73. data/app/components/better_ui/general/dropdown/divider_component.rb +0 -41
  74. data/app/components/better_ui/general/dropdown/item_component.html.erb +0 -6
  75. data/app/components/better_ui/general/dropdown/item_component.rb +0 -119
  76. data/app/components/better_ui/general/field/component.html.erb +0 -27
  77. data/app/components/better_ui/general/field/component.rb +0 -37
  78. data/app/components/better_ui/general/grid/cell_component.html.erb +0 -3
  79. data/app/components/better_ui/general/grid/cell_component.rb +0 -390
  80. data/app/components/better_ui/general/grid/component.html.erb +0 -3
  81. data/app/components/better_ui/general/grid/component.rb +0 -301
  82. data/app/components/better_ui/general/heading/component.html.erb +0 -22
  83. data/app/components/better_ui/general/heading/component.rb +0 -257
  84. data/app/components/better_ui/general/icon/component.html.erb +0 -7
  85. data/app/components/better_ui/general/icon/component.rb +0 -240
  86. data/app/components/better_ui/general/input/checkbox/component.html.erb +0 -5
  87. data/app/components/better_ui/general/input/checkbox/component.rb +0 -238
  88. data/app/components/better_ui/general/input/datetime/component.html.erb +0 -5
  89. data/app/components/better_ui/general/input/datetime/component.rb +0 -223
  90. data/app/components/better_ui/general/input/pin/component.html.erb +0 -1
  91. data/app/components/better_ui/general/input/pin/component.rb +0 -201
  92. data/app/components/better_ui/general/input/radio/component.html.erb +0 -5
  93. data/app/components/better_ui/general/input/radio/component.rb +0 -230
  94. data/app/components/better_ui/general/input/rating/component.html.erb +0 -4
  95. data/app/components/better_ui/general/input/rating/component.rb +0 -272
  96. data/app/components/better_ui/general/input/select/component.html.erb +0 -78
  97. data/app/components/better_ui/general/input/select/component.rb +0 -249
  98. data/app/components/better_ui/general/input/select/select_component.html.erb +0 -5
  99. data/app/components/better_ui/general/input/select/select_component.rb +0 -37
  100. data/app/components/better_ui/general/input/text/component.html.erb +0 -5
  101. data/app/components/better_ui/general/input/text/component.rb +0 -171
  102. data/app/components/better_ui/general/input/textarea/component.html.erb +0 -5
  103. data/app/components/better_ui/general/input/textarea/component.rb +0 -166
  104. data/app/components/better_ui/general/input/toggle/component.html.erb +0 -5
  105. data/app/components/better_ui/general/input/toggle/component.rb +0 -242
  106. data/app/components/better_ui/general/link/component.html.erb +0 -18
  107. data/app/components/better_ui/general/link/component.rb +0 -258
  108. data/app/components/better_ui/general/modal/component.html.erb +0 -5
  109. data/app/components/better_ui/general/modal/component.rb +0 -47
  110. data/app/components/better_ui/general/modal/modal_component.html.erb +0 -52
  111. data/app/components/better_ui/general/modal/modal_component.rb +0 -160
  112. data/app/components/better_ui/general/pagination/component.html.erb +0 -85
  113. data/app/components/better_ui/general/pagination/component.rb +0 -216
  114. data/app/components/better_ui/general/panel/component.html.erb +0 -28
  115. data/app/components/better_ui/general/panel/component.rb +0 -249
  116. data/app/components/better_ui/general/progress/component.html.erb +0 -11
  117. data/app/components/better_ui/general/progress/component.rb +0 -160
  118. data/app/components/better_ui/general/spinner/component.html.erb +0 -35
  119. data/app/components/better_ui/general/spinner/component.rb +0 -93
  120. data/app/components/better_ui/general/table/component.html.erb +0 -5
  121. data/app/components/better_ui/general/table/component.rb +0 -217
  122. data/app/components/better_ui/general/table/tbody_component.html.erb +0 -3
  123. data/app/components/better_ui/general/table/tbody_component.rb +0 -30
  124. data/app/components/better_ui/general/table/td_component.html.erb +0 -3
  125. data/app/components/better_ui/general/table/td_component.rb +0 -44
  126. data/app/components/better_ui/general/table/tfoot_component.html.erb +0 -3
  127. data/app/components/better_ui/general/table/tfoot_component.rb +0 -28
  128. data/app/components/better_ui/general/table/th_component.html.erb +0 -6
  129. data/app/components/better_ui/general/table/th_component.rb +0 -51
  130. data/app/components/better_ui/general/table/thead_component.html.erb +0 -3
  131. data/app/components/better_ui/general/table/thead_component.rb +0 -28
  132. data/app/components/better_ui/general/table/tr_component.html.erb +0 -3
  133. data/app/components/better_ui/general/table/tr_component.rb +0 -30
  134. data/app/components/better_ui/general/tabs/component.html.erb +0 -11
  135. data/app/components/better_ui/general/tabs/component.rb +0 -120
  136. data/app/components/better_ui/general/tabs/panel_component.html.erb +0 -3
  137. data/app/components/better_ui/general/tabs/panel_component.rb +0 -37
  138. data/app/components/better_ui/general/tabs/tab_component.html.erb +0 -13
  139. data/app/components/better_ui/general/tabs/tab_component.rb +0 -111
  140. data/app/components/better_ui/general/tag/component.html.erb +0 -3
  141. data/app/components/better_ui/general/tag/component.rb +0 -104
  142. data/app/components/better_ui/general/text/component.html.erb +0 -1
  143. data/app/components/better_ui/general/text/component.rb +0 -194
  144. data/app/components/better_ui/general/tooltip/component.html.erb +0 -7
  145. data/app/components/better_ui/general/tooltip/component.rb +0 -239
  146. data/app/helpers/better_ui/application/components/card/card_helper.rb +0 -96
  147. data/app/helpers/better_ui/application/components/card.rb +0 -11
  148. data/app/helpers/better_ui/application/components/main/main_helper.rb +0 -64
  149. data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +0 -77
  150. data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +0 -51
  151. data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +0 -73
  152. data/app/helpers/better_ui/general/components/alert/alert_helper.rb +0 -57
  153. data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +0 -29
  154. data/app/helpers/better_ui/general/components/badge/badge_helper.rb +0 -53
  155. data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +0 -37
  156. data/app/helpers/better_ui/general/components/button/button_helper.rb +0 -65
  157. data/app/helpers/better_ui/general/components/container/container_helper.rb +0 -60
  158. data/app/helpers/better_ui/general/components/divider/divider_helper.rb +0 -63
  159. data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +0 -32
  160. data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +0 -88
  161. data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -68
  162. data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
  163. data/app/helpers/better_ui/general/components/grid/grid_helper.rb +0 -145
  164. data/app/helpers/better_ui/general/components/heading/heading_helper.rb +0 -72
  165. data/app/helpers/better_ui/general/components/icon/icon_helper.rb +0 -16
  166. data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +0 -81
  167. data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +0 -91
  168. data/app/helpers/better_ui/general/components/input/pin/pin_helper.rb +0 -76
  169. data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +0 -79
  170. data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +0 -124
  171. data/app/helpers/better_ui/general/components/input/rating/rating_helper.rb +0 -70
  172. data/app/helpers/better_ui/general/components/input/select/select_helper.rb +0 -86
  173. data/app/helpers/better_ui/general/components/input/text/text_helper.rb +0 -138
  174. data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +0 -73
  175. data/app/helpers/better_ui/general/components/input/toggle/toggle_helper.rb +0 -77
  176. data/app/helpers/better_ui/general/components/link/link_helper.rb +0 -89
  177. data/app/helpers/better_ui/general/components/modal/modal_helper.rb +0 -85
  178. data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +0 -82
  179. data/app/helpers/better_ui/general/components/panel/panel_helper.rb +0 -83
  180. data/app/helpers/better_ui/general/components/progress/progress_helper.rb +0 -53
  181. data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +0 -19
  182. data/app/helpers/better_ui/general/components/table/table_helper.rb +0 -53
  183. data/app/helpers/better_ui/general/components/table/tbody_helper.rb +0 -13
  184. data/app/helpers/better_ui/general/components/table/td_helper.rb +0 -19
  185. data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +0 -13
  186. data/app/helpers/better_ui/general/components/table/th_helper.rb +0 -19
  187. data/app/helpers/better_ui/general/components/table/thead_helper.rb +0 -13
  188. data/app/helpers/better_ui/general/components/table/tr_helper.rb +0 -13
  189. data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +0 -62
  190. data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +0 -55
  191. data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +0 -95
  192. data/app/helpers/better_ui/general/components/tag/tag_helper.rb +0 -26
  193. data/app/helpers/better_ui/general/components/text/text_helper.rb +0 -83
  194. data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +0 -60
  195. data/app/jobs/better_ui/application_job.rb +0 -4
  196. data/app/mailers/better_ui/application_mailer.rb +0 -6
  197. data/config/initializers/lookbook.rb +0 -23
  198. data/lib/better_ui/railtie.rb +0 -20
@@ -0,0 +1,467 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ # Custom Rails form builder for rendering form inputs using BetterUi::Forms components.
5
+ #
6
+ # This form builder integrates seamlessly with ActiveModel objects to automatically
7
+ # populate field values, validation errors, and required status from the model.
8
+ # All form inputs are rendered using ViewComponents from the BetterUi::Forms namespace,
9
+ # ensuring consistent styling and behavior across the application.
10
+ #
11
+ # @example Basic usage with form_with
12
+ # <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
13
+ # <%= f.bui_text_input :name %>
14
+ # <%= f.bui_text_input :email, hint: "We'll never share your email" %>
15
+ # <%= f.bui_number_input :age, min: 0, max: 120 %>
16
+ # <% end %>
17
+ #
18
+ # @example With custom labels and sizes
19
+ # <%= form_with model: @product, builder: BetterUi::UiFormBuilder do |f| %>
20
+ # <%= f.bui_text_input :title, label: "Product Name", size: :lg %>
21
+ # <%= f.bui_number_input :price, label: "Price ($)", min: 0, step: 0.01 %>
22
+ # <% end %>
23
+ #
24
+ # @example With icon slots
25
+ # <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
26
+ # <%= f.bui_text_input :email do |component| %>
27
+ # <% component.with_prefix_icon do %>
28
+ # <svg class="h-5 w-5 text-gray-400">...</svg>
29
+ # <% end %>
30
+ # <% end %>
31
+ #
32
+ # <%= f.bui_number_input :budget do |component| %>
33
+ # <% component.with_prefix_icon { "$" } %>
34
+ # <% end %>
35
+ # <% end %>
36
+ #
37
+ # @example Automatic error handling
38
+ # # When @user has validation errors:
39
+ # <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
40
+ # <%= f.bui_text_input :email %>
41
+ # # Automatically displays error messages and applies error styling
42
+ # <% end %>
43
+ #
44
+ # @see BetterUi::Forms::TextInputComponent
45
+ # @see BetterUi::Forms::NumberInputComponent
46
+ # @see BetterUi::Forms::PasswordInputComponent
47
+ # @see BetterUi::Forms::TextareaComponent
48
+ # @see BetterUi::Forms::BaseComponent
49
+ class UiFormBuilder < ActionView::Helpers::FormBuilder
50
+ # Renders a text input field using BetterUi::Forms::TextInputComponent
51
+ #
52
+ # @param attribute [Symbol] The attribute name
53
+ # @param options [Hash] Additional options to pass to the component
54
+ # @option options [String] :label Custom label text (defaults to humanized attribute name)
55
+ # @option options [String] :hint Hint text to display below the input
56
+ # @option options [String] :placeholder Placeholder text
57
+ # @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
58
+ # @option options [Boolean] :disabled Whether the input is disabled
59
+ # @option options [Boolean] :readonly Whether the input is readonly
60
+ # @option options [Boolean] :required Whether the input is required
61
+ # @return [String] Rendered component HTML
62
+ #
63
+ # @example Basic usage
64
+ # <%= f.bui_text_input :email %>
65
+ #
66
+ # @example With options
67
+ # <%= f.bui_text_input :email, size: :lg, hint: "We'll never share your email" %>
68
+ #
69
+ # @example With icon
70
+ # <%= f.bui_text_input :email do |c| %>
71
+ # <% c.with_prefix_icon { "📧" } %>
72
+ # <% end %>
73
+ def bui_text_input(attribute, options = {}, &block)
74
+ component_options = build_input_options(attribute, options)
75
+
76
+ if block_given?
77
+ @template.render(BetterUi::Forms::TextInputComponent.new(**component_options), &block)
78
+ else
79
+ @template.render(BetterUi::Forms::TextInputComponent.new(**component_options))
80
+ end
81
+ end
82
+
83
+ # Renders a number input field using BetterUi::Forms::NumberInputComponent
84
+ #
85
+ # @param attribute [Symbol] The attribute name
86
+ # @param options [Hash] Additional options to pass to the component
87
+ # @option options [String] :label Custom label text (defaults to humanized attribute name)
88
+ # @option options [String] :hint Hint text to display below the input
89
+ # @option options [String] :placeholder Placeholder text
90
+ # @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
91
+ # @option options [Boolean] :disabled Whether the input is disabled
92
+ # @option options [Boolean] :readonly Whether the input is readonly
93
+ # @option options [Boolean] :required Whether the input is required
94
+ # @option options [Numeric] :min Minimum value
95
+ # @option options [Numeric] :max Maximum value
96
+ # @option options [Numeric] :step Step value
97
+ # @option options [Boolean] :show_spinner Whether to show up/down spinner arrows (default: true)
98
+ # @return [String] Rendered component HTML
99
+ #
100
+ # @example Basic usage
101
+ # <%= f.bui_number_input :age %>
102
+ #
103
+ # @example With range and step
104
+ # <%= f.bui_number_input :price, min: 0, max: 10000, step: 0.01 %>
105
+ #
106
+ # @example Without spinners
107
+ # <%= f.bui_number_input :price, show_spinner: false %>
108
+ #
109
+ # @example With icon
110
+ # <%= f.bui_number_input :price do |c| %>
111
+ # <% c.with_prefix_icon { "$" } %>
112
+ # <% end %>
113
+ def bui_number_input(attribute, options = {}, &block)
114
+ component_options = build_input_options(attribute, options)
115
+
116
+ # Add number-specific options
117
+ component_options[:min] = options[:min] if options.key?(:min)
118
+ component_options[:max] = options[:max] if options.key?(:max)
119
+ component_options[:step] = options[:step] if options.key?(:step)
120
+ component_options[:show_spinner] = options.fetch(:show_spinner, true)
121
+
122
+ if block_given?
123
+ @template.render(BetterUi::Forms::NumberInputComponent.new(**component_options), &block)
124
+ else
125
+ @template.render(BetterUi::Forms::NumberInputComponent.new(**component_options))
126
+ end
127
+ end
128
+
129
+ # Renders a password input field using BetterUi::Forms::PasswordInputComponent
130
+ #
131
+ # @param attribute [Symbol] The attribute name
132
+ # @param options [Hash] Additional options to pass to the component
133
+ # @option options [String] :label Custom label text (defaults to humanized attribute name)
134
+ # @option options [String] :hint Hint text to display below the input
135
+ # @option options [String] :placeholder Placeholder text
136
+ # @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
137
+ # @option options [Boolean] :disabled Whether the input is disabled
138
+ # @option options [Boolean] :readonly Whether the input is readonly
139
+ # @option options [Boolean] :required Whether the input is required
140
+ # @return [String] Rendered component HTML
141
+ #
142
+ # @example Basic usage
143
+ # <%= f.bui_password_input :password %>
144
+ #
145
+ # @example With confirmation field
146
+ # <%= f.bui_password_input :password, hint: "Must be at least 8 characters" %>
147
+ # <%= f.bui_password_input :password_confirmation %>
148
+ #
149
+ # @example With icon
150
+ # <%= f.bui_password_input :password do |c| %>
151
+ # <% c.with_prefix_icon do %>
152
+ # <svg class="h-5 w-5 text-gray-400">...</svg>
153
+ # <% end %>
154
+ # <% end %>
155
+ def bui_password_input(attribute, options = {}, &block)
156
+ component_options = build_input_options(attribute, options)
157
+
158
+ if block_given?
159
+ @template.render(BetterUi::Forms::PasswordInputComponent.new(**component_options), &block)
160
+ else
161
+ @template.render(BetterUi::Forms::PasswordInputComponent.new(**component_options))
162
+ end
163
+ end
164
+
165
+ # Renders a textarea field using BetterUi::Forms::TextareaComponent
166
+ #
167
+ # @param attribute [Symbol] The attribute name
168
+ # @param options [Hash] Additional options to pass to the component
169
+ # @option options [String] :label Custom label text (defaults to humanized attribute name)
170
+ # @option options [String] :hint Hint text to display below the textarea
171
+ # @option options [String] :placeholder Placeholder text
172
+ # @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
173
+ # @option options [Boolean] :disabled Whether the textarea is disabled
174
+ # @option options [Boolean] :readonly Whether the textarea is readonly
175
+ # @option options [Boolean] :required Whether the textarea is required
176
+ # @option options [Integer] :rows Number of visible text lines (default: 4)
177
+ # @option options [Integer] :cols Width in characters (optional)
178
+ # @option options [Integer] :maxlength Maximum number of characters allowed
179
+ # @option options [Symbol] :resize CSS resize behavior (:none, :vertical, :horizontal, :both)
180
+ # @return [String] Rendered component HTML
181
+ #
182
+ # @example Basic usage
183
+ # <%= f.bui_textarea :description %>
184
+ #
185
+ # @example With custom rows and maxlength
186
+ # <%= f.bui_textarea :bio, rows: 6, maxlength: 500, hint: "Maximum 500 characters" %>
187
+ #
188
+ # @example With resize disabled
189
+ # <%= f.bui_textarea :notes, resize: :none %>
190
+ #
191
+ # @example With icon
192
+ # <%= f.bui_textarea :comment do |c| %>
193
+ # <% c.with_prefix_icon do %>
194
+ # <svg class="h-5 w-5 text-gray-400">...</svg>
195
+ # <% end %>
196
+ # <% end %>
197
+ def bui_textarea(attribute, options = {}, &block)
198
+ component_options = build_input_options(attribute, options)
199
+
200
+ # Add textarea-specific options
201
+ component_options[:rows] = options.fetch(:rows, 4)
202
+ component_options[:cols] = options[:cols] if options.key?(:cols)
203
+ component_options[:maxlength] = options[:maxlength] if options.key?(:maxlength)
204
+ component_options[:resize] = options.fetch(:resize, :vertical)
205
+
206
+ if block_given?
207
+ @template.render(BetterUi::Forms::TextareaComponent.new(**component_options), &block)
208
+ else
209
+ @template.render(BetterUi::Forms::TextareaComponent.new(**component_options))
210
+ end
211
+ end
212
+
213
+ # Renders a checkbox input field using BetterUi::Forms::CheckboxComponent
214
+ #
215
+ # @param attribute [Symbol] The attribute name
216
+ # @param options [Hash] Additional options to pass to the component
217
+ # @option options [String] :label Custom label text (defaults to humanized attribute name)
218
+ # @option options [String] :value The value submitted when checkbox is checked (default: "1")
219
+ # @option options [String] :hint Hint text to display below the checkbox
220
+ # @option options [Symbol] :variant Color variant (:primary, :secondary, :accent, :success, :danger, :warning, :info, :light, :dark)
221
+ # @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
222
+ # @option options [Symbol] :label_position Position of label relative to checkbox (:left, :right)
223
+ # @option options [Boolean] :disabled Whether the checkbox is disabled
224
+ # @option options [Boolean] :readonly Whether the checkbox is readonly
225
+ # @option options [Boolean] :required Whether the checkbox is required
226
+ # @return [String] Rendered component HTML
227
+ #
228
+ # @example Basic usage
229
+ # <%= f.bui_checkbox :newsletter %>
230
+ #
231
+ # @example With custom label
232
+ # <%= f.bui_checkbox :terms, label: "I agree to the terms and conditions" %>
233
+ #
234
+ # @example With variant
235
+ # <%= f.bui_checkbox :active, variant: :success, label: "Active" %>
236
+ def bui_checkbox(attribute, options = {})
237
+ component_options = build_checkbox_options(attribute, options)
238
+
239
+ @template.render(BetterUi::Forms::CheckboxComponent.new(**component_options))
240
+ end
241
+
242
+ # Renders a checkbox group using BetterUi::Forms::CheckboxGroupComponent
243
+ #
244
+ # @param attribute [Symbol] The attribute name
245
+ # @param collection [Array] The collection of options, can be:
246
+ # - Array of values (e.g., ["Admin", "Editor"])
247
+ # - Array of [label, value] pairs (e.g., [["Admin", "admin"], ["Editor", "editor"]])
248
+ # @param options [Hash] Additional options to pass to the component
249
+ # @option options [String] :legend Legend text for the fieldset (defaults to humanized attribute name)
250
+ # @option options [String] :hint Hint text to display below the checkboxes
251
+ # @option options [Symbol] :variant Color variant for all checkboxes (:primary, :secondary, etc.)
252
+ # @option options [Symbol] :size Size variant (:xs, :sm, :md, :lg, :xl)
253
+ # @option options [Symbol] :orientation Layout orientation (:vertical, :horizontal)
254
+ # @option options [Boolean] :disabled Whether all checkboxes are disabled
255
+ # @option options [Boolean] :required Whether the field is required
256
+ # @return [String] Rendered component HTML
257
+ #
258
+ # @example Basic usage
259
+ # <%= f.bui_checkbox_group :roles, ["Admin", "Editor", "Viewer"] %>
260
+ #
261
+ # @example With label/value pairs
262
+ # <%= f.bui_checkbox_group :permissions, [["Read", "read"], ["Write", "write"]] %>
263
+ #
264
+ # @example Horizontal layout
265
+ # <%= f.bui_checkbox_group :interests, ["Sports", "Music", "Art"], orientation: :horizontal %>
266
+ def bui_checkbox_group(attribute, collection, options = {})
267
+ component_options = build_checkbox_group_options(attribute, collection, options)
268
+
269
+ @template.render(BetterUi::Forms::CheckboxGroupComponent.new(**component_options))
270
+ end
271
+
272
+ private
273
+
274
+ # Builds common options for input components.
275
+ #
276
+ # Extracts and merges options from the form model, user-provided options,
277
+ # and default values. Automatically populates field name, value, errors,
278
+ # and required status from the ActiveModel object.
279
+ #
280
+ # @param attribute [Symbol] the attribute name
281
+ # @param options [Hash] user-provided options to override defaults
282
+ # @return [Hash] complete hash of component options ready for rendering
283
+ # @api private
284
+ def build_input_options(attribute, options)
285
+ {
286
+ name: field_name(attribute),
287
+ value: object_value(attribute),
288
+ label: options.fetch(:label, attribute.to_s.humanize),
289
+ hint: options[:hint],
290
+ placeholder: options[:placeholder],
291
+ size: options.fetch(:size, :md),
292
+ disabled: options.fetch(:disabled, false),
293
+ readonly: options.fetch(:readonly, false),
294
+ required: options.fetch(:required, field_required?(attribute)),
295
+ errors: object_errors(attribute),
296
+ container_classes: options[:container_classes],
297
+ label_classes: options[:label_classes],
298
+ input_classes: options[:input_classes],
299
+ hint_classes: options[:hint_classes],
300
+ error_classes: options[:error_classes]
301
+ }.merge(extract_html_attributes(options))
302
+ end
303
+
304
+ # Gets the value of an attribute from the object.
305
+ #
306
+ # Safely retrieves the attribute value from the form's model object,
307
+ # returning nil if the object doesn't respond to the attribute method.
308
+ #
309
+ # @param attribute [Symbol] the attribute name
310
+ # @return [Object, nil] the attribute value, or nil if not accessible
311
+ # @api private
312
+ def object_value(attribute)
313
+ @object&.public_send(attribute)
314
+ rescue NoMethodError
315
+ nil
316
+ end
317
+
318
+ # Gets validation errors for an attribute.
319
+ #
320
+ # Retrieves full error messages for the specified attribute from the
321
+ # model's errors object. Returns empty array if model doesn't support errors.
322
+ #
323
+ # @param attribute [Symbol] the attribute name
324
+ # @return [Array<String>] array of full error messages for the attribute
325
+ # @api private
326
+ def object_errors(attribute)
327
+ return [] unless @object&.respond_to?(:errors)
328
+
329
+ @object.errors.full_messages_for(attribute)
330
+ end
331
+
332
+ # Determines if a field is required based on model validations.
333
+ #
334
+ # Inspects the model's validators to detect presence validators on the
335
+ # specified attribute. Returns true if a PresenceValidator is found.
336
+ #
337
+ # @param attribute [Symbol] the attribute name
338
+ # @return [Boolean] true if the field has a presence validator, false otherwise
339
+ # @api private
340
+ def field_required?(attribute)
341
+ return false unless @object.class.respond_to?(:validators_on)
342
+
343
+ validators = @object.class.validators_on(attribute)
344
+ validators.any? { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }
345
+ rescue NoMethodError
346
+ false
347
+ end
348
+
349
+ # Extracts HTML attributes from options.
350
+ #
351
+ # Filters user-provided options to extract standard HTML attributes
352
+ # (id, class, data, aria, etc.) that should be passed through to the
353
+ # input element.
354
+ #
355
+ # @param options [Hash] user-provided options
356
+ # @return [Hash] hash of HTML attributes to pass through
357
+ # @api private
358
+ def extract_html_attributes(options)
359
+ html_attrs = {}
360
+
361
+ # Pass through common HTML attributes
362
+ html_attrs[:id] = options[:id] if options.key?(:id)
363
+ html_attrs[:class] = options[:class] if options.key?(:class)
364
+ html_attrs[:data] = options[:data] if options.key?(:data)
365
+ html_attrs[:aria] = options[:aria] if options.key?(:aria)
366
+ html_attrs[:autocomplete] = options[:autocomplete] if options.key?(:autocomplete)
367
+ html_attrs[:maxlength] = options[:maxlength] if options.key?(:maxlength)
368
+ html_attrs[:pattern] = options[:pattern] if options.key?(:pattern)
369
+
370
+ html_attrs
371
+ end
372
+
373
+ # Generates the field name for form submission.
374
+ #
375
+ # Creates the proper HTML name attribute for the input field, following
376
+ # Rails conventions for nested attributes (e.g., "user[email]").
377
+ # If no object_name is present, returns the attribute as a string.
378
+ #
379
+ # @param attribute [Symbol] the attribute name
380
+ # @return [String] the properly formatted field name for form submission
381
+ # @api private
382
+ # @example With object name
383
+ # field_name(:email) # => "user[email]" (when object_name is "user")
384
+ # @example Without object name
385
+ # field_name(:email) # => "email"
386
+ def field_name(attribute)
387
+ @object_name ? "#{@object_name}[#{attribute}]" : attribute.to_s
388
+ end
389
+
390
+ # Builds options for checkbox components.
391
+ #
392
+ # Creates the complete options hash for CheckboxComponent including
393
+ # auto-populated values from the model for checked state and errors.
394
+ #
395
+ # @param attribute [Symbol] the attribute name
396
+ # @param options [Hash] user-provided options to override defaults
397
+ # @return [Hash] complete hash of component options ready for rendering
398
+ # @api private
399
+ def build_checkbox_options(attribute, options)
400
+ {
401
+ name: field_name(attribute),
402
+ value: options.fetch(:value, "1"),
403
+ checked: checkbox_checked?(attribute),
404
+ label: options.fetch(:label, attribute.to_s.humanize),
405
+ hint: options[:hint],
406
+ variant: options.fetch(:variant, :primary),
407
+ size: options.fetch(:size, :md),
408
+ label_position: options.fetch(:label_position, :right),
409
+ disabled: options.fetch(:disabled, false),
410
+ readonly: options.fetch(:readonly, false),
411
+ required: options.fetch(:required, field_required?(attribute)),
412
+ errors: object_errors(attribute),
413
+ container_classes: options[:container_classes],
414
+ label_classes: options[:label_classes],
415
+ checkbox_classes: options[:checkbox_classes],
416
+ hint_classes: options[:hint_classes],
417
+ error_classes: options[:error_classes]
418
+ }.merge(extract_html_attributes(options))
419
+ end
420
+
421
+ # Builds options for checkbox group components.
422
+ #
423
+ # Creates the complete options hash for CheckboxGroupComponent including
424
+ # auto-populated values from the model for selected values and errors.
425
+ #
426
+ # @param attribute [Symbol] the attribute name
427
+ # @param collection [Array] the collection of options
428
+ # @param options [Hash] user-provided options to override defaults
429
+ # @return [Hash] complete hash of component options ready for rendering
430
+ # @api private
431
+ def build_checkbox_group_options(attribute, collection, options)
432
+ {
433
+ name: field_name(attribute),
434
+ collection: collection,
435
+ selected: Array(object_value(attribute)),
436
+ legend: options.fetch(:legend, attribute.to_s.humanize),
437
+ hint: options[:hint],
438
+ variant: options.fetch(:variant, :primary),
439
+ size: options.fetch(:size, :md),
440
+ orientation: options.fetch(:orientation, :vertical),
441
+ disabled: options.fetch(:disabled, false),
442
+ required: options.fetch(:required, field_required?(attribute)),
443
+ errors: object_errors(attribute),
444
+ container_classes: options[:container_classes],
445
+ legend_classes: options[:legend_classes],
446
+ items_classes: options[:items_classes],
447
+ hint_classes: options[:hint_classes],
448
+ error_classes: options[:error_classes]
449
+ }.merge(extract_html_attributes(options))
450
+ end
451
+
452
+ # Determines if a checkbox should be checked based on the model's attribute value.
453
+ #
454
+ # For boolean attributes, returns the boolean value.
455
+ # For other types, checks if the value is present/truthy.
456
+ #
457
+ # @param attribute [Symbol] the attribute name
458
+ # @return [Boolean] true if the checkbox should be checked, false otherwise
459
+ # @api private
460
+ def checkbox_checked?(attribute)
461
+ value = object_value(attribute)
462
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
463
+
464
+ value.present?
465
+ end
466
+ end
467
+ end