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,419 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Forms
5
+ # A checkbox input component with support for labels, hints, errors, and color variants.
6
+ #
7
+ # This component provides a styled checkbox with customizable colors, sizes, and label positioning.
8
+ # Unlike text inputs, the label appears inline (left or right) with the checkbox rather than above.
9
+ #
10
+ # @example Basic checkbox
11
+ # <%= render BetterUi::Forms::CheckboxComponent.new(
12
+ # name: "user[terms]",
13
+ # label: "I agree to the terms and conditions"
14
+ # ) %>
15
+ #
16
+ # @example Checkbox with different variant
17
+ # <%= render BetterUi::Forms::CheckboxComponent.new(
18
+ # name: "settings[notifications]",
19
+ # label: "Enable notifications",
20
+ # variant: :success,
21
+ # checked: true
22
+ # ) %>
23
+ #
24
+ # @example Checkbox with label on left
25
+ # <%= render BetterUi::Forms::CheckboxComponent.new(
26
+ # name: "user[active]",
27
+ # label: "Active",
28
+ # label_position: :left
29
+ # ) %>
30
+ #
31
+ # @example With validation errors
32
+ # <%= render BetterUi::Forms::CheckboxComponent.new(
33
+ # name: "user[terms]",
34
+ # label: "I agree to the terms",
35
+ # errors: ["You must agree to the terms"]
36
+ # ) %>
37
+ #
38
+ # @example Using with Rails form builder
39
+ # <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
40
+ # <%= f.bui_checkbox :newsletter, label: "Subscribe to newsletter" %>
41
+ # <% end %>
42
+ #
43
+ # @see BetterUi::UiFormBuilder#bui_checkbox
44
+ class CheckboxComponent < ApplicationComponent
45
+ # Available size variants for checkbox inputs.
46
+ # Each size adjusts the checkbox dimensions and spacing proportionally.
47
+ #
48
+ # @return [Array<Symbol>] the list of valid size options (:xs, :sm, :md, :lg, :xl)
49
+ SIZES = %i[xs sm md lg xl].freeze
50
+
51
+ # Available label positions relative to the checkbox.
52
+ #
53
+ # @return [Array<Symbol>] the list of valid label positions (:left, :right)
54
+ LABEL_POSITIONS = %i[left right].freeze
55
+
56
+ # Initializes a new checkbox component.
57
+ #
58
+ # @param name [String] the name attribute for the checkbox (required for form submission)
59
+ # @param value [String] the value submitted when checkbox is checked (defaults to "1")
60
+ # @param checked [Boolean] whether the checkbox is initially checked
61
+ # @param label [String, nil] the label text displayed next to the checkbox
62
+ # @param hint [String, nil] helpful hint text displayed below the checkbox
63
+ # @param variant [Symbol] the color variant (:primary, :secondary, :accent, :success, :danger, :warning, :info, :light, :dark)
64
+ # @param size [Symbol] the size variant (:xs, :sm, :md, :lg, :xl), defaults to :md
65
+ # @param label_position [Symbol] where to position the label (:left, :right), defaults to :right
66
+ # @param disabled [Boolean] whether the checkbox should be disabled (non-interactive), defaults to false
67
+ # @param readonly [Boolean] whether the checkbox should be readonly (viewable but not editable), defaults to false
68
+ # @param required [Boolean] whether the field is required (shows asterisk indicator), defaults to false
69
+ # @param errors [Array<String>, String, nil] validation error messages to display below the checkbox
70
+ # @param container_classes [String, Array<String>, nil] additional CSS classes for the outer wrapper
71
+ # @param label_classes [String, Array<String>, nil] additional CSS classes for the label element
72
+ # @param checkbox_classes [String, Array<String>, nil] additional CSS classes for the checkbox element
73
+ # @param hint_classes [String, Array<String>, nil] additional CSS classes for the hint text
74
+ # @param error_classes [String, Array<String>, nil] additional CSS classes for error messages
75
+ # @param options [Hash] additional HTML attributes to pass through to the checkbox element
76
+ #
77
+ # @raise [ArgumentError] if variant, size, or label_position is invalid
78
+ def initialize(
79
+ name:,
80
+ value: "1",
81
+ checked: false,
82
+ label: nil,
83
+ hint: nil,
84
+ variant: :primary,
85
+ size: :md,
86
+ label_position: :right,
87
+ disabled: false,
88
+ readonly: false,
89
+ required: false,
90
+ errors: nil,
91
+ container_classes: nil,
92
+ label_classes: nil,
93
+ checkbox_classes: nil,
94
+ hint_classes: nil,
95
+ error_classes: nil,
96
+ **options
97
+ )
98
+ @name = name
99
+ @value = value
100
+ @checked = checked
101
+ @label = label
102
+ @hint = hint
103
+ @variant = validate_variant(variant)
104
+ @size = validate_size(size)
105
+ @label_position = validate_label_position(label_position)
106
+ @disabled = disabled
107
+ @readonly = readonly
108
+ @required = required
109
+ @errors = Array(errors).compact.reject(&:blank?)
110
+ @container_classes = container_classes
111
+ @label_classes = label_classes
112
+ @checkbox_classes = checkbox_classes
113
+ @hint_classes = hint_classes
114
+ @error_classes = error_classes
115
+ @options = options
116
+ end
117
+
118
+ private
119
+
120
+ # Validates that the provided variant is one of the allowed VARIANTS.
121
+ #
122
+ # @param variant [Symbol] the variant to validate
123
+ # @return [Symbol] the validated variant
124
+ # @raise [ArgumentError] if variant is not in VARIANTS
125
+ # @api private
126
+ def validate_variant(variant)
127
+ unless BetterUi::ApplicationComponent::VARIANTS.key?(variant)
128
+ raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{BetterUi::ApplicationComponent::VARIANTS.keys.join(', ')}"
129
+ end
130
+ variant
131
+ end
132
+
133
+ # Validates that the provided size is one of the allowed SIZES.
134
+ #
135
+ # @param size [Symbol] the size to validate
136
+ # @return [Symbol] the validated size
137
+ # @raise [ArgumentError] if size is not in SIZES
138
+ # @api private
139
+ def validate_size(size)
140
+ unless SIZES.include?(size)
141
+ raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.join(', ')}"
142
+ end
143
+ size
144
+ end
145
+
146
+ # Validates that the provided label_position is one of the allowed LABEL_POSITIONS.
147
+ #
148
+ # @param label_position [Symbol] the label_position to validate
149
+ # @return [Symbol] the validated label_position
150
+ # @raise [ArgumentError] if label_position is not in LABEL_POSITIONS
151
+ # @api private
152
+ def validate_label_position(label_position)
153
+ unless LABEL_POSITIONS.include?(label_position)
154
+ raise ArgumentError, "Invalid label_position: #{label_position}. Must be one of: #{LABEL_POSITIONS.join(', ')}"
155
+ end
156
+ label_position
157
+ end
158
+
159
+ # Checks if the component has any validation errors to display.
160
+ #
161
+ # @return [Boolean] true if errors are present, false otherwise
162
+ # @api private
163
+ def has_errors?
164
+ @errors.present?
165
+ end
166
+
167
+ # Returns the CSS classes for the outermost wrapper element.
168
+ #
169
+ # @return [String] the merged CSS class string
170
+ # @api private
171
+ def wrapper_classes
172
+ css_classes([
173
+ "form-field-wrapper",
174
+ @container_classes
175
+ ].flatten.compact)
176
+ end
177
+
178
+ # Returns the CSS classes for the checkbox and label wrapper.
179
+ #
180
+ # Handles flex direction based on label_position.
181
+ #
182
+ # @return [String] the merged CSS class string
183
+ # @api private
184
+ def checkbox_wrapper_classes
185
+ css_classes([
186
+ "flex",
187
+ "items-start",
188
+ @label_position == :left ? "flex-row-reverse" : "flex-row",
189
+ gap_classes
190
+ ].flatten.compact)
191
+ end
192
+
193
+ # Returns gap classes based on component size.
194
+ #
195
+ # @return [String] the gap class for the current component size
196
+ # @api private
197
+ def gap_classes
198
+ case @size
199
+ when :xs then "gap-1.5"
200
+ when :sm then "gap-2"
201
+ when :md then "gap-2.5"
202
+ when :lg then "gap-3"
203
+ when :xl then "gap-3.5"
204
+ end
205
+ end
206
+
207
+ # Returns the CSS classes for the checkbox element itself.
208
+ #
209
+ # @return [String] the merged CSS class string for the checkbox element
210
+ # @api private
211
+ def checkbox_element_classes
212
+ css_classes([
213
+ base_checkbox_classes,
214
+ size_checkbox_classes,
215
+ variant_checkbox_classes,
216
+ state_checkbox_classes,
217
+ @checkbox_classes
218
+ ].flatten.compact)
219
+ end
220
+
221
+ # Returns the base CSS classes common to all checkboxes.
222
+ #
223
+ # @return [Array<String>] array of base CSS class strings
224
+ # @api private
225
+ def base_checkbox_classes
226
+ [
227
+ "appearance-none",
228
+ "shrink-0",
229
+ "rounded",
230
+ "border",
231
+ "cursor-pointer",
232
+ "transition-colors",
233
+ "duration-200",
234
+ "focus:outline-none",
235
+ "focus:ring-2",
236
+ "focus:ring-offset-2"
237
+ ]
238
+ end
239
+
240
+ # Returns size-specific CSS classes for the checkbox.
241
+ #
242
+ # @return [Array<String>] array of size-specific CSS class strings
243
+ # @api private
244
+ def size_checkbox_classes
245
+ case @size
246
+ when :xs then [ "w-3", "h-3" ]
247
+ when :sm then [ "w-4", "h-4" ]
248
+ when :md then [ "w-5", "h-5" ]
249
+ when :lg then [ "w-6", "h-6" ]
250
+ when :xl then [ "w-7", "h-7" ]
251
+ end
252
+ end
253
+
254
+ # Returns variant-specific CSS classes for the checkbox.
255
+ # Uses literal strings for Tailwind JIT detection.
256
+ #
257
+ # @return [Array<String>] array of variant-specific CSS class strings
258
+ # @api private
259
+ def variant_checkbox_classes
260
+ case @variant
261
+ when :primary
262
+ [ "border-gray-300", "checked:bg-primary-600", "checked:border-primary-600", "focus:ring-primary-500" ]
263
+ when :secondary
264
+ [ "border-gray-300", "checked:bg-secondary-600", "checked:border-secondary-600", "focus:ring-secondary-500" ]
265
+ when :accent
266
+ [ "border-gray-300", "checked:bg-accent-600", "checked:border-accent-600", "focus:ring-accent-500" ]
267
+ when :success
268
+ [ "border-gray-300", "checked:bg-success-600", "checked:border-success-600", "focus:ring-success-500" ]
269
+ when :danger
270
+ [ "border-gray-300", "checked:bg-danger-600", "checked:border-danger-600", "focus:ring-danger-500" ]
271
+ when :warning
272
+ [ "border-gray-300", "checked:bg-warning-600", "checked:border-warning-600", "focus:ring-warning-500" ]
273
+ when :info
274
+ [ "border-gray-300", "checked:bg-info-600", "checked:border-info-600", "focus:ring-info-500" ]
275
+ when :light
276
+ [ "border-gray-300", "checked:bg-gray-200", "checked:border-gray-400", "focus:ring-gray-400", "checked-dark" ]
277
+ when :dark
278
+ [ "border-gray-400", "checked:bg-gray-800", "checked:border-gray-800", "focus:ring-gray-600" ]
279
+ end
280
+ end
281
+
282
+ # Returns state-specific CSS classes for the checkbox.
283
+ #
284
+ # @return [Array<String>] array of state-specific CSS class strings
285
+ # @api private
286
+ def state_checkbox_classes
287
+ if @disabled
288
+ [ "bg-gray-100", "cursor-not-allowed", "opacity-60" ]
289
+ elsif @readonly
290
+ [ "bg-gray-50", "cursor-default", "pointer-events-none" ]
291
+ elsif has_errors?
292
+ [ "bg-white", "border-danger-500", "focus:ring-danger-500" ]
293
+ else
294
+ [ "bg-white" ]
295
+ end
296
+ end
297
+
298
+ # Returns the CSS classes for the label element.
299
+ #
300
+ # @return [String] the merged CSS class string for the label
301
+ # @api private
302
+ def label_element_classes
303
+ css_classes([
304
+ "select-none",
305
+ "cursor-pointer",
306
+ @disabled ? "text-gray-400 cursor-not-allowed" : "text-gray-700",
307
+ label_size_classes,
308
+ @label_classes
309
+ ].flatten.compact)
310
+ end
311
+
312
+ # Returns size-specific CSS classes for the label text.
313
+ #
314
+ # @return [String] the text size class for the current component size
315
+ # @api private
316
+ def label_size_classes
317
+ case @size
318
+ when :xs then "text-xs"
319
+ when :sm then "text-sm"
320
+ when :md then "text-sm"
321
+ when :lg then "text-base"
322
+ when :xl then "text-lg"
323
+ end
324
+ end
325
+
326
+ # Returns the CSS classes for the hint text element.
327
+ #
328
+ # @return [String] the merged CSS class string for the hint text
329
+ # @api private
330
+ def hint_element_classes
331
+ css_classes([
332
+ "block",
333
+ "text-gray-600",
334
+ "mt-1",
335
+ hint_size_classes,
336
+ @hint_classes
337
+ ].flatten.compact)
338
+ end
339
+
340
+ # Returns size-specific CSS classes for the hint text.
341
+ #
342
+ # @return [String] the text size class for the current component size
343
+ # @api private
344
+ def hint_size_classes
345
+ case @size
346
+ when :xs then "text-xs"
347
+ when :sm then "text-xs"
348
+ when :md then "text-sm"
349
+ when :lg then "text-sm"
350
+ when :xl then "text-base"
351
+ end
352
+ end
353
+
354
+ # Returns the CSS classes for the error messages container.
355
+ #
356
+ # @return [String] the merged CSS class string for the error messages container
357
+ # @api private
358
+ def errors_element_classes
359
+ css_classes([
360
+ "text-danger-600",
361
+ "mt-1",
362
+ "space-y-0.5",
363
+ error_size_classes,
364
+ @error_classes
365
+ ].flatten.compact)
366
+ end
367
+
368
+ # Returns size-specific CSS classes for error message text.
369
+ #
370
+ # @return [String] the text size class for the current component size
371
+ # @api private
372
+ def error_size_classes
373
+ case @size
374
+ when :xs then "text-xs"
375
+ when :sm then "text-xs"
376
+ when :md then "text-sm"
377
+ when :lg then "text-sm"
378
+ when :xl then "text-base"
379
+ end
380
+ end
381
+
382
+ # Returns the complete set of HTML attributes for the checkbox element.
383
+ #
384
+ # @return [Hash] hash of HTML attributes for the checkbox element
385
+ # @api private
386
+ def checkbox_attributes
387
+ attrs = {
388
+ type: "checkbox",
389
+ id: input_id,
390
+ name: @name,
391
+ value: @value,
392
+ checked: @checked || nil,
393
+ disabled: @disabled || nil,
394
+ readonly: @readonly || nil,
395
+ required: @required || nil,
396
+ class: checkbox_element_classes,
397
+ **@options
398
+ }.compact
399
+
400
+ # Handle readonly by adding aria attribute since checkboxes don't support readonly HTML attribute
401
+ if @readonly
402
+ attrs[:aria] ||= {}
403
+ attrs[:aria][:readonly] = "true"
404
+ attrs.delete(:readonly)
405
+ end
406
+
407
+ attrs
408
+ end
409
+
410
+ # Returns the input ID, either from options or generated from name.
411
+ #
412
+ # @return [String] the input ID
413
+ # @api private
414
+ def input_id
415
+ @options[:id] || @name.to_s.gsub(/\[|\]/, "_").gsub(/_+/, "_").chomp("_")
416
+ end
417
+ end
418
+ end
419
+ end
@@ -0,0 +1,40 @@
1
+ <fieldset <%= tag.attributes(fieldset_attributes) %>>
2
+ <% if @legend.present? %>
3
+ <legend class="<%= legend_element_classes %>">
4
+ <%= @legend %>
5
+ <% if @required %>
6
+ <span class="text-danger-600 ml-0.5">*</span>
7
+ <% end %>
8
+ </legend>
9
+ <% end %>
10
+
11
+ <div class="<%= items_wrapper_classes %>">
12
+ <% @collection.each_with_index do |item, index| %>
13
+ <%= render BetterUi::Forms::CheckboxComponent.new(
14
+ name: field_name,
15
+ value: item_value(item),
16
+ checked: item_checked?(item_value(item)),
17
+ label: item_label(item),
18
+ variant: @variant,
19
+ size: @size,
20
+ disabled: @disabled,
21
+ id: item_id(index),
22
+ errors: nil
23
+ ) %>
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
+ </fieldset>