better_ui 0.2.0 → 0.6.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.
- checksums.yaml +4 -4
- data/app/components/better_ui/application/main/component.html.erb +1 -1
- data/app/components/better_ui/application/sidebar/component.html.erb +77 -18
- data/app/components/better_ui/application/sidebar/component.rb +63 -5
- data/app/components/better_ui/general/accordion/component.html.erb +5 -0
- data/app/components/better_ui/general/accordion/component.rb +92 -0
- data/app/components/better_ui/general/accordion/item_component.html.erb +12 -0
- data/app/components/better_ui/general/accordion/item_component.rb +176 -0
- data/app/components/better_ui/general/button/component.html.erb +8 -8
- data/app/components/better_ui/general/button/component.rb +11 -11
- data/app/components/better_ui/general/dropdown/component.html.erb +21 -7
- data/app/components/better_ui/general/dropdown/component.rb +27 -54
- data/app/components/better_ui/general/dropdown/item_component.rb +2 -1
- data/app/components/better_ui/general/field/component.html.erb +3 -3
- data/app/components/better_ui/general/field/component.rb +3 -3
- data/app/components/better_ui/general/grid/cell_component.html.erb +3 -0
- data/app/components/better_ui/general/grid/cell_component.rb +390 -0
- data/app/components/better_ui/general/grid/component.html.erb +3 -0
- data/app/components/better_ui/general/grid/component.rb +301 -0
- data/app/components/better_ui/general/heading/component.html.erb +1 -1
- data/app/components/better_ui/general/icon/component.rb +2 -1
- data/app/components/better_ui/general/input/checkbox/component.rb +10 -10
- data/app/components/better_ui/general/input/pin/component.html.erb +1 -0
- data/app/components/better_ui/general/input/pin/component.rb +201 -0
- data/app/components/better_ui/general/input/radio/component.rb +10 -10
- data/app/components/better_ui/general/input/rating/component.html.erb +4 -0
- data/app/components/better_ui/general/input/rating/component.rb +272 -0
- data/app/components/better_ui/general/input/select/component.html.erb +76 -14
- data/app/components/better_ui/general/input/select/component.rb +166 -101
- data/app/components/better_ui/general/input/toggle/component.html.erb +5 -0
- data/app/components/better_ui/general/input/toggle/component.rb +242 -0
- data/app/components/better_ui/general/link/component.rb +1 -1
- data/app/components/better_ui/general/modal/component.html.erb +5 -42
- data/app/components/better_ui/general/modal/component.rb +22 -140
- data/app/components/better_ui/general/modal/modal_component.html.erb +52 -0
- data/app/components/better_ui/general/modal/modal_component.rb +160 -0
- data/app/components/better_ui/general/tabs/component.html.erb +10 -2
- data/app/components/better_ui/general/tabs/component.rb +26 -8
- data/app/components/better_ui/general/tabs/panel_component.rb +1 -1
- data/app/components/better_ui/general/tabs/tab_component.rb +1 -1
- data/app/components/better_ui/general/text/component.html.erb +1 -0
- data/app/components/better_ui/general/text/component.rb +194 -0
- data/app/helpers/better_ui/application_helper.rb +11 -4
- data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +73 -0
- data/app/helpers/better_ui/general/components/button/button_helper.rb +6 -6
- data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +9 -0
- data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +13 -7
- data/app/helpers/better_ui/general/components/field/field_helper.rb +4 -4
- data/app/helpers/better_ui/general/components/grid/grid_helper.rb +145 -0
- data/app/helpers/better_ui/general/components/input/pin/pin_helper.rb +76 -0
- data/app/helpers/better_ui/general/components/input/rating/rating_helper.rb +70 -0
- data/app/helpers/better_ui/general/components/input/select/select_helper.rb +47 -31
- data/app/helpers/better_ui/general/components/input/toggle/toggle_helper.rb +77 -0
- data/app/helpers/better_ui/general/components/modal/modal_helper.rb +34 -44
- data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +59 -26
- data/app/helpers/better_ui/general/components/text/text_helper.rb +83 -0
- data/lib/better_ui/version.rb +1 -1
- data/lib/better_ui.rb +1 -0
- metadata +26 -2
@@ -0,0 +1,301 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BetterUi
|
4
|
+
module General
|
5
|
+
module Grid
|
6
|
+
class Component < ViewComponent::Base
|
7
|
+
attr_reader :cols, :rows, :gap, :flow, :align_items, :justify_items, :classes, :id, :html_options
|
8
|
+
|
9
|
+
# Classi base grid
|
10
|
+
GRID_BASE_CLASSES = "grid"
|
11
|
+
|
12
|
+
# Valori supportati per validazione
|
13
|
+
VALID_GRID_COLS = (1..12).to_a + [:auto, :none].freeze
|
14
|
+
VALID_GRID_ROWS = (1..6).to_a + [:auto, :none].freeze
|
15
|
+
VALID_BREAKPOINTS = [:sm, :md, :lg, :xl].freeze
|
16
|
+
|
17
|
+
# Gap (spaziatura)
|
18
|
+
GRID_GAP = {
|
19
|
+
none: 'gap-0', small: 'gap-2', medium: 'gap-4', large: 'gap-6'
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
# Flow (direzione)
|
23
|
+
GRID_FLOW = {
|
24
|
+
row: 'grid-flow-row', col: 'grid-flow-col',
|
25
|
+
row_dense: 'grid-flow-row-dense', col_dense: 'grid-flow-col-dense'
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
# Align items
|
29
|
+
GRID_ALIGN_ITEMS = {
|
30
|
+
start: 'items-start', center: 'items-center',
|
31
|
+
end: 'items-end', stretch: 'items-stretch'
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
# Justify items
|
35
|
+
GRID_JUSTIFY_ITEMS = {
|
36
|
+
start: 'justify-items-start', center: 'justify-items-center',
|
37
|
+
end: 'justify-items-end', stretch: 'justify-items-stretch'
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
def initialize(
|
41
|
+
cols: 1,
|
42
|
+
rows: nil,
|
43
|
+
gap: :medium,
|
44
|
+
flow: :row,
|
45
|
+
align_items: nil,
|
46
|
+
justify_items: nil,
|
47
|
+
classes: '',
|
48
|
+
id: nil,
|
49
|
+
**html_options
|
50
|
+
)
|
51
|
+
@cols = normalize_grid_cols_with_defaults(cols)
|
52
|
+
@rows = normalize_grid_rows_with_defaults(rows) if rows
|
53
|
+
@gap = normalize_grid_gap_with_defaults(gap)
|
54
|
+
@flow = flow.to_sym
|
55
|
+
@align_items = align_items&.to_sym
|
56
|
+
@justify_items = justify_items&.to_sym
|
57
|
+
@classes = classes
|
58
|
+
@id = id
|
59
|
+
@html_options = html_options
|
60
|
+
|
61
|
+
validate_grid_params
|
62
|
+
end
|
63
|
+
|
64
|
+
def combined_classes
|
65
|
+
[
|
66
|
+
GRID_BASE_CLASSES,
|
67
|
+
generate_cols_classes,
|
68
|
+
generate_rows_classes,
|
69
|
+
generate_gap_classes,
|
70
|
+
GRID_FLOW[@flow],
|
71
|
+
@align_items ? GRID_ALIGN_ITEMS[@align_items] : nil,
|
72
|
+
@justify_items ? GRID_JUSTIFY_ITEMS[@justify_items] : nil,
|
73
|
+
@classes,
|
74
|
+
@html_options[:class]
|
75
|
+
].compact.join(" ")
|
76
|
+
end
|
77
|
+
|
78
|
+
def grid_attributes
|
79
|
+
attrs = {
|
80
|
+
class: combined_classes,
|
81
|
+
id: @id
|
82
|
+
}
|
83
|
+
|
84
|
+
@html_options.except(:class).each do |key, value|
|
85
|
+
attrs[key] = value
|
86
|
+
end
|
87
|
+
|
88
|
+
attrs
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Genera classi CSS dinamicamente invece di usare costanti massive
|
94
|
+
def generate_grid_cols_class(cols, breakpoint = nil)
|
95
|
+
prefix = breakpoint ? "#{breakpoint}:" : ""
|
96
|
+
"#{prefix}grid-cols-#{cols}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def generate_grid_rows_class(rows, breakpoint = nil)
|
100
|
+
prefix = breakpoint ? "#{breakpoint}:" : ""
|
101
|
+
"#{prefix}grid-rows-#{rows}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def generate_grid_gap_class(gap, breakpoint = nil)
|
105
|
+
prefix = breakpoint ? "#{breakpoint}:" : ""
|
106
|
+
gap_class = GRID_GAP[gap.to_sym]
|
107
|
+
return nil unless gap_class
|
108
|
+
|
109
|
+
if breakpoint
|
110
|
+
"#{breakpoint}:#{gap_class.sub(/^gap-/, 'gap-')}"
|
111
|
+
else
|
112
|
+
gap_class
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Normalizza cols con default intelligenti per tutti i breakpoint
|
117
|
+
def normalize_grid_cols_with_defaults(cols)
|
118
|
+
# Se è un valore semplice, lo usiamo per tutti i breakpoint
|
119
|
+
return cols unless cols.is_a?(Hash)
|
120
|
+
|
121
|
+
# Se è un hash, riempiamo i breakpoint mancanti con fallback intelligente
|
122
|
+
normalized = {}
|
123
|
+
last_value = 1 # Default base
|
124
|
+
|
125
|
+
# Base (senza breakpoint)
|
126
|
+
normalized[:base] = 1
|
127
|
+
|
128
|
+
# Processa i breakpoint in ordine
|
129
|
+
VALID_BREAKPOINTS.each do |breakpoint|
|
130
|
+
if cols.key?(breakpoint)
|
131
|
+
last_value = cols[breakpoint]
|
132
|
+
end
|
133
|
+
normalized[breakpoint] = last_value
|
134
|
+
end
|
135
|
+
|
136
|
+
normalized
|
137
|
+
end
|
138
|
+
|
139
|
+
# Normalizza rows con default intelligenti (solo se specificato)
|
140
|
+
def normalize_grid_rows_with_defaults(rows)
|
141
|
+
return rows unless rows.is_a?(Hash)
|
142
|
+
|
143
|
+
normalized = {}
|
144
|
+
last_value = nil
|
145
|
+
|
146
|
+
VALID_BREAKPOINTS.each do |breakpoint|
|
147
|
+
if rows.key?(breakpoint)
|
148
|
+
last_value = rows[breakpoint]
|
149
|
+
end
|
150
|
+
normalized[breakpoint] = last_value if last_value
|
151
|
+
end
|
152
|
+
|
153
|
+
normalized
|
154
|
+
end
|
155
|
+
|
156
|
+
# Normalizza gap con default intelligenti
|
157
|
+
def normalize_grid_gap_with_defaults(gap)
|
158
|
+
return gap unless gap.is_a?(Hash)
|
159
|
+
|
160
|
+
normalized = {}
|
161
|
+
last_value = :medium # Default base
|
162
|
+
|
163
|
+
VALID_BREAKPOINTS.each do |breakpoint|
|
164
|
+
if gap.key?(breakpoint)
|
165
|
+
last_value = gap[breakpoint]
|
166
|
+
end
|
167
|
+
normalized[breakpoint] = last_value
|
168
|
+
end
|
169
|
+
|
170
|
+
normalized
|
171
|
+
end
|
172
|
+
|
173
|
+
def generate_cols_classes
|
174
|
+
return nil unless @cols
|
175
|
+
|
176
|
+
if @cols.is_a?(Hash)
|
177
|
+
classes = []
|
178
|
+
|
179
|
+
# Base class (senza breakpoint)
|
180
|
+
if @cols[:base]
|
181
|
+
classes << generate_grid_cols_class(@cols[:base])
|
182
|
+
end
|
183
|
+
|
184
|
+
# Responsive classes
|
185
|
+
VALID_BREAKPOINTS.each do |breakpoint|
|
186
|
+
if @cols[breakpoint]
|
187
|
+
classes << generate_grid_cols_class(@cols[breakpoint], breakpoint)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
classes.join(" ")
|
192
|
+
else
|
193
|
+
generate_grid_cols_class(@cols)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def generate_rows_classes
|
198
|
+
return nil unless @rows
|
199
|
+
|
200
|
+
if @rows.is_a?(Hash)
|
201
|
+
@rows.map do |breakpoint, rows_value|
|
202
|
+
generate_grid_rows_class(rows_value, breakpoint)
|
203
|
+
end.compact.join(" ")
|
204
|
+
else
|
205
|
+
generate_grid_rows_class(@rows)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def generate_gap_classes
|
210
|
+
if @gap.is_a?(Hash)
|
211
|
+
@gap.map do |breakpoint, gap_value|
|
212
|
+
generate_grid_gap_class(gap_value, breakpoint)
|
213
|
+
end.compact.join(" ")
|
214
|
+
else
|
215
|
+
GRID_GAP[@gap.to_sym]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def validate_grid_params
|
220
|
+
validate_grid_cols
|
221
|
+
validate_grid_rows if @rows
|
222
|
+
validate_grid_gap
|
223
|
+
validate_grid_flow
|
224
|
+
validate_grid_align_items if @align_items
|
225
|
+
validate_grid_justify_items if @justify_items
|
226
|
+
end
|
227
|
+
|
228
|
+
def validate_grid_cols
|
229
|
+
if @cols.is_a?(Hash)
|
230
|
+
@cols.each do |breakpoint, cols_value|
|
231
|
+
next if breakpoint == :base # Skip validation for base
|
232
|
+
|
233
|
+
unless VALID_BREAKPOINTS.include?(breakpoint)
|
234
|
+
raise ArgumentError, "Breakpoint #{breakpoint} non supportato per cols"
|
235
|
+
end
|
236
|
+
unless VALID_GRID_COLS.include?(cols_value)
|
237
|
+
raise ArgumentError, "Valore cols #{cols_value} non supportato per breakpoint #{breakpoint}"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
else
|
241
|
+
unless VALID_GRID_COLS.include?(@cols)
|
242
|
+
raise ArgumentError, "cols deve essere uno tra: #{VALID_GRID_COLS.join(', ')}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def validate_grid_rows
|
248
|
+
if @rows.is_a?(Hash)
|
249
|
+
@rows.each do |breakpoint, rows_value|
|
250
|
+
unless VALID_BREAKPOINTS.include?(breakpoint)
|
251
|
+
raise ArgumentError, "Breakpoint #{breakpoint} non supportato per rows"
|
252
|
+
end
|
253
|
+
unless VALID_GRID_ROWS.include?(rows_value)
|
254
|
+
raise ArgumentError, "Valore rows #{rows_value} non supportato per breakpoint #{breakpoint}"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
else
|
258
|
+
unless VALID_GRID_ROWS.include?(@rows)
|
259
|
+
raise ArgumentError, "rows deve essere uno tra: #{VALID_GRID_ROWS.join(', ')}"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def validate_grid_gap
|
265
|
+
if @gap.is_a?(Hash)
|
266
|
+
@gap.each do |breakpoint, gap_value|
|
267
|
+
unless VALID_BREAKPOINTS.include?(breakpoint)
|
268
|
+
raise ArgumentError, "Breakpoint #{breakpoint} non supportato per gap"
|
269
|
+
end
|
270
|
+
unless GRID_GAP.keys.include?(gap_value.to_sym)
|
271
|
+
raise ArgumentError, "Valore gap #{gap_value} non supportato per breakpoint #{breakpoint}"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
else
|
275
|
+
unless GRID_GAP.keys.include?(@gap.to_sym)
|
276
|
+
raise ArgumentError, "gap deve essere uno tra: #{GRID_GAP.keys.join(', ')}"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def validate_grid_flow
|
282
|
+
unless GRID_FLOW.keys.include?(@flow)
|
283
|
+
raise ArgumentError, "flow deve essere uno tra: #{GRID_FLOW.keys.join(', ')}"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def validate_grid_align_items
|
288
|
+
unless GRID_ALIGN_ITEMS.keys.include?(@align_items)
|
289
|
+
raise ArgumentError, "align_items deve essere uno tra: #{GRID_ALIGN_ITEMS.keys.join(', ')}"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def validate_grid_justify_items
|
294
|
+
unless GRID_JUSTIFY_ITEMS.keys.include?(@justify_items)
|
295
|
+
raise ArgumentError, "justify_items deve essere uno tra: #{GRID_JUSTIFY_ITEMS.keys.join(', ')}"
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
<%= tag.public_send(heading_tag, **heading_attributes) do %>
|
4
4
|
<% if show_icon? %>
|
5
5
|
<span class="<%= icon_classes %>">
|
6
|
-
<%= render BetterUi::General::
|
6
|
+
<%= render BetterUi::General::Icon::Component.new(name: @icon) %>
|
7
7
|
</span>
|
8
8
|
<% end %>
|
9
9
|
|
@@ -9,7 +9,8 @@ module BetterUi
|
|
9
9
|
ICON_SIZE_CLASSES = {
|
10
10
|
small: "bui-icon--small w-4 h-4 text-sm",
|
11
11
|
medium: "bui-icon--medium w-5 h-5 text-base",
|
12
|
-
large: "bui-icon--large w-6 h-6 text-lg"
|
12
|
+
large: "bui-icon--large w-6 h-6 text-lg",
|
13
|
+
xlarge: "bui-icon--large w-7 h-7 text-xl"
|
13
14
|
}.freeze
|
14
15
|
|
15
16
|
# Temi dell'icona con colori coerenti
|
@@ -7,7 +7,7 @@ module BetterUi
|
|
7
7
|
class Component < ViewComponent::Base
|
8
8
|
# Costanti con classi Tailwind dirette
|
9
9
|
CHECKBOX_THEME = {
|
10
|
-
default: 'border-gray-300 text-
|
10
|
+
default: 'border-gray-300 text-gray-800 focus:border-gray-600 focus:ring-gray-600 checked:bg-gray-800 checked:border-gray-800',
|
11
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
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
13
|
rose: 'border-gray-300 text-rose-600 focus:border-rose-500 focus:ring-rose-500 checked:bg-rose-600 checked:border-rose-600',
|
@@ -19,9 +19,9 @@ module BetterUi
|
|
19
19
|
}.freeze
|
20
20
|
|
21
21
|
CHECKBOX_SIZE = {
|
22
|
-
small: 'h-
|
23
|
-
medium: 'h-
|
24
|
-
large: 'h-
|
22
|
+
small: 'h-2.5 w-2.5',
|
23
|
+
medium: 'h-3 w-3',
|
24
|
+
large: 'h-4 w-4'
|
25
25
|
}.freeze
|
26
26
|
|
27
27
|
CHECKBOX_ROUNDED = {
|
@@ -35,15 +35,15 @@ module BetterUi
|
|
35
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
36
|
|
37
37
|
CHECKBOX_LABEL_GAP = {
|
38
|
-
small: 'gap-
|
39
|
-
medium: 'gap-2
|
40
|
-
large: 'gap-
|
38
|
+
small: 'gap-1.5',
|
39
|
+
medium: 'gap-2',
|
40
|
+
large: 'gap-2.5'
|
41
41
|
}.freeze
|
42
42
|
|
43
43
|
CHECKBOX_LABEL_TEXT = {
|
44
|
-
small: 'text-
|
45
|
-
medium: 'text-
|
46
|
-
large: 'text-
|
44
|
+
small: 'text-xs',
|
45
|
+
medium: 'text-sm',
|
46
|
+
large: 'text-base'
|
47
47
|
}.freeze
|
48
48
|
|
49
49
|
attr_reader :name, :value, :checked, :required, :disabled, :indeterminate,
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render_pin_container %>
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BetterUi
|
4
|
+
module General
|
5
|
+
module Input
|
6
|
+
module Pin
|
7
|
+
class Component < ViewComponent::Base
|
8
|
+
# Costanti con classi Tailwind dirette
|
9
|
+
PIN_THEME = {
|
10
|
+
default: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
11
|
+
white: 'border-gray-300 bg-white focus:border-gray-900 focus:ring-gray-900',
|
12
|
+
red: 'border-red-300 focus:border-red-500 focus:ring-red-500',
|
13
|
+
rose: 'border-rose-300 focus:border-rose-500 focus:ring-rose-500',
|
14
|
+
orange: 'border-orange-300 focus:border-orange-500 focus:ring-orange-500',
|
15
|
+
green: 'border-green-300 focus:border-green-500 focus:ring-green-500',
|
16
|
+
blue: 'border-blue-300 focus:border-blue-500 focus:ring-blue-500',
|
17
|
+
yellow: 'border-yellow-300 focus:border-yellow-500 focus:ring-yellow-500',
|
18
|
+
violet: 'border-violet-300 focus:border-violet-500 focus:ring-violet-500'
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
PIN_SIZE = {
|
22
|
+
small: 'w-8 h-8 text-sm',
|
23
|
+
medium: 'w-12 h-12 text-base',
|
24
|
+
large: 'w-16 h-16 text-lg'
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
PIN_GAP = {
|
28
|
+
small: 'gap-2',
|
29
|
+
medium: 'gap-3',
|
30
|
+
large: 'gap-4'
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
PIN_BASE_CLASSES = 'rounded-md border text-center font-mono focus:outline-none focus:ring-2 focus:ring-offset-1 transition-colors'.freeze
|
34
|
+
|
35
|
+
attr_reader :name, :value, :length, :placeholder, :required, :disabled,
|
36
|
+
:theme, :size, :form, :classes, :options
|
37
|
+
|
38
|
+
# @param name [String] Nome del campo pin (obbligatorio)
|
39
|
+
# @param value [String] Valore del pin preimpostato
|
40
|
+
# @param length [Integer] Numero di campi pin (4-8, default: 6)
|
41
|
+
# @param placeholder [String] Placeholder per campi vuoti (default: '•')
|
42
|
+
# @param required [Boolean] Se il campo è obbligatorio
|
43
|
+
# @param disabled [Boolean] Se il campo è disabilitato
|
44
|
+
# @param theme [Symbol] Tema del componente (:default, :white, :red, :rose, :orange, :green, :blue, :yellow, :violet)
|
45
|
+
# @param size [Symbol] Dimensione del componente (:small, :medium, :large)
|
46
|
+
# @param form [ActionView::Helpers::FormBuilder, nil] Form builder Rails opzionale
|
47
|
+
# @param classes [String] Classi CSS aggiuntive
|
48
|
+
# @param options [Hash] Opzioni aggiuntive per attributi HTML
|
49
|
+
def initialize(name:, value: '', length: 6, placeholder: '•', required: false, disabled: false,
|
50
|
+
theme: :default, size: :medium, form: nil, classes: '', **options)
|
51
|
+
@name = name
|
52
|
+
@value = value.to_s
|
53
|
+
@length = length.to_i
|
54
|
+
@placeholder = placeholder
|
55
|
+
@required = required
|
56
|
+
@disabled = disabled
|
57
|
+
@theme = theme.to_sym
|
58
|
+
@size = size.to_sym
|
59
|
+
@form = form
|
60
|
+
@classes = classes
|
61
|
+
@options = options
|
62
|
+
|
63
|
+
validate_params
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def validate_params
|
69
|
+
validate_theme
|
70
|
+
validate_size
|
71
|
+
validate_length
|
72
|
+
validate_name
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_theme
|
76
|
+
return if PIN_THEME.key?(@theme)
|
77
|
+
|
78
|
+
raise ArgumentError, "Invalid theme: #{@theme}. Valid themes are: #{PIN_THEME.keys.join(', ')}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate_size
|
82
|
+
return if PIN_SIZE.key?(@size)
|
83
|
+
|
84
|
+
raise ArgumentError, "Invalid size: #{@size}. Valid sizes are: #{PIN_SIZE.keys.join(', ')}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_length
|
88
|
+
return if @length >= 4 && @length <= 8
|
89
|
+
|
90
|
+
raise ArgumentError, "Length must be between 4 and 8, got: #{@length}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def validate_name
|
94
|
+
return if @name.present?
|
95
|
+
|
96
|
+
raise ArgumentError, "Name is required for pin input components"
|
97
|
+
end
|
98
|
+
|
99
|
+
def container_classes
|
100
|
+
base_classes = ['flex', 'items-center']
|
101
|
+
base_classes << PIN_GAP[@size]
|
102
|
+
base_classes << @classes if @classes.present?
|
103
|
+
base_classes.join(' ')
|
104
|
+
end
|
105
|
+
|
106
|
+
def input_classes
|
107
|
+
[
|
108
|
+
PIN_BASE_CLASSES,
|
109
|
+
PIN_SIZE[@size],
|
110
|
+
PIN_THEME[@theme],
|
111
|
+
(@disabled ? 'opacity-50 cursor-not-allowed' : '')
|
112
|
+
].compact.join(' ')
|
113
|
+
end
|
114
|
+
|
115
|
+
def controller_attributes
|
116
|
+
{
|
117
|
+
data: {
|
118
|
+
controller: 'bui-pin',
|
119
|
+
'bui-pin-length-value': @length,
|
120
|
+
'bui-pin-name-value': @name,
|
121
|
+
'bui-pin-placeholder-value': @placeholder
|
122
|
+
}
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
def input_attributes(index)
|
127
|
+
attrs = {
|
128
|
+
type: 'text',
|
129
|
+
class: input_classes,
|
130
|
+
maxlength: '1',
|
131
|
+
autocomplete: 'off',
|
132
|
+
inputmode: 'numeric',
|
133
|
+
pattern: '[0-9]*',
|
134
|
+
placeholder: @placeholder,
|
135
|
+
disabled: @disabled,
|
136
|
+
required: @required && index == 0, # Solo il primo campo è required per validazione
|
137
|
+
data: {
|
138
|
+
'bui-pin-target': 'input',
|
139
|
+
action: [
|
140
|
+
'input->bui-pin#inputChange',
|
141
|
+
'keydown->bui-pin#inputKeydown',
|
142
|
+
'paste->bui-pin#inputPaste'
|
143
|
+
].join(' ')
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
# Imposta valore se presente
|
148
|
+
if @value.present? && @value[index]
|
149
|
+
attrs[:value] = @value[index]
|
150
|
+
end
|
151
|
+
|
152
|
+
attrs.merge(@options.except(:id))
|
153
|
+
end
|
154
|
+
|
155
|
+
def hidden_input_attributes
|
156
|
+
attrs = {
|
157
|
+
type: 'hidden',
|
158
|
+
name: input_name,
|
159
|
+
value: @value,
|
160
|
+
data: { 'bui-pin-target': 'hiddenInput' }
|
161
|
+
}
|
162
|
+
|
163
|
+
# Aggiungi ID se specificato
|
164
|
+
if @options[:id]
|
165
|
+
attrs[:id] = @options[:id]
|
166
|
+
end
|
167
|
+
|
168
|
+
attrs
|
169
|
+
end
|
170
|
+
|
171
|
+
def input_name
|
172
|
+
if @form
|
173
|
+
@form.field_name(@name)
|
174
|
+
else
|
175
|
+
@name
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def render_pin_inputs
|
180
|
+
@length.times.map do |index|
|
181
|
+
tag(:input, input_attributes(index))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def render_hidden_input
|
186
|
+
tag(:input, hidden_input_attributes)
|
187
|
+
end
|
188
|
+
|
189
|
+
def render_pin_container
|
190
|
+
content_tag(:div, class: container_classes, **controller_attributes) do
|
191
|
+
safe_join([
|
192
|
+
render_hidden_input,
|
193
|
+
*render_pin_inputs
|
194
|
+
])
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -7,7 +7,7 @@ module BetterUi
|
|
7
7
|
class Component < ViewComponent::Base
|
8
8
|
# Costanti con classi Tailwind dirette
|
9
9
|
RADIO_THEME = {
|
10
|
-
default: 'border-gray-300 text-
|
10
|
+
default: 'border-gray-300 text-gray-800 focus:border-gray-600 focus:ring-gray-600 checked:bg-gray-800 checked:border-gray-800',
|
11
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
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
13
|
rose: 'border-gray-300 text-rose-600 focus:border-rose-500 focus:ring-rose-500 checked:bg-rose-600 checked:border-rose-600',
|
@@ -19,9 +19,9 @@ module BetterUi
|
|
19
19
|
}.freeze
|
20
20
|
|
21
21
|
RADIO_SIZE = {
|
22
|
-
small: 'h-
|
23
|
-
medium: 'h-
|
24
|
-
large: 'h-
|
22
|
+
small: 'h-2.5 w-2.5',
|
23
|
+
medium: 'h-3 w-3',
|
24
|
+
large: 'h-4 w-4'
|
25
25
|
}.freeze
|
26
26
|
|
27
27
|
RADIO_ROUNDED = {
|
@@ -35,15 +35,15 @@ module BetterUi
|
|
35
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
36
|
|
37
37
|
RADIO_LABEL_GAP = {
|
38
|
-
small: 'gap-
|
39
|
-
medium: 'gap-2
|
40
|
-
large: 'gap-
|
38
|
+
small: 'gap-1.5',
|
39
|
+
medium: 'gap-2',
|
40
|
+
large: 'gap-2.5'
|
41
41
|
}.freeze
|
42
42
|
|
43
43
|
RADIO_LABEL_TEXT = {
|
44
|
-
small: 'text-
|
45
|
-
medium: 'text-
|
46
|
-
large: 'text-
|
44
|
+
small: 'text-xs',
|
45
|
+
medium: 'text-sm',
|
46
|
+
large: 'text-base'
|
47
47
|
}.freeze
|
48
48
|
|
49
49
|
attr_reader :name, :value, :checked, :required, :disabled,
|