better_ui 0.3.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 +25 -3
- data/app/components/better_ui/application/sidebar/component.rb +62 -5
- 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 +7 -4
- data/app/components/better_ui/general/dropdown/component.rb +23 -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/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 +7 -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/text/text_helper.rb +83 -0
- data/lib/better_ui/version.rb +1 -1
- data/lib/better_ui.rb +1 -0
- metadata +19 -4
- data/app/helpers/better_ui/general/components/accordion.rb +0 -11
- data/app/helpers/better_ui/general/components/modal.rb +0 -11
@@ -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,
|
@@ -0,0 +1,272 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BetterUi
|
4
|
+
module General
|
5
|
+
module Input
|
6
|
+
module Rating
|
7
|
+
class Component < ViewComponent::Base
|
8
|
+
# Costanti con classi Tailwind dirette
|
9
|
+
RATING_THEME = {
|
10
|
+
default: 'text-yellow-400',
|
11
|
+
yellow: 'text-yellow-400',
|
12
|
+
orange: 'text-orange-400',
|
13
|
+
red: 'text-red-400',
|
14
|
+
pink: 'text-pink-400',
|
15
|
+
purple: 'text-purple-400',
|
16
|
+
blue: 'text-blue-400',
|
17
|
+
green: 'text-green-400',
|
18
|
+
gray: 'text-gray-400'
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
RATING_SIZE = {
|
22
|
+
small: 'text-sm',
|
23
|
+
medium: 'text-xl',
|
24
|
+
large: 'text-3xl'
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
RATING_EMPTY_COLOR = 'text-gray-300'.freeze
|
28
|
+
RATING_HALF_COLOR = 'text-yellow-300'.freeze
|
29
|
+
|
30
|
+
attr_reader :name, :value, :max_stars, :readonly, :half_stars,
|
31
|
+
:theme, :size, :show_value, :form, :classes, :options
|
32
|
+
|
33
|
+
# @param name [String] Nome del campo rating (obbligatorio se non readonly)
|
34
|
+
# @param value [Float, Integer] Valore del rating attuale (0.0 - max_stars)
|
35
|
+
# @param max_stars [Integer] Numero massimo di stelle (default: 5)
|
36
|
+
# @param readonly [Boolean] Se il rating è in sola lettura
|
37
|
+
# @param half_stars [Boolean] Se supportare mezze stelle
|
38
|
+
# @param theme [Symbol] Tema del componente (:default, :yellow, :orange, :red, :pink, :purple, :blue, :green, :gray)
|
39
|
+
# @param size [Symbol] Dimensione del componente (:small, :medium, :large)
|
40
|
+
# @param show_value [Boolean] Se mostrare il valore numerico accanto alle stelle
|
41
|
+
# @param form [ActionView::Helpers::FormBuilder, nil] Form builder Rails opzionale
|
42
|
+
# @param classes [String] Classi CSS aggiuntive
|
43
|
+
# @param options [Hash] Opzioni aggiuntive per attributi HTML
|
44
|
+
def initialize(name: nil, value: 0, max_stars: 5, readonly: false, half_stars: true,
|
45
|
+
theme: :default, size: :medium, show_value: false, form: nil,
|
46
|
+
classes: '', **options)
|
47
|
+
@name = name
|
48
|
+
@value = value.to_f
|
49
|
+
@max_stars = max_stars.to_i
|
50
|
+
@readonly = readonly
|
51
|
+
@half_stars = half_stars
|
52
|
+
@theme = theme.to_sym
|
53
|
+
@size = size.to_sym
|
54
|
+
@show_value = show_value
|
55
|
+
@form = form
|
56
|
+
@classes = classes
|
57
|
+
@options = options
|
58
|
+
|
59
|
+
validate_params
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def validate_params
|
65
|
+
validate_theme
|
66
|
+
validate_size
|
67
|
+
validate_value
|
68
|
+
validate_max_stars
|
69
|
+
validate_name if interactive?
|
70
|
+
end
|
71
|
+
|
72
|
+
def validate_theme
|
73
|
+
return if RATING_THEME.key?(@theme)
|
74
|
+
|
75
|
+
raise ArgumentError, "Invalid theme: #{@theme}. Valid themes are: #{RATING_THEME.keys.join(', ')}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_size
|
79
|
+
return if RATING_SIZE.key?(@size)
|
80
|
+
|
81
|
+
raise ArgumentError, "Invalid size: #{@size}. Valid sizes are: #{RATING_SIZE.keys.join(', ')}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_value
|
85
|
+
return if @value >= 0 && @value <= @max_stars
|
86
|
+
|
87
|
+
raise ArgumentError, "Value must be between 0 and #{@max_stars}, got: #{@value}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate_max_stars
|
91
|
+
return if @max_stars > 0
|
92
|
+
|
93
|
+
raise ArgumentError, "Max stars must be greater than 0, got: #{@max_stars}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def validate_name
|
97
|
+
return if @name.present?
|
98
|
+
|
99
|
+
raise ArgumentError, "Name is required for interactive rating components"
|
100
|
+
end
|
101
|
+
|
102
|
+
def interactive?
|
103
|
+
!@readonly
|
104
|
+
end
|
105
|
+
|
106
|
+
def container_classes
|
107
|
+
base_classes = ['inline-flex', 'items-center', 'gap-1']
|
108
|
+
base_classes << @classes if @classes.present?
|
109
|
+
base_classes.join(' ')
|
110
|
+
end
|
111
|
+
|
112
|
+
def rating_container_classes
|
113
|
+
base_classes = ['flex', 'items-center']
|
114
|
+
base_classes << (interactive? ? 'cursor-pointer' : 'cursor-default')
|
115
|
+
base_classes.join(' ')
|
116
|
+
end
|
117
|
+
|
118
|
+
def star_classes(index)
|
119
|
+
base_classes = [RATING_SIZE[@size], 'transition-colors', 'duration-150']
|
120
|
+
base_classes << (interactive? ? 'hover:scale-110 transform transition-transform' : '')
|
121
|
+
base_classes.compact.join(' ')
|
122
|
+
end
|
123
|
+
|
124
|
+
def controller_attributes
|
125
|
+
return {} unless interactive?
|
126
|
+
|
127
|
+
{
|
128
|
+
data: {
|
129
|
+
controller: 'bui-rating',
|
130
|
+
'bui-rating-rating-value': @value,
|
131
|
+
'bui-rating-max-value': @max_stars,
|
132
|
+
'bui-rating-readonly-value': @readonly,
|
133
|
+
'bui-rating-half-stars-value': @half_stars,
|
134
|
+
'bui-rating-name-value': @name,
|
135
|
+
action: 'keydown->bui-rating#keydown'
|
136
|
+
},
|
137
|
+
tabindex: '0'
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def star_attributes(index)
|
142
|
+
attrs = {
|
143
|
+
data: {
|
144
|
+
'bui-rating-target': 'star',
|
145
|
+
index: index
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
if interactive?
|
150
|
+
attrs[:data][:action] = [
|
151
|
+
'click->bui-rating#starClick',
|
152
|
+
'mouseover->bui-rating#starHover',
|
153
|
+
'mouseleave->bui-rating#starLeave'
|
154
|
+
].join(' ')
|
155
|
+
end
|
156
|
+
|
157
|
+
attrs
|
158
|
+
end
|
159
|
+
|
160
|
+
def hidden_input_attributes
|
161
|
+
return {} unless interactive?
|
162
|
+
|
163
|
+
{
|
164
|
+
type: 'hidden',
|
165
|
+
name: input_name,
|
166
|
+
value: @value,
|
167
|
+
data: { 'bui-rating-target': 'hiddenInput' }
|
168
|
+
}
|
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 input_id
|
180
|
+
@options[:id] || "rating_#{@name}"
|
181
|
+
end
|
182
|
+
|
183
|
+
def display_value
|
184
|
+
if @value % 1 == 0
|
185
|
+
@value.to_i.to_s
|
186
|
+
else
|
187
|
+
@value.to_s
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def star_display_state(index)
|
192
|
+
star_number = index + 1
|
193
|
+
|
194
|
+
if @value >= star_number
|
195
|
+
:full
|
196
|
+
elsif @half_stars && @value >= star_number - 0.5
|
197
|
+
:half
|
198
|
+
else
|
199
|
+
:empty
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def star_color(state)
|
204
|
+
case state
|
205
|
+
when :full
|
206
|
+
RATING_THEME[@theme]
|
207
|
+
when :half
|
208
|
+
RATING_HALF_COLOR
|
209
|
+
when :empty
|
210
|
+
RATING_EMPTY_COLOR
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def star_symbol(state)
|
215
|
+
case state
|
216
|
+
when :full, :half
|
217
|
+
'★'
|
218
|
+
when :empty
|
219
|
+
'☆'
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def render_star(index)
|
224
|
+
state = star_display_state(index)
|
225
|
+
|
226
|
+
content_tag(:span, star_attributes(index).merge(class: "#{star_classes(index)} #{star_color(state)}")) do
|
227
|
+
if state == :half
|
228
|
+
render_half_star
|
229
|
+
else
|
230
|
+
star_symbol(state)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def render_half_star
|
236
|
+
content_tag(:span, class: 'relative inline-block') do
|
237
|
+
safe_join([
|
238
|
+
content_tag(:span, '☆', class: RATING_EMPTY_COLOR),
|
239
|
+
content_tag(:span, class: 'absolute inset-0 overflow-hidden w-1/2') do
|
240
|
+
content_tag(:span, '★', class: RATING_THEME[@theme])
|
241
|
+
end
|
242
|
+
])
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def render_hidden_input
|
247
|
+
return unless interactive?
|
248
|
+
|
249
|
+
tag(:input, hidden_input_attributes)
|
250
|
+
end
|
251
|
+
|
252
|
+
def render_value_display
|
253
|
+
return unless @show_value
|
254
|
+
|
255
|
+
content_tag(:span, class: "ml-2 text-sm text-gray-600", data: { 'bui-rating-target': 'display' }) do
|
256
|
+
"(#{display_value})"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def render_stars_container
|
261
|
+
content_tag(:div, class: rating_container_classes, **controller_attributes) do
|
262
|
+
safe_join([
|
263
|
+
render_hidden_input,
|
264
|
+
*@max_stars.times.map { |i| render_star(i) }
|
265
|
+
].compact)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -1,16 +1,78 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
1
|
+
<%= tag.div(**container_attributes) do %>
|
2
|
+
<!-- Hidden input for form compatibility -->
|
3
|
+
<input
|
4
|
+
type="hidden"
|
5
|
+
name="<%= input_name %>"
|
6
|
+
id="<%= input_id %>"
|
7
|
+
value="<%= hidden_input_value %>"
|
8
|
+
data-select-target="hiddenInput"
|
9
|
+
<%= "required" if @required %>
|
10
|
+
<%= "disabled" if @disabled %>
|
11
|
+
/>
|
12
|
+
|
13
|
+
<!-- Trigger button -->
|
14
|
+
<button
|
15
|
+
type="button"
|
16
|
+
class="<%= trigger_classes %>"
|
17
|
+
data-select-target="trigger"
|
18
|
+
data-action="click->select#toggle"
|
19
|
+
<%= "disabled" if @disabled %>
|
20
|
+
>
|
21
|
+
<span data-select-text class="<%= trigger_text_classes %>">
|
22
|
+
<%= trigger_text %>
|
23
|
+
</span>
|
24
|
+
<%= chevron_icon %>
|
25
|
+
</button>
|
26
|
+
|
27
|
+
<!-- Badge container for multi-select -->
|
28
|
+
<% if @multiple %>
|
29
|
+
<div
|
30
|
+
class="<%= badge_container_classes %>"
|
31
|
+
data-select-target="badgeContainer"
|
32
|
+
style="<%= @selected.empty? ? 'display: none;' : '' %>"
|
11
33
|
>
|
12
|
-
|
13
|
-
</
|
34
|
+
<!-- Badges will be populated by Stimulus -->
|
35
|
+
</div>
|
14
36
|
<% end %>
|
15
|
-
|
16
|
-
|
37
|
+
|
38
|
+
<!-- Dropdown panel -->
|
39
|
+
<div class="<%= dropdown_classes %>" data-select-target="dropdown">
|
40
|
+
<!-- Search input -->
|
41
|
+
<% if @searchable %>
|
42
|
+
<div class="p-1">
|
43
|
+
<input
|
44
|
+
type="text"
|
45
|
+
placeholder="<%= @search_placeholder %>"
|
46
|
+
class="<%= search_input_classes %>"
|
47
|
+
data-select-target="search"
|
48
|
+
data-action="input->select#search"
|
49
|
+
/>
|
50
|
+
</div>
|
51
|
+
<% end %>
|
52
|
+
|
53
|
+
<!-- Options container -->
|
54
|
+
<div class="<%= options_container_classes %>">
|
55
|
+
<% @options.each do |option| %>
|
56
|
+
<div
|
57
|
+
class="<%= option_classes %> <%= 'bg-gray-100' if option_selected?(option) %>"
|
58
|
+
data-select-target="option"
|
59
|
+
data-value="<%= option[:value] %>"
|
60
|
+
data-action="click->select#selectOption"
|
61
|
+
<%= "data-disabled" if option[:disabled] %>
|
62
|
+
>
|
63
|
+
<span><%= option[:label] %></span>
|
64
|
+
<% if option_selected?(option) %>
|
65
|
+
<span class="checkmark text-gray-600">✓</span>
|
66
|
+
<% end %>
|
67
|
+
</div>
|
68
|
+
<% end %>
|
69
|
+
</div>
|
70
|
+
|
71
|
+
<!-- Empty state -->
|
72
|
+
<% if @options.empty? %>
|
73
|
+
<div class="px-3 py-6 text-center text-gray-500 text-sm">
|
74
|
+
Nessuna opzione disponibile
|
75
|
+
</div>
|
76
|
+
<% end %>
|
77
|
+
</div>
|
78
|
+
<% end %>
|