better_ui 0.1.1 → 0.3.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/sidebar/component.html.erb +53 -16
- data/app/components/better_ui/application/sidebar/component.rb +3 -2
- 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 +4 -4
- data/app/components/better_ui/general/dropdown/component.html.erb +25 -0
- data/app/components/better_ui/general/dropdown/component.rb +170 -0
- data/app/components/better_ui/general/dropdown/divider_component.html.erb +1 -0
- data/app/components/better_ui/general/dropdown/divider_component.rb +41 -0
- data/app/components/better_ui/general/dropdown/item_component.html.erb +6 -0
- data/app/components/better_ui/general/dropdown/item_component.rb +119 -0
- data/app/components/better_ui/general/modal/component.html.erb +5 -0
- data/app/components/better_ui/general/modal/component.rb +47 -0
- 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/pagination/component.html.erb +85 -0
- data/app/components/better_ui/general/pagination/component.rb +216 -0
- data/app/components/better_ui/general/tabs/component.html.erb +11 -0
- data/app/components/better_ui/general/tabs/component.rb +120 -0
- data/app/components/better_ui/general/tabs/panel_component.html.erb +3 -0
- data/app/components/better_ui/general/tabs/panel_component.rb +37 -0
- data/app/components/better_ui/general/tabs/tab_component.html.erb +13 -0
- data/app/components/better_ui/general/tabs/tab_component.rb +111 -0
- data/app/helpers/better_ui/application_helper.rb +12 -3
- data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +73 -0
- data/app/helpers/better_ui/general/components/accordion.rb +11 -0
- data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +32 -0
- data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +79 -0
- data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +62 -0
- data/app/helpers/better_ui/general/components/modal/modal_helper.rb +85 -0
- data/app/helpers/better_ui/general/components/modal.rb +11 -0
- data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +82 -0
- data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +62 -0
- data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +55 -0
- data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +95 -0
- data/lib/better_ui/version.rb +1 -1
- metadata +35 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f8b92fdba76f5e84f09fb6a3bf9879b209e0e7dfd8f864186784db24b808c59
|
4
|
+
data.tar.gz: 4d21e971baeecb10c7a3c5952041ac7e6911342e0efe4d0015efb92f9f214652
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 778ee8d067130b37947a227ccf1322e8cf5b82d19c9b3f7a0371cc16b62071daaeb5eb6a191898ef2b06419486fe656bc22bea4314fbf98884fdf16259ac8409
|
7
|
+
data.tar.gz: 1df545b428bdb281fb6f78b7be2e58e455bd6bd67f0202684f51b9e756c9ccc3e928a283f97d98a924185e1bb859b63f8cf2a91857ef49478cf6bfc3f254629d
|
@@ -1,10 +1,21 @@
|
|
1
|
-
<
|
1
|
+
<div data-controller="bui-sidebar"
|
2
|
+
data-bui-sidebar-width-value="<%= @width == :md ? 256 : 320 %>"
|
3
|
+
data-bui-sidebar-min-width-value="200"
|
4
|
+
data-bui-sidebar-max-width-value="400"
|
5
|
+
data-bui-sidebar-pinned-value="true">
|
6
|
+
|
7
|
+
<!-- Mobile Overlay -->
|
8
|
+
<div data-bui-sidebar-target="overlay" class="fixed inset-0 bg-black bg-opacity-50 z-40 hidden md:hidden"></div>
|
9
|
+
|
10
|
+
<!-- Sidebar Container -->
|
11
|
+
<aside data-bui-sidebar-target="container" class="<%= container_classes %>">
|
2
12
|
<!-- Header Section -->
|
3
13
|
<% if has_header? %>
|
4
14
|
<div class="px-6 py-4 border-b border-gray-200">
|
5
|
-
|
6
|
-
|
7
|
-
<div class="flex-
|
15
|
+
<div class="flex items-center justify-between">
|
16
|
+
<% if header[:logo].present? %>
|
17
|
+
<div class="flex items-center">
|
18
|
+
<div class="flex-shrink-0">
|
8
19
|
<% if header[:logo].is_a?(Hash) %>
|
9
20
|
<%= bui_avatar(**header[:logo]) %>
|
10
21
|
<% else %>
|
@@ -20,19 +31,30 @@
|
|
20
31
|
</div>
|
21
32
|
<% end %>
|
22
33
|
</div>
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
34
|
+
<% elsif header[:title].present? %>
|
35
|
+
<div>
|
36
|
+
<h2 class="text-lg font-semibold text-gray-900"><%= header[:title] %></h2>
|
37
|
+
<% if header[:subtitle].present? %>
|
38
|
+
<p class="text-sm text-gray-500"><%= header[:subtitle] %></p>
|
39
|
+
<% end %>
|
40
|
+
</div>
|
41
|
+
<% end %>
|
42
|
+
|
43
|
+
<!-- Collapse Button (solo se collapsible) -->
|
44
|
+
<% if collapsible %>
|
45
|
+
<%= bui_button(
|
46
|
+
icon: "arrow-left",
|
47
|
+
type: :white,
|
48
|
+
size: :small,
|
49
|
+
title: "Comprimi sidebar"
|
50
|
+
) %>
|
51
|
+
<% end %>
|
52
|
+
</div>
|
31
53
|
</div>
|
32
54
|
<% end %>
|
33
55
|
|
34
56
|
<!-- Navigation Section -->
|
35
|
-
<nav class="flex-1 px-4 py-6 space-y-6">
|
57
|
+
<nav class="flex-1 px-4 py-6 space-y-6 overflow-y-auto">
|
36
58
|
<% navigation_sections.each do |section| %>
|
37
59
|
<div class="space-y-2">
|
38
60
|
<!-- Section Title -->
|
@@ -57,7 +79,9 @@
|
|
57
79
|
<button
|
58
80
|
type="button"
|
59
81
|
class="group flex items-center justify-between w-full px-3 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-50 hover:text-gray-900 transition-colors duration-150"
|
60
|
-
data-
|
82
|
+
data-bui-sidebar-target="sectionTrigger"
|
83
|
+
data-bui-sidebar-section-id="<%= item[:id] %>"
|
84
|
+
data-action="click->bui-sidebar#toggleSection"
|
61
85
|
aria-expanded="<%= item[:expanded] || false %>"
|
62
86
|
>
|
63
87
|
<div class="flex items-center">
|
@@ -68,13 +92,14 @@
|
|
68
92
|
<% end %>
|
69
93
|
<span><%= item[:label] %></span>
|
70
94
|
</div>
|
71
|
-
<span class="ml-3 transform transition-transform duration-150 <%= 'rotate-90' if item[:expanded] %>">
|
95
|
+
<span class="ml-3 transform transition-transform duration-150 <%= 'rotate-90' if item[:expanded] %>" data-bui-sidebar-chevron>
|
72
96
|
<%= bui_icon("chevron-right", size: :small) %>
|
73
97
|
</span>
|
74
98
|
</button>
|
75
99
|
|
76
100
|
<div
|
77
|
-
|
101
|
+
data-bui-sidebar-target="sectionContent"
|
102
|
+
data-bui-sidebar-section-id="<%= item[:id] %>"
|
78
103
|
class="<%= item[:expanded] ? 'block' : 'hidden' %> mt-1 space-y-1"
|
79
104
|
>
|
80
105
|
<% (item[:children] || []).each do |child| %>
|
@@ -187,4 +212,16 @@
|
|
187
212
|
<% end %>
|
188
213
|
</div>
|
189
214
|
<% end %>
|
215
|
+
|
216
|
+
<!-- Resize Handle (solo se NON collapsible) -->
|
217
|
+
<% unless collapsible %>
|
218
|
+
<div data-bui-sidebar-target="resizeHandle"
|
219
|
+
class="absolute top-0 right-0 w-1 h-full bg-transparent hover:bg-blue-500 cursor-col-resize transition-colors duration-150 group">
|
220
|
+
<div class="absolute inset-y-0 -right-1 w-3 flex items-center justify-center">
|
221
|
+
<div class="w-0.5 h-8 bg-gray-300 group-hover:bg-blue-500 transition-colors duration-150"></div>
|
222
|
+
</div>
|
223
|
+
</div>
|
224
|
+
<% end %>
|
190
225
|
</aside>
|
226
|
+
|
227
|
+
</div>
|
@@ -4,9 +4,10 @@ module BetterUi
|
|
4
4
|
module Application
|
5
5
|
module Sidebar
|
6
6
|
class Component < ViewComponent::Base
|
7
|
-
# Include degli helper per utilizzare bui_icon e
|
7
|
+
# Include degli helper per utilizzare bui_icon, bui_avatar e bui_button
|
8
8
|
include BetterUi::General::Components::Icon::IconHelper
|
9
9
|
include BetterUi::General::Components::Avatar::AvatarHelper
|
10
|
+
include BetterUi::General::Components::Button::ButtonHelper
|
10
11
|
attr_reader :width, :position, :theme, :shadow, :border, :header, :footer, :navigation_sections, :collapsible, :classes
|
11
12
|
|
12
13
|
# Larghezze sidebar con classi Tailwind dirette
|
@@ -74,7 +75,7 @@ module BetterUi
|
|
74
75
|
end
|
75
76
|
|
76
77
|
def container_classes
|
77
|
-
base_classes = %w[fixed inset-y-0 z-50 flex flex-col
|
78
|
+
base_classes = %w[fixed inset-y-0 h-screen z-50 flex flex-col]
|
78
79
|
|
79
80
|
# Posizione
|
80
81
|
base_classes << (position == :right ? "right-0" : "left-0")
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BetterUi
|
4
|
+
module General
|
5
|
+
module Accordion
|
6
|
+
class Component < ViewComponent::Base
|
7
|
+
renders_many :items, "BetterUi::General::Accordion::ItemComponent"
|
8
|
+
|
9
|
+
ACCORDION_THEME = {
|
10
|
+
default: 'border-gray-200',
|
11
|
+
white: 'border-gray-100 bg-white',
|
12
|
+
blue: 'border-blue-200',
|
13
|
+
red: 'border-red-200',
|
14
|
+
green: 'border-green-200',
|
15
|
+
yellow: 'border-yellow-200',
|
16
|
+
violet: 'border-violet-200',
|
17
|
+
orange: 'border-orange-200',
|
18
|
+
rose: 'border-rose-200'
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
ACCORDION_VARIANT = {
|
22
|
+
minimal: '',
|
23
|
+
bordered: 'border rounded-lg',
|
24
|
+
separated: 'space-y-2'
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
ACCORDION_SIZE = {
|
28
|
+
small: 'text-sm',
|
29
|
+
medium: 'text-base',
|
30
|
+
large: 'text-lg'
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
def initialize(multiple: false, theme: :default, variant: :bordered, size: :medium,
|
34
|
+
classes: '', **options)
|
35
|
+
@multiple = multiple
|
36
|
+
@theme = theme
|
37
|
+
@variant = variant
|
38
|
+
@size = size
|
39
|
+
@classes = classes
|
40
|
+
@options = options
|
41
|
+
|
42
|
+
validate_params
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :multiple, :theme, :variant, :size, :classes, :options
|
48
|
+
|
49
|
+
def validate_params
|
50
|
+
validate_theme
|
51
|
+
validate_variant
|
52
|
+
validate_size
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_theme
|
56
|
+
return if ACCORDION_THEME.key?(theme)
|
57
|
+
|
58
|
+
raise ArgumentError, "Invalid theme: #{theme}. Must be one of #{ACCORDION_THEME.keys}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_variant
|
62
|
+
return if ACCORDION_VARIANT.key?(variant)
|
63
|
+
|
64
|
+
raise ArgumentError, "Invalid variant: #{variant}. Must be one of #{ACCORDION_VARIANT.keys}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate_size
|
68
|
+
return if ACCORDION_SIZE.key?(size)
|
69
|
+
|
70
|
+
raise ArgumentError, "Invalid size: #{size}. Must be one of #{ACCORDION_SIZE.keys}"
|
71
|
+
end
|
72
|
+
|
73
|
+
# Attributi per il wrapper principale
|
74
|
+
def wrapper_attributes
|
75
|
+
base_classes = [
|
76
|
+
'bui-accordion',
|
77
|
+
ACCORDION_SIZE[size],
|
78
|
+
ACCORDION_VARIANT[variant],
|
79
|
+
ACCORDION_THEME[theme],
|
80
|
+
classes
|
81
|
+
].compact.join(' ')
|
82
|
+
|
83
|
+
{
|
84
|
+
class: base_classes,
|
85
|
+
'data-controller': 'bui-accordion',
|
86
|
+
'data-bui-accordion-multiple-value': multiple
|
87
|
+
}.merge(options)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<div <%= tag.attributes(item_attributes) %>>
|
2
|
+
<!-- Header -->
|
3
|
+
<button <%= tag.attributes(header_attributes) %>>
|
4
|
+
<span><%= title %></span>
|
5
|
+
<i class="fas fa-<%= icon %> <%= icon_attributes[:class] %>" <%= tag.attributes(icon_attributes.except(:class)) %>></i>
|
6
|
+
</button>
|
7
|
+
|
8
|
+
<!-- Content -->
|
9
|
+
<div <%= tag.attributes(content_attributes) %>>
|
10
|
+
<%= content %>
|
11
|
+
</div>
|
12
|
+
</div>
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BetterUi
|
4
|
+
module General
|
5
|
+
module Accordion
|
6
|
+
class ItemComponent < ViewComponent::Base
|
7
|
+
ITEM_THEME = {
|
8
|
+
default: 'border-gray-200 bg-white',
|
9
|
+
white: 'border-gray-100 bg-white',
|
10
|
+
blue: 'border-blue-200 bg-blue-50',
|
11
|
+
red: 'border-red-200 bg-red-50',
|
12
|
+
green: 'border-green-200 bg-green-50',
|
13
|
+
yellow: 'border-yellow-200 bg-yellow-50',
|
14
|
+
violet: 'border-violet-200 bg-violet-50',
|
15
|
+
orange: 'border-orange-200 bg-orange-50',
|
16
|
+
rose: 'border-rose-200 bg-rose-50'
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
ITEM_HEADER_THEME = {
|
20
|
+
default: 'text-gray-900 hover:bg-gray-50',
|
21
|
+
white: 'text-gray-900 hover:bg-gray-50',
|
22
|
+
blue: 'text-blue-900 hover:bg-blue-100',
|
23
|
+
red: 'text-red-900 hover:bg-red-100',
|
24
|
+
green: 'text-green-900 hover:bg-green-100',
|
25
|
+
yellow: 'text-yellow-900 hover:bg-yellow-100',
|
26
|
+
violet: 'text-violet-900 hover:bg-violet-100',
|
27
|
+
orange: 'text-orange-900 hover:bg-orange-100',
|
28
|
+
rose: 'text-rose-900 hover:bg-rose-100'
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
ITEM_SIZE_HEADER = {
|
32
|
+
small: 'text-sm px-4 py-3',
|
33
|
+
medium: 'text-base px-5 py-4',
|
34
|
+
large: 'text-lg px-6 py-5'
|
35
|
+
}.freeze
|
36
|
+
|
37
|
+
ITEM_SIZE_CONTENT = {
|
38
|
+
small: 'px-4 py-3',
|
39
|
+
medium: 'px-5 py-4',
|
40
|
+
large: 'px-6 py-5'
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
ITEM_SIZE_ICON = {
|
44
|
+
small: 'w-4 h-4',
|
45
|
+
medium: 'w-5 h-5',
|
46
|
+
large: 'w-6 h-6'
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
def initialize(title:, expanded: false, disabled: false, icon: 'chevron-down',
|
50
|
+
theme: :default, size: :medium, classes: '', **options)
|
51
|
+
@title = title
|
52
|
+
@expanded = expanded
|
53
|
+
@disabled = disabled
|
54
|
+
@icon = icon
|
55
|
+
@theme = theme
|
56
|
+
@size = size
|
57
|
+
@classes = classes
|
58
|
+
@options = options
|
59
|
+
|
60
|
+
validate_params
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
attr_reader :title, :expanded, :disabled, :icon, :theme, :size, :classes, :options
|
66
|
+
|
67
|
+
def validate_params
|
68
|
+
validate_theme
|
69
|
+
validate_size
|
70
|
+
|
71
|
+
raise ArgumentError, 'title cannot be blank' if title.blank?
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_theme
|
75
|
+
return if ITEM_THEME.key?(theme)
|
76
|
+
|
77
|
+
raise ArgumentError, "Invalid theme: #{theme}. Must be one of #{ITEM_THEME.keys}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_size
|
81
|
+
return if ITEM_SIZE_HEADER.key?(size)
|
82
|
+
|
83
|
+
raise ArgumentError, "Invalid size: #{size}. Must be one of #{ITEM_SIZE_HEADER.keys}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def unique_id
|
87
|
+
@unique_id ||= "accordion-item-#{SecureRandom.hex(4)}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def content_id
|
91
|
+
"#{unique_id}-content"
|
92
|
+
end
|
93
|
+
|
94
|
+
def header_id
|
95
|
+
"#{unique_id}-header"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Attributi per il wrapper dell'item
|
99
|
+
def item_attributes
|
100
|
+
base_classes = [
|
101
|
+
'bui-accordion-item',
|
102
|
+
ITEM_THEME[theme],
|
103
|
+
classes
|
104
|
+
].compact.join(' ')
|
105
|
+
|
106
|
+
{
|
107
|
+
class: base_classes,
|
108
|
+
'data-bui-accordion-target': 'item'
|
109
|
+
}.merge(options)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Attributi per il button header
|
113
|
+
def header_attributes
|
114
|
+
base_classes = [
|
115
|
+
'bui-accordion-header',
|
116
|
+
'w-full flex items-center justify-between text-left font-medium transition-colors duration-200',
|
117
|
+
ITEM_SIZE_HEADER[size],
|
118
|
+
ITEM_HEADER_THEME[theme],
|
119
|
+
disabled? ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
|
120
|
+
].compact.join(' ')
|
121
|
+
|
122
|
+
{
|
123
|
+
class: base_classes,
|
124
|
+
type: 'button',
|
125
|
+
id: header_id,
|
126
|
+
'aria-expanded': expanded,
|
127
|
+
'aria-controls': content_id,
|
128
|
+
'data-action': 'click->bui-accordion#toggle',
|
129
|
+
'data-bui-accordion-target': 'trigger',
|
130
|
+
disabled: disabled
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
# Attributi per il contenuto
|
135
|
+
def content_attributes
|
136
|
+
base_classes = [
|
137
|
+
'bui-accordion-content',
|
138
|
+
'transition-all duration-300 ease-in-out overflow-hidden',
|
139
|
+
ITEM_SIZE_CONTENT[size],
|
140
|
+
expanded? ? 'block' : 'hidden'
|
141
|
+
].compact.join(' ')
|
142
|
+
|
143
|
+
{
|
144
|
+
class: base_classes,
|
145
|
+
id: content_id,
|
146
|
+
'aria-labelledby': header_id,
|
147
|
+
'data-bui-accordion-target': 'content'
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
# Attributi per l'icona
|
152
|
+
def icon_attributes
|
153
|
+
base_classes = [
|
154
|
+
'bui-accordion-icon',
|
155
|
+
'transition-transform duration-200',
|
156
|
+
ITEM_SIZE_ICON[size],
|
157
|
+
expanded? ? 'rotate-180' : ''
|
158
|
+
].compact.join(' ')
|
159
|
+
|
160
|
+
{
|
161
|
+
class: base_classes,
|
162
|
+
'data-bui-accordion-target': 'icon'
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def disabled?
|
167
|
+
disabled
|
168
|
+
end
|
169
|
+
|
170
|
+
def expanded?
|
171
|
+
expanded
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<% if link? %>
|
3
3
|
<%= link_to @href, **link_attributes do %>
|
4
4
|
<% if @icon && @icon_position == :left %>
|
5
|
-
<span class="flex-shrink-0 mr-2"><%= render_icon(@icon) %></span>
|
5
|
+
<span class="flex-shrink-0<%= @label.present? ? ' mr-2' : '' %>"><%= render_icon(@icon) %></span>
|
6
6
|
<% end %>
|
7
7
|
|
8
8
|
<% if @label %>
|
@@ -10,7 +10,7 @@
|
|
10
10
|
<% end %>
|
11
11
|
|
12
12
|
<% if @icon && @icon_position == :right %>
|
13
|
-
<span class="flex-shrink-0 ml-2"><%= render_icon(@icon) %></span>
|
13
|
+
<span class="flex-shrink-0<%= @label.present? ? ' ml-2' : '' %>"><%= render_icon(@icon) %></span>
|
14
14
|
<% end %>
|
15
15
|
|
16
16
|
<%= content %>
|
@@ -18,7 +18,7 @@
|
|
18
18
|
<% else %>
|
19
19
|
<%= tag.button(**button_attributes) do %>
|
20
20
|
<% if @icon && @icon_position == :left %>
|
21
|
-
<span class="flex-shrink-0 mr-2"><%= render_icon(@icon) %></span>
|
21
|
+
<span class="flex-shrink-0<%= @label.present? ? ' mr-2' : '' %>"><%= render_icon(@icon) %></span>
|
22
22
|
<% end %>
|
23
23
|
|
24
24
|
<% if @label %>
|
@@ -26,7 +26,7 @@
|
|
26
26
|
<% end %>
|
27
27
|
|
28
28
|
<% if @icon && @icon_position == :right %>
|
29
|
-
<span class="flex-shrink-0 ml-2"><%= render_icon(@icon) %></span>
|
29
|
+
<span class="flex-shrink-0<%= @label.present? ? ' ml-2' : '' %>"><%= render_icon(@icon) %></span>
|
30
30
|
<% end %>
|
31
31
|
|
32
32
|
<%= content %>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<div class="relative inline-block <%= @classes %>"
|
2
|
+
data-controller="bui-dropdown"
|
3
|
+
data-bui-dropdown-open-value="false"
|
4
|
+
<%= tag.attributes(@html_options.except(:class)) %>>
|
5
|
+
|
6
|
+
<button type="button"
|
7
|
+
class="inline-flex items-center justify-center border font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors <%= dynamic_trigger_classes %>"
|
8
|
+
data-bui-dropdown-target="trigger"
|
9
|
+
data-action="click->bui-dropdown#toggle keydown->bui-dropdown#keydown"
|
10
|
+
aria-expanded="false"
|
11
|
+
aria-haspopup="true">
|
12
|
+
<%= @trigger %>
|
13
|
+
<%= bui_icon("chevron-down", size: :small, classes: "ml-2 -mr-1") %>
|
14
|
+
</button>
|
15
|
+
|
16
|
+
<div class="absolute z-50 mt-2 origin-top-right bg-white border border-gray-200 shadow-lg focus:outline-none <%= dynamic_menu_classes %>"
|
17
|
+
data-bui-dropdown-target="menu"
|
18
|
+
role="menu"
|
19
|
+
aria-orientation="vertical"
|
20
|
+
style="display: none;">
|
21
|
+
<div class="py-1" role="none" data-action="click->bui-dropdown#itemClick">
|
22
|
+
<%= content %>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
</div>
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BetterUi
|
4
|
+
module General
|
5
|
+
module Dropdown
|
6
|
+
class Component < ViewComponent::Base
|
7
|
+
include BetterUi::General::Components::Icon::IconHelper
|
8
|
+
|
9
|
+
attr_reader :trigger, :position, :theme, :size, :rounded, :animation, :classes, :html_options
|
10
|
+
|
11
|
+
# Classi base spostate nel template HTML per migliore leggibilità
|
12
|
+
|
13
|
+
# Temi per il trigger del dropdown con classi Tailwind dirette
|
14
|
+
DROPDOWN_TRIGGER_THEME = {
|
15
|
+
default: "bg-white border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-blue-500",
|
16
|
+
white: "bg-white border-gray-300 text-gray-900 hover:bg-gray-50 focus:ring-gray-500",
|
17
|
+
red: "bg-red-600 border-red-600 text-white hover:bg-red-700 focus:ring-red-500",
|
18
|
+
rose: "bg-rose-600 border-rose-600 text-white hover:bg-rose-700 focus:ring-rose-500",
|
19
|
+
orange: "bg-orange-600 border-orange-600 text-white hover:bg-orange-700 focus:ring-orange-500",
|
20
|
+
green: "bg-green-600 border-green-600 text-white hover:bg-green-700 focus:ring-green-500",
|
21
|
+
blue: "bg-blue-600 border-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
|
22
|
+
yellow: "bg-yellow-500 border-yellow-500 text-white hover:bg-yellow-600 focus:ring-yellow-500",
|
23
|
+
violet: "bg-violet-600 border-violet-600 text-white hover:bg-violet-700 focus:ring-violet-500"
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
# Dimensioni del trigger con classi Tailwind dirette
|
27
|
+
DROPDOWN_TRIGGER_SIZE = {
|
28
|
+
small: "px-3 py-1.5 text-sm",
|
29
|
+
medium: "px-4 py-2 text-sm",
|
30
|
+
large: "px-6 py-3 text-base"
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
# Border radius con classi Tailwind dirette
|
34
|
+
DROPDOWN_ROUNDED = {
|
35
|
+
none: "rounded-none",
|
36
|
+
small: "rounded-md",
|
37
|
+
medium: "rounded-lg",
|
38
|
+
large: "rounded-xl",
|
39
|
+
full: "rounded-full"
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
# Posizioni del menu dropdown
|
43
|
+
DROPDOWN_POSITION = {
|
44
|
+
bottom: "top-full left-0",
|
45
|
+
top: "bottom-full left-0",
|
46
|
+
left: "top-0 right-full mr-2",
|
47
|
+
right: "top-0 left-full ml-2"
|
48
|
+
}.freeze
|
49
|
+
|
50
|
+
# Animazioni del dropdown
|
51
|
+
DROPDOWN_ANIMATION = {
|
52
|
+
fade: "transition-opacity duration-150",
|
53
|
+
slide: "transition-all duration-150 transform",
|
54
|
+
none: ""
|
55
|
+
}.freeze
|
56
|
+
|
57
|
+
def initialize(
|
58
|
+
trigger:,
|
59
|
+
position: :bottom,
|
60
|
+
theme: :default,
|
61
|
+
size: :medium,
|
62
|
+
rounded: :medium,
|
63
|
+
animation: :fade,
|
64
|
+
classes: nil,
|
65
|
+
**html_options
|
66
|
+
)
|
67
|
+
@trigger = trigger
|
68
|
+
@position = position.to_sym
|
69
|
+
@theme = theme.to_sym
|
70
|
+
@size = size.to_sym
|
71
|
+
@rounded = rounded.to_sym
|
72
|
+
@animation = animation.to_sym
|
73
|
+
@classes = classes
|
74
|
+
@html_options = html_options
|
75
|
+
|
76
|
+
validate_params
|
77
|
+
end
|
78
|
+
|
79
|
+
# Restituisce solo le classi dinamiche per il trigger
|
80
|
+
def dynamic_trigger_classes
|
81
|
+
[
|
82
|
+
get_trigger_theme_classes,
|
83
|
+
get_trigger_size_classes,
|
84
|
+
get_trigger_rounded_classes
|
85
|
+
].compact.join(" ")
|
86
|
+
end
|
87
|
+
|
88
|
+
# Restituisce solo le classi dinamiche per il menu
|
89
|
+
def dynamic_menu_classes
|
90
|
+
[
|
91
|
+
get_position_classes,
|
92
|
+
get_animation_classes,
|
93
|
+
get_menu_rounded_classes
|
94
|
+
].compact.join(" ")
|
95
|
+
end
|
96
|
+
|
97
|
+
# Metodi per attributi rimossi - ora gestiti direttamente nel template HTML
|
98
|
+
|
99
|
+
# Verifica se rendere il componente
|
100
|
+
def render?
|
101
|
+
@trigger.present?
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def get_trigger_theme_classes
|
107
|
+
DROPDOWN_TRIGGER_THEME[@theme] || DROPDOWN_TRIGGER_THEME[:default]
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_trigger_size_classes
|
111
|
+
DROPDOWN_TRIGGER_SIZE[@size] || DROPDOWN_TRIGGER_SIZE[:medium]
|
112
|
+
end
|
113
|
+
|
114
|
+
def get_trigger_rounded_classes
|
115
|
+
DROPDOWN_ROUNDED[@rounded] || DROPDOWN_ROUNDED[:medium]
|
116
|
+
end
|
117
|
+
|
118
|
+
def get_menu_rounded_classes
|
119
|
+
DROPDOWN_ROUNDED[@rounded] || DROPDOWN_ROUNDED[:medium]
|
120
|
+
end
|
121
|
+
|
122
|
+
def get_position_classes
|
123
|
+
DROPDOWN_POSITION[@position] || DROPDOWN_POSITION[:bottom]
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_animation_classes
|
127
|
+
DROPDOWN_ANIMATION[@animation] || DROPDOWN_ANIMATION[:fade]
|
128
|
+
end
|
129
|
+
|
130
|
+
def validate_params
|
131
|
+
validate_theme
|
132
|
+
validate_size
|
133
|
+
validate_rounded
|
134
|
+
validate_position
|
135
|
+
validate_animation
|
136
|
+
end
|
137
|
+
|
138
|
+
def validate_theme
|
139
|
+
unless DROPDOWN_TRIGGER_THEME.keys.include?(@theme)
|
140
|
+
raise ArgumentError, "Il tema deve essere uno tra: #{DROPDOWN_TRIGGER_THEME.keys.join(', ')}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def validate_size
|
145
|
+
unless DROPDOWN_TRIGGER_SIZE.keys.include?(@size)
|
146
|
+
raise ArgumentError, "La dimensione deve essere una tra: #{DROPDOWN_TRIGGER_SIZE.keys.join(', ')}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def validate_rounded
|
151
|
+
unless DROPDOWN_ROUNDED.keys.include?(@rounded)
|
152
|
+
raise ArgumentError, "Il border radius deve essere uno tra: #{DROPDOWN_ROUNDED.keys.join(', ')}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def validate_position
|
157
|
+
unless DROPDOWN_POSITION.keys.include?(@position)
|
158
|
+
raise ArgumentError, "La posizione deve essere una tra: #{DROPDOWN_POSITION.keys.join(', ')}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def validate_animation
|
163
|
+
unless DROPDOWN_ANIMATION.keys.include?(@animation)
|
164
|
+
raise ArgumentError, "L'animazione deve essere una tra: #{DROPDOWN_ANIMATION.keys.join(', ')}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<div <%= tag.attributes(divider_attributes) %>></div>
|