better_ui 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) 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/field/component.html.erb +27 -0
  26. data/app/components/better_ui/general/field/component.rb +37 -0
  27. data/app/components/better_ui/general/heading/component.html.erb +22 -0
  28. data/app/components/better_ui/general/heading/component.rb +257 -0
  29. data/app/components/better_ui/general/icon/component.html.erb +7 -0
  30. data/app/components/better_ui/general/icon/component.rb +239 -0
  31. data/app/components/better_ui/general/input/checkbox/component.html.erb +5 -0
  32. data/app/components/better_ui/general/input/checkbox/component.rb +238 -0
  33. data/app/components/better_ui/general/input/datetime/component.html.erb +5 -0
  34. data/app/components/better_ui/general/input/datetime/component.rb +223 -0
  35. data/app/components/better_ui/general/input/radio/component.html.erb +5 -0
  36. data/app/components/better_ui/general/input/radio/component.rb +230 -0
  37. data/app/components/better_ui/general/input/select/component.html.erb +16 -0
  38. data/app/components/better_ui/general/input/select/component.rb +184 -0
  39. data/app/components/better_ui/general/input/select/select_component.html.erb +5 -0
  40. data/app/components/better_ui/general/input/select/select_component.rb +37 -0
  41. data/app/components/better_ui/general/input/text/component.html.erb +5 -0
  42. data/app/components/better_ui/general/input/text/component.rb +171 -0
  43. data/app/components/better_ui/general/input/textarea/component.html.erb +5 -0
  44. data/app/components/better_ui/general/input/textarea/component.rb +166 -0
  45. data/app/components/better_ui/general/link/component.html.erb +18 -0
  46. data/app/components/better_ui/general/link/component.rb +258 -0
  47. data/app/components/better_ui/general/panel/component.html.erb +28 -0
  48. data/app/components/better_ui/general/panel/component.rb +249 -0
  49. data/app/components/better_ui/general/progress/component.html.erb +11 -0
  50. data/app/components/better_ui/general/progress/component.rb +160 -0
  51. data/app/components/better_ui/general/spinner/component.html.erb +35 -0
  52. data/app/components/better_ui/general/spinner/component.rb +93 -0
  53. data/app/components/better_ui/general/table/component.html.erb +5 -0
  54. data/app/components/better_ui/general/table/component.rb +217 -0
  55. data/app/components/better_ui/general/table/tbody_component.html.erb +3 -0
  56. data/app/components/better_ui/general/table/tbody_component.rb +30 -0
  57. data/app/components/better_ui/general/table/td_component.html.erb +3 -0
  58. data/app/components/better_ui/general/table/td_component.rb +44 -0
  59. data/app/components/better_ui/general/table/tfoot_component.html.erb +3 -0
  60. data/app/components/better_ui/general/table/tfoot_component.rb +28 -0
  61. data/app/components/better_ui/general/table/th_component.html.erb +6 -0
  62. data/app/components/better_ui/general/table/th_component.rb +51 -0
  63. data/app/components/better_ui/general/table/thead_component.html.erb +3 -0
  64. data/app/components/better_ui/general/table/thead_component.rb +28 -0
  65. data/app/components/better_ui/general/table/tr_component.html.erb +3 -0
  66. data/app/components/better_ui/general/table/tr_component.rb +30 -0
  67. data/app/components/better_ui/general/tag/component.html.erb +3 -0
  68. data/app/components/better_ui/general/tag/component.rb +104 -0
  69. data/app/components/better_ui/general/tooltip/component.html.erb +7 -0
  70. data/app/components/better_ui/general/tooltip/component.rb +239 -0
  71. data/app/helpers/better_ui/application/components/card/card_helper.rb +96 -0
  72. data/app/helpers/better_ui/application/components/card.rb +11 -0
  73. data/app/helpers/better_ui/application/components/main/main_helper.rb +64 -0
  74. data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +77 -0
  75. data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +51 -0
  76. data/app/helpers/better_ui/application_helper.rb +42 -179
  77. data/app/helpers/better_ui/general/components/alert/alert_helper.rb +57 -0
  78. data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +29 -0
  79. data/app/helpers/better_ui/general/components/badge/badge_helper.rb +53 -0
  80. data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +37 -0
  81. data/app/helpers/better_ui/general/components/button/button_helper.rb +65 -0
  82. data/app/helpers/better_ui/general/components/container/container_helper.rb +60 -0
  83. data/app/helpers/better_ui/general/components/divider/divider_helper.rb +63 -0
  84. data/app/helpers/better_ui/general/components/field/field_helper.rb +26 -0
  85. data/app/helpers/better_ui/general/components/heading/heading_helper.rb +72 -0
  86. data/app/helpers/better_ui/general/components/icon/icon_helper.rb +16 -0
  87. data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +81 -0
  88. data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +91 -0
  89. data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +79 -0
  90. data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +124 -0
  91. data/app/helpers/better_ui/general/components/input/select/select_helper.rb +70 -0
  92. data/app/helpers/better_ui/general/components/input/text/text_helper.rb +138 -0
  93. data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +73 -0
  94. data/app/helpers/better_ui/general/components/link/link_helper.rb +89 -0
  95. data/app/helpers/better_ui/general/components/panel/panel_helper.rb +83 -0
  96. data/app/helpers/better_ui/general/components/progress/progress_helper.rb +53 -0
  97. data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +19 -0
  98. data/app/helpers/better_ui/general/components/table/table_helper.rb +53 -0
  99. data/app/helpers/better_ui/general/components/table/tbody_helper.rb +13 -0
  100. data/app/helpers/better_ui/general/components/table/td_helper.rb +19 -0
  101. data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +13 -0
  102. data/app/helpers/better_ui/general/components/table/th_helper.rb +19 -0
  103. data/app/helpers/better_ui/general/components/table/thead_helper.rb +13 -0
  104. data/app/helpers/better_ui/general/components/table/tr_helper.rb +13 -0
  105. data/app/helpers/better_ui/general/components/tag/tag_helper.rb +26 -0
  106. data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +60 -0
  107. data/app/views/layouts/better_ui/application.html.erb +6 -124
  108. data/config/initializers/lookbook.rb +23 -0
  109. data/config/routes.rb +0 -8
  110. data/lib/better_ui/engine.rb +5 -19
  111. data/lib/better_ui/railtie.rb +20 -0
  112. data/lib/better_ui/version.rb +1 -1
  113. data/lib/better_ui.rb +4 -20
  114. metadata +131 -28
  115. data/app/controllers/better_ui/docs_controller.rb +0 -41
  116. data/app/views/better_ui/docs/component.html.erb +0 -365
  117. data/app/views/better_ui/docs/index.html.erb +0 -100
  118. data/app/views/better_ui/docs/show.html.erb +0 -60
@@ -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
@@ -0,0 +1,16 @@
1
+ <select <%= tag.attributes(select_attributes).html_safe %>>
2
+ <% if @placeholder.present? && !@multiple %>
3
+ <option value="" disabled <%= "selected" if @selected.nil? %>><%= @placeholder %></option>
4
+ <% end %>
5
+ <% @options.each do |option| %>
6
+ <option
7
+ value="<%= option[:value] %>"
8
+ <%= "selected" if selected?(option) %>
9
+ <%= "disabled" if option[:disabled] %>
10
+ <%= option[:html]&.map { |k, v| "#{k}=\"#{v}\"" }&.join(' ').to_s.html_safe %>
11
+ >
12
+ <%= option[:label] %>
13
+ </option>
14
+ <% end %>
15
+ <%= content %>
16
+ </select>
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module General
5
+ module Input
6
+ module Select
7
+ class Component < ViewComponent::Base
8
+ attr_reader :name, :options, :selected, :required, :disabled, :multiple,
9
+ :theme, :size, :rounded, :placeholder, :form, :options_html,
10
+ :classes, :html_options
11
+
12
+ # Classi base sempre presenti
13
+ SELECT_BASE_CLASSES = "block w-full border shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-0"
14
+
15
+ # Temi di select con classi Tailwind
16
+ SELECT_THEME = {
17
+ default: "bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-500",
18
+ white: "bg-white border-gray-300 focus:border-blue-500 focus:ring-blue-500",
19
+ red: "bg-white border-red-300 focus:border-red-500 focus:ring-red-500",
20
+ rose: "bg-white border-rose-300 focus:border-rose-500 focus:ring-rose-500",
21
+ orange: "bg-white border-orange-300 focus:border-orange-500 focus:ring-orange-500",
22
+ green: "bg-white border-green-300 focus:border-green-500 focus:ring-green-500",
23
+ blue: "bg-white border-blue-300 focus:border-blue-500 focus:ring-blue-500",
24
+ yellow: "bg-white border-yellow-300 focus:border-yellow-500 focus:ring-yellow-500",
25
+ violet: "bg-white border-violet-300 focus:border-violet-500 focus:ring-violet-500"
26
+ }.freeze
27
+
28
+ # Dimensioni con classi Tailwind
29
+ SELECT_SIZE = {
30
+ small: "px-2 py-1.5 text-xs",
31
+ medium: "px-3 py-2 text-sm",
32
+ large: "px-4 py-3 text-base"
33
+ }.freeze
34
+
35
+ # Border radius con classi Tailwind
36
+ SELECT_ROUNDED = {
37
+ none: "rounded-none",
38
+ small: "rounded-sm",
39
+ medium: "rounded-md",
40
+ large: "rounded-lg",
41
+ full: "rounded-full"
42
+ }.freeze
43
+
44
+ # Stati del select
45
+ SELECT_STATE = {
46
+ disabled: "bg-gray-100 cursor-not-allowed opacity-75"
47
+ }.freeze
48
+
49
+ # @param name [String] Name of the select field (required)
50
+ # @param options [Array<Hash>] Array of options for the select in format [{value: 'value', label: 'label'}, ...]
51
+ # @param selected [String, Array, nil] Selected value or values
52
+ # @param required [Boolean] Whether the field is required
53
+ # @param disabled [Boolean] Whether the field is disabled
54
+ # @param multiple [Boolean] Whether multiple options can be selected
55
+ # @param theme [Symbol] Component theme (:default, :white, :red, :rose, :orange, :green, :blue, :yellow, :violet)
56
+ # @param size [Symbol] Component size (:small, :medium, :large)
57
+ # @param rounded [Symbol] Border radius (:none, :small, :medium, :large, :full)
58
+ # @param placeholder [String, nil] Placeholder text for the field (creates an initial disabled option)
59
+ # @param form [ActionView::Helpers::FormBuilder, nil] Optional Rails form builder
60
+ # @param options_html [Hash] Additional HTML attributes for option tags
61
+ # @param classes [String] Additional CSS classes
62
+ # @param html_options [Hash] Additional HTML attributes for the select tag
63
+ def initialize(
64
+ name:,
65
+ options:,
66
+ selected: nil,
67
+ required: false,
68
+ disabled: false,
69
+ multiple: false,
70
+ theme: :default,
71
+ size: :medium,
72
+ rounded: :medium,
73
+ placeholder: nil,
74
+ form: nil,
75
+ options_html: {},
76
+ classes: '',
77
+ **html_options
78
+ )
79
+ @name = name
80
+ @options = options
81
+ @selected = selected
82
+ @required = required
83
+ @disabled = disabled
84
+ @multiple = multiple
85
+ @theme = theme.to_sym
86
+ @size = size.to_sym
87
+ @rounded = rounded.to_sym
88
+ @placeholder = placeholder
89
+ @form = form
90
+ @options_html = options_html
91
+ @classes = classes
92
+ @html_options = html_options
93
+
94
+ validate_params
95
+ end
96
+
97
+ def combined_classes
98
+ [
99
+ SELECT_BASE_CLASSES,
100
+ SELECT_THEME[@theme],
101
+ SELECT_SIZE[@size],
102
+ SELECT_ROUNDED[@rounded],
103
+ @disabled ? SELECT_STATE[:disabled] : nil,
104
+ @classes
105
+ ].compact.join(' ')
106
+ end
107
+
108
+ def select_attributes
109
+ attrs = {
110
+ name: form_field_name,
111
+ id: @html_options[:id] || form_field_id,
112
+ class: combined_classes
113
+ }
114
+
115
+ # Aggiungi attributi booleani solo se true
116
+ attrs[:required] = @required if @required
117
+ attrs[:disabled] = @disabled if @disabled
118
+ attrs[:multiple] = @multiple if @multiple
119
+
120
+ # Aggiungi altri attributi HTML
121
+ @html_options.except(:id, :class).each do |key, value|
122
+ attrs[key] = value unless value.nil?
123
+ end
124
+
125
+ # Rimuovi attributi con valori nil o false
126
+ attrs.compact
127
+ end
128
+
129
+ def form_field_name
130
+ if @form.present?
131
+ @form.field_name(@name)
132
+ else
133
+ @name
134
+ end
135
+ end
136
+
137
+ def form_field_id
138
+ if @form.present?
139
+ @form.field_id(@name)
140
+ else
141
+ @name.to_s.gsub(/[\[\]]+/, '_').gsub(/_$/, '')
142
+ end
143
+ end
144
+
145
+ def selected?(option)
146
+ return false if @selected.nil?
147
+
148
+ if @selected.is_a?(Array)
149
+ @selected.include?(option[:value])
150
+ else
151
+ option[:value].to_s == @selected.to_s
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def validate_params
158
+ validate_theme
159
+ validate_size
160
+ validate_rounded
161
+ end
162
+
163
+ def validate_theme
164
+ unless SELECT_THEME.keys.include?(@theme)
165
+ raise ArgumentError, "Invalid theme: #{@theme}. Valid themes are: #{SELECT_THEME.keys.join(', ')}"
166
+ end
167
+ end
168
+
169
+ def validate_size
170
+ unless SELECT_SIZE.keys.include?(@size)
171
+ raise ArgumentError, "Invalid size: #{@size}. Valid sizes are: #{SELECT_SIZE.keys.join(', ')}"
172
+ end
173
+ end
174
+
175
+ def validate_rounded
176
+ unless SELECT_ROUNDED.keys.include?(@rounded)
177
+ raise ArgumentError, "Invalid rounded: #{@rounded}. Valid rounded options are: #{SELECT_ROUNDED.keys.join(', ')}"
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,5 @@
1
+ <select name="<%= name %>" class="<%= classes %>" <%= "required" if required %> <%= "disabled" if disabled %> <%= "multiple" if multiple %> <%= options_html.map { |key, value| "#{key}=\"#{value}\"" }.join(" ") %>>
2
+ <% options.each do |option| %>
3
+ <option value="<%= option[:value] %>" <%= "selected" if option[:value] == selected %>><%= option[:label] %></option>
4
+ <% end %>
5
+ </select>