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,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Forms
5
+ # A checkbox group component for selecting multiple options from a collection.
6
+ #
7
+ # This component renders a group of checkboxes within a fieldset, allowing users
8
+ # to select multiple values. It supports both vertical and horizontal orientations,
9
+ # and integrates seamlessly with Rails form builders for array attribute submission.
10
+ #
11
+ # @example Basic checkbox group
12
+ # <%= render BetterUi::Forms::CheckboxGroupComponent.new(
13
+ # name: "user[roles]",
14
+ # collection: ["Admin", "Editor", "Viewer"],
15
+ # legend: "User Roles"
16
+ # ) %>
17
+ #
18
+ # @example With label/value pairs
19
+ # <%= render BetterUi::Forms::CheckboxGroupComponent.new(
20
+ # name: "user[permissions]",
21
+ # collection: [["Read", "read"], ["Write", "write"], ["Delete", "delete"]],
22
+ # selected: ["read", "write"],
23
+ # legend: "Permissions"
24
+ # ) %>
25
+ #
26
+ # @example Horizontal orientation
27
+ # <%= render BetterUi::Forms::CheckboxGroupComponent.new(
28
+ # name: "options",
29
+ # collection: ["Option A", "Option B", "Option C"],
30
+ # orientation: :horizontal
31
+ # ) %>
32
+ #
33
+ # @example With validation errors
34
+ # <%= render BetterUi::Forms::CheckboxGroupComponent.new(
35
+ # name: "user[interests]",
36
+ # collection: ["Sports", "Music", "Art"],
37
+ # errors: ["Please select at least one interest"]
38
+ # ) %>
39
+ #
40
+ # @example Using with Rails form builder
41
+ # <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
42
+ # <%= f.bui_checkbox_group :roles, [["Admin", "admin"], ["Editor", "editor"]] %>
43
+ # <% end %>
44
+ #
45
+ # @see CheckboxComponent
46
+ # @see BetterUi::UiFormBuilder#bui_checkbox_group
47
+ class CheckboxGroupComponent < ApplicationComponent
48
+ # Available size variants for checkbox groups.
49
+ # Each size adjusts checkbox dimensions and spacing proportionally.
50
+ #
51
+ # @return [Array<Symbol>] the list of valid size options (:xs, :sm, :md, :lg, :xl)
52
+ SIZES = %i[xs sm md lg xl].freeze
53
+
54
+ # Available orientation options for checkbox group layout.
55
+ #
56
+ # @return [Array<Symbol>] the list of valid orientations (:vertical, :horizontal)
57
+ ORIENTATIONS = %i[vertical horizontal].freeze
58
+
59
+ # Initializes a new checkbox group component.
60
+ #
61
+ # @param name [String] the base name attribute for the checkboxes (required for form submission)
62
+ # @param collection [Array] the collection of options, can be:
63
+ # - Array of values (e.g., ["Admin", "Editor"])
64
+ # - Array of [label, value] pairs (e.g., [["Admin", "admin"], ["Editor", "editor"]])
65
+ # @param selected [Array, String, nil] the currently selected value(s)
66
+ # @param legend [String, nil] the legend text for the fieldset
67
+ # @param hint [String, nil] helpful hint text displayed below the checkboxes
68
+ # @param variant [Symbol] the color variant for all checkboxes (:primary, :secondary, etc.)
69
+ # @param size [Symbol] the size variant (:xs, :sm, :md, :lg, :xl), defaults to :md
70
+ # @param orientation [Symbol] the layout orientation (:vertical, :horizontal), defaults to :vertical
71
+ # @param disabled [Boolean] whether all checkboxes should be disabled, defaults to false
72
+ # @param required [Boolean] whether the field is required (shows asterisk indicator), defaults to false
73
+ # @param errors [Array<String>, String, nil] validation error messages to display
74
+ # @param container_classes [String, Array<String>, nil] additional CSS classes for the outer wrapper
75
+ # @param legend_classes [String, Array<String>, nil] additional CSS classes for the legend element
76
+ # @param items_classes [String, Array<String>, nil] additional CSS classes for the items container
77
+ # @param hint_classes [String, Array<String>, nil] additional CSS classes for the hint text
78
+ # @param error_classes [String, Array<String>, nil] additional CSS classes for error messages
79
+ # @param options [Hash] additional HTML attributes to pass through to the fieldset element
80
+ #
81
+ # @raise [ArgumentError] if variant, size, or orientation is invalid
82
+ def initialize(
83
+ name:,
84
+ collection: [],
85
+ selected: [],
86
+ legend: nil,
87
+ hint: nil,
88
+ variant: :primary,
89
+ size: :md,
90
+ orientation: :vertical,
91
+ disabled: false,
92
+ required: false,
93
+ errors: nil,
94
+ container_classes: nil,
95
+ legend_classes: nil,
96
+ items_classes: nil,
97
+ hint_classes: nil,
98
+ error_classes: nil,
99
+ **options
100
+ )
101
+ @name = name
102
+ @collection = collection
103
+ @selected = Array(selected).map(&:to_s)
104
+ @legend = legend
105
+ @hint = hint
106
+ @variant = validate_variant(variant)
107
+ @size = validate_size(size)
108
+ @orientation = validate_orientation(orientation)
109
+ @disabled = disabled
110
+ @required = required
111
+ @errors = Array(errors).compact.reject(&:blank?)
112
+ @container_classes = container_classes
113
+ @legend_classes = legend_classes
114
+ @items_classes = items_classes
115
+ @hint_classes = hint_classes
116
+ @error_classes = error_classes
117
+ @options = options
118
+ end
119
+
120
+ private
121
+
122
+ # Validates that the provided variant is one of the allowed VARIANTS.
123
+ #
124
+ # @param variant [Symbol] the variant to validate
125
+ # @return [Symbol] the validated variant
126
+ # @raise [ArgumentError] if variant is not in VARIANTS
127
+ # @api private
128
+ def validate_variant(variant)
129
+ unless BetterUi::ApplicationComponent::VARIANTS.key?(variant)
130
+ raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{BetterUi::ApplicationComponent::VARIANTS.keys.join(', ')}"
131
+ end
132
+ variant
133
+ end
134
+
135
+ # Validates that the provided size is one of the allowed SIZES.
136
+ #
137
+ # @param size [Symbol] the size to validate
138
+ # @return [Symbol] the validated size
139
+ # @raise [ArgumentError] if size is not in SIZES
140
+ # @api private
141
+ def validate_size(size)
142
+ unless SIZES.include?(size)
143
+ raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.join(', ')}"
144
+ end
145
+ size
146
+ end
147
+
148
+ # Validates that the provided orientation is one of the allowed ORIENTATIONS.
149
+ #
150
+ # @param orientation [Symbol] the orientation to validate
151
+ # @return [Symbol] the validated orientation
152
+ # @raise [ArgumentError] if orientation is not in ORIENTATIONS
153
+ # @api private
154
+ def validate_orientation(orientation)
155
+ unless ORIENTATIONS.include?(orientation)
156
+ raise ArgumentError, "Invalid orientation: #{orientation}. Must be one of: #{ORIENTATIONS.join(', ')}"
157
+ end
158
+ orientation
159
+ end
160
+
161
+ # Checks if the component has any validation errors to display.
162
+ #
163
+ # @return [Boolean] true if errors are present, false otherwise
164
+ # @api private
165
+ def has_errors?
166
+ @errors.present?
167
+ end
168
+
169
+ # Returns the field name for checkbox inputs (with array notation).
170
+ #
171
+ # @return [String] the field name with [] suffix for array submission
172
+ # @api private
173
+ def field_name
174
+ "#{@name}[]"
175
+ end
176
+
177
+ # Checks if a specific value is currently selected.
178
+ #
179
+ # @param value [String, Integer] the value to check
180
+ # @return [Boolean] true if the value is selected, false otherwise
181
+ # @api private
182
+ def item_checked?(value)
183
+ @selected.include?(value.to_s)
184
+ end
185
+
186
+ # Extracts the label from a collection item.
187
+ #
188
+ # @param item [String, Array] the collection item
189
+ # @return [String] the label text
190
+ # @api private
191
+ def item_label(item)
192
+ item.is_a?(Array) ? item.first.to_s : item.to_s.humanize
193
+ end
194
+
195
+ # Extracts the value from a collection item.
196
+ #
197
+ # @param item [String, Array] the collection item
198
+ # @return [String] the value
199
+ # @api private
200
+ def item_value(item)
201
+ item.is_a?(Array) ? item.last.to_s : item.to_s
202
+ end
203
+
204
+ # Generates a unique ID for each checkbox item.
205
+ #
206
+ # @param index [Integer] the item index
207
+ # @return [String] the generated ID
208
+ # @api private
209
+ def item_id(index)
210
+ base_id = @name.to_s.gsub(/\[|\]/, "_").gsub(/_+/, "_").chomp("_")
211
+ "#{base_id}_#{index}"
212
+ end
213
+
214
+ # Returns the CSS classes for the fieldset wrapper.
215
+ #
216
+ # @return [String] the merged CSS class string
217
+ # @api private
218
+ def wrapper_classes
219
+ css_classes([
220
+ "form-field-wrapper",
221
+ @container_classes
222
+ ].flatten.compact)
223
+ end
224
+
225
+ # Returns the CSS classes for the legend element.
226
+ #
227
+ # @return [String] the merged CSS class string for the legend
228
+ # @api private
229
+ def legend_element_classes
230
+ css_classes([
231
+ "block",
232
+ "font-medium",
233
+ "text-gray-700",
234
+ "mb-2",
235
+ legend_size_classes,
236
+ @legend_classes
237
+ ].flatten.compact)
238
+ end
239
+
240
+ # Returns size-specific CSS classes for the legend text.
241
+ #
242
+ # @return [String] the text size class for the current component size
243
+ # @api private
244
+ def legend_size_classes
245
+ case @size
246
+ when :xs then "text-xs"
247
+ when :sm then "text-sm"
248
+ when :md then "text-sm"
249
+ when :lg then "text-base"
250
+ when :xl then "text-lg"
251
+ end
252
+ end
253
+
254
+ # Returns the CSS classes for the items container.
255
+ #
256
+ # @return [String] the merged CSS class string
257
+ # @api private
258
+ def items_wrapper_classes
259
+ css_classes([
260
+ "flex",
261
+ orientation_classes,
262
+ items_gap_classes,
263
+ @items_classes
264
+ ].flatten.compact)
265
+ end
266
+
267
+ # Returns CSS classes for orientation layout.
268
+ #
269
+ # @return [Array<String>] array of orientation CSS class strings
270
+ # @api private
271
+ def orientation_classes
272
+ case @orientation
273
+ when :vertical
274
+ [ "flex-col" ]
275
+ when :horizontal
276
+ [ "flex-row", "flex-wrap" ]
277
+ end
278
+ end
279
+
280
+ # Returns gap classes based on component size and orientation.
281
+ #
282
+ # @return [String] the gap class for the current component size
283
+ # @api private
284
+ def items_gap_classes
285
+ case @size
286
+ when :xs then @orientation == :horizontal ? "gap-x-4 gap-y-1.5" : "gap-1.5"
287
+ when :sm then @orientation == :horizontal ? "gap-x-5 gap-y-2" : "gap-2"
288
+ when :md then @orientation == :horizontal ? "gap-x-6 gap-y-2.5" : "gap-2.5"
289
+ when :lg then @orientation == :horizontal ? "gap-x-7 gap-y-3" : "gap-3"
290
+ when :xl then @orientation == :horizontal ? "gap-x-8 gap-y-3.5" : "gap-3.5"
291
+ end
292
+ end
293
+
294
+ # Returns the CSS classes for the hint text element.
295
+ #
296
+ # @return [String] the merged CSS class string for the hint text
297
+ # @api private
298
+ def hint_element_classes
299
+ css_classes([
300
+ "block",
301
+ "text-gray-600",
302
+ "mt-2",
303
+ hint_size_classes,
304
+ @hint_classes
305
+ ].flatten.compact)
306
+ end
307
+
308
+ # Returns size-specific CSS classes for the hint text.
309
+ #
310
+ # @return [String] the text size class for the current component size
311
+ # @api private
312
+ def hint_size_classes
313
+ case @size
314
+ when :xs then "text-xs"
315
+ when :sm then "text-xs"
316
+ when :md then "text-sm"
317
+ when :lg then "text-sm"
318
+ when :xl then "text-base"
319
+ end
320
+ end
321
+
322
+ # Returns the CSS classes for the error messages container.
323
+ #
324
+ # @return [String] the merged CSS class string for the error messages container
325
+ # @api private
326
+ def errors_element_classes
327
+ css_classes([
328
+ "text-danger-600",
329
+ "mt-2",
330
+ "space-y-0.5",
331
+ error_size_classes,
332
+ @error_classes
333
+ ].flatten.compact)
334
+ end
335
+
336
+ # Returns size-specific CSS classes for error message text.
337
+ #
338
+ # @return [String] the text size class for the current component size
339
+ # @api private
340
+ def error_size_classes
341
+ case @size
342
+ when :xs then "text-xs"
343
+ when :sm then "text-xs"
344
+ when :md then "text-sm"
345
+ when :lg then "text-sm"
346
+ when :xl then "text-base"
347
+ end
348
+ end
349
+
350
+ # Returns the HTML attributes for the fieldset element.
351
+ #
352
+ # @return [Hash] hash of HTML attributes for the fieldset
353
+ # @api private
354
+ def fieldset_attributes
355
+ {
356
+ class: wrapper_classes,
357
+ disabled: @disabled || nil,
358
+ **@options
359
+ }.compact
360
+ end
361
+ end
362
+ end
363
+ 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>