better_ui 0.3.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 (183) 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 -51
  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 -4
  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 -185
  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 -227
  53. data/app/components/better_ui/application/sidebar/component.rb +0 -130
  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 -25
  71. data/app/components/better_ui/general/dropdown/component.rb +0 -170
  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/heading/component.html.erb +0 -22
  79. data/app/components/better_ui/general/heading/component.rb +0 -257
  80. data/app/components/better_ui/general/icon/component.html.erb +0 -7
  81. data/app/components/better_ui/general/icon/component.rb +0 -239
  82. data/app/components/better_ui/general/input/checkbox/component.html.erb +0 -5
  83. data/app/components/better_ui/general/input/checkbox/component.rb +0 -238
  84. data/app/components/better_ui/general/input/datetime/component.html.erb +0 -5
  85. data/app/components/better_ui/general/input/datetime/component.rb +0 -223
  86. data/app/components/better_ui/general/input/radio/component.html.erb +0 -5
  87. data/app/components/better_ui/general/input/radio/component.rb +0 -230
  88. data/app/components/better_ui/general/input/select/component.html.erb +0 -16
  89. data/app/components/better_ui/general/input/select/component.rb +0 -184
  90. data/app/components/better_ui/general/input/select/select_component.html.erb +0 -5
  91. data/app/components/better_ui/general/input/select/select_component.rb +0 -37
  92. data/app/components/better_ui/general/input/text/component.html.erb +0 -5
  93. data/app/components/better_ui/general/input/text/component.rb +0 -171
  94. data/app/components/better_ui/general/input/textarea/component.html.erb +0 -5
  95. data/app/components/better_ui/general/input/textarea/component.rb +0 -166
  96. data/app/components/better_ui/general/link/component.html.erb +0 -18
  97. data/app/components/better_ui/general/link/component.rb +0 -258
  98. data/app/components/better_ui/general/modal/component.html.erb +0 -5
  99. data/app/components/better_ui/general/modal/component.rb +0 -47
  100. data/app/components/better_ui/general/modal/modal_component.html.erb +0 -52
  101. data/app/components/better_ui/general/modal/modal_component.rb +0 -160
  102. data/app/components/better_ui/general/pagination/component.html.erb +0 -85
  103. data/app/components/better_ui/general/pagination/component.rb +0 -216
  104. data/app/components/better_ui/general/panel/component.html.erb +0 -28
  105. data/app/components/better_ui/general/panel/component.rb +0 -249
  106. data/app/components/better_ui/general/progress/component.html.erb +0 -11
  107. data/app/components/better_ui/general/progress/component.rb +0 -160
  108. data/app/components/better_ui/general/spinner/component.html.erb +0 -35
  109. data/app/components/better_ui/general/spinner/component.rb +0 -93
  110. data/app/components/better_ui/general/table/component.html.erb +0 -5
  111. data/app/components/better_ui/general/table/component.rb +0 -217
  112. data/app/components/better_ui/general/table/tbody_component.html.erb +0 -3
  113. data/app/components/better_ui/general/table/tbody_component.rb +0 -30
  114. data/app/components/better_ui/general/table/td_component.html.erb +0 -3
  115. data/app/components/better_ui/general/table/td_component.rb +0 -44
  116. data/app/components/better_ui/general/table/tfoot_component.html.erb +0 -3
  117. data/app/components/better_ui/general/table/tfoot_component.rb +0 -28
  118. data/app/components/better_ui/general/table/th_component.html.erb +0 -6
  119. data/app/components/better_ui/general/table/th_component.rb +0 -51
  120. data/app/components/better_ui/general/table/thead_component.html.erb +0 -3
  121. data/app/components/better_ui/general/table/thead_component.rb +0 -28
  122. data/app/components/better_ui/general/table/tr_component.html.erb +0 -3
  123. data/app/components/better_ui/general/table/tr_component.rb +0 -30
  124. data/app/components/better_ui/general/tabs/component.html.erb +0 -11
  125. data/app/components/better_ui/general/tabs/component.rb +0 -120
  126. data/app/components/better_ui/general/tabs/panel_component.html.erb +0 -3
  127. data/app/components/better_ui/general/tabs/panel_component.rb +0 -37
  128. data/app/components/better_ui/general/tabs/tab_component.html.erb +0 -13
  129. data/app/components/better_ui/general/tabs/tab_component.rb +0 -111
  130. data/app/components/better_ui/general/tag/component.html.erb +0 -3
  131. data/app/components/better_ui/general/tag/component.rb +0 -104
  132. data/app/components/better_ui/general/tooltip/component.html.erb +0 -7
  133. data/app/components/better_ui/general/tooltip/component.rb +0 -239
  134. data/app/helpers/better_ui/application/components/card/card_helper.rb +0 -96
  135. data/app/helpers/better_ui/application/components/card.rb +0 -11
  136. data/app/helpers/better_ui/application/components/main/main_helper.rb +0 -64
  137. data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +0 -77
  138. data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +0 -51
  139. data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +0 -73
  140. data/app/helpers/better_ui/general/components/accordion.rb +0 -11
  141. data/app/helpers/better_ui/general/components/alert/alert_helper.rb +0 -57
  142. data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +0 -29
  143. data/app/helpers/better_ui/general/components/badge/badge_helper.rb +0 -53
  144. data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +0 -37
  145. data/app/helpers/better_ui/general/components/button/button_helper.rb +0 -65
  146. data/app/helpers/better_ui/general/components/container/container_helper.rb +0 -60
  147. data/app/helpers/better_ui/general/components/divider/divider_helper.rb +0 -63
  148. data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +0 -32
  149. data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +0 -79
  150. data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -62
  151. data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
  152. data/app/helpers/better_ui/general/components/heading/heading_helper.rb +0 -72
  153. data/app/helpers/better_ui/general/components/icon/icon_helper.rb +0 -16
  154. data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +0 -81
  155. data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +0 -91
  156. data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +0 -79
  157. data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +0 -124
  158. data/app/helpers/better_ui/general/components/input/select/select_helper.rb +0 -70
  159. data/app/helpers/better_ui/general/components/input/text/text_helper.rb +0 -138
  160. data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +0 -73
  161. data/app/helpers/better_ui/general/components/link/link_helper.rb +0 -89
  162. data/app/helpers/better_ui/general/components/modal/modal_helper.rb +0 -85
  163. data/app/helpers/better_ui/general/components/modal.rb +0 -11
  164. data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +0 -82
  165. data/app/helpers/better_ui/general/components/panel/panel_helper.rb +0 -83
  166. data/app/helpers/better_ui/general/components/progress/progress_helper.rb +0 -53
  167. data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +0 -19
  168. data/app/helpers/better_ui/general/components/table/table_helper.rb +0 -53
  169. data/app/helpers/better_ui/general/components/table/tbody_helper.rb +0 -13
  170. data/app/helpers/better_ui/general/components/table/td_helper.rb +0 -19
  171. data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +0 -13
  172. data/app/helpers/better_ui/general/components/table/th_helper.rb +0 -19
  173. data/app/helpers/better_ui/general/components/table/thead_helper.rb +0 -13
  174. data/app/helpers/better_ui/general/components/table/tr_helper.rb +0 -13
  175. data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +0 -62
  176. data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +0 -55
  177. data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +0 -95
  178. data/app/helpers/better_ui/general/components/tag/tag_helper.rb +0 -26
  179. data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +0 -60
  180. data/app/jobs/better_ui/application_job.rb +0 -4
  181. data/app/mailers/better_ui/application_mailer.rb +0 -6
  182. data/config/initializers/lookbook.rb +0 -23
  183. data/lib/better_ui/railtie.rb +0 -20
@@ -0,0 +1,450 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Forms
5
+ # Base component for all form input components in the BetterUi::Forms namespace.
6
+ #
7
+ # This abstract class provides a common structure and behavior for form input components,
8
+ # including label positioning (vertical/above input), hint text display, error message
9
+ # handling, and consistent sizing/styling across all input types.
10
+ #
11
+ # @abstract Subclasses must implement the {#call} method to render the specific input type.
12
+ #
13
+ # @example Extending BaseComponent to create a custom input
14
+ # module BetterUi
15
+ # module Forms
16
+ # class CustomInputComponent < BaseComponent
17
+ # def call
18
+ # content_tag :div, class: wrapper_classes do
19
+ # # Render label, input, hint, and errors
20
+ # end
21
+ # end
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # @example Usage with Rails form builder (via BetterUi::UiFormBuilder)
27
+ # <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
28
+ # <%= f.ui_text_input :email, label: "Email Address", hint: "We'll never share your email" %>
29
+ # <% end %>
30
+ #
31
+ # @see TextInputComponent
32
+ # @see NumberInputComponent
33
+ # @see BetterUi::UiFormBuilder
34
+ class BaseComponent < ApplicationComponent
35
+ # Available size variants for form inputs.
36
+ # Each size adjusts font size, padding, and spacing proportionally.
37
+ #
38
+ # @return [Array<Symbol>] the list of valid size options (:xs, :sm, :md, :lg, :xl)
39
+ SIZES = %i[xs sm md lg xl].freeze
40
+
41
+ # Initializes a new form input component with common form field attributes.
42
+ #
43
+ # @param name [String] the name attribute for the input field (required for form submission)
44
+ # @param value [String, nil] the current value of the input field
45
+ # @param label [String, nil] the label text to display above the input
46
+ # @param hint [String, nil] helpful hint text displayed below the input
47
+ # @param placeholder [String, nil] placeholder text shown when input is empty
48
+ # @param size [Symbol] the size variant (:xs, :sm, :md, :lg, :xl), defaults to :md
49
+ # @param disabled [Boolean] whether the input should be disabled (non-interactive), defaults to false
50
+ # @param readonly [Boolean] whether the input should be readonly (viewable but not editable), defaults to false
51
+ # @param required [Boolean] whether the field is required (shows asterisk indicator), defaults to false
52
+ # @param errors [Array<String>, String, nil] validation error messages to display below the input
53
+ # @param container_classes [String, Array<String>, nil] additional CSS classes for the outer wrapper
54
+ # @param label_classes [String, Array<String>, nil] additional CSS classes for the label element
55
+ # @param input_classes [String, Array<String>, nil] additional CSS classes for the input element
56
+ # @param hint_classes [String, Array<String>, nil] additional CSS classes for the hint text
57
+ # @param error_classes [String, Array<String>, nil] additional CSS classes for error messages
58
+ # @param options [Hash] additional HTML attributes to pass through to the input element
59
+ #
60
+ # @raise [ArgumentError] if size is not one of the valid SIZES
61
+ #
62
+ # @example Basic initialization
63
+ # BetterUi::Forms::TextInputComponent.new(
64
+ # name: "user[email]",
65
+ # label: "Email Address",
66
+ # hint: "We'll never share your email",
67
+ # required: true
68
+ # )
69
+ #
70
+ # @example With errors
71
+ # BetterUi::Forms::TextInputComponent.new(
72
+ # name: "user[email]",
73
+ # value: "invalid",
74
+ # errors: ["Email is invalid", "Email can't be blank"]
75
+ # )
76
+ #
77
+ # @example Custom styling
78
+ # BetterUi::Forms::TextInputComponent.new(
79
+ # name: "search",
80
+ # size: :lg,
81
+ # container_classes: "my-4",
82
+ # input_classes: "font-mono"
83
+ # )
84
+ def initialize(
85
+ name:,
86
+ value: nil,
87
+ label: nil,
88
+ hint: nil,
89
+ placeholder: nil,
90
+ size: :md,
91
+ disabled: false,
92
+ readonly: false,
93
+ required: false,
94
+ errors: nil,
95
+ container_classes: nil,
96
+ label_classes: nil,
97
+ input_classes: nil,
98
+ hint_classes: nil,
99
+ error_classes: nil,
100
+ **options
101
+ )
102
+ @name = name
103
+ @value = value
104
+ @label = label
105
+ @hint = hint
106
+ @placeholder = placeholder
107
+ @size = validate_size(size)
108
+ @disabled = disabled
109
+ @readonly = readonly
110
+ @required = required
111
+ @errors = Array(errors).compact.reject(&:blank?)
112
+ @container_classes = container_classes
113
+ @label_classes = label_classes
114
+ @input_classes = input_classes
115
+ @hint_classes = hint_classes
116
+ @error_classes = error_classes
117
+ @options = options
118
+ end
119
+
120
+ # Renders the component markup.
121
+ #
122
+ # This method must be implemented by subclasses to define the specific
123
+ # rendering logic for each input type (text, number, select, etc.).
124
+ #
125
+ # @abstract Subclasses must implement this method
126
+ # @raise [NotImplementedError] if called directly on BaseComponent
127
+ # @return [String] the rendered HTML markup
128
+ def call
129
+ raise NotImplementedError, "Subclasses must implement the #call method"
130
+ end
131
+
132
+ private
133
+
134
+ # Validates that the provided size is one of the allowed SIZES.
135
+ #
136
+ # @param size [Symbol] the size to validate
137
+ # @return [Symbol] the validated size
138
+ # @raise [ArgumentError] if size is not in SIZES
139
+ # @api private
140
+ def validate_size(size)
141
+ unless SIZES.include?(size)
142
+ raise ArgumentError, "Invalid size: #{size}. Must be one of #{SIZES.join(', ')}"
143
+ end
144
+ size
145
+ end
146
+
147
+ # Checks if the component has any validation errors to display.
148
+ #
149
+ # @return [Boolean] true if errors are present, false otherwise
150
+ # @api private
151
+ def has_errors?
152
+ @errors.present?
153
+ end
154
+
155
+ # Returns the CSS classes for the outermost wrapper element.
156
+ #
157
+ # Merges base wrapper classes with any custom container_classes provided.
158
+ #
159
+ # @return [String] the merged CSS class string
160
+ # @api private
161
+ def wrapper_classes
162
+ css_classes([
163
+ "form-field-wrapper",
164
+ @container_classes
165
+ ].flatten.compact)
166
+ end
167
+
168
+ # Returns the CSS classes for the label element.
169
+ #
170
+ # Combines base label styles with size-specific classes and custom label_classes.
171
+ #
172
+ # @return [String] the merged CSS class string for the label
173
+ # @api private
174
+ def label_element_classes
175
+ css_classes([
176
+ "block",
177
+ "font-medium",
178
+ "text-gray-700",
179
+ "mb-1",
180
+ label_size_classes,
181
+ @label_classes
182
+ ].flatten.compact)
183
+ end
184
+
185
+ # Returns size-specific CSS classes for the label text.
186
+ #
187
+ # @return [String] the text size class for the current component size
188
+ # @api private
189
+ def label_size_classes
190
+ case @size
191
+ when :xs then "text-xs"
192
+ when :sm then "text-sm"
193
+ when :md then "text-sm"
194
+ when :lg then "text-base"
195
+ when :xl then "text-lg"
196
+ end
197
+ end
198
+
199
+ # Returns the CSS classes for the input wrapper element.
200
+ #
201
+ # This wrapper is used to position prefix/suffix icons relative to the input.
202
+ #
203
+ # @return [String] the merged CSS class string for the input wrapper
204
+ # @api private
205
+ def input_wrapper_classes
206
+ css_classes([
207
+ "relative",
208
+ "flex",
209
+ "items-center"
210
+ ].flatten.compact)
211
+ end
212
+
213
+ # Returns the CSS classes for the input element itself.
214
+ #
215
+ # Combines base input styles, size-specific classes, state-specific classes,
216
+ # and any custom input_classes.
217
+ #
218
+ # @return [String] the merged CSS class string for the input element
219
+ # @api private
220
+ def input_element_classes
221
+ css_classes([
222
+ base_input_classes,
223
+ size_input_classes,
224
+ state_input_classes,
225
+ @input_classes
226
+ ].flatten.compact)
227
+ end
228
+
229
+ # Returns the base CSS classes common to all input elements.
230
+ #
231
+ # These classes apply regardless of size, state, or variant.
232
+ #
233
+ # @return [Array<String>] array of base CSS class strings
234
+ # @api private
235
+ def base_input_classes
236
+ [
237
+ "block",
238
+ "w-full",
239
+ "rounded-md",
240
+ "border",
241
+ "shadow-sm",
242
+ "transition-colors",
243
+ "duration-200"
244
+ ]
245
+ end
246
+
247
+ # Returns size-specific CSS classes for the input element.
248
+ #
249
+ # Controls font size, padding, and spacing based on the component size.
250
+ #
251
+ # @return [Array<String>] array of size-specific CSS class strings
252
+ # @api private
253
+ def size_input_classes
254
+ case @size
255
+ when :xs
256
+ [ "text-xs", "py-1", "px-2" ]
257
+ when :sm
258
+ [ "text-sm", "py-1.5", "px-3" ]
259
+ when :md
260
+ [ "text-base", "py-2", "px-4" ]
261
+ when :lg
262
+ [ "text-lg", "py-2.5", "px-5" ]
263
+ when :xl
264
+ [ "text-xl", "py-3", "px-6" ]
265
+ end
266
+ end
267
+
268
+ # Returns state-specific CSS classes for the input element.
269
+ #
270
+ # Determines which state classes to apply based on the input's current state:
271
+ # disabled, readonly, error, or normal. Only one state is applied at a time,
272
+ # with priority: disabled > readonly > error > normal.
273
+ #
274
+ # @return [Array<String>] array of state-specific CSS class strings
275
+ # @api private
276
+ def state_input_classes
277
+ if @disabled
278
+ disabled_classes
279
+ elsif @readonly
280
+ readonly_classes
281
+ elsif has_errors?
282
+ error_state_classes
283
+ else
284
+ normal_state_classes
285
+ end
286
+ end
287
+
288
+ # Returns CSS classes for the normal (default) input state.
289
+ #
290
+ # Applied when the input is not disabled, readonly, or in error state.
291
+ # Includes border, background, text colors, and focus ring styles.
292
+ #
293
+ # @return [Array<String>] array of CSS class strings for normal state
294
+ # @api private
295
+ def normal_state_classes
296
+ [
297
+ "border-gray-300",
298
+ "bg-white",
299
+ "text-gray-900",
300
+ "placeholder-gray-400",
301
+ "focus:border-primary-500",
302
+ "focus:ring-2",
303
+ "focus:ring-primary-500",
304
+ "focus:ring-opacity-20",
305
+ "focus:outline-none"
306
+ ]
307
+ end
308
+
309
+ # Returns CSS classes for the error input state.
310
+ #
311
+ # Applied when the input has validation errors. Uses danger color variant
312
+ # for border and focus ring to indicate the error state visually.
313
+ #
314
+ # @return [Array<String>] array of CSS class strings for error state
315
+ # @api private
316
+ def error_state_classes
317
+ [
318
+ "border-danger-500",
319
+ "bg-white",
320
+ "text-gray-900",
321
+ "placeholder-gray-400",
322
+ "focus:border-danger-600",
323
+ "focus:ring-2",
324
+ "focus:ring-danger-500",
325
+ "focus:ring-opacity-20",
326
+ "focus:outline-none"
327
+ ]
328
+ end
329
+
330
+ # Returns CSS classes for the disabled input state.
331
+ #
332
+ # Applied when the input is disabled (non-interactive).
333
+ # Reduces opacity and changes cursor to indicate non-interactive state.
334
+ #
335
+ # @return [Array<String>] array of CSS class strings for disabled state
336
+ # @api private
337
+ def disabled_classes
338
+ [
339
+ "border-gray-300",
340
+ "bg-gray-100",
341
+ "text-gray-500",
342
+ "placeholder-gray-400",
343
+ "cursor-not-allowed",
344
+ "opacity-60"
345
+ ]
346
+ end
347
+
348
+ # Returns CSS classes for the readonly input state.
349
+ #
350
+ # Applied when the input is readonly (viewable but not editable).
351
+ # Uses lighter background and default cursor to differentiate from disabled state.
352
+ #
353
+ # @return [Array<String>] array of CSS class strings for readonly state
354
+ # @api private
355
+ def readonly_classes
356
+ [
357
+ "border-gray-300",
358
+ "bg-gray-50",
359
+ "text-gray-700",
360
+ "cursor-default"
361
+ ]
362
+ end
363
+
364
+ # Returns the CSS classes for the hint text element.
365
+ #
366
+ # Combines base hint styles with size-specific classes and custom hint_classes.
367
+ #
368
+ # @return [String] the merged CSS class string for the hint text
369
+ # @api private
370
+ def hint_element_classes
371
+ css_classes([
372
+ "block",
373
+ "text-gray-600",
374
+ "mt-1",
375
+ hint_size_classes,
376
+ @hint_classes
377
+ ].flatten.compact)
378
+ end
379
+
380
+ # Returns size-specific CSS classes for the hint text.
381
+ #
382
+ # Hint text is typically smaller than the input text for visual hierarchy.
383
+ #
384
+ # @return [String] the text size class for the current component size
385
+ # @api private
386
+ def hint_size_classes
387
+ case @size
388
+ when :xs then "text-xs"
389
+ when :sm then "text-xs"
390
+ when :md then "text-sm"
391
+ when :lg then "text-sm"
392
+ when :xl then "text-base"
393
+ end
394
+ end
395
+
396
+ # Returns the CSS classes for the error messages container.
397
+ #
398
+ # Combines base error styles with size-specific classes and custom error_classes.
399
+ #
400
+ # @return [String] the merged CSS class string for the error messages container
401
+ # @api private
402
+ def errors_element_classes
403
+ css_classes([
404
+ "text-danger-600",
405
+ "mt-1",
406
+ "space-y-0.5",
407
+ error_size_classes,
408
+ @error_classes
409
+ ].flatten.compact)
410
+ end
411
+
412
+ # Returns size-specific CSS classes for error message text.
413
+ #
414
+ # Error text sizing matches hint text sizing for consistency.
415
+ #
416
+ # @return [String] the text size class for the current component size
417
+ # @api private
418
+ def error_size_classes
419
+ case @size
420
+ when :xs then "text-xs"
421
+ when :sm then "text-xs"
422
+ when :md then "text-sm"
423
+ when :lg then "text-sm"
424
+ when :xl then "text-base"
425
+ end
426
+ end
427
+
428
+ # Returns the complete set of HTML attributes for the input element.
429
+ #
430
+ # Combines standard form attributes (name, value, placeholder, etc.)
431
+ # with state attributes (disabled, readonly, required) and any custom
432
+ # options passed during initialization. Nil values are removed via compact.
433
+ #
434
+ # @return [Hash] hash of HTML attributes for the input element
435
+ # @api private
436
+ def input_attributes
437
+ {
438
+ name: @name,
439
+ value: @value,
440
+ placeholder: @placeholder,
441
+ disabled: @disabled || nil,
442
+ readonly: @readonly || nil,
443
+ required: @required || nil,
444
+ class: input_element_classes,
445
+ **@options
446
+ }.compact
447
+ end
448
+ end
449
+ end
450
+ end
@@ -0,0 +1,28 @@
1
+ <div class="<%= wrapper_classes %>">
2
+ <div class="<%= checkbox_wrapper_classes %>">
3
+ <input <%= tag.attributes(checkbox_attributes) %> />
4
+
5
+ <% if @label.present? %>
6
+ <label for="<%= input_id %>" class="<%= label_element_classes %>">
7
+ <%= @label %>
8
+ <% if @required %>
9
+ <span class="text-danger-600 ml-0.5">*</span>
10
+ <% end %>
11
+ </label>
12
+ <% end %>
13
+ </div>
14
+
15
+ <% if @hint.present? %>
16
+ <div class="<%= hint_element_classes %>">
17
+ <%= @hint %>
18
+ </div>
19
+ <% end %>
20
+
21
+ <% if has_errors? %>
22
+ <div class="<%= errors_element_classes %>">
23
+ <% @errors.each do |error| %>
24
+ <div><%= error %></div>
25
+ <% end %>
26
+ </div>
27
+ <% end %>
28
+ </div>