better_ui 0.1.0 → 0.5.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 +199 -75
- 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 +202 -0
- data/app/components/better_ui/application/card_component.html.erb +24 -0
- data/app/components/better_ui/application/card_component.rb +53 -0
- data/app/components/better_ui/application/card_container_component.html.erb +8 -0
- data/app/components/better_ui/application/card_container_component.rb +14 -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 +223 -0
- data/app/components/better_ui/general/avatar_component.html.erb +19 -0
- data/app/components/better_ui/general/avatar_component.rb +171 -0
- data/app/components/better_ui/general/badge_component.html.erb +19 -0
- data/app/components/better_ui/general/badge_component.rb +123 -0
- data/app/components/better_ui/general/breadcrumb_component.html.erb +15 -0
- data/app/components/better_ui/general/breadcrumb_component.rb +130 -0
- data/app/components/better_ui/general/button_component.html.erb +34 -0
- data/app/components/better_ui/general/button_component.rb +162 -0
- data/app/components/better_ui/general/heading_component.html.erb +25 -0
- data/app/components/better_ui/general/heading_component.rb +148 -0
- data/app/components/better_ui/general/icon_component.html.erb +2 -0
- data/app/components/better_ui/general/icon_component.rb +100 -0
- data/app/components/better_ui/general/link_component.html.erb +17 -0
- data/app/components/better_ui/general/link_component.rb +132 -0
- data/app/components/better_ui/general/panel_component.html.erb +27 -0
- data/app/components/better_ui/general/panel_component.rb +103 -0
- data/app/components/better_ui/general/spinner_component.html.erb +15 -0
- data/app/components/better_ui/general/spinner_component.rb +79 -0
- data/app/components/better_ui/general/table_component.html.erb +73 -0
- data/app/components/better_ui/general/table_component.rb +167 -0
- data/app/components/better_ui/theme_helper.rb +171 -0
- data/app/controllers/better_ui/application_controller.rb +1 -0
- data/app/controllers/better_ui/docs_controller.rb +18 -25
- data/app/views/components/better_ui/general/table/_custom_body_row.html.erb +17 -0
- data/app/views/components/better_ui/general/table/_custom_footer_rows.html.erb +17 -0
- data/app/views/components/better_ui/general/table/_custom_header_rows.html.erb +12 -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 +18 -1
- data/lib/better_ui/version.rb +1 -1
- data/lib/better_ui.rb +4 -0
- data/lib/generators/better_ui/stylesheet_generator.rb +96 -0
- data/lib/generators/better_ui/templates/README +56 -0
- data/lib/generators/better_ui/templates/components/_avatar.scss +151 -0
- data/lib/generators/better_ui/templates/components/_badge.scss +142 -0
- data/lib/generators/better_ui/templates/components/_breadcrumb.scss +107 -0
- data/lib/generators/better_ui/templates/components/_button.scss +106 -0
- data/lib/generators/better_ui/templates/components/_card.scss +69 -0
- data/lib/generators/better_ui/templates/components/_heading.scss +180 -0
- data/lib/generators/better_ui/templates/components/_icon.scss +90 -0
- data/lib/generators/better_ui/templates/components/_link.scss +130 -0
- data/lib/generators/better_ui/templates/components/_panel.scss +144 -0
- data/lib/generators/better_ui/templates/components/_spinner.scss +132 -0
- data/lib/generators/better_ui/templates/components/_table.scss +105 -0
- data/lib/generators/better_ui/templates/components/_variables.scss +33 -0
- data/lib/generators/better_ui/templates/components_stylesheet.scss +66 -0
- metadata +135 -10
- 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
@@ -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
|
+
}
|
@@ -0,0 +1,159 @@
|
|
1
|
+
// Entry point per tutti i componenti JavaScript di Better UI
|
2
|
+
import { Application } from "@hotwired/stimulus"
|
3
|
+
import ToastController from "./controllers/toast_controller"
|
4
|
+
import NavbarController from "./controllers/navbar_controller"
|
5
|
+
import SidebarController from "./controllers/sidebar_controller"
|
6
|
+
|
7
|
+
// Configura Stimulus
|
8
|
+
const application = Application.start()
|
9
|
+
|
10
|
+
// Registra i controller
|
11
|
+
application.register("toast", ToastController)
|
12
|
+
application.register("navbar", NavbarController)
|
13
|
+
application.register("sidebar", SidebarController)
|
14
|
+
|
15
|
+
// Esporta i controller e altri componenti
|
16
|
+
export { ToastController, NavbarController, SidebarController }
|
17
|
+
export { default as ToastManager } from "./toast_manager"
|
18
|
+
|
19
|
+
// Funzione di utilità per creare e mostrare un toast programmaticamente
|
20
|
+
export function showToast(options = {}) {
|
21
|
+
const {
|
22
|
+
title = null,
|
23
|
+
message = "Notifica",
|
24
|
+
variant = "info",
|
25
|
+
position = "top-right",
|
26
|
+
duration = 5000,
|
27
|
+
dismissible = true,
|
28
|
+
autoHide = true,
|
29
|
+
icon = null
|
30
|
+
} = options;
|
31
|
+
|
32
|
+
// Crea l'elemento toast
|
33
|
+
const toast = document.createElement("div");
|
34
|
+
toast.setAttribute("role", "status");
|
35
|
+
toast.setAttribute("aria-live", "polite");
|
36
|
+
toast.classList.add(
|
37
|
+
"fixed", "z-50", "rounded-lg", "p-4", "border", "shadow-lg",
|
38
|
+
"transform", "transition-transform", "duration-300",
|
39
|
+
"min-w-[20rem]", "max-w-sm", "flex", "items-start",
|
40
|
+
position
|
41
|
+
);
|
42
|
+
|
43
|
+
// Aggiungi classi specifiche per la variante
|
44
|
+
const variantClasses = {
|
45
|
+
primary: ["bg-orange-50", "border-orange-300"],
|
46
|
+
info: ["bg-blue-50", "border-blue-300"],
|
47
|
+
success: ["bg-green-50", "border-green-300"],
|
48
|
+
warning: ["bg-yellow-50", "border-yellow-300"],
|
49
|
+
danger: ["bg-red-50", "border-red-300"],
|
50
|
+
dark: ["bg-gray-800", "border-gray-700"]
|
51
|
+
};
|
52
|
+
|
53
|
+
// Applica le classi della variante
|
54
|
+
const selectedVariant = variantClasses[variant] || variantClasses.info;
|
55
|
+
toast.classList.add(...selectedVariant);
|
56
|
+
|
57
|
+
// Aggiungi attributi per Stimulus
|
58
|
+
toast.setAttribute("data-controller", "toast");
|
59
|
+
toast.setAttribute("data-toast-duration-value", duration);
|
60
|
+
toast.setAttribute("data-toast-auto-hide-value", autoHide);
|
61
|
+
toast.setAttribute("data-toast-position-value", position);
|
62
|
+
|
63
|
+
// Costruisci il contenuto HTML
|
64
|
+
let html = "";
|
65
|
+
|
66
|
+
// Icona
|
67
|
+
const defaultIcons = {
|
68
|
+
primary: "bell",
|
69
|
+
info: "info-circle",
|
70
|
+
success: "check-circle",
|
71
|
+
warning: "exclamation-triangle",
|
72
|
+
danger: "exclamation-circle",
|
73
|
+
dark: "shield-exclamation"
|
74
|
+
};
|
75
|
+
|
76
|
+
const iconName = icon || defaultIcons[variant] || defaultIcons.info;
|
77
|
+
|
78
|
+
if (iconName) {
|
79
|
+
const iconColorClasses = {
|
80
|
+
primary: "text-orange-500",
|
81
|
+
info: "text-blue-500",
|
82
|
+
success: "text-green-500",
|
83
|
+
warning: "text-yellow-500",
|
84
|
+
danger: "text-red-500",
|
85
|
+
dark: "text-gray-400"
|
86
|
+
};
|
87
|
+
|
88
|
+
html += `
|
89
|
+
<div class="flex-shrink-0 mr-3 mt-0.5 ${iconColorClasses[variant] || iconColorClasses.info}">
|
90
|
+
<i class="fas fa-${iconName}"></i>
|
91
|
+
</div>
|
92
|
+
`;
|
93
|
+
}
|
94
|
+
|
95
|
+
// Contenuto
|
96
|
+
const titleColorClasses = {
|
97
|
+
primary: "text-orange-800",
|
98
|
+
info: "text-blue-800",
|
99
|
+
success: "text-green-800",
|
100
|
+
warning: "text-yellow-800",
|
101
|
+
danger: "text-red-800",
|
102
|
+
dark: "text-white"
|
103
|
+
};
|
104
|
+
|
105
|
+
const messageColorClasses = {
|
106
|
+
primary: "text-orange-700",
|
107
|
+
info: "text-blue-700",
|
108
|
+
success: "text-green-700",
|
109
|
+
warning: "text-yellow-700",
|
110
|
+
danger: "text-red-700",
|
111
|
+
dark: "text-gray-300"
|
112
|
+
};
|
113
|
+
|
114
|
+
html += '<div class="flex-1">';
|
115
|
+
|
116
|
+
if (title) {
|
117
|
+
html += `<div class="font-medium ${titleColorClasses[variant] || titleColorClasses.info}">${title}</div>`;
|
118
|
+
}
|
119
|
+
|
120
|
+
if (message) {
|
121
|
+
html += `<div class="mt-1 ${messageColorClasses[variant] || messageColorClasses.info}">${message}</div>`;
|
122
|
+
}
|
123
|
+
|
124
|
+
if (autoHide) {
|
125
|
+
html += `
|
126
|
+
<div class="w-full bg-gray-200 h-1 mt-2 rounded overflow-hidden">
|
127
|
+
<div class="bg-current h-1 transition-all" data-toast-target="progressBar"></div>
|
128
|
+
</div>
|
129
|
+
`;
|
130
|
+
}
|
131
|
+
|
132
|
+
html += '</div>';
|
133
|
+
|
134
|
+
// Pulsante di chiusura
|
135
|
+
if (dismissible) {
|
136
|
+
const closeButtonColorClasses = {
|
137
|
+
primary: "text-orange-500 hover:bg-orange-100",
|
138
|
+
info: "text-blue-500 hover:bg-blue-100",
|
139
|
+
success: "text-green-500 hover:bg-green-100",
|
140
|
+
warning: "text-yellow-500 hover:bg-yellow-100",
|
141
|
+
danger: "text-red-500 hover:bg-red-100",
|
142
|
+
dark: "text-gray-400 hover:bg-gray-700"
|
143
|
+
};
|
144
|
+
|
145
|
+
html += `
|
146
|
+
<button type="button" class="ml-auto -mr-1.5 -mt-1.5 inline-flex h-8 w-8 rounded-lg p-1.5 focus:ring-2 focus:ring-gray-400 ${closeButtonColorClasses[variant] || closeButtonColorClasses.info}" data-action="toast#hide" aria-label="Chiudi">
|
147
|
+
<i class="fas fa-xmark"></i>
|
148
|
+
</button>
|
149
|
+
`;
|
150
|
+
}
|
151
|
+
|
152
|
+
// Inserisci il contenuto
|
153
|
+
toast.innerHTML = html;
|
154
|
+
|
155
|
+
// Aggiungi il toast al documento
|
156
|
+
document.body.appendChild(toast);
|
157
|
+
|
158
|
+
return toast;
|
159
|
+
}
|
@@ -0,0 +1,77 @@
|
|
1
|
+
// Gestore per l'impilamento dei toast in diverse posizioni
|
2
|
+
class ToastManager {
|
3
|
+
constructor() {
|
4
|
+
// Mantiene traccia dei toast attivi per posizione
|
5
|
+
this.toasts = {
|
6
|
+
'top-right': [],
|
7
|
+
'top-left': [],
|
8
|
+
'bottom-right': [],
|
9
|
+
'bottom-left': [],
|
10
|
+
'top-center': [],
|
11
|
+
'bottom-center': []
|
12
|
+
}
|
13
|
+
|
14
|
+
// Offset di base per ogni posizione (in px)
|
15
|
+
this.baseOffset = 16; // equivalente a Tailwind top-4 (16px)
|
16
|
+
|
17
|
+
// Spaziatura tra i toast (in px)
|
18
|
+
this.spacing = 12;
|
19
|
+
}
|
20
|
+
|
21
|
+
// Registra un nuovo toast e calcola la sua posizione
|
22
|
+
registerToast(toast, position) {
|
23
|
+
if (!this.toasts[position]) {
|
24
|
+
console.warn(`Posizione toast non valida: ${position}. Utilizzo 'top-right' come predefinita.`);
|
25
|
+
position = 'top-right';
|
26
|
+
}
|
27
|
+
|
28
|
+
// Aggiungi il toast all'array della posizione specifica
|
29
|
+
this.toasts[position].push(toast);
|
30
|
+
|
31
|
+
// Ricalcola le posizioni per tutti i toast in questa posizione
|
32
|
+
this.updatePositions(position);
|
33
|
+
|
34
|
+
return () => this.removeToast(toast, position);
|
35
|
+
}
|
36
|
+
|
37
|
+
// Rimuove un toast e ricalcola le posizioni
|
38
|
+
removeToast(toast, position) {
|
39
|
+
const index = this.toasts[position].indexOf(toast);
|
40
|
+
if (index !== -1) {
|
41
|
+
this.toasts[position].splice(index, 1);
|
42
|
+
this.updatePositions(position);
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
// Aggiorna le posizioni di tutti i toast in una data posizione
|
47
|
+
updatePositions(position) {
|
48
|
+
const isTopPosition = position.startsWith('top-');
|
49
|
+
const toastArray = this.toasts[position];
|
50
|
+
|
51
|
+
let currentOffset = this.baseOffset;
|
52
|
+
|
53
|
+
toastArray.forEach((toast, index) => {
|
54
|
+
// Applica la posizione corretta
|
55
|
+
if (isTopPosition) {
|
56
|
+
toast.style.top = `${currentOffset}px`;
|
57
|
+
} else {
|
58
|
+
toast.style.bottom = `${currentOffset}px`;
|
59
|
+
}
|
60
|
+
|
61
|
+
// Aggiorna l'offset per il prossimo toast
|
62
|
+
const height = toast.offsetHeight;
|
63
|
+
currentOffset += height + this.spacing;
|
64
|
+
});
|
65
|
+
}
|
66
|
+
|
67
|
+
// Metodo statico per accedere all'istanza singleton
|
68
|
+
static getInstance() {
|
69
|
+
if (!ToastManager.instance) {
|
70
|
+
ToastManager.instance = new ToastManager();
|
71
|
+
}
|
72
|
+
return ToastManager.instance;
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
// Esporta il gestore come singleton
|
77
|
+
export default ToastManager.getInstance();
|