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,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Forms
5
+ # A text input component with support for labels, hints, errors, and prefix/suffix icons.
6
+ #
7
+ # This component extends {BaseComponent} to provide a standard text input field with
8
+ # optional decorative or functional icons positioned before (prefix) or after (suffix)
9
+ # the input text. Perfect for search fields, email inputs, URL fields, and more.
10
+ #
11
+ # @example Basic text input
12
+ # <%= render BetterUi::Forms::TextInputComponent.new(
13
+ # name: "user[email]",
14
+ # label: "Email Address",
15
+ # placeholder: "you@example.com"
16
+ # ) %>
17
+ #
18
+ # @example Text input with prefix icon (search)
19
+ # <%= render BetterUi::Forms::TextInputComponent.new(
20
+ # name: "search",
21
+ # label: "Search",
22
+ # placeholder: "Search..."
23
+ # ) do |component| %>
24
+ # <% component.with_prefix_icon do %>
25
+ # <svg class="h-5 w-5 text-gray-400">...</svg>
26
+ # <% end %>
27
+ # <% end %>
28
+ #
29
+ # @example Text input with suffix icon (verified checkmark)
30
+ # <%= render BetterUi::Forms::TextInputComponent.new(
31
+ # name: "user[email]",
32
+ # value: "user@example.com",
33
+ # label: "Verified Email"
34
+ # ) do |component| %>
35
+ # <% component.with_suffix_icon do %>
36
+ # <svg class="h-5 w-5 text-success-600">...</svg>
37
+ # <% end %>
38
+ # <% end %>
39
+ #
40
+ # @example Text input with both icons
41
+ # <%= render BetterUi::Forms::TextInputComponent.new(
42
+ # name: "website",
43
+ # label: "Website URL",
44
+ # placeholder: "example.com"
45
+ # ) do |component| %>
46
+ # <% component.with_prefix_icon do %>
47
+ # <span class="text-gray-500">https://</span>
48
+ # <% end %>
49
+ # <% component.with_suffix_icon do %>
50
+ # <svg class="h-5 w-5 text-gray-400">...</svg>
51
+ # <% end %>
52
+ # <% end %>
53
+ #
54
+ # @example With validation errors
55
+ # <%= render BetterUi::Forms::TextInputComponent.new(
56
+ # name: "user[email]",
57
+ # value: "invalid",
58
+ # label: "Email",
59
+ # errors: ["Email is invalid", "Email can't be blank"]
60
+ # ) %>
61
+ #
62
+ # @example Using with Rails form builder
63
+ # <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
64
+ # <%= f.ui_text_input :email do |component| %>
65
+ # <% component.with_prefix_icon do %>
66
+ # <svg>...</svg>
67
+ # <% end %>
68
+ # <% end %>
69
+ # <% end %>
70
+ #
71
+ # @see BaseComponent
72
+ # @see BetterUi::UiFormBuilder#ui_text_input
73
+ class TextInputComponent < BaseComponent
74
+ # @!method with_prefix_icon
75
+ # Slot for rendering an icon or content before (left of) the input text.
76
+ # The icon is positioned absolutely and input padding is adjusted automatically.
77
+ # @yieldreturn [String] the HTML content for the prefix icon
78
+ renders_one :prefix_icon
79
+
80
+ # @!method with_suffix_icon
81
+ # Slot for rendering an icon or content after (right of) the input text.
82
+ # The icon is positioned absolutely and input padding is adjusted automatically.
83
+ # @yieldreturn [String] the HTML content for the suffix icon
84
+ renders_one :suffix_icon
85
+
86
+ # Initializes a new text input component.
87
+ #
88
+ # All parameters are passed to {BaseComponent#initialize}. See the parent class
89
+ # for detailed parameter descriptions.
90
+ #
91
+ # @param (see BaseComponent#initialize)
92
+ # @option (see BaseComponent#initialize)
93
+ # @raise (see BaseComponent#initialize)
94
+ #
95
+ # @see BaseComponent#initialize
96
+ def initialize(
97
+ name:,
98
+ value: nil,
99
+ label: nil,
100
+ hint: nil,
101
+ placeholder: nil,
102
+ size: :md,
103
+ disabled: false,
104
+ readonly: false,
105
+ required: false,
106
+ errors: nil,
107
+ container_classes: nil,
108
+ label_classes: nil,
109
+ input_classes: nil,
110
+ hint_classes: nil,
111
+ error_classes: nil,
112
+ **options
113
+ )
114
+ super(
115
+ name: name,
116
+ value: value,
117
+ label: label,
118
+ hint: hint,
119
+ placeholder: placeholder,
120
+ size: size,
121
+ disabled: disabled,
122
+ readonly: readonly,
123
+ required: required,
124
+ errors: errors,
125
+ container_classes: container_classes,
126
+ label_classes: label_classes,
127
+ input_classes: input_classes,
128
+ hint_classes: hint_classes,
129
+ error_classes: error_classes,
130
+ **options
131
+ )
132
+ end
133
+
134
+ private
135
+
136
+ # Returns the HTML input type attribute.
137
+ #
138
+ # @return [String] the input type ("text")
139
+ # @api private
140
+ def input_type
141
+ "text"
142
+ end
143
+
144
+ # Checks if a prefix icon has been provided via the slot.
145
+ #
146
+ # @return [Boolean] true if prefix_icon slot is present, false otherwise
147
+ # @api private
148
+ def has_prefix_icon?
149
+ prefix_icon.present?
150
+ end
151
+
152
+ # Checks if a suffix icon has been provided via the slot.
153
+ #
154
+ # @return [Boolean] true if suffix_icon slot is present, false otherwise
155
+ # @api private
156
+ def has_suffix_icon?
157
+ suffix_icon.present?
158
+ end
159
+
160
+ # Returns the base CSS classes for icon wrapper elements.
161
+ #
162
+ # Icons are positioned absolutely within the input wrapper and include
163
+ # size-specific padding to ensure proper spacing.
164
+ #
165
+ # @return [String] the merged CSS class string for icon wrappers
166
+ # @api private
167
+ def icon_wrapper_classes
168
+ css_classes([
169
+ "absolute",
170
+ "inset-y-0",
171
+ "flex",
172
+ "items-center",
173
+ "pointer-events-none",
174
+ icon_size_padding
175
+ ].flatten.compact)
176
+ end
177
+
178
+ # Returns the CSS classes for the prefix icon wrapper.
179
+ #
180
+ # Extends icon_wrapper_classes with left positioning.
181
+ #
182
+ # @return [String] the merged CSS class string for the prefix icon wrapper
183
+ # @api private
184
+ def prefix_icon_classes
185
+ css_classes([
186
+ icon_wrapper_classes,
187
+ "left-0"
188
+ ].flatten.compact)
189
+ end
190
+
191
+ # Returns the CSS classes for the suffix icon wrapper.
192
+ #
193
+ # Extends icon_wrapper_classes with right positioning.
194
+ #
195
+ # @return [String] the merged CSS class string for the suffix icon wrapper
196
+ # @api private
197
+ def suffix_icon_classes
198
+ css_classes([
199
+ icon_wrapper_classes,
200
+ "right-0"
201
+ ].flatten.compact)
202
+ end
203
+
204
+ # Returns size-specific horizontal padding for icon wrappers.
205
+ #
206
+ # Ensures icons maintain proper spacing from the input borders across all sizes.
207
+ #
208
+ # @return [String] the padding class for the current component size
209
+ # @api private
210
+ def icon_size_padding
211
+ case @size
212
+ when :xs then "px-2"
213
+ when :sm then "px-3"
214
+ when :md then "px-4"
215
+ when :lg then "px-5"
216
+ when :xl then "px-6"
217
+ end
218
+ end
219
+
220
+ # Returns input element classes with icon-adjusted padding.
221
+ #
222
+ # When prefix or suffix icons are present, this method adds extra padding to the
223
+ # input element to prevent text from overlapping with the icons. Padding amount
224
+ # is proportional to the component size.
225
+ #
226
+ # @return [String] the CSS class string with icon padding adjustments
227
+ # @api private
228
+ def input_element_classes_with_icons
229
+ classes = input_element_classes
230
+ classes = "#{classes} pl-10" if has_prefix_icon? && @size == :md
231
+ classes = "#{classes} pr-10" if has_suffix_icon? && @size == :md
232
+ classes = "#{classes} pl-8" if has_prefix_icon? && @size == :sm
233
+ classes = "#{classes} pr-8" if has_suffix_icon? && @size == :sm
234
+ classes = "#{classes} pl-6" if has_prefix_icon? && @size == :xs
235
+ classes = "#{classes} pr-6" if has_suffix_icon? && @size == :xs
236
+ classes = "#{classes} pl-12" if has_prefix_icon? && @size == :lg
237
+ classes = "#{classes} pr-12" if has_suffix_icon? && @size == :lg
238
+ classes = "#{classes} pl-14" if has_prefix_icon? && @size == :xl
239
+ classes = "#{classes} pr-14" if has_suffix_icon? && @size == :xl
240
+ classes
241
+ end
242
+
243
+ # Returns the complete set of HTML attributes for the input element.
244
+ #
245
+ # Extends the parent implementation to add the input type and conditionally
246
+ # apply icon-adjusted classes when icons are present.
247
+ #
248
+ # @return [Hash] hash of HTML attributes for the input element
249
+ # @api private
250
+ def input_attributes
251
+ attrs = super
252
+ attrs[:type] = input_type
253
+ attrs[:class] = input_element_classes_with_icons if has_prefix_icon? || has_suffix_icon?
254
+ attrs
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,40 @@
1
+ <div class="<%= wrapper_classes %>">
2
+ <% if @label.present? %>
3
+ <label for="<%= @name %>" class="<%= label_element_classes %>">
4
+ <%= @label %>
5
+ <% if @required %>
6
+ <span class="text-danger-600">*</span>
7
+ <% end %>
8
+ </label>
9
+ <% end %>
10
+
11
+ <div class="<%= input_wrapper_classes %>">
12
+ <% if has_prefix_icon? %>
13
+ <div class="<%= prefix_icon_classes %>">
14
+ <%= prefix_icon %>
15
+ </div>
16
+ <% end %>
17
+
18
+ <textarea <%= tag.attributes(input_attributes) %>><%= @value %></textarea>
19
+
20
+ <% if has_suffix_icon? %>
21
+ <div class="<%= suffix_icon_classes %>">
22
+ <%= suffix_icon %>
23
+ </div>
24
+ <% end %>
25
+ </div>
26
+
27
+ <% if @hint.present? %>
28
+ <div class="<%= hint_element_classes %>">
29
+ <%= @hint %>
30
+ </div>
31
+ <% end %>
32
+
33
+ <% if has_errors? %>
34
+ <div class="<%= errors_element_classes %>">
35
+ <% @errors.each do |error| %>
36
+ <div><%= error %></div>
37
+ <% end %>
38
+ </div>
39
+ <% end %>
40
+ </div>
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Forms
5
+ # A textarea component with support for labels, hints, errors, and prefix/suffix icons.
6
+ #
7
+ # This component extends {BaseComponent} to provide a standard textarea field with
8
+ # optional decorative or functional icons positioned before (prefix) or after (suffix)
9
+ # the textarea. Perfect for multi-line text inputs like comments, descriptions, and messages.
10
+ #
11
+ # @example Basic textarea
12
+ # <%= render BetterUi::Forms::TextareaComponent.new(
13
+ # name: "post[content]",
14
+ # label: "Post Content",
15
+ # placeholder: "Write your post here..."
16
+ # ) %>
17
+ #
18
+ # @example Textarea with custom rows
19
+ # <%= render BetterUi::Forms::TextareaComponent.new(
20
+ # name: "comment[body]",
21
+ # label: "Comment",
22
+ # rows: 6,
23
+ # placeholder: "Enter your comment..."
24
+ # ) %>
25
+ #
26
+ # @example Textarea with prefix icon
27
+ # <%= render BetterUi::Forms::TextareaComponent.new(
28
+ # name: "description",
29
+ # label: "Description",
30
+ # placeholder: "Enter description..."
31
+ # ) do |component| %>
32
+ # <% component.with_prefix_icon do %>
33
+ # <svg class="h-5 w-5 text-gray-400">...</svg>
34
+ # <% end %>
35
+ # <% end %>
36
+ #
37
+ # @example Textarea with character limit
38
+ # <%= render BetterUi::Forms::TextareaComponent.new(
39
+ # name: "bio",
40
+ # label: "Bio",
41
+ # maxlength: 500,
42
+ # hint: "Maximum 500 characters"
43
+ # ) %>
44
+ #
45
+ # @example Textarea with disabled resize
46
+ # <%= render BetterUi::Forms::TextareaComponent.new(
47
+ # name: "notes",
48
+ # label: "Notes",
49
+ # resize: :none
50
+ # ) %>
51
+ #
52
+ # @example With validation errors
53
+ # <%= render BetterUi::Forms::TextareaComponent.new(
54
+ # name: "post[content]",
55
+ # value: "",
56
+ # label: "Content",
57
+ # errors: ["Content can't be blank", "Content is too short"]
58
+ # ) %>
59
+ #
60
+ # @example Using with Rails form builder
61
+ # <%= form_with model: @post, builder: BetterUi::UiFormBuilder do |f| %>
62
+ # <%= f.ui_textarea :content do |component| %>
63
+ # <% component.with_prefix_icon do %>
64
+ # <svg>...</svg>
65
+ # <% end %>
66
+ # <% end %>
67
+ # <% end %>
68
+ #
69
+ # @see BaseComponent
70
+ # @see BetterUi::UiFormBuilder#ui_textarea
71
+ class TextareaComponent < BaseComponent
72
+ # @!method with_prefix_icon
73
+ # Slot for rendering an icon or content before (left of) the textarea.
74
+ # The icon is positioned absolutely at the top left and textarea padding is adjusted automatically.
75
+ # @yieldreturn [String] the HTML content for the prefix icon
76
+ renders_one :prefix_icon
77
+
78
+ # @!method with_suffix_icon
79
+ # Slot for rendering an icon or content after (right of) the textarea.
80
+ # The icon is positioned absolutely at the top right and textarea padding is adjusted automatically.
81
+ # @yieldreturn [String] the HTML content for the suffix icon
82
+ renders_one :suffix_icon
83
+
84
+ # Initializes a new textarea component.
85
+ #
86
+ # All standard parameters are passed to {BaseComponent#initialize}. See the parent class
87
+ # for detailed parameter descriptions.
88
+ #
89
+ # @param name [String] the field name for form submission (required)
90
+ # @param value [String, nil] the current value of the textarea
91
+ # @param label [String, nil] the label text displayed above the textarea
92
+ # @param hint [String, nil] helper text displayed below the textarea
93
+ # @param placeholder [String, nil] placeholder text shown when textarea is empty
94
+ # @param size [Symbol] the size variant (:xs, :sm, :md, :lg, :xl), defaults to :md
95
+ # @param disabled [Boolean] whether the textarea is disabled, defaults to false
96
+ # @param readonly [Boolean] whether the textarea is read-only, defaults to false
97
+ # @param required [Boolean] whether the field is required (shows asterisk), defaults to false
98
+ # @param errors [Array<String>, String, nil] validation error messages to display
99
+ # @param rows [Integer] number of visible text lines, defaults to 4
100
+ # @param cols [Integer, nil] width in characters (optional, usually controlled by CSS)
101
+ # @param maxlength [Integer, nil] maximum number of characters allowed
102
+ # @param resize [Symbol] CSS resize behavior (:none, :vertical, :horizontal, :both), defaults to :vertical
103
+ # @param container_classes [String, nil] additional CSS classes for the outer wrapper
104
+ # @param label_classes [String, nil] additional CSS classes for the label element
105
+ # @param input_classes [String, nil] additional CSS classes for the textarea element
106
+ # @param hint_classes [String, nil] additional CSS classes for the hint text
107
+ # @param error_classes [String, nil] additional CSS classes for error messages
108
+ # @param options [Hash] additional HTML attributes passed to the textarea element
109
+ #
110
+ # @raise [ArgumentError] if size is not one of the allowed values
111
+ #
112
+ # @see BaseComponent#initialize
113
+ def initialize(
114
+ name:,
115
+ value: nil,
116
+ label: nil,
117
+ hint: nil,
118
+ placeholder: nil,
119
+ size: :md,
120
+ disabled: false,
121
+ readonly: false,
122
+ required: false,
123
+ errors: nil,
124
+ rows: 4,
125
+ cols: nil,
126
+ maxlength: nil,
127
+ resize: :vertical,
128
+ container_classes: nil,
129
+ label_classes: nil,
130
+ input_classes: nil,
131
+ hint_classes: nil,
132
+ error_classes: nil,
133
+ **options
134
+ )
135
+ super(
136
+ name: name,
137
+ value: value,
138
+ label: label,
139
+ hint: hint,
140
+ placeholder: placeholder,
141
+ size: size,
142
+ disabled: disabled,
143
+ readonly: readonly,
144
+ required: required,
145
+ errors: errors,
146
+ container_classes: container_classes,
147
+ label_classes: label_classes,
148
+ input_classes: input_classes,
149
+ hint_classes: hint_classes,
150
+ error_classes: error_classes,
151
+ **options
152
+ )
153
+
154
+ @rows = rows
155
+ @cols = cols
156
+ @maxlength = maxlength
157
+ @resize = resize
158
+ end
159
+
160
+ private
161
+
162
+ # Returns the HTML element type.
163
+ #
164
+ # @return [String] the element type ("textarea")
165
+ # @api private
166
+ def input_type
167
+ "textarea"
168
+ end
169
+
170
+ # Checks if a prefix icon has been provided via the slot.
171
+ #
172
+ # @return [Boolean] true if prefix_icon slot is present, false otherwise
173
+ # @api private
174
+ def has_prefix_icon?
175
+ prefix_icon.present?
176
+ end
177
+
178
+ # Checks if a suffix icon has been provided via the slot.
179
+ #
180
+ # @return [Boolean] true if suffix_icon slot is present, false otherwise
181
+ # @api private
182
+ def has_suffix_icon?
183
+ suffix_icon.present?
184
+ end
185
+
186
+ # Returns the base CSS classes for icon wrapper elements.
187
+ #
188
+ # Icons are positioned absolutely at the top of the textarea wrapper and include
189
+ # size-specific padding to ensure proper spacing.
190
+ #
191
+ # @return [String] the merged CSS class string for icon wrappers
192
+ # @api private
193
+ def icon_wrapper_classes
194
+ css_classes([
195
+ "absolute",
196
+ "top-0",
197
+ "flex",
198
+ "items-start",
199
+ "pointer-events-none",
200
+ icon_size_padding,
201
+ icon_vertical_padding
202
+ ].flatten.compact)
203
+ end
204
+
205
+ # Returns the CSS classes for the prefix icon wrapper.
206
+ #
207
+ # Extends icon_wrapper_classes with left positioning.
208
+ #
209
+ # @return [String] the merged CSS class string for the prefix icon wrapper
210
+ # @api private
211
+ def prefix_icon_classes
212
+ css_classes([
213
+ icon_wrapper_classes,
214
+ "left-0"
215
+ ].flatten.compact)
216
+ end
217
+
218
+ # Returns the CSS classes for the suffix icon wrapper.
219
+ #
220
+ # Extends icon_wrapper_classes with right positioning.
221
+ #
222
+ # @return [String] the merged CSS class string for the suffix icon wrapper
223
+ # @api private
224
+ def suffix_icon_classes
225
+ css_classes([
226
+ icon_wrapper_classes,
227
+ "right-0"
228
+ ].flatten.compact)
229
+ end
230
+
231
+ # Returns size-specific horizontal padding for icon wrappers.
232
+ #
233
+ # Ensures icons maintain proper spacing from the textarea borders across all sizes.
234
+ #
235
+ # @return [String] the padding class for the current component size
236
+ # @api private
237
+ def icon_size_padding
238
+ case @size
239
+ when :xs then "px-2"
240
+ when :sm then "px-3"
241
+ when :md then "px-4"
242
+ when :lg then "px-5"
243
+ when :xl then "px-6"
244
+ end
245
+ end
246
+
247
+ # Returns size-specific vertical padding for icon wrappers.
248
+ #
249
+ # Aligns icons properly with the top of the textarea text.
250
+ #
251
+ # @return [String] the vertical padding class for the current component size
252
+ # @api private
253
+ def icon_vertical_padding
254
+ case @size
255
+ when :xs then "pt-1"
256
+ when :sm then "pt-1.5"
257
+ when :md then "pt-2"
258
+ when :lg then "pt-2.5"
259
+ when :xl then "pt-3"
260
+ end
261
+ end
262
+
263
+ # Returns textarea element classes with icon-adjusted padding.
264
+ #
265
+ # When prefix or suffix icons are present, this method adds extra padding to the
266
+ # textarea element to prevent text from overlapping with the icons. Padding amount
267
+ # is proportional to the component size.
268
+ #
269
+ # @return [String] the CSS class string with icon padding adjustments
270
+ # @api private
271
+ def input_element_classes_with_icons
272
+ classes = input_element_classes
273
+ classes = "#{classes} pl-10" if has_prefix_icon? && @size == :md
274
+ classes = "#{classes} pr-10" if has_suffix_icon? && @size == :md
275
+ classes = "#{classes} pl-8" if has_prefix_icon? && @size == :sm
276
+ classes = "#{classes} pr-8" if has_suffix_icon? && @size == :sm
277
+ classes = "#{classes} pl-6" if has_prefix_icon? && @size == :xs
278
+ classes = "#{classes} pr-6" if has_suffix_icon? && @size == :xs
279
+ classes = "#{classes} pl-12" if has_prefix_icon? && @size == :lg
280
+ classes = "#{classes} pr-12" if has_suffix_icon? && @size == :lg
281
+ classes = "#{classes} pl-14" if has_prefix_icon? && @size == :xl
282
+ classes = "#{classes} pr-14" if has_suffix_icon? && @size == :xl
283
+ classes
284
+ end
285
+
286
+ # Returns CSS classes for resize behavior.
287
+ #
288
+ # Maps the resize parameter to Tailwind resize classes.
289
+ #
290
+ # @return [String] the resize class based on @resize value
291
+ # @api private
292
+ def resize_classes
293
+ case @resize
294
+ when :none then "resize-none"
295
+ when :vertical then "resize-y"
296
+ when :horizontal then "resize-x"
297
+ when :both then "resize"
298
+ else "resize-y" # default to vertical
299
+ end
300
+ end
301
+
302
+ # Returns the complete set of HTML attributes for the textarea element.
303
+ #
304
+ # Extends the parent implementation to add textarea-specific attributes (rows, cols, maxlength)
305
+ # and conditionally apply icon-adjusted classes and resize classes.
306
+ #
307
+ # @return [Hash] hash of HTML attributes for the textarea element
308
+ # @api private
309
+ def input_attributes
310
+ attrs = super
311
+ attrs.delete(:type) # textareas don't have a type attribute
312
+ attrs[:rows] = @rows
313
+ attrs[:cols] = @cols if @cols.present?
314
+ attrs[:maxlength] = @maxlength if @maxlength.present?
315
+
316
+ # Build classes: base classes (with icon adjustments if needed) + resize classes
317
+ if has_prefix_icon? || has_suffix_icon?
318
+ classes = "#{input_element_classes_with_icons} #{resize_classes}".strip
319
+ else
320
+ classes = attrs[:class] || ""
321
+ classes = "#{classes} #{resize_classes}".strip
322
+ end
323
+
324
+ attrs[:class] = classes
325
+ attrs
326
+ end
327
+ end
328
+ end
329
+ end