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,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 %>
|