better_ui 0.6.0 → 0.7.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 (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 +55 -203
  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,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Forms
5
+ # A number input component with support for min/max/step constraints, spinner controls,
6
+ # and prefix/suffix icons.
7
+ #
8
+ # This component extends {BaseComponent} to provide a number input field with optional
9
+ # decorative or functional icons, numeric constraints (min, max, step), and the ability
10
+ # to hide the browser's default up/down arrow spinner buttons.
11
+ #
12
+ # @example Basic number input
13
+ # <%= render BetterUi::Forms::NumberInputComponent.new(
14
+ # name: "user[age]",
15
+ # label: "Age",
16
+ # min: 0,
17
+ # max: 120
18
+ # ) %>
19
+ #
20
+ # @example Number input with step (decimal values)
21
+ # <%= render BetterUi::Forms::NumberInputComponent.new(
22
+ # name: "product[price]",
23
+ # label: "Price",
24
+ # min: 0,
25
+ # step: 0.01,
26
+ # placeholder: "0.00"
27
+ # ) %>
28
+ #
29
+ # @example Number input without spinner controls
30
+ # <%= render BetterUi::Forms::NumberInputComponent.new(
31
+ # name: "quantity",
32
+ # label: "Quantity",
33
+ # min: 1,
34
+ # show_spinner: false
35
+ # ) %>
36
+ #
37
+ # @example Number input with prefix icon (currency)
38
+ # <%= render BetterUi::Forms::NumberInputComponent.new(
39
+ # name: "amount",
40
+ # label: "Amount",
41
+ # min: 0,
42
+ # step: 0.01
43
+ # ) do |component| %>
44
+ # <% component.with_prefix_icon do %>
45
+ # <span class="text-gray-500">$</span>
46
+ # <% end %>
47
+ # <% end %>
48
+ #
49
+ # @example Number input with suffix icon (unit)
50
+ # <%= render BetterUi::Forms::NumberInputComponent.new(
51
+ # name: "weight",
52
+ # label: "Weight",
53
+ # min: 0
54
+ # ) do |component| %>
55
+ # <% component.with_suffix_icon do %>
56
+ # <span class="text-gray-500">kg</span>
57
+ # <% end %>
58
+ # <% end %>
59
+ #
60
+ # @example With validation errors
61
+ # <%= render BetterUi::Forms::NumberInputComponent.new(
62
+ # name: "user[age]",
63
+ # value: "150",
64
+ # label: "Age",
65
+ # errors: ["Age must be between 0 and 120"]
66
+ # ) %>
67
+ #
68
+ # @example Using with Rails form builder
69
+ # <%= form_with model: @product, builder: BetterUi::UiFormBuilder do |f| %>
70
+ # <%= f.ui_number_input :price, min: 0, step: 0.01 do |component| %>
71
+ # <% component.with_prefix_icon do %>
72
+ # <span>$</span>
73
+ # <% end %>
74
+ # <% end %>
75
+ # <% end %>
76
+ #
77
+ # @see BaseComponent
78
+ # @see TextInputComponent
79
+ # @see BetterUi::UiFormBuilder#ui_number_input
80
+ class NumberInputComponent < BaseComponent
81
+ # @!method with_prefix_icon
82
+ # Slot for rendering an icon or content before (left of) the input text.
83
+ # The icon is positioned absolutely and input padding is adjusted automatically.
84
+ # @yieldreturn [String] the HTML content for the prefix icon
85
+ renders_one :prefix_icon
86
+
87
+ # @!method with_suffix_icon
88
+ # Slot for rendering an icon or content after (right of) the input text.
89
+ # The icon is positioned absolutely and input padding is adjusted automatically.
90
+ # @yieldreturn [String] the HTML content for the suffix icon
91
+ renders_one :suffix_icon
92
+
93
+ # Initializes a new number input component.
94
+ #
95
+ # Accepts all parameters from {BaseComponent#initialize} plus additional
96
+ # number-specific parameters for constraints and spinner control.
97
+ #
98
+ # @param name [String] the name attribute for the input field (required for form submission)
99
+ # @param value [String, Numeric, nil] the current value of the input field
100
+ # @param label [String, nil] the label text to display above the input
101
+ # @param hint [String, nil] helpful hint text displayed below the input
102
+ # @param placeholder [String, nil] placeholder text shown when input is empty
103
+ # @param size [Symbol] the size variant (:xs, :sm, :md, :lg, :xl), defaults to :md
104
+ # @param disabled [Boolean] whether the input should be disabled (non-interactive), defaults to false
105
+ # @param readonly [Boolean] whether the input should be readonly (viewable but not editable), defaults to false
106
+ # @param required [Boolean] whether the field is required (shows asterisk indicator), defaults to false
107
+ # @param errors [Array<String>, String, nil] validation error messages to display below the input
108
+ # @param min [Numeric, nil] the minimum allowed value (HTML5 min attribute)
109
+ # @param max [Numeric, nil] the maximum allowed value (HTML5 max attribute)
110
+ # @param step [Numeric, nil] the step increment for value changes (e.g., 0.01 for currency, 1 for integers)
111
+ # @param show_spinner [Boolean] whether to display browser's up/down arrow buttons, defaults to true
112
+ # @param container_classes [String, Array<String>, nil] additional CSS classes for the outer wrapper
113
+ # @param label_classes [String, Array<String>, nil] additional CSS classes for the label element
114
+ # @param input_classes [String, Array<String>, nil] additional CSS classes for the input element
115
+ # @param hint_classes [String, Array<String>, nil] additional CSS classes for the hint text
116
+ # @param error_classes [String, Array<String>, nil] additional CSS classes for error messages
117
+ # @param options [Hash] additional HTML attributes to pass through to the input element
118
+ #
119
+ # @raise [ArgumentError] if size is not one of the valid SIZES
120
+ #
121
+ # @example Basic initialization with constraints
122
+ # BetterUi::Forms::NumberInputComponent.new(
123
+ # name: "product[quantity]",
124
+ # label: "Quantity",
125
+ # min: 1,
126
+ # max: 100,
127
+ # step: 1
128
+ # )
129
+ #
130
+ # @example Currency input with decimal precision
131
+ # BetterUi::Forms::NumberInputComponent.new(
132
+ # name: "product[price]",
133
+ # label: "Price",
134
+ # min: 0,
135
+ # step: 0.01,
136
+ # show_spinner: false
137
+ # )
138
+ #
139
+ # @see BaseComponent#initialize
140
+ def initialize(
141
+ name:,
142
+ value: nil,
143
+ label: nil,
144
+ hint: nil,
145
+ placeholder: nil,
146
+ size: :md,
147
+ disabled: false,
148
+ readonly: false,
149
+ required: false,
150
+ errors: nil,
151
+ min: nil,
152
+ max: nil,
153
+ step: nil,
154
+ show_spinner: true,
155
+ container_classes: nil,
156
+ label_classes: nil,
157
+ input_classes: nil,
158
+ hint_classes: nil,
159
+ error_classes: nil,
160
+ **options
161
+ )
162
+ @min = min
163
+ @max = max
164
+ @step = step
165
+ @show_spinner = show_spinner
166
+
167
+ super(
168
+ name: name,
169
+ value: value,
170
+ label: label,
171
+ hint: hint,
172
+ placeholder: placeholder,
173
+ size: size,
174
+ disabled: disabled,
175
+ readonly: readonly,
176
+ required: required,
177
+ errors: errors,
178
+ container_classes: container_classes,
179
+ label_classes: label_classes,
180
+ input_classes: input_classes,
181
+ hint_classes: hint_classes,
182
+ error_classes: error_classes,
183
+ **options
184
+ )
185
+ end
186
+
187
+ private
188
+
189
+ # Returns the HTML input type attribute.
190
+ #
191
+ # @return [String] the input type ("number")
192
+ # @api private
193
+ def input_type
194
+ "number"
195
+ end
196
+
197
+ # Checks if a prefix icon has been provided via the slot.
198
+ #
199
+ # @return [Boolean] true if prefix_icon slot is present, false otherwise
200
+ # @api private
201
+ def has_prefix_icon?
202
+ prefix_icon.present?
203
+ end
204
+
205
+ # Checks if a suffix icon has been provided via the slot.
206
+ #
207
+ # @return [Boolean] true if suffix_icon slot is present, false otherwise
208
+ # @api private
209
+ def has_suffix_icon?
210
+ suffix_icon.present?
211
+ end
212
+
213
+ # Returns the base CSS classes for icon wrapper elements.
214
+ #
215
+ # Icons are positioned absolutely within the input wrapper and include
216
+ # size-specific padding to ensure proper spacing.
217
+ #
218
+ # @return [String] the merged CSS class string for icon wrappers
219
+ # @api private
220
+ def icon_wrapper_classes
221
+ css_classes([
222
+ "absolute",
223
+ "inset-y-0",
224
+ "flex",
225
+ "items-center",
226
+ "pointer-events-none",
227
+ icon_size_padding
228
+ ].flatten.compact)
229
+ end
230
+
231
+ # Returns the CSS classes for the prefix icon wrapper.
232
+ #
233
+ # Extends icon_wrapper_classes with left positioning.
234
+ #
235
+ # @return [String] the merged CSS class string for the prefix icon wrapper
236
+ # @api private
237
+ def prefix_icon_classes
238
+ css_classes([
239
+ icon_wrapper_classes,
240
+ "left-0"
241
+ ].flatten.compact)
242
+ end
243
+
244
+ # Returns the CSS classes for the suffix icon wrapper.
245
+ #
246
+ # Extends icon_wrapper_classes with right positioning.
247
+ #
248
+ # @return [String] the merged CSS class string for the suffix icon wrapper
249
+ # @api private
250
+ def suffix_icon_classes
251
+ css_classes([
252
+ icon_wrapper_classes,
253
+ "right-0"
254
+ ].flatten.compact)
255
+ end
256
+
257
+ # Returns size-specific horizontal padding for icon wrappers.
258
+ #
259
+ # Ensures icons maintain proper spacing from the input borders across all sizes.
260
+ #
261
+ # @return [String] the padding class for the current component size
262
+ # @api private
263
+ def icon_size_padding
264
+ case @size
265
+ when :xs then "px-2"
266
+ when :sm then "px-3"
267
+ when :md then "px-4"
268
+ when :lg then "px-5"
269
+ when :xl then "px-6"
270
+ end
271
+ end
272
+
273
+ # Returns input element classes with icon-adjusted padding and optional spinner hiding.
274
+ #
275
+ # When prefix or suffix icons are present, this method adds extra padding to the
276
+ # input element to prevent text from overlapping with the icons. When show_spinner
277
+ # is false, adds the 'hide-number-spinner' class to hide browser's up/down arrows.
278
+ # Padding amount is proportional to the component size.
279
+ #
280
+ # @return [String] the CSS class string with icon padding and spinner visibility adjustments
281
+ # @api private
282
+ def input_element_classes_with_icons
283
+ classes = input_element_classes
284
+ classes = "#{classes} pl-10" if has_prefix_icon? && @size == :md
285
+ classes = "#{classes} pr-10" if has_suffix_icon? && @size == :md
286
+ classes = "#{classes} pl-8" if has_prefix_icon? && @size == :sm
287
+ classes = "#{classes} pr-8" if has_suffix_icon? && @size == :sm
288
+ classes = "#{classes} pl-6" if has_prefix_icon? && @size == :xs
289
+ classes = "#{classes} pr-6" if has_suffix_icon? && @size == :xs
290
+ classes = "#{classes} pl-12" if has_prefix_icon? && @size == :lg
291
+ classes = "#{classes} pr-12" if has_suffix_icon? && @size == :lg
292
+ classes = "#{classes} pl-14" if has_prefix_icon? && @size == :xl
293
+ classes = "#{classes} pr-14" if has_suffix_icon? && @size == :xl
294
+ classes = "#{classes} hide-number-spinner" unless @show_spinner
295
+ classes
296
+ end
297
+
298
+ # Returns the complete set of HTML attributes for the number input element.
299
+ #
300
+ # Extends the parent implementation to add number-specific attributes:
301
+ # - type: "number"
302
+ # - min: minimum allowed value (if specified)
303
+ # - max: maximum allowed value (if specified)
304
+ # - step: increment step for value changes (if specified)
305
+ # - class: icon-adjusted classes when icons are present or spinner is hidden
306
+ #
307
+ # @return [Hash] hash of HTML attributes for the input element
308
+ # @api private
309
+ def input_attributes
310
+ attrs = super
311
+ attrs[:type] = input_type
312
+ attrs[:min] = @min if @min
313
+ attrs[:max] = @max if @max
314
+ attrs[:step] = @step if @step
315
+ attrs[:class] = input_element_classes_with_icons if has_prefix_icon? || has_suffix_icon? || !@show_spinner
316
+ attrs
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,71 @@
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 <%= tag.attributes(input_wrapper_attributes) %>>
12
+ <% if has_prefix_icon? %>
13
+ <div class="<%= prefix_icon_classes %>">
14
+ <%= prefix_icon %>
15
+ </div>
16
+ <% end %>
17
+
18
+ <input <%= tag.attributes(input_attributes) %> />
19
+
20
+ <% if has_toggle_button? %>
21
+ <div class="<%= toggle_button_classes %>">
22
+ <button
23
+ type="button"
24
+ tabindex="-1"
25
+ class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 transition-colors duration-150"
26
+ data-action="mousedown->better-ui--forms--password-input#toggle"
27
+ aria-label="Toggle password visibility"
28
+ >
29
+ <!-- Eye icon (shown when password is hidden) -->
30
+ <svg
31
+ class="<%= toggle_icon_size %>"
32
+ fill="none"
33
+ stroke="currentColor"
34
+ viewBox="0 0 24 24"
35
+ xmlns="http://www.w3.org/2000/svg"
36
+ data-better-ui--forms--password-input-target="eyeIcon"
37
+ >
38
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
39
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
40
+ </svg>
41
+
42
+ <!-- Eye-slash icon (shown when password is visible) -->
43
+ <svg
44
+ class="<%= toggle_icon_size %> hidden"
45
+ fill="none"
46
+ stroke="currentColor"
47
+ viewBox="0 0 24 24"
48
+ xmlns="http://www.w3.org/2000/svg"
49
+ data-better-ui--forms--password-input-target="eyeSlashIcon"
50
+ >
51
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
52
+ </svg>
53
+ </button>
54
+ </div>
55
+ <% end %>
56
+ </div>
57
+
58
+ <% if @hint.present? %>
59
+ <div class="<%= hint_element_classes %>">
60
+ <%= @hint %>
61
+ </div>
62
+ <% end %>
63
+
64
+ <% if has_errors? %>
65
+ <div class="<%= errors_element_classes %>">
66
+ <% @errors.each do |error| %>
67
+ <div><%= error %></div>
68
+ <% end %>
69
+ </div>
70
+ <% end %>
71
+ </div>
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Forms
5
+ # A password input component with visibility toggle functionality.
6
+ #
7
+ # This component extends {TextInputComponent} to provide a password input field with
8
+ # a toggle button that allows users to show or hide the password text. The toggle
9
+ # button displays an eye icon when the password is hidden and an eye-slash icon when
10
+ # the password is visible.
11
+ #
12
+ # The component inherits all features from TextInputComponent including labels, hints,
13
+ # errors, validation states, sizes, and the prefix_icon slot. The suffix position is
14
+ # reserved for the password visibility toggle button.
15
+ #
16
+ # @example Basic password input
17
+ # <%= render BetterUi::Forms::PasswordInputComponent.new(
18
+ # name: "user[password]",
19
+ # label: "Password",
20
+ # placeholder: "Enter your password"
21
+ # ) %>
22
+ #
23
+ # @example Password input with prefix icon (lock)
24
+ # <%= render BetterUi::Forms::PasswordInputComponent.new(
25
+ # name: "user[password]",
26
+ # label: "Password",
27
+ # required: true
28
+ # ) do |component| %>
29
+ # <% component.with_prefix_icon do %>
30
+ # <svg class="h-5 w-5 text-gray-400">...</svg>
31
+ # <% end %>
32
+ # <% end %>
33
+ #
34
+ # @example With validation errors
35
+ # <%= render BetterUi::Forms::PasswordInputComponent.new(
36
+ # name: "user[password]",
37
+ # label: "Password",
38
+ # errors: ["Password is too short", "Password must include a number"]
39
+ # ) %>
40
+ #
41
+ # @example With hint text
42
+ # <%= render BetterUi::Forms::PasswordInputComponent.new(
43
+ # name: "user[password]",
44
+ # label: "Password",
45
+ # hint: "Must be at least 8 characters with 1 number and 1 special character",
46
+ # required: true
47
+ # ) %>
48
+ #
49
+ # @example Using with Rails form builder
50
+ # <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
51
+ # <%= f.ui_password_input :password, hint: "Minimum 8 characters" %>
52
+ # <% end %>
53
+ #
54
+ # @note The suffix_icon slot from TextInputComponent is not available in PasswordInputComponent
55
+ # as the suffix position is occupied by the visibility toggle button.
56
+ #
57
+ # @see TextInputComponent
58
+ # @see BaseComponent
59
+ # @see BetterUi::UiFormBuilder#ui_password_input
60
+ class PasswordInputComponent < TextInputComponent
61
+ # Initializes a new password input component.
62
+ #
63
+ # All parameters are passed to {TextInputComponent#initialize}. See the parent class
64
+ # for detailed parameter descriptions.
65
+ #
66
+ # @param (see TextInputComponent#initialize)
67
+ # @option (see TextInputComponent#initialize)
68
+ # @raise (see TextInputComponent#initialize)
69
+ #
70
+ # @see TextInputComponent#initialize
71
+ def initialize(
72
+ name:,
73
+ value: nil,
74
+ label: nil,
75
+ hint: nil,
76
+ placeholder: nil,
77
+ size: :md,
78
+ disabled: false,
79
+ readonly: false,
80
+ required: false,
81
+ errors: nil,
82
+ container_classes: nil,
83
+ label_classes: nil,
84
+ input_classes: nil,
85
+ hint_classes: nil,
86
+ error_classes: nil,
87
+ **options
88
+ )
89
+ super(
90
+ name: name,
91
+ value: value,
92
+ label: label,
93
+ hint: hint,
94
+ placeholder: placeholder,
95
+ size: size,
96
+ disabled: disabled,
97
+ readonly: readonly,
98
+ required: required,
99
+ errors: errors,
100
+ container_classes: container_classes,
101
+ label_classes: label_classes,
102
+ input_classes: input_classes,
103
+ hint_classes: hint_classes,
104
+ error_classes: error_classes,
105
+ **options
106
+ )
107
+ end
108
+
109
+ private
110
+
111
+ # Returns the HTML input type attribute.
112
+ #
113
+ # For password inputs, this returns "password" by default. The Stimulus controller
114
+ # toggles this between "password" and "text" to show/hide the password.
115
+ #
116
+ # @return [String] the input type ("password")
117
+ # @api private
118
+ def input_type
119
+ "password"
120
+ end
121
+
122
+ # Checks if the password visibility toggle button is present.
123
+ #
124
+ # For PasswordInputComponent, the toggle button is always present unless the
125
+ # input is disabled or readonly (as toggling would not be functional).
126
+ #
127
+ # @return [Boolean] true if toggle button should be rendered
128
+ # @api private
129
+ def has_toggle_button?
130
+ !@disabled && !@readonly
131
+ end
132
+
133
+ # Returns size-specific classes for the toggle button icon.
134
+ #
135
+ # Icon sizes are proportional to the input size to maintain visual balance.
136
+ #
137
+ # @return [String] the icon size class for the current component size
138
+ # @api private
139
+ def toggle_icon_size
140
+ case @size
141
+ when :xs then "w-4 h-4"
142
+ when :sm then "w-4 h-4"
143
+ when :md then "w-5 h-5"
144
+ when :lg then "w-5 h-5"
145
+ when :xl then "w-6 h-6"
146
+ end
147
+ end
148
+
149
+ # Returns the CSS classes for the toggle button wrapper.
150
+ #
151
+ # Positions the toggle button absolutely within the input wrapper.
152
+ #
153
+ # @return [String] the merged CSS class string for the toggle button wrapper
154
+ # @api private
155
+ def toggle_button_classes
156
+ css_classes([
157
+ "absolute",
158
+ "inset-y-0",
159
+ "right-0",
160
+ "flex",
161
+ "items-center",
162
+ icon_size_padding
163
+ ].flatten.compact)
164
+ end
165
+
166
+ # Returns the complete set of HTML attributes for the input element.
167
+ #
168
+ # Extends the parent implementation to add Stimulus target attribute
169
+ # for the password visibility toggle functionality.
170
+ #
171
+ # @return [Hash] hash of HTML attributes for the input element
172
+ # @api private
173
+ def input_attributes
174
+ attrs = super
175
+ attrs[:data] ||= {}
176
+ attrs[:data][:"better-ui--forms--password-input-target"] = "input"
177
+ attrs
178
+ end
179
+
180
+ # Returns the HTML attributes for the input wrapper div.
181
+ #
182
+ # Adds Stimulus controller to the wrapper so all child targets are accessible.
183
+ #
184
+ # @return [Hash] hash of HTML attributes for the wrapper div
185
+ # @api private
186
+ def input_wrapper_attributes
187
+ {
188
+ class: input_wrapper_classes,
189
+ data: {
190
+ controller: "better-ui--forms--password-input"
191
+ }
192
+ }
193
+ end
194
+
195
+ # Override parent method to always consider toggle button as suffix icon.
196
+ #
197
+ # This ensures input padding is adjusted for the toggle button.
198
+ #
199
+ # @return [Boolean] true if toggle button is present
200
+ # @api private
201
+ def has_suffix_icon?
202
+ has_toggle_button?
203
+ end
204
+ end
205
+ end
206
+ 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
+ <input <%= tag.attributes(input_attributes) %> />
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>