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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/app/components/better_ui/application/main/component.html.erb +1 -1
  3. data/app/components/better_ui/application/sidebar/component.html.erb +77 -18
  4. data/app/components/better_ui/application/sidebar/component.rb +63 -5
  5. data/app/components/better_ui/general/accordion/component.html.erb +5 -0
  6. data/app/components/better_ui/general/accordion/component.rb +92 -0
  7. data/app/components/better_ui/general/accordion/item_component.html.erb +12 -0
  8. data/app/components/better_ui/general/accordion/item_component.rb +176 -0
  9. data/app/components/better_ui/general/button/component.html.erb +8 -8
  10. data/app/components/better_ui/general/button/component.rb +11 -11
  11. data/app/components/better_ui/general/dropdown/component.html.erb +21 -7
  12. data/app/components/better_ui/general/dropdown/component.rb +27 -54
  13. data/app/components/better_ui/general/dropdown/item_component.rb +2 -1
  14. data/app/components/better_ui/general/field/component.html.erb +3 -3
  15. data/app/components/better_ui/general/field/component.rb +3 -3
  16. data/app/components/better_ui/general/grid/cell_component.html.erb +3 -0
  17. data/app/components/better_ui/general/grid/cell_component.rb +390 -0
  18. data/app/components/better_ui/general/grid/component.html.erb +3 -0
  19. data/app/components/better_ui/general/grid/component.rb +301 -0
  20. data/app/components/better_ui/general/heading/component.html.erb +1 -1
  21. data/app/components/better_ui/general/icon/component.rb +2 -1
  22. data/app/components/better_ui/general/input/checkbox/component.rb +10 -10
  23. data/app/components/better_ui/general/input/pin/component.html.erb +1 -0
  24. data/app/components/better_ui/general/input/pin/component.rb +201 -0
  25. data/app/components/better_ui/general/input/radio/component.rb +10 -10
  26. data/app/components/better_ui/general/input/rating/component.html.erb +4 -0
  27. data/app/components/better_ui/general/input/rating/component.rb +272 -0
  28. data/app/components/better_ui/general/input/select/component.html.erb +76 -14
  29. data/app/components/better_ui/general/input/select/component.rb +166 -101
  30. data/app/components/better_ui/general/input/toggle/component.html.erb +5 -0
  31. data/app/components/better_ui/general/input/toggle/component.rb +242 -0
  32. data/app/components/better_ui/general/link/component.rb +1 -1
  33. data/app/components/better_ui/general/modal/component.html.erb +5 -42
  34. data/app/components/better_ui/general/modal/component.rb +22 -140
  35. data/app/components/better_ui/general/modal/modal_component.html.erb +52 -0
  36. data/app/components/better_ui/general/modal/modal_component.rb +160 -0
  37. data/app/components/better_ui/general/tabs/component.html.erb +10 -2
  38. data/app/components/better_ui/general/tabs/component.rb +26 -8
  39. data/app/components/better_ui/general/tabs/panel_component.rb +1 -1
  40. data/app/components/better_ui/general/tabs/tab_component.rb +1 -1
  41. data/app/components/better_ui/general/text/component.html.erb +1 -0
  42. data/app/components/better_ui/general/text/component.rb +194 -0
  43. data/app/helpers/better_ui/application_helper.rb +11 -4
  44. data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +73 -0
  45. data/app/helpers/better_ui/general/components/button/button_helper.rb +6 -6
  46. data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +9 -0
  47. data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +13 -7
  48. data/app/helpers/better_ui/general/components/field/field_helper.rb +4 -4
  49. data/app/helpers/better_ui/general/components/grid/grid_helper.rb +145 -0
  50. data/app/helpers/better_ui/general/components/input/pin/pin_helper.rb +76 -0
  51. data/app/helpers/better_ui/general/components/input/rating/rating_helper.rb +70 -0
  52. data/app/helpers/better_ui/general/components/input/select/select_helper.rb +47 -31
  53. data/app/helpers/better_ui/general/components/input/toggle/toggle_helper.rb +77 -0
  54. data/app/helpers/better_ui/general/components/modal/modal_helper.rb +34 -44
  55. data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +59 -26
  56. data/app/helpers/better_ui/general/components/text/text_helper.rb +83 -0
  57. data/lib/better_ui/version.rb +1 -1
  58. data/lib/better_ui.rb +1 -0
  59. 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
- <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 %>
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
- <%= option[:label] %>
13
- </option>
34
+ <!-- Badges will be populated by Stimulus -->
35
+ </div>
14
36
  <% end %>
15
- <%= content %>
16
- </select>
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 %>