better_ui 0.1.0 → 0.4.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/README.md +65 -1
- data/app/assets/javascripts/better_ui/controllers/navbar_controller.js +138 -0
- data/app/assets/javascripts/better_ui/controllers/sidebar_controller.js +211 -0
- data/app/assets/javascripts/better_ui/controllers/toast_controller.js +161 -0
- data/app/assets/javascripts/better_ui/index.js +159 -0
- data/app/assets/javascripts/better_ui/toast_manager.js +77 -0
- data/app/assets/stylesheets/better_ui/application.css +25 -351
- data/app/components/better_ui/application/alert_component.html.erb +27 -0
- data/app/components/better_ui/application/alert_component.rb +196 -0
- data/app/components/better_ui/application/header_component.html.erb +88 -0
- data/app/components/better_ui/application/header_component.rb +188 -0
- data/app/components/better_ui/application/navbar_component.html.erb +294 -0
- data/app/components/better_ui/application/navbar_component.rb +249 -0
- data/app/components/better_ui/application/sidebar_component.html.erb +207 -0
- data/app/components/better_ui/application/sidebar_component.rb +318 -0
- data/app/components/better_ui/application/toast_component.html.erb +35 -0
- data/app/components/better_ui/application/toast_component.rb +188 -0
- data/app/components/better_ui/general/breadcrumb_component.html.erb +39 -0
- data/app/components/better_ui/general/breadcrumb_component.rb +132 -0
- data/app/components/better_ui/general/button_component.html.erb +34 -0
- data/app/components/better_ui/general/button_component.rb +193 -0
- data/app/components/better_ui/general/heading_component.html.erb +25 -0
- data/app/components/better_ui/general/heading_component.rb +142 -0
- data/app/components/better_ui/general/icon_component.html.erb +2 -0
- data/app/components/better_ui/general/icon_component.rb +101 -0
- data/app/components/better_ui/general/panel_component.html.erb +27 -0
- data/app/components/better_ui/general/panel_component.rb +97 -0
- data/app/components/better_ui/general/table_component.html.erb +37 -0
- data/app/components/better_ui/general/table_component.rb +141 -0
- data/app/components/better_ui/theme_helper.rb +169 -0
- data/app/controllers/better_ui/application_controller.rb +1 -0
- data/app/controllers/better_ui/docs_controller.rb +18 -25
- data/app/helpers/better_ui_application_helper.rb +99 -0
- data/app/views/layouts/component_preview.html.erb +32 -0
- data/config/initializers/lookbook.rb +23 -0
- data/config/routes.rb +6 -1
- data/lib/better_ui/engine.rb +24 -1
- data/lib/better_ui/version.rb +1 -1
- metadata +103 -7
- data/app/helpers/better_ui/application_helper.rb +0 -183
- data/app/views/better_ui/docs/component.html.erb +0 -365
- data/app/views/better_ui/docs/index.html.erb +0 -100
- data/app/views/better_ui/docs/show.html.erb +0 -60
- data/app/views/layouts/better_ui/application.html.erb +0 -135
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2242792aeee217a121cb73ec14ccc317d8f9c7af9e3dcdcf52629d05eca55731
|
4
|
+
data.tar.gz: 86015104cb387605208eaa8f9ffbdeb3764542fd089e09cf2dd011cecc37640b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df1b73b3f9c3782a562ef7b585fd9bbac50c45fc8cc9661fe6ff8aa335c53f988c65ce4ab430c7c6dc82d74419b9dd80e026ad50c4533742b7940153fba2e653
|
7
|
+
data.tar.gz: 1010406d6a425b0ca48e1acf0af262634a7f5babf54edd34dfd6c7b6943dbdb1a18c250d800f68b9e41ec7bc94d095a8ed9d3e3ca7efb540bbb3ddbe4d310105
|
data/README.md
CHANGED
@@ -14,6 +14,7 @@ BetterUI è una gemma Rails che fornisce componenti UI riutilizzabili con docume
|
|
14
14
|
- **Documentazione integrata** - Visualizza esempi e documentazione direttamente nel browser
|
15
15
|
- **Altamente personalizzabile** - Adatta facilmente lo stile al tuo brand
|
16
16
|
- **Componenti modulari** - Usa solo ciò di cui hai bisogno
|
17
|
+
- **Preview dei componenti** - Visualizza e interagisci con i componenti usando Lookbook
|
17
18
|
|
18
19
|
## 📦 Componenti disponibili
|
19
20
|
|
@@ -24,6 +25,10 @@ BetterUI è una gemma Rails che fornisce componenti UI riutilizzabili con docume
|
|
24
25
|
| **Card** | Contenitori flessibili con header, body e footer |
|
25
26
|
| **Modal** | Finestre di dialogo modali |
|
26
27
|
| **Tabs** | Navigazione a schede |
|
28
|
+
| **Navbar** | Barra di navigazione responsive con supporto per menu dropdown |
|
29
|
+
| **Sidebar** | Menu laterale con supporto per navigazione gerarchica |
|
30
|
+
| **Toast** | Notifiche temporanee con timer e animazioni |
|
31
|
+
| **Header** | Intestazioni di pagina con titolo, sottotitolo, breadcrumb e azioni |
|
27
32
|
| **Form Elements** | Campi di input stilizzati (in arrivo) |
|
28
33
|
|
29
34
|
## 🚀 Installazione
|
@@ -82,11 +87,70 @@ Una volta installato, puoi iniziare ad usare i componenti:
|
|
82
87
|
<p>Contenuto della card...</p>
|
83
88
|
<% end %>
|
84
89
|
<% end %>
|
90
|
+
|
91
|
+
<%# Header con breadcrumb %>
|
92
|
+
<%= render BetterUi::Application::HeaderComponent.new(
|
93
|
+
title: "Dashboard",
|
94
|
+
subtitle: "Gestisci tutto da qui",
|
95
|
+
breadcrumbs: [
|
96
|
+
{ text: "Home", url: "/" },
|
97
|
+
{ text: "Dashboard" }
|
98
|
+
],
|
99
|
+
actions: [
|
100
|
+
{ content: button_html("Nuovo", "primary") }
|
101
|
+
]
|
102
|
+
) %>
|
85
103
|
```
|
86
104
|
|
87
105
|
Visita `/better_ui` nella tua applicazione per vedere la documentazione completa e gli esempi.
|
88
106
|
|
89
|
-
|
107
|
+
### Il componente Header
|
108
|
+
|
109
|
+
Il componente Header è progettato per creare intestazioni di pagina complete con numerose funzionalità:
|
110
|
+
|
111
|
+
```erb
|
112
|
+
<%= render BetterUi::Application::HeaderComponent.new(
|
113
|
+
title: {
|
114
|
+
text: "Impostazioni",
|
115
|
+
icon: "settings"
|
116
|
+
},
|
117
|
+
subtitle: "Configura le preferenze del sistema",
|
118
|
+
breadcrumbs: [
|
119
|
+
{ text: "Home", url: "/" },
|
120
|
+
{ text: "Admin", url: "/admin" },
|
121
|
+
{ text: "Impostazioni" }
|
122
|
+
],
|
123
|
+
variant: :modern,
|
124
|
+
fixed: :top,
|
125
|
+
show_breadcrumbs: true,
|
126
|
+
actions: [
|
127
|
+
{ content: button_html("Salva", "primary") },
|
128
|
+
{ content: button_html("Annulla", "secondary") }
|
129
|
+
]
|
130
|
+
) %>
|
131
|
+
```
|
132
|
+
|
133
|
+
Il componente supporta:
|
134
|
+
- Titolo con opzionale icona integrata
|
135
|
+
- Sottotitolo descrittivo
|
136
|
+
- Breadcrumbs completi con link di navigazione
|
137
|
+
- Azioni contestuali (pulsanti, menu, ecc.)
|
138
|
+
- Varianti di stile: modern, light, dark, primary, transparent
|
139
|
+
- Posizionamento fisso (in alto o in basso)
|
140
|
+
- Controllo della visibilità dei breadcrumb
|
141
|
+
|
142
|
+
### Preview dei componenti con Lookbook
|
143
|
+
|
144
|
+
BetterUI integra [Lookbook](https://github.com/allmarkedup/lookbook) per visualizzare in anteprima i componenti UI:
|
145
|
+
|
146
|
+
1. Accedi a `/better_ui/lookbook` nella tua applicazione per visualizzare il catalogo componenti
|
147
|
+
2. Esplora le varianti e le configurazioni disponibili per ogni componente
|
148
|
+
3. Visualizza il codice sorgente e il markup generato
|
149
|
+
4. Interagisci con i componenti in tempo reale
|
150
|
+
|
151
|
+
Lookbook è disponibile solo negli ambienti di sviluppo e test.
|
152
|
+
|
153
|
+
## 🎮 Personalizzazione
|
90
154
|
|
91
155
|
### Usa l'inizializzatore
|
92
156
|
|
@@ -0,0 +1,138 @@
|
|
1
|
+
// Navbar controller per gestire il comportamento del menu mobile e dei dropdown
|
2
|
+
import { Controller } from "@hotwired/stimulus"
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["menu", "dropdown", "submenu"]
|
6
|
+
|
7
|
+
connect() {
|
8
|
+
// Verifica se siamo su mobile e aggiorna lo stato del menu
|
9
|
+
this.updateMenuState();
|
10
|
+
|
11
|
+
// Aggiungi un event listener per il resize della finestra
|
12
|
+
window.addEventListener("resize", this.updateMenuState.bind(this));
|
13
|
+
|
14
|
+
// Chiudi menu quando si clicca su un link (solo su mobile)
|
15
|
+
if (this.hasMenuTarget) {
|
16
|
+
const links = this.menuTarget.querySelectorAll("a");
|
17
|
+
links.forEach(link => {
|
18
|
+
link.addEventListener("click", () => {
|
19
|
+
// Se siamo su mobile, chiudi il menu
|
20
|
+
if (window.innerWidth < 768) {
|
21
|
+
this.closeMenu();
|
22
|
+
}
|
23
|
+
});
|
24
|
+
});
|
25
|
+
}
|
26
|
+
|
27
|
+
// Aggiungi listener per i click all'esterno del menu
|
28
|
+
document.addEventListener("click", this.handleClickOutside.bind(this))
|
29
|
+
}
|
30
|
+
|
31
|
+
disconnect() {
|
32
|
+
// Rimuovi event listener per il resize della finestra
|
33
|
+
window.removeEventListener("resize", this.updateMenuState.bind(this));
|
34
|
+
|
35
|
+
// Rimuovi listener al disconnette
|
36
|
+
document.removeEventListener("click", this.handleClickOutside.bind(this))
|
37
|
+
}
|
38
|
+
|
39
|
+
// Metodo per alternare l'apertura/chiusura del menu
|
40
|
+
toggleMenu(event) {
|
41
|
+
event.stopPropagation()
|
42
|
+
const isExpanded = this.menuTarget.classList.contains("hidden") === false
|
43
|
+
|
44
|
+
if (isExpanded) {
|
45
|
+
this.closeMenu()
|
46
|
+
} else {
|
47
|
+
this.openMenu()
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
// Metodo per chiudere il menu
|
52
|
+
closeMenu() {
|
53
|
+
this.menuTarget.classList.add("hidden")
|
54
|
+
|
55
|
+
// Aggiorna l'attributo aria-expanded
|
56
|
+
const button = this.element.querySelector("[aria-controls='navbar-menu']")
|
57
|
+
if (button) {
|
58
|
+
button.setAttribute("aria-expanded", "false")
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
// Metodo per aggiornare lo stato del menu in base alla dimensione della finestra
|
63
|
+
updateMenuState() {
|
64
|
+
if (this.hasMenuTarget) {
|
65
|
+
// Se siamo su desktop (md breakpoint - 768px)
|
66
|
+
if (window.innerWidth >= 768) {
|
67
|
+
// Assicurati che il menu sia visibile su desktop
|
68
|
+
this.menuTarget.classList.remove("hidden");
|
69
|
+
this.menuTarget.classList.add("md:block");
|
70
|
+
} else {
|
71
|
+
// Su mobile, nascondi il menu di default
|
72
|
+
this.menuTarget.classList.add("hidden");
|
73
|
+
}
|
74
|
+
|
75
|
+
// Aggiorna l'attributo aria-expanded
|
76
|
+
const button = this.element.querySelector("[aria-controls='navbar-menu']");
|
77
|
+
if (button) {
|
78
|
+
const isExpanded = window.innerWidth >= 768 ? "true" : "false";
|
79
|
+
button.setAttribute("aria-expanded", isExpanded);
|
80
|
+
}
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
openMenu() {
|
85
|
+
this.menuTarget.classList.remove("hidden")
|
86
|
+
|
87
|
+
// Chiudi tutti i dropdown nel menu mobile
|
88
|
+
if (this.hasSubmenuTarget) {
|
89
|
+
this.submenuTargets.forEach(submenu => {
|
90
|
+
submenu.classList.add("hidden")
|
91
|
+
})
|
92
|
+
}
|
93
|
+
|
94
|
+
// Aggiorna stato del pulsante
|
95
|
+
event.currentTarget.setAttribute("aria-expanded", "true")
|
96
|
+
}
|
97
|
+
|
98
|
+
toggleDropdown(event) {
|
99
|
+
event.stopPropagation()
|
100
|
+
const button = event.currentTarget
|
101
|
+
const dropdownId = button.getAttribute("aria-controls")
|
102
|
+
const dropdown = document.getElementById(dropdownId)
|
103
|
+
|
104
|
+
if (dropdown) {
|
105
|
+
const isExpanded = button.getAttribute("aria-expanded") === "true"
|
106
|
+
|
107
|
+
if (isExpanded) {
|
108
|
+
dropdown.classList.add("hidden")
|
109
|
+
button.setAttribute("aria-expanded", "false")
|
110
|
+
} else {
|
111
|
+
// Chiudi tutti gli altri dropdown prima di aprire quello corrente
|
112
|
+
if (this.hasSubmenuTarget) {
|
113
|
+
this.submenuTargets.forEach(submenu => {
|
114
|
+
if (submenu.id !== dropdownId) {
|
115
|
+
submenu.classList.add("hidden")
|
116
|
+
|
117
|
+
// Trova e aggiorna il pulsante associato
|
118
|
+
const associatedButton = this.element.querySelector(`[aria-controls='${submenu.id}']`)
|
119
|
+
if (associatedButton) {
|
120
|
+
associatedButton.setAttribute("aria-expanded", "false")
|
121
|
+
}
|
122
|
+
}
|
123
|
+
})
|
124
|
+
}
|
125
|
+
|
126
|
+
dropdown.classList.remove("hidden")
|
127
|
+
button.setAttribute("aria-expanded", "true")
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
handleClickOutside(event) {
|
133
|
+
// Chiudi il menu se si fa clic all'esterno
|
134
|
+
if (this.element.contains(event.target) === false) {
|
135
|
+
this.closeMenu()
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
@@ -0,0 +1,211 @@
|
|
1
|
+
// Sidebar controller per gestire il comportamento del menu laterale
|
2
|
+
import { Controller } from "@hotwired/stimulus"
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["container", "overlay", "toggleButton", "toggleIcon", "dropdown", "submenu", "chevron"]
|
6
|
+
static values = {
|
7
|
+
collapsed: { type: Boolean, default: false },
|
8
|
+
position: { type: String, default: "left" },
|
9
|
+
overlayOnMobile: { type: Boolean, default: true }
|
10
|
+
}
|
11
|
+
|
12
|
+
connect() {
|
13
|
+
// Applica lo stato iniziale
|
14
|
+
if (this.collapsedValue) {
|
15
|
+
this.collapse();
|
16
|
+
} else {
|
17
|
+
// Assicuriamo che la sidebar sia espansa all'inizio
|
18
|
+
this.containerTarget.style.transform = "translateX(0)";
|
19
|
+
}
|
20
|
+
|
21
|
+
// Imposta i listener per il ridimensionamento della finestra
|
22
|
+
window.addEventListener("resize", this.handleResize.bind(this));
|
23
|
+
|
24
|
+
// Gestisci lo stato iniziale in base alla dimensione della finestra
|
25
|
+
this.handleResize();
|
26
|
+
|
27
|
+
// Trova e apri i sottomenu con elementi attivi
|
28
|
+
this.openSubmenuWithActiveItems();
|
29
|
+
}
|
30
|
+
|
31
|
+
disconnect() {
|
32
|
+
// Rimuovi i listener all'uscita
|
33
|
+
window.removeEventListener("resize", this.handleResize.bind(this));
|
34
|
+
}
|
35
|
+
|
36
|
+
// Toggle dell'intera sidebar
|
37
|
+
toggle() {
|
38
|
+
if (this.isCollapsed()) {
|
39
|
+
this.expand();
|
40
|
+
} else {
|
41
|
+
this.collapse();
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
// Espandi la sidebar
|
46
|
+
expand() {
|
47
|
+
// Mostra la sidebar completa
|
48
|
+
this.containerTarget.classList.remove("transform-translate");
|
49
|
+
this.containerTarget.style.transform = "translateX(0)";
|
50
|
+
|
51
|
+
// Ruota l'icona del toggle button
|
52
|
+
if (this.hasToggleIconTarget) {
|
53
|
+
if (this.positionValue === "left") {
|
54
|
+
this.toggleIconTarget.style.transform = "rotate(0deg)";
|
55
|
+
} else {
|
56
|
+
this.toggleIconTarget.style.transform = "rotate(0deg)";
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
// Aggiorna lo stato
|
61
|
+
this.collapsedValue = false;
|
62
|
+
}
|
63
|
+
|
64
|
+
// Contrai la sidebar
|
65
|
+
collapse() {
|
66
|
+
const width = this.containerTarget.offsetWidth;
|
67
|
+
// Usa un valore più piccolo per mantenere visibile una parte della sidebar
|
68
|
+
const translateValue = this.positionValue === "left" ? `-${width - 10}px` : `${width - 10}px`;
|
69
|
+
|
70
|
+
// Nascondi la sidebar
|
71
|
+
this.containerTarget.classList.add("transform-translate");
|
72
|
+
this.containerTarget.style.transform = `translateX(${translateValue})`;
|
73
|
+
|
74
|
+
// Ruota l'icona del toggle button
|
75
|
+
if (this.hasToggleIconTarget) {
|
76
|
+
if (this.positionValue === "left") {
|
77
|
+
this.toggleIconTarget.style.transform = "rotate(180deg)";
|
78
|
+
} else {
|
79
|
+
this.toggleIconTarget.style.transform = "rotate(180deg)";
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
// Aggiorna lo stato
|
84
|
+
this.collapsedValue = true;
|
85
|
+
}
|
86
|
+
|
87
|
+
// Apri la sidebar
|
88
|
+
open() {
|
89
|
+
// Mostra la sidebar
|
90
|
+
this.containerTarget.style.transform = "translateX(0)";
|
91
|
+
|
92
|
+
// Mostra l'overlay se necessario
|
93
|
+
if (this.overlayOnMobileValue && window.innerWidth < 768) {
|
94
|
+
this.overlayTarget.classList.remove("hidden");
|
95
|
+
this.overlayTarget.classList.add("opacity-100");
|
96
|
+
document.body.classList.add("overflow-hidden");
|
97
|
+
}
|
98
|
+
|
99
|
+
// Aggiorna lo stato
|
100
|
+
this.collapsedValue = false;
|
101
|
+
}
|
102
|
+
|
103
|
+
// Chiudi la sidebar
|
104
|
+
close() {
|
105
|
+
// Se è già contratta su desktop, non fare nulla
|
106
|
+
if (this.collapsedValue && window.innerWidth >= 768) {
|
107
|
+
return;
|
108
|
+
}
|
109
|
+
|
110
|
+
// Su mobile, nascondi completamente
|
111
|
+
if (window.innerWidth < 768) {
|
112
|
+
const width = this.containerTarget.offsetWidth;
|
113
|
+
const translateValue = this.positionValue === "left" ? `-${width}px` : `${width}px`;
|
114
|
+
this.containerTarget.style.transform = `translateX(${translateValue})`;
|
115
|
+
|
116
|
+
// Nascondi l'overlay
|
117
|
+
this.overlayTarget.classList.add("hidden");
|
118
|
+
this.overlayTarget.classList.remove("opacity-100");
|
119
|
+
document.body.classList.remove("overflow-hidden");
|
120
|
+
} else {
|
121
|
+
// Su desktop, contrai
|
122
|
+
this.collapse();
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
// Verifica se la sidebar è contratta
|
127
|
+
isCollapsed() {
|
128
|
+
return this.collapsedValue;
|
129
|
+
}
|
130
|
+
|
131
|
+
// Handler per il ridimensionamento della finestra
|
132
|
+
handleResize() {
|
133
|
+
// Su mobile, mostra l'overlay se la sidebar è aperta
|
134
|
+
if (window.innerWidth < 768) {
|
135
|
+
const isHidden = this.containerTarget.style.transform.includes("translateX");
|
136
|
+
|
137
|
+
if (!isHidden && this.overlayOnMobileValue) {
|
138
|
+
this.overlayTarget.classList.remove("hidden");
|
139
|
+
} else {
|
140
|
+
this.overlayTarget.classList.add("hidden");
|
141
|
+
}
|
142
|
+
} else {
|
143
|
+
// Su desktop, nascondi l'overlay
|
144
|
+
this.overlayTarget.classList.add("hidden");
|
145
|
+
document.body.classList.remove("overflow-hidden");
|
146
|
+
|
147
|
+
// Ripristina lo stato della sidebar in base al valore di collapsed
|
148
|
+
if (this.collapsedValue) {
|
149
|
+
this.collapse();
|
150
|
+
} else {
|
151
|
+
this.expand();
|
152
|
+
}
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
// Toggle di un sottomenu
|
157
|
+
toggleSubmenu(event) {
|
158
|
+
const button = event.currentTarget;
|
159
|
+
const submenuId = button.getAttribute("aria-controls");
|
160
|
+
const submenu = document.getElementById(submenuId);
|
161
|
+
const chevron = button.querySelector("[data-sidebar-target='chevron']");
|
162
|
+
|
163
|
+
if (submenu) {
|
164
|
+
const isExpanded = button.getAttribute("aria-expanded") === "true";
|
165
|
+
|
166
|
+
if (isExpanded) {
|
167
|
+
// Chiudi il sottomenu
|
168
|
+
submenu.classList.add("hidden");
|
169
|
+
button.setAttribute("aria-expanded", "false");
|
170
|
+
if (chevron) {
|
171
|
+
chevron.querySelector("svg").style.transform = "rotate(0deg)";
|
172
|
+
}
|
173
|
+
} else {
|
174
|
+
// Apri il sottomenu
|
175
|
+
submenu.classList.remove("hidden");
|
176
|
+
button.setAttribute("aria-expanded", "true");
|
177
|
+
if (chevron) {
|
178
|
+
chevron.querySelector("svg").style.transform = "rotate(180deg)";
|
179
|
+
}
|
180
|
+
}
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
// Apri automaticamente i sottomenu che contengono elementi attivi
|
185
|
+
openSubmenuWithActiveItems() {
|
186
|
+
// Trova tutti i dropdown
|
187
|
+
if (this.hasDropdownTarget) {
|
188
|
+
this.dropdownTargets.forEach(dropdown => {
|
189
|
+
const submenuId = dropdown.getAttribute("aria-controls");
|
190
|
+
const submenu = document.getElementById(submenuId);
|
191
|
+
|
192
|
+
if (submenu) {
|
193
|
+
// Verifica se il sottomenu contiene elementi attivi
|
194
|
+
const activeItems = submenu.querySelectorAll(".bg-gray-100, .bg-gray-700, .bg-orange-700, .bg-blue-700");
|
195
|
+
|
196
|
+
if (activeItems.length > 0) {
|
197
|
+
// Apri il sottomenu
|
198
|
+
submenu.classList.remove("hidden");
|
199
|
+
dropdown.setAttribute("aria-expanded", "true");
|
200
|
+
|
201
|
+
// Ruota l'icona chevron se presente
|
202
|
+
const chevron = dropdown.querySelector("[data-sidebar-target='chevron']");
|
203
|
+
if (chevron) {
|
204
|
+
chevron.querySelector("svg").style.transform = "rotate(180deg)";
|
205
|
+
}
|
206
|
+
}
|
207
|
+
}
|
208
|
+
});
|
209
|
+
}
|
210
|
+
}
|
211
|
+
}
|
@@ -0,0 +1,161 @@
|
|
1
|
+
// Toast controller per gestire il comportamento delle notifiche toast
|
2
|
+
import { Controller } from "@hotwired/stimulus"
|
3
|
+
import ToastManager from "../toast_manager"
|
4
|
+
|
5
|
+
export default class extends Controller {
|
6
|
+
static targets = ["progressBar"]
|
7
|
+
static values = {
|
8
|
+
duration: { type: Number, default: 5000 },
|
9
|
+
autoHide: { type: Boolean, default: true },
|
10
|
+
position: { type: String, default: "top-right" }
|
11
|
+
}
|
12
|
+
|
13
|
+
connect() {
|
14
|
+
// Estrai la posizione dalle classi del toast o utilizza il valore predefinito
|
15
|
+
this.position = this.getPositionFromClasses() || this.positionValue;
|
16
|
+
|
17
|
+
// Rimuovi le classi di posizione originali per evitare conflitti
|
18
|
+
this.removePositionClasses();
|
19
|
+
|
20
|
+
// Registra il toast con il manager e ottieni la funzione di pulizia
|
21
|
+
this.cleanupFunction = ToastManager.registerToast(this.element, this.position);
|
22
|
+
|
23
|
+
// Applica l'animazione di entrata
|
24
|
+
this.element.classList.add("opacity-0", "translate-y-2");
|
25
|
+
|
26
|
+
// Permetti al DOM di essere aggiornato prima di mostrare
|
27
|
+
setTimeout(() => {
|
28
|
+
this.element.classList.remove("opacity-0", "translate-y-2");
|
29
|
+
this.element.classList.add("opacity-100", "translate-y-0");
|
30
|
+
}, 10);
|
31
|
+
|
32
|
+
// Se auto-hide è attivato, imposta il timer
|
33
|
+
if (this.autoHideValue) {
|
34
|
+
this.startProgressBar();
|
35
|
+
this.hideTimeout = setTimeout(() => {
|
36
|
+
this.hide();
|
37
|
+
}, this.durationValue);
|
38
|
+
}
|
39
|
+
|
40
|
+
// Aggiungi eventi per pausa/ripresa dell'auto-hide
|
41
|
+
this.element.addEventListener("mouseenter", this.mouseEnter.bind(this));
|
42
|
+
this.element.addEventListener("mouseleave", this.mouseLeave.bind(this));
|
43
|
+
}
|
44
|
+
|
45
|
+
// Estrae la posizione dalle classi del toast
|
46
|
+
getPositionFromClasses() {
|
47
|
+
const positionClasses = [
|
48
|
+
"top-right", "top-left", "bottom-right", "bottom-left", "top-center", "bottom-center"
|
49
|
+
];
|
50
|
+
|
51
|
+
for (const position of positionClasses) {
|
52
|
+
if (this.element.classList.contains(position)) {
|
53
|
+
return position;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
return null;
|
58
|
+
}
|
59
|
+
|
60
|
+
// Rimuove le classi di posizione originali
|
61
|
+
removePositionClasses() {
|
62
|
+
const positionClasses = [
|
63
|
+
"top-4", "right-4", "bottom-4", "left-4", "left-1/2", "transform", "-translate-x-1/2"
|
64
|
+
];
|
65
|
+
|
66
|
+
positionClasses.forEach(cls => {
|
67
|
+
this.element.classList.remove(cls);
|
68
|
+
});
|
69
|
+
}
|
70
|
+
|
71
|
+
startProgressBar() {
|
72
|
+
if (this.hasProgressBarTarget) {
|
73
|
+
this.progressBarTarget.style.width = "100%";
|
74
|
+
|
75
|
+
// Imposta una transizione che dura quanto il timeout
|
76
|
+
this.progressBarTarget.style.transition = `width ${this.durationValue}ms linear`;
|
77
|
+
|
78
|
+
// Forza un reflow per assicurare che la transizione inizi
|
79
|
+
void this.progressBarTarget.offsetWidth;
|
80
|
+
|
81
|
+
// Inizia la transizione
|
82
|
+
this.progressBarTarget.style.width = "0%";
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
disconnect() {
|
87
|
+
if (this.hideTimeout) {
|
88
|
+
clearTimeout(this.hideTimeout);
|
89
|
+
}
|
90
|
+
|
91
|
+
// Rimuovi gli event listener
|
92
|
+
this.element.removeEventListener("mouseenter", this.mouseEnter.bind(this));
|
93
|
+
this.element.removeEventListener("mouseleave", this.mouseLeave.bind(this));
|
94
|
+
|
95
|
+
// Esegui la funzione di pulizia per rimuovere il toast dal manager
|
96
|
+
if (this.cleanupFunction) {
|
97
|
+
this.cleanupFunction();
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
hide(event) {
|
102
|
+
if (event) {
|
103
|
+
event.preventDefault();
|
104
|
+
}
|
105
|
+
|
106
|
+
// Applica l'animazione di uscita
|
107
|
+
this.element.classList.remove("opacity-100", "translate-y-0");
|
108
|
+
this.element.classList.add("opacity-0", "translate-y-2");
|
109
|
+
|
110
|
+
// Rimuovi l'elemento dopo l'animazione
|
111
|
+
setTimeout(() => {
|
112
|
+
this.element.remove();
|
113
|
+
}, 300);
|
114
|
+
}
|
115
|
+
|
116
|
+
// Metodo per pausa e ripresa dell'autoHide quando l'utente interagisce con il toast
|
117
|
+
pauseAutoHide() {
|
118
|
+
if (this.hideTimeout) {
|
119
|
+
clearTimeout(this.hideTimeout);
|
120
|
+
|
121
|
+
if (this.hasProgressBarTarget) {
|
122
|
+
// Salva la larghezza corrente per riprendere da lì
|
123
|
+
const computedStyle = window.getComputedStyle(this.progressBarTarget);
|
124
|
+
this.pausedWidth = computedStyle.width;
|
125
|
+
this.progressBarTarget.style.transition = "none";
|
126
|
+
this.progressBarTarget.style.width = this.pausedWidth;
|
127
|
+
}
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
resumeAutoHide() {
|
132
|
+
if (this.autoHideValue) {
|
133
|
+
// Calcola il tempo rimanente basato sulla larghezza della barra di progresso
|
134
|
+
let remainingTime = this.durationValue;
|
135
|
+
|
136
|
+
if (this.hasProgressBarTarget && this.pausedWidth) {
|
137
|
+
const percentage = parseFloat(this.pausedWidth) / parseFloat(window.getComputedStyle(this.progressBarTarget.parentElement).width);
|
138
|
+
remainingTime = this.durationValue * percentage;
|
139
|
+
}
|
140
|
+
|
141
|
+
this.hideTimeout = setTimeout(() => {
|
142
|
+
this.hide();
|
143
|
+
}, remainingTime);
|
144
|
+
|
145
|
+
if (this.hasProgressBarTarget && this.pausedWidth) {
|
146
|
+
this.progressBarTarget.style.transition = `width ${remainingTime}ms linear`;
|
147
|
+
void this.progressBarTarget.offsetWidth;
|
148
|
+
this.progressBarTarget.style.width = "0%";
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
// Gestori eventi per pausa/ripresa dell'autoHide
|
154
|
+
mouseEnter() {
|
155
|
+
this.pauseAutoHide();
|
156
|
+
}
|
157
|
+
|
158
|
+
mouseLeave() {
|
159
|
+
this.resumeAutoHide();
|
160
|
+
}
|
161
|
+
}
|