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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +225 -119
- data/app/assets/stylesheets/better_ui/application.css +0 -356
- data/app/components/better_ui/application/card/component.html.erb +20 -0
- data/app/components/better_ui/application/card/component.rb +214 -0
- data/app/components/better_ui/application/main/component.html.erb +9 -0
- data/app/components/better_ui/application/main/component.rb +123 -0
- data/app/components/better_ui/application/navbar/component.html.erb +92 -0
- data/app/components/better_ui/application/navbar/component.rb +136 -0
- data/app/components/better_ui/application/sidebar/component.html.erb +190 -0
- data/app/components/better_ui/application/sidebar/component.rb +129 -0
- data/app/components/better_ui/general/alert/component.html.erb +32 -0
- data/app/components/better_ui/general/alert/component.rb +242 -0
- data/app/components/better_ui/general/avatar/component.html.erb +20 -0
- data/app/components/better_ui/general/avatar/component.rb +301 -0
- data/app/components/better_ui/general/badge/component.html.erb +23 -0
- data/app/components/better_ui/general/badge/component.rb +248 -0
- data/app/components/better_ui/general/breadcrumb/component.html.erb +15 -0
- data/app/components/better_ui/general/breadcrumb/component.rb +187 -0
- data/app/components/better_ui/general/button/component.html.erb +34 -0
- data/app/components/better_ui/general/button/component.rb +214 -0
- data/app/components/better_ui/general/divider/component.html.erb +10 -0
- data/app/components/better_ui/general/divider/component.rb +226 -0
- data/app/components/better_ui/general/field/component.html.erb +27 -0
- data/app/components/better_ui/general/field/component.rb +37 -0
- data/app/components/better_ui/general/heading/component.html.erb +22 -0
- data/app/components/better_ui/general/heading/component.rb +257 -0
- data/app/components/better_ui/general/icon/component.html.erb +7 -0
- data/app/components/better_ui/general/icon/component.rb +239 -0
- data/app/components/better_ui/general/input/checkbox/component.html.erb +5 -0
- data/app/components/better_ui/general/input/checkbox/component.rb +238 -0
- data/app/components/better_ui/general/input/datetime/component.html.erb +5 -0
- data/app/components/better_ui/general/input/datetime/component.rb +223 -0
- data/app/components/better_ui/general/input/radio/component.html.erb +5 -0
- data/app/components/better_ui/general/input/radio/component.rb +230 -0
- data/app/components/better_ui/general/input/select/component.html.erb +16 -0
- data/app/components/better_ui/general/input/select/component.rb +184 -0
- data/app/components/better_ui/general/input/select/select_component.html.erb +5 -0
- data/app/components/better_ui/general/input/select/select_component.rb +37 -0
- data/app/components/better_ui/general/input/text/component.html.erb +5 -0
- data/app/components/better_ui/general/input/text/component.rb +171 -0
- data/app/components/better_ui/general/input/textarea/component.html.erb +5 -0
- data/app/components/better_ui/general/input/textarea/component.rb +166 -0
- data/app/components/better_ui/general/link/component.html.erb +18 -0
- data/app/components/better_ui/general/link/component.rb +258 -0
- data/app/components/better_ui/general/panel/component.html.erb +28 -0
- data/app/components/better_ui/general/panel/component.rb +249 -0
- data/app/components/better_ui/general/progress/component.html.erb +11 -0
- data/app/components/better_ui/general/progress/component.rb +160 -0
- data/app/components/better_ui/general/spinner/component.html.erb +35 -0
- data/app/components/better_ui/general/spinner/component.rb +93 -0
- data/app/components/better_ui/general/table/component.html.erb +5 -0
- data/app/components/better_ui/general/table/component.rb +217 -0
- data/app/components/better_ui/general/table/tbody_component.html.erb +3 -0
- data/app/components/better_ui/general/table/tbody_component.rb +30 -0
- data/app/components/better_ui/general/table/td_component.html.erb +3 -0
- data/app/components/better_ui/general/table/td_component.rb +44 -0
- data/app/components/better_ui/general/table/tfoot_component.html.erb +3 -0
- data/app/components/better_ui/general/table/tfoot_component.rb +28 -0
- data/app/components/better_ui/general/table/th_component.html.erb +6 -0
- data/app/components/better_ui/general/table/th_component.rb +51 -0
- data/app/components/better_ui/general/table/thead_component.html.erb +3 -0
- data/app/components/better_ui/general/table/thead_component.rb +28 -0
- data/app/components/better_ui/general/table/tr_component.html.erb +3 -0
- data/app/components/better_ui/general/table/tr_component.rb +30 -0
- data/app/components/better_ui/general/tag/component.html.erb +3 -0
- data/app/components/better_ui/general/tag/component.rb +104 -0
- data/app/components/better_ui/general/tooltip/component.html.erb +7 -0
- data/app/components/better_ui/general/tooltip/component.rb +239 -0
- data/app/helpers/better_ui/application/components/card/card_helper.rb +96 -0
- data/app/helpers/better_ui/application/components/card.rb +11 -0
- data/app/helpers/better_ui/application/components/main/main_helper.rb +64 -0
- data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +77 -0
- data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +51 -0
- data/app/helpers/better_ui/application_helper.rb +42 -179
- data/app/helpers/better_ui/general/components/alert/alert_helper.rb +57 -0
- data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +29 -0
- data/app/helpers/better_ui/general/components/badge/badge_helper.rb +53 -0
- data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +37 -0
- data/app/helpers/better_ui/general/components/button/button_helper.rb +65 -0
- data/app/helpers/better_ui/general/components/container/container_helper.rb +60 -0
- data/app/helpers/better_ui/general/components/divider/divider_helper.rb +63 -0
- data/app/helpers/better_ui/general/components/field/field_helper.rb +26 -0
- data/app/helpers/better_ui/general/components/heading/heading_helper.rb +72 -0
- data/app/helpers/better_ui/general/components/icon/icon_helper.rb +16 -0
- data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +81 -0
- data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +91 -0
- data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +79 -0
- data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +124 -0
- data/app/helpers/better_ui/general/components/input/select/select_helper.rb +70 -0
- data/app/helpers/better_ui/general/components/input/text/text_helper.rb +138 -0
- data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +73 -0
- data/app/helpers/better_ui/general/components/link/link_helper.rb +89 -0
- data/app/helpers/better_ui/general/components/panel/panel_helper.rb +83 -0
- data/app/helpers/better_ui/general/components/progress/progress_helper.rb +53 -0
- data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +19 -0
- data/app/helpers/better_ui/general/components/table/table_helper.rb +53 -0
- data/app/helpers/better_ui/general/components/table/tbody_helper.rb +13 -0
- data/app/helpers/better_ui/general/components/table/td_helper.rb +19 -0
- data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +13 -0
- data/app/helpers/better_ui/general/components/table/th_helper.rb +19 -0
- data/app/helpers/better_ui/general/components/table/thead_helper.rb +13 -0
- data/app/helpers/better_ui/general/components/table/tr_helper.rb +13 -0
- data/app/helpers/better_ui/general/components/tag/tag_helper.rb +26 -0
- data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +60 -0
- data/app/views/layouts/better_ui/application.html.erb +6 -124
- data/config/initializers/lookbook.rb +23 -0
- data/config/routes.rb +0 -8
- data/lib/better_ui/engine.rb +5 -19
- data/lib/better_ui/railtie.rb +20 -0
- data/lib/better_ui/version.rb +1 -1
- data/lib/better_ui.rb +4 -20
- metadata +131 -28
- data/app/controllers/better_ui/docs_controller.rb +0 -41
- data/app/views/better_ui/docs/component.html.erb +0 -365
- data/app/views/better_ui/docs/index.html.erb +0 -100
- 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,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>
|