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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +199 -75
  3. data/app/assets/javascripts/better_ui/controllers/navbar_controller.js +138 -0
  4. data/app/assets/javascripts/better_ui/controllers/sidebar_controller.js +211 -0
  5. data/app/assets/javascripts/better_ui/controllers/toast_controller.js +161 -0
  6. data/app/assets/javascripts/better_ui/index.js +159 -0
  7. data/app/assets/javascripts/better_ui/toast_manager.js +77 -0
  8. data/app/assets/stylesheets/better_ui/application.css +25 -351
  9. data/app/components/better_ui/application/alert_component.html.erb +27 -0
  10. data/app/components/better_ui/application/alert_component.rb +202 -0
  11. data/app/components/better_ui/application/card_component.html.erb +24 -0
  12. data/app/components/better_ui/application/card_component.rb +53 -0
  13. data/app/components/better_ui/application/card_container_component.html.erb +8 -0
  14. data/app/components/better_ui/application/card_container_component.rb +14 -0
  15. data/app/components/better_ui/application/header_component.html.erb +88 -0
  16. data/app/components/better_ui/application/header_component.rb +188 -0
  17. data/app/components/better_ui/application/navbar_component.html.erb +294 -0
  18. data/app/components/better_ui/application/navbar_component.rb +249 -0
  19. data/app/components/better_ui/application/sidebar_component.html.erb +207 -0
  20. data/app/components/better_ui/application/sidebar_component.rb +318 -0
  21. data/app/components/better_ui/application/toast_component.html.erb +35 -0
  22. data/app/components/better_ui/application/toast_component.rb +223 -0
  23. data/app/components/better_ui/general/avatar_component.html.erb +19 -0
  24. data/app/components/better_ui/general/avatar_component.rb +171 -0
  25. data/app/components/better_ui/general/badge_component.html.erb +19 -0
  26. data/app/components/better_ui/general/badge_component.rb +123 -0
  27. data/app/components/better_ui/general/breadcrumb_component.html.erb +15 -0
  28. data/app/components/better_ui/general/breadcrumb_component.rb +130 -0
  29. data/app/components/better_ui/general/button_component.html.erb +34 -0
  30. data/app/components/better_ui/general/button_component.rb +162 -0
  31. data/app/components/better_ui/general/heading_component.html.erb +25 -0
  32. data/app/components/better_ui/general/heading_component.rb +148 -0
  33. data/app/components/better_ui/general/icon_component.html.erb +2 -0
  34. data/app/components/better_ui/general/icon_component.rb +100 -0
  35. data/app/components/better_ui/general/link_component.html.erb +17 -0
  36. data/app/components/better_ui/general/link_component.rb +132 -0
  37. data/app/components/better_ui/general/panel_component.html.erb +27 -0
  38. data/app/components/better_ui/general/panel_component.rb +103 -0
  39. data/app/components/better_ui/general/spinner_component.html.erb +15 -0
  40. data/app/components/better_ui/general/spinner_component.rb +79 -0
  41. data/app/components/better_ui/general/table_component.html.erb +73 -0
  42. data/app/components/better_ui/general/table_component.rb +167 -0
  43. data/app/components/better_ui/theme_helper.rb +171 -0
  44. data/app/controllers/better_ui/application_controller.rb +1 -0
  45. data/app/controllers/better_ui/docs_controller.rb +18 -25
  46. data/app/views/components/better_ui/general/table/_custom_body_row.html.erb +17 -0
  47. data/app/views/components/better_ui/general/table/_custom_footer_rows.html.erb +17 -0
  48. data/app/views/components/better_ui/general/table/_custom_header_rows.html.erb +12 -0
  49. data/app/views/layouts/component_preview.html.erb +32 -0
  50. data/config/initializers/lookbook.rb +23 -0
  51. data/config/routes.rb +6 -1
  52. data/lib/better_ui/engine.rb +18 -1
  53. data/lib/better_ui/version.rb +1 -1
  54. data/lib/better_ui.rb +4 -0
  55. data/lib/generators/better_ui/stylesheet_generator.rb +96 -0
  56. data/lib/generators/better_ui/templates/README +56 -0
  57. data/lib/generators/better_ui/templates/components/_avatar.scss +151 -0
  58. data/lib/generators/better_ui/templates/components/_badge.scss +142 -0
  59. data/lib/generators/better_ui/templates/components/_breadcrumb.scss +107 -0
  60. data/lib/generators/better_ui/templates/components/_button.scss +106 -0
  61. data/lib/generators/better_ui/templates/components/_card.scss +69 -0
  62. data/lib/generators/better_ui/templates/components/_heading.scss +180 -0
  63. data/lib/generators/better_ui/templates/components/_icon.scss +90 -0
  64. data/lib/generators/better_ui/templates/components/_link.scss +130 -0
  65. data/lib/generators/better_ui/templates/components/_panel.scss +144 -0
  66. data/lib/generators/better_ui/templates/components/_spinner.scss +132 -0
  67. data/lib/generators/better_ui/templates/components/_table.scss +105 -0
  68. data/lib/generators/better_ui/templates/components/_variables.scss +33 -0
  69. data/lib/generators/better_ui/templates/components_stylesheet.scss +66 -0
  70. metadata +135 -10
  71. data/app/helpers/better_ui/application_helper.rb +0 -183
  72. data/app/views/better_ui/docs/component.html.erb +0 -365
  73. data/app/views/better_ui/docs/index.html.erb +0 -100
  74. data/app/views/better_ui/docs/show.html.erb +0 -60
  75. 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();