better_ui 0.1.0 → 0.2.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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +225 -119
  4. data/app/assets/stylesheets/better_ui/application.css +0 -356
  5. data/app/components/better_ui/application/card/component.html.erb +20 -0
  6. data/app/components/better_ui/application/card/component.rb +214 -0
  7. data/app/components/better_ui/application/main/component.html.erb +9 -0
  8. data/app/components/better_ui/application/main/component.rb +123 -0
  9. data/app/components/better_ui/application/navbar/component.html.erb +92 -0
  10. data/app/components/better_ui/application/navbar/component.rb +136 -0
  11. data/app/components/better_ui/application/sidebar/component.html.erb +190 -0
  12. data/app/components/better_ui/application/sidebar/component.rb +129 -0
  13. data/app/components/better_ui/general/alert/component.html.erb +32 -0
  14. data/app/components/better_ui/general/alert/component.rb +242 -0
  15. data/app/components/better_ui/general/avatar/component.html.erb +20 -0
  16. data/app/components/better_ui/general/avatar/component.rb +301 -0
  17. data/app/components/better_ui/general/badge/component.html.erb +23 -0
  18. data/app/components/better_ui/general/badge/component.rb +248 -0
  19. data/app/components/better_ui/general/breadcrumb/component.html.erb +15 -0
  20. data/app/components/better_ui/general/breadcrumb/component.rb +187 -0
  21. data/app/components/better_ui/general/button/component.html.erb +34 -0
  22. data/app/components/better_ui/general/button/component.rb +214 -0
  23. data/app/components/better_ui/general/divider/component.html.erb +10 -0
  24. data/app/components/better_ui/general/divider/component.rb +226 -0
  25. data/app/components/better_ui/general/dropdown/component.html.erb +14 -0
  26. data/app/components/better_ui/general/dropdown/component.rb +219 -0
  27. data/app/components/better_ui/general/dropdown/divider_component.html.erb +1 -0
  28. data/app/components/better_ui/general/dropdown/divider_component.rb +41 -0
  29. data/app/components/better_ui/general/dropdown/item_component.html.erb +6 -0
  30. data/app/components/better_ui/general/dropdown/item_component.rb +118 -0
  31. data/app/components/better_ui/general/field/component.html.erb +27 -0
  32. data/app/components/better_ui/general/field/component.rb +37 -0
  33. data/app/components/better_ui/general/heading/component.html.erb +22 -0
  34. data/app/components/better_ui/general/heading/component.rb +257 -0
  35. data/app/components/better_ui/general/icon/component.html.erb +7 -0
  36. data/app/components/better_ui/general/icon/component.rb +239 -0
  37. data/app/components/better_ui/general/input/checkbox/component.html.erb +5 -0
  38. data/app/components/better_ui/general/input/checkbox/component.rb +238 -0
  39. data/app/components/better_ui/general/input/datetime/component.html.erb +5 -0
  40. data/app/components/better_ui/general/input/datetime/component.rb +223 -0
  41. data/app/components/better_ui/general/input/radio/component.html.erb +5 -0
  42. data/app/components/better_ui/general/input/radio/component.rb +230 -0
  43. data/app/components/better_ui/general/input/select/component.html.erb +16 -0
  44. data/app/components/better_ui/general/input/select/component.rb +184 -0
  45. data/app/components/better_ui/general/input/select/select_component.html.erb +5 -0
  46. data/app/components/better_ui/general/input/select/select_component.rb +37 -0
  47. data/app/components/better_ui/general/input/text/component.html.erb +5 -0
  48. data/app/components/better_ui/general/input/text/component.rb +171 -0
  49. data/app/components/better_ui/general/input/textarea/component.html.erb +5 -0
  50. data/app/components/better_ui/general/input/textarea/component.rb +166 -0
  51. data/app/components/better_ui/general/link/component.html.erb +18 -0
  52. data/app/components/better_ui/general/link/component.rb +258 -0
  53. data/app/components/better_ui/general/modal/component.html.erb +42 -0
  54. data/app/components/better_ui/general/modal/component.rb +165 -0
  55. data/app/components/better_ui/general/pagination/component.html.erb +85 -0
  56. data/app/components/better_ui/general/pagination/component.rb +216 -0
  57. data/app/components/better_ui/general/panel/component.html.erb +28 -0
  58. data/app/components/better_ui/general/panel/component.rb +249 -0
  59. data/app/components/better_ui/general/progress/component.html.erb +11 -0
  60. data/app/components/better_ui/general/progress/component.rb +160 -0
  61. data/app/components/better_ui/general/spinner/component.html.erb +35 -0
  62. data/app/components/better_ui/general/spinner/component.rb +93 -0
  63. data/app/components/better_ui/general/table/component.html.erb +5 -0
  64. data/app/components/better_ui/general/table/component.rb +217 -0
  65. data/app/components/better_ui/general/table/tbody_component.html.erb +3 -0
  66. data/app/components/better_ui/general/table/tbody_component.rb +30 -0
  67. data/app/components/better_ui/general/table/td_component.html.erb +3 -0
  68. data/app/components/better_ui/general/table/td_component.rb +44 -0
  69. data/app/components/better_ui/general/table/tfoot_component.html.erb +3 -0
  70. data/app/components/better_ui/general/table/tfoot_component.rb +28 -0
  71. data/app/components/better_ui/general/table/th_component.html.erb +6 -0
  72. data/app/components/better_ui/general/table/th_component.rb +51 -0
  73. data/app/components/better_ui/general/table/thead_component.html.erb +3 -0
  74. data/app/components/better_ui/general/table/thead_component.rb +28 -0
  75. data/app/components/better_ui/general/table/tr_component.html.erb +3 -0
  76. data/app/components/better_ui/general/table/tr_component.rb +30 -0
  77. data/app/components/better_ui/general/tabs/component.html.erb +3 -0
  78. data/app/components/better_ui/general/tabs/component.rb +102 -0
  79. data/app/components/better_ui/general/tabs/panel_component.html.erb +3 -0
  80. data/app/components/better_ui/general/tabs/panel_component.rb +37 -0
  81. data/app/components/better_ui/general/tabs/tab_component.html.erb +13 -0
  82. data/app/components/better_ui/general/tabs/tab_component.rb +111 -0
  83. data/app/components/better_ui/general/tag/component.html.erb +3 -0
  84. data/app/components/better_ui/general/tag/component.rb +104 -0
  85. data/app/components/better_ui/general/tooltip/component.html.erb +7 -0
  86. data/app/components/better_ui/general/tooltip/component.rb +239 -0
  87. data/app/helpers/better_ui/application/components/card/card_helper.rb +96 -0
  88. data/app/helpers/better_ui/application/components/card.rb +11 -0
  89. data/app/helpers/better_ui/application/components/main/main_helper.rb +64 -0
  90. data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +77 -0
  91. data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +51 -0
  92. data/app/helpers/better_ui/application_helper.rb +51 -179
  93. data/app/helpers/better_ui/general/components/alert/alert_helper.rb +57 -0
  94. data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +29 -0
  95. data/app/helpers/better_ui/general/components/badge/badge_helper.rb +53 -0
  96. data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +37 -0
  97. data/app/helpers/better_ui/general/components/button/button_helper.rb +65 -0
  98. data/app/helpers/better_ui/general/components/container/container_helper.rb +60 -0
  99. data/app/helpers/better_ui/general/components/divider/divider_helper.rb +63 -0
  100. data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +32 -0
  101. data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +79 -0
  102. data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +62 -0
  103. data/app/helpers/better_ui/general/components/field/field_helper.rb +26 -0
  104. data/app/helpers/better_ui/general/components/heading/heading_helper.rb +72 -0
  105. data/app/helpers/better_ui/general/components/icon/icon_helper.rb +16 -0
  106. data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +81 -0
  107. data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +91 -0
  108. data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +79 -0
  109. data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +124 -0
  110. data/app/helpers/better_ui/general/components/input/select/select_helper.rb +70 -0
  111. data/app/helpers/better_ui/general/components/input/text/text_helper.rb +138 -0
  112. data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +73 -0
  113. data/app/helpers/better_ui/general/components/link/link_helper.rb +89 -0
  114. data/app/helpers/better_ui/general/components/modal/modal_helper.rb +95 -0
  115. data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +82 -0
  116. data/app/helpers/better_ui/general/components/panel/panel_helper.rb +83 -0
  117. data/app/helpers/better_ui/general/components/progress/progress_helper.rb +53 -0
  118. data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +19 -0
  119. data/app/helpers/better_ui/general/components/table/table_helper.rb +53 -0
  120. data/app/helpers/better_ui/general/components/table/tbody_helper.rb +13 -0
  121. data/app/helpers/better_ui/general/components/table/td_helper.rb +19 -0
  122. data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +13 -0
  123. data/app/helpers/better_ui/general/components/table/th_helper.rb +19 -0
  124. data/app/helpers/better_ui/general/components/table/thead_helper.rb +13 -0
  125. data/app/helpers/better_ui/general/components/table/tr_helper.rb +13 -0
  126. data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +62 -0
  127. data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +55 -0
  128. data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +62 -0
  129. data/app/helpers/better_ui/general/components/tag/tag_helper.rb +26 -0
  130. data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +60 -0
  131. data/app/views/layouts/better_ui/application.html.erb +6 -124
  132. data/config/initializers/lookbook.rb +23 -0
  133. data/config/routes.rb +0 -8
  134. data/lib/better_ui/engine.rb +5 -19
  135. data/lib/better_ui/railtie.rb +20 -0
  136. data/lib/better_ui/version.rb +1 -1
  137. data/lib/better_ui.rb +4 -20
  138. metadata +155 -28
  139. data/app/controllers/better_ui/docs_controller.rb +0 -41
  140. data/app/views/better_ui/docs/component.html.erb +0 -365
  141. data/app/views/better_ui/docs/index.html.erb +0 -100
  142. data/app/views/better_ui/docs/show.html.erb +0 -60
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module General
5
+ module Input
6
+ module Checkbox
7
+ class Component < ViewComponent::Base
8
+ # Costanti con classi Tailwind dirette
9
+ CHECKBOX_THEME = {
10
+ default: 'border-gray-300 text-blue-600 focus:border-blue-500 focus:ring-blue-500 checked:bg-blue-600 checked:border-blue-600',
11
+ white: 'border-gray-300 text-gray-900 focus:border-gray-500 focus:ring-gray-500 checked:bg-white checked:border-gray-900 checked:text-gray-900',
12
+ red: 'border-gray-300 text-red-600 focus:border-red-500 focus:ring-red-500 checked:bg-red-600 checked:border-red-600',
13
+ rose: 'border-gray-300 text-rose-600 focus:border-rose-500 focus:ring-rose-500 checked:bg-rose-600 checked:border-rose-600',
14
+ orange: 'border-gray-300 text-orange-600 focus:border-orange-500 focus:ring-orange-500 checked:bg-orange-600 checked:border-orange-600',
15
+ green: 'border-gray-300 text-green-600 focus:border-green-500 focus:ring-green-500 checked:bg-green-600 checked:border-green-600',
16
+ blue: 'border-gray-300 text-blue-600 focus:border-blue-500 focus:ring-blue-500 checked:bg-blue-600 checked:border-blue-600',
17
+ yellow: 'border-gray-300 text-yellow-600 focus:border-yellow-500 focus:ring-yellow-500 checked:bg-yellow-600 checked:border-yellow-600',
18
+ violet: 'border-gray-300 text-violet-600 focus:border-violet-500 focus:ring-violet-500 checked:bg-violet-600 checked:border-violet-600'
19
+ }.freeze
20
+
21
+ CHECKBOX_SIZE = {
22
+ small: 'h-4 w-4',
23
+ medium: 'h-5 w-5',
24
+ large: 'h-6 w-6'
25
+ }.freeze
26
+
27
+ CHECKBOX_ROUNDED = {
28
+ none: 'rounded-none',
29
+ small: 'rounded-sm',
30
+ medium: 'rounded',
31
+ large: 'rounded-lg',
32
+ full: 'rounded-full'
33
+ }.freeze
34
+
35
+ CHECKBOX_BASE_CLASSES = 'appearance-none border-2 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors duration-200 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50'.freeze
36
+
37
+ CHECKBOX_LABEL_GAP = {
38
+ small: 'gap-2',
39
+ medium: 'gap-2.5',
40
+ large: 'gap-3'
41
+ }.freeze
42
+
43
+ CHECKBOX_LABEL_TEXT = {
44
+ small: 'text-sm',
45
+ medium: 'text-base',
46
+ large: 'text-lg'
47
+ }.freeze
48
+
49
+ attr_reader :name, :value, :checked, :required, :disabled, :indeterminate,
50
+ :label, :label_position, :theme, :size, :rounded, :classes, :form, :options
51
+
52
+ # @param name [String] Nome del campo checkbox (obbligatorio)
53
+ # @param value [String] Valore del checkbox (default: "1")
54
+ # @param checked [Boolean] Se il checkbox è selezionato
55
+ # @param required [Boolean] Se il campo è obbligatorio
56
+ # @param disabled [Boolean] Se il campo è disabilitato
57
+ # @param indeterminate [Boolean] Se il checkbox è in stato indeterminate
58
+ # @param label [String, nil] Testo della label associata al checkbox
59
+ # @param label_position [Symbol] Posizione della label (:left, :right)
60
+ # @param theme [Symbol] Tema del componente (:default, :white, :red, :rose, :orange, :green, :blue, :yellow, :violet)
61
+ # @param size [Symbol] Dimensione del componente (:small, :medium, :large)
62
+ # @param rounded [Symbol] Border radius (:none, :small, :medium, :large, :full)
63
+ # @param classes [String] Classi CSS aggiuntive
64
+ # @param form [ActionView::Helpers::FormBuilder, nil] Form builder Rails opzionale
65
+ # @param options [Hash] Opzioni aggiuntive per l'input (es. data attributes, aria attributes)
66
+ def initialize(name:, value: "1", checked: false, required: false, disabled: false,
67
+ indeterminate: false, label: nil, label_position: :right, theme: :default,
68
+ size: :medium, rounded: :medium, classes: '', form: nil, **options)
69
+ @name = name
70
+ @value = value
71
+ @checked = checked
72
+ @required = required
73
+ @disabled = disabled
74
+ @indeterminate = indeterminate
75
+ @label = label
76
+ @label_position = label_position.to_sym
77
+ @theme = theme.to_sym
78
+ @size = size.to_sym
79
+ @rounded = rounded.to_sym
80
+ @classes = classes
81
+ @form = form
82
+ @options = options
83
+
84
+ validate_params
85
+ end
86
+
87
+ private
88
+
89
+ def validate_params
90
+ validate_theme
91
+ validate_size
92
+ validate_rounded
93
+ validate_label_position
94
+ end
95
+
96
+ def validate_theme
97
+ return if CHECKBOX_THEME.key?(@theme)
98
+
99
+ raise ArgumentError, "Invalid theme: #{@theme}. Valid themes are: #{CHECKBOX_THEME.keys.join(', ')}"
100
+ end
101
+
102
+ def validate_size
103
+ return if CHECKBOX_SIZE.key?(@size)
104
+
105
+ raise ArgumentError, "Invalid size: #{@size}. Valid sizes are: #{CHECKBOX_SIZE.keys.join(', ')}"
106
+ end
107
+
108
+ def validate_rounded
109
+ return if CHECKBOX_ROUNDED.key?(@rounded)
110
+
111
+ raise ArgumentError, "Invalid rounded: #{@rounded}. Valid rounded options are: #{CHECKBOX_ROUNDED.keys.join(', ')}"
112
+ end
113
+
114
+ def validate_label_position
115
+ return if [:left, :right].include?(@label_position)
116
+
117
+ raise ArgumentError, "Invalid label_position: #{@label_position}. Valid positions are: left, right"
118
+ end
119
+
120
+ def checkbox_classes
121
+ [
122
+ CHECKBOX_BASE_CLASSES,
123
+ CHECKBOX_THEME[@theme],
124
+ CHECKBOX_SIZE[@size],
125
+ CHECKBOX_ROUNDED[@rounded],
126
+ @classes
127
+ ].compact.join(' ')
128
+ end
129
+
130
+ def input_attributes
131
+ attrs = {
132
+ type: 'checkbox',
133
+ name: input_name,
134
+ value: @value,
135
+ class: checkbox_classes,
136
+ checked: @checked,
137
+ required: @required,
138
+ disabled: @disabled,
139
+ id: input_id
140
+ }
141
+
142
+ # Aggiungi indeterminate via JavaScript se necessario
143
+ if @indeterminate
144
+ attrs['data-indeterminate'] = 'true'
145
+ end
146
+
147
+ # Unisci le opzioni personalizzate
148
+ attrs.merge(@options)
149
+ end
150
+
151
+ def input_name
152
+ if @form
153
+ @form.field_name(@name)
154
+ else
155
+ @name
156
+ end
157
+ end
158
+
159
+ def input_id
160
+ @options[:id] || "checkbox_#{@name}"
161
+ end
162
+
163
+ def label_classes
164
+ [
165
+ 'flex items-center cursor-pointer',
166
+ @disabled ? 'opacity-50 cursor-not-allowed' : '',
167
+ CHECKBOX_LABEL_GAP[@size]
168
+ ].compact.join(' ')
169
+ end
170
+
171
+ def label_text_classes
172
+ CHECKBOX_LABEL_TEXT[@size]
173
+ end
174
+
175
+ def input_tag
176
+ if @form
177
+ form_checkbox
178
+ else
179
+ manual_input
180
+ end
181
+ end
182
+
183
+ def form_checkbox
184
+ @form.check_box(@name, {
185
+ class: checkbox_classes,
186
+ id: input_id,
187
+ checked: @checked,
188
+ disabled: @disabled,
189
+ required: @required,
190
+ data: @indeterminate ? { indeterminate: 'true' } : {},
191
+ **@options
192
+ }, @value)
193
+ end
194
+
195
+ def manual_input
196
+ attrs = input_attributes.map do |key, value|
197
+ if value == true
198
+ key.to_s
199
+ elsif value == false || value.nil?
200
+ nil
201
+ else
202
+ "#{key}=\"#{value}\""
203
+ end
204
+ end.compact.join(' ')
205
+
206
+ "<input #{attrs} />".html_safe
207
+ end
208
+
209
+ def render_checkbox_with_label
210
+ if @label_position == :left
211
+ label_left_content
212
+ else
213
+ label_right_content
214
+ end
215
+ end
216
+
217
+ def label_left_content
218
+ content_tag(:label, class: label_classes, for: input_id) do
219
+ safe_join([
220
+ content_tag(:span, @label, class: label_text_classes),
221
+ input_tag
222
+ ])
223
+ end
224
+ end
225
+
226
+ def label_right_content
227
+ content_tag(:label, class: label_classes, for: input_id) do
228
+ safe_join([
229
+ input_tag,
230
+ content_tag(:span, @label, class: label_text_classes)
231
+ ])
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,5 @@
1
+ <% if @form %>
2
+ <%= @form.public_send(form_field_method, @name, **form_input_attributes) %>
3
+ <% else %>
4
+ <%= tag.input(**input_attributes) %>
5
+ <% end %>
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module General
5
+ module Input
6
+ module Datetime
7
+ class Component < ViewComponent::Base
8
+ attr_reader :name, :value, :required, :disabled, :classes, :options,
9
+ :theme, :size, :rounded, :form, :min, :max, :type
10
+
11
+ # Temi supportati per il Datetime Input
12
+ DATETIME_INPUT_THEME = {
13
+ default: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
14
+ white: 'border-white focus:border-gray-300 focus:ring-gray-300 bg-white',
15
+ red: 'border-red-300 focus:border-red-500 focus:ring-red-500',
16
+ rose: 'border-rose-300 focus:border-rose-500 focus:ring-rose-500',
17
+ orange: 'border-orange-300 focus:border-orange-500 focus:ring-orange-500',
18
+ green: 'border-green-300 focus:border-green-500 focus:ring-green-500',
19
+ blue: 'border-blue-300 focus:border-blue-500 focus:ring-blue-500',
20
+ yellow: 'border-yellow-300 focus:border-yellow-500 focus:ring-yellow-500',
21
+ violet: 'border-violet-300 focus:border-violet-500 focus:ring-violet-500'
22
+ }.freeze
23
+
24
+ # Dimensioni supportate per il Datetime Input
25
+ DATETIME_INPUT_SIZES = {
26
+ small: 'h-8 px-2 py-1 text-xs',
27
+ medium: 'h-10 px-3 py-2 text-sm',
28
+ large: 'h-12 px-4 py-3 text-base'
29
+ }.freeze
30
+
31
+ # Border radius supportati per il Datetime Input
32
+ DATETIME_INPUT_RADIUS = {
33
+ none: 'rounded-none',
34
+ small: 'rounded-sm',
35
+ medium: 'rounded-md',
36
+ large: 'rounded-lg',
37
+ full: 'rounded-full'
38
+ }.freeze
39
+
40
+ # Tipi supportati per il Datetime Input
41
+ DATETIME_INPUT_TYPES = [
42
+ :date, :month, :week, :time
43
+ ].freeze
44
+
45
+ # Formati di validazione per tipo
46
+ DATETIME_FORMAT_PATTERNS = {
47
+ date: /^\d{4}-\d{2}-\d{2}$/,
48
+ month: /^\d{4}-\d{2}$/,
49
+ week: /^\d{4}-W\d{2}$/,
50
+ time: /^\d{2}:\d{2}$/
51
+ }.freeze
52
+
53
+ # Classi base per il Datetime Input
54
+ DATETIME_INPUT_BASE_CLASSES = 'block w-full border shadow-sm disabled:bg-gray-100 disabled:cursor-not-allowed focus:outline-none focus:ring-1'
55
+
56
+ # @param name [String] Nome del campo input
57
+ # @param type [Symbol] Tipo del campo datetime (:date, :month, :week, :time)
58
+ # @param value [String] Valore del campo nel formato appropriato per il tipo
59
+ # @param required [Boolean] Se il campo è obbligatorio
60
+ # @param disabled [Boolean] Se il campo è disabilitato
61
+ # @param min [String] Valore minimo selezionabile nel formato appropriato
62
+ # @param max [String] Valore massimo selezionabile nel formato appropriato
63
+ # @param theme [Symbol] Tema del componente (:default, :white, :red, :rose, :orange, :green, :blue, :yellow, :violet)
64
+ # @param size [Symbol] Dimensione del componente (:small, :medium, :large)
65
+ # @param rounded [Symbol] Border radius (:none, :small, :medium, :large, :full)
66
+ # @param classes [String] Classi CSS aggiuntive
67
+ # @param form [ActionView::Helpers::FormBuilder] Form builder Rails opzionale
68
+ # @param options [Hash] Opzioni aggiuntive per l'input
69
+ def initialize(name:, type: :date, value: nil, required: false, disabled: false,
70
+ min: nil, max: nil, theme: :default, size: :medium, rounded: :medium, classes: '', form: nil, **options)
71
+ @name = name
72
+ @type = type
73
+ @value = value
74
+ @required = required
75
+ @disabled = disabled
76
+ @min = min
77
+ @max = max
78
+ @theme = theme
79
+ @size = size
80
+ @rounded = rounded
81
+ @classes = classes
82
+ @form = form
83
+ @options = options
84
+
85
+ validate_params
86
+ super()
87
+ end
88
+
89
+ # Attributi per l'elemento input standalone
90
+ def input_attributes
91
+ {
92
+ type: @type,
93
+ name: @name,
94
+ id: @name,
95
+ value: @value,
96
+ required: @required,
97
+ disabled: @disabled,
98
+ min: @min,
99
+ max: @max,
100
+ class: build_classes
101
+ }.compact.merge(@options)
102
+ end
103
+
104
+ # Attributi per l'elemento input con form builder
105
+ def form_input_attributes
106
+ {
107
+ class: build_classes,
108
+ required: @required,
109
+ disabled: @disabled,
110
+ min: @min,
111
+ max: @max
112
+ }.compact.merge(@options)
113
+ end
114
+
115
+ # Metodo helper per Rails form builder specifico per tipo
116
+ def form_field_method
117
+ case @type
118
+ when :date then :date_field
119
+ when :month then :month_field
120
+ when :week then :week_field
121
+ when :time then :time_field
122
+ else :date_field
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ # Costruisce le classi CSS complete
129
+ def build_classes
130
+ [
131
+ DATETIME_INPUT_BASE_CLASSES,
132
+ get_theme_classes,
133
+ get_size_classes,
134
+ get_rounded_classes,
135
+ @classes
136
+ ].compact.join(' ')
137
+ end
138
+
139
+ # Restituisce le classi del tema
140
+ def get_theme_classes
141
+ DATETIME_INPUT_THEME[@theme]
142
+ end
143
+
144
+ # Restituisce le classi della dimensione
145
+ def get_size_classes
146
+ DATETIME_INPUT_SIZES[@size]
147
+ end
148
+
149
+ # Restituisce le classi del border radius
150
+ def get_rounded_classes
151
+ DATETIME_INPUT_RADIUS[@rounded]
152
+ end
153
+
154
+ # Valida i parametri del componente
155
+ def validate_params
156
+ validate_type
157
+ validate_theme
158
+ validate_size
159
+ validate_rounded
160
+ validate_datetime_format
161
+ end
162
+
163
+ # Valida il tipo datetime
164
+ def validate_type
165
+ return if DATETIME_INPUT_TYPES.include?(@type)
166
+
167
+ raise ArgumentError, "Tipo non valido: #{@type}. Tipi supportati: #{DATETIME_INPUT_TYPES.join(', ')}"
168
+ end
169
+
170
+ # Valida il tema
171
+ def validate_theme
172
+ return if DATETIME_INPUT_THEME.key?(@theme)
173
+
174
+ raise ArgumentError, "Tema non valido: #{@theme}. Temi supportati: #{DATETIME_INPUT_THEME.keys.join(', ')}"
175
+ end
176
+
177
+ # Valida la dimensione
178
+ def validate_size
179
+ return if DATETIME_INPUT_SIZES.key?(@size)
180
+
181
+ raise ArgumentError, "Dimensione non valida: #{@size}. Dimensioni supportate: #{DATETIME_INPUT_SIZES.keys.join(', ')}"
182
+ end
183
+
184
+ # Valida il border radius
185
+ def validate_rounded
186
+ return if DATETIME_INPUT_RADIUS.key?(@rounded)
187
+
188
+ raise ArgumentError, "Border radius non valido: #{@rounded}. Valori supportati: #{DATETIME_INPUT_RADIUS.keys.join(', ')}"
189
+ end
190
+
191
+ # Valida il formato dei valori datetime
192
+ def validate_datetime_format
193
+ validate_single_datetime(@value, 'value') if @value
194
+ validate_single_datetime(@min, 'min') if @min
195
+ validate_single_datetime(@max, 'max') if @max
196
+ end
197
+
198
+ # Valida un singolo valore datetime
199
+ def validate_single_datetime(datetime_string, field_name)
200
+ return if datetime_string.nil? || datetime_string.to_s.strip.empty?
201
+
202
+ pattern = DATETIME_FORMAT_PATTERNS[@type]
203
+ return if pattern && datetime_string.match?(pattern)
204
+
205
+ expected_format = get_expected_format(@type)
206
+ raise ArgumentError, "Il campo #{field_name} deve essere nel formato #{expected_format} per il tipo #{@type}: #{datetime_string}"
207
+ end
208
+
209
+ # Restituisce il formato atteso per il tipo
210
+ def get_expected_format(type)
211
+ case type
212
+ when :date then 'YYYY-MM-DD'
213
+ when :month then 'YYYY-MM'
214
+ when :week then 'YYYY-WXX'
215
+ when :time then 'HH:MM'
216
+ else 'formato valido'
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,5 @@
1
+ <% if @label %>
2
+ <%= render_radio_with_label %>
3
+ <% else %>
4
+ <%= input_tag %>
5
+ <% end %>
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module General
5
+ module Input
6
+ module Radio
7
+ class Component < ViewComponent::Base
8
+ # Costanti con classi Tailwind dirette
9
+ RADIO_THEME = {
10
+ default: 'border-gray-300 text-blue-600 focus:border-blue-500 focus:ring-blue-500 checked:bg-blue-600 checked:border-blue-600',
11
+ white: 'border-gray-300 text-gray-900 focus:border-gray-500 focus:ring-gray-500 checked:bg-white checked:border-gray-900 checked:text-gray-900',
12
+ red: 'border-gray-300 text-red-600 focus:border-red-500 focus:ring-red-500 checked:bg-red-600 checked:border-red-600',
13
+ rose: 'border-gray-300 text-rose-600 focus:border-rose-500 focus:ring-rose-500 checked:bg-rose-600 checked:border-rose-600',
14
+ orange: 'border-gray-300 text-orange-600 focus:border-orange-500 focus:ring-orange-500 checked:bg-orange-600 checked:border-orange-600',
15
+ green: 'border-gray-300 text-green-600 focus:border-green-500 focus:ring-green-500 checked:bg-green-600 checked:border-green-600',
16
+ blue: 'border-gray-300 text-blue-600 focus:border-blue-500 focus:ring-blue-500 checked:bg-blue-600 checked:border-blue-600',
17
+ yellow: 'border-gray-300 text-yellow-600 focus:border-yellow-500 focus:ring-yellow-500 checked:bg-yellow-600 checked:border-yellow-600',
18
+ violet: 'border-gray-300 text-violet-600 focus:border-violet-500 focus:ring-violet-500 checked:bg-violet-600 checked:border-violet-600'
19
+ }.freeze
20
+
21
+ RADIO_SIZE = {
22
+ small: 'h-4 w-4',
23
+ medium: 'h-5 w-5',
24
+ large: 'h-6 w-6'
25
+ }.freeze
26
+
27
+ RADIO_ROUNDED = {
28
+ none: 'rounded-none',
29
+ small: 'rounded-sm',
30
+ medium: 'rounded',
31
+ large: 'rounded-lg',
32
+ full: 'rounded-full'
33
+ }.freeze
34
+
35
+ RADIO_BASE_CLASSES = 'appearance-none border-2 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors duration-200 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50'.freeze
36
+
37
+ RADIO_LABEL_GAP = {
38
+ small: 'gap-2',
39
+ medium: 'gap-2.5',
40
+ large: 'gap-3'
41
+ }.freeze
42
+
43
+ RADIO_LABEL_TEXT = {
44
+ small: 'text-sm',
45
+ medium: 'text-base',
46
+ large: 'text-lg'
47
+ }.freeze
48
+
49
+ attr_reader :name, :value, :checked, :required, :disabled,
50
+ :label, :label_position, :theme, :size, :rounded, :classes, :form, :options
51
+
52
+ # @param name [String] Nome del campo radio (obbligatorio)
53
+ # @param value [String] Valore del radio button (obbligatorio)
54
+ # @param checked [Boolean] Se il radio è selezionato
55
+ # @param required [Boolean] Se il campo è obbligatorio
56
+ # @param disabled [Boolean] Se il campo è disabilitato
57
+ # @param label [String, nil] Testo della label associata al radio
58
+ # @param label_position [Symbol] Posizione della label (:left, :right)
59
+ # @param theme [Symbol] Tema del componente (:default, :white, :red, :rose, :orange, :green, :blue, :yellow, :violet)
60
+ # @param size [Symbol] Dimensione del componente (:small, :medium, :large)
61
+ # @param rounded [Symbol] Border radius (:none, :small, :medium, :large, :full)
62
+ # @param classes [String] Classi CSS aggiuntive
63
+ # @param form [ActionView::Helpers::FormBuilder, nil] Form builder Rails opzionale
64
+ # @param options [Hash] Opzioni aggiuntive per l'input (es. data attributes, aria attributes)
65
+ def initialize(name:, value:, checked: false, required: false, disabled: false,
66
+ label: nil, label_position: :right, theme: :default,
67
+ size: :medium, rounded: :full, classes: '', form: nil, **options)
68
+ @name = name
69
+ @value = value
70
+ @checked = checked
71
+ @required = required
72
+ @disabled = disabled
73
+ @label = label
74
+ @label_position = label_position.to_sym
75
+ @theme = theme.to_sym
76
+ @size = size.to_sym
77
+ @rounded = rounded.to_sym
78
+ @classes = classes
79
+ @form = form
80
+ @options = options
81
+
82
+ validate_params
83
+ end
84
+
85
+ private
86
+
87
+ def validate_params
88
+ validate_theme
89
+ validate_size
90
+ validate_rounded
91
+ validate_label_position
92
+ end
93
+
94
+ def validate_theme
95
+ return if RADIO_THEME.key?(@theme)
96
+
97
+ raise ArgumentError, "Invalid theme: #{@theme}. Valid themes are: #{RADIO_THEME.keys.join(', ')}"
98
+ end
99
+
100
+ def validate_size
101
+ return if RADIO_SIZE.key?(@size)
102
+
103
+ raise ArgumentError, "Invalid size: #{@size}. Valid sizes are: #{RADIO_SIZE.keys.join(', ')}"
104
+ end
105
+
106
+ def validate_rounded
107
+ return if RADIO_ROUNDED.key?(@rounded)
108
+
109
+ raise ArgumentError, "Invalid rounded: #{@rounded}. Valid rounded options are: #{RADIO_ROUNDED.keys.join(', ')}"
110
+ end
111
+
112
+ def validate_label_position
113
+ return if [:left, :right].include?(@label_position)
114
+
115
+ raise ArgumentError, "Invalid label_position: #{@label_position}. Valid positions are: left, right"
116
+ end
117
+
118
+ def radio_classes
119
+ [
120
+ RADIO_BASE_CLASSES,
121
+ RADIO_THEME[@theme],
122
+ RADIO_SIZE[@size],
123
+ RADIO_ROUNDED[@rounded],
124
+ @classes
125
+ ].compact.join(' ')
126
+ end
127
+
128
+ def input_attributes
129
+ attrs = {
130
+ type: 'radio',
131
+ name: input_name,
132
+ value: @value,
133
+ class: radio_classes,
134
+ checked: @checked,
135
+ required: @required,
136
+ disabled: @disabled,
137
+ id: input_id
138
+ }
139
+
140
+ # Unisci le opzioni personalizzate
141
+ attrs.merge(@options)
142
+ end
143
+
144
+ def input_name
145
+ if @form
146
+ @form.field_name(@name)
147
+ else
148
+ @name
149
+ end
150
+ end
151
+
152
+ def input_id
153
+ @options[:id] || "radio_#{@name}_#{@value}"
154
+ end
155
+
156
+ def label_classes
157
+ [
158
+ 'flex items-center cursor-pointer',
159
+ @disabled ? 'opacity-50 cursor-not-allowed' : '',
160
+ RADIO_LABEL_GAP[@size]
161
+ ].compact.join(' ')
162
+ end
163
+
164
+ def label_text_classes
165
+ RADIO_LABEL_TEXT[@size]
166
+ end
167
+
168
+ def input_tag
169
+ if @form
170
+ form_radio
171
+ else
172
+ manual_input
173
+ end
174
+ end
175
+
176
+ def form_radio
177
+ @form.radio_button(@name, @value, {
178
+ class: radio_classes,
179
+ id: input_id,
180
+ checked: @checked,
181
+ disabled: @disabled,
182
+ required: @required,
183
+ **@options
184
+ })
185
+ end
186
+
187
+ def manual_input
188
+ attrs = input_attributes.map do |key, value|
189
+ if value == true
190
+ key.to_s
191
+ elsif value == false || value.nil?
192
+ nil
193
+ else
194
+ "#{key}=\"#{value}\""
195
+ end
196
+ end.compact.join(' ')
197
+
198
+ "<input #{attrs} />".html_safe
199
+ end
200
+
201
+ def render_radio_with_label
202
+ if @label_position == :left
203
+ label_left_content
204
+ else
205
+ label_right_content
206
+ end
207
+ end
208
+
209
+ def label_left_content
210
+ content_tag(:label, class: label_classes, for: input_id) do
211
+ safe_join([
212
+ content_tag(:span, @label, class: label_text_classes),
213
+ input_tag
214
+ ])
215
+ end
216
+ end
217
+
218
+ def label_right_content
219
+ content_tag(:label, class: label_classes, for: input_id) do
220
+ safe_join([
221
+ input_tag,
222
+ content_tag(:span, @label, class: label_text_classes)
223
+ ])
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end