fernotify 1.0.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.
- package/LICENSE +21 -0
- package/NOTIFICATION_SYSTEM_GUIDE.md +757 -0
- package/README.md +402 -0
- package/dist/notification-system.esm.js +929 -0
- package/dist/notification-system.esm.min.js +1 -0
- package/dist/notification-system.js +902 -0
- package/dist/notification-system.min.js +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
# Sistema de Notificaciones - Guía de Uso
|
|
2
|
+
|
|
3
|
+
## 🚀 Instalación
|
|
4
|
+
|
|
5
|
+
### CDN (Lo más fácil)
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<!-- ¡Es todo lo que necesitas! Las dependencias se cargan automáticamente -->
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/gh/Fernandocabal/fernotify@latest/dist/notification-system.js"></script>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### ES Module
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
import NotificationSystem from 'https://cdn.jsdelivr.net/gh/Fernandocabal/fernotify@latest/dist/notification-system.esm.js';
|
|
16
|
+
|
|
17
|
+
window.notify = new NotificationSystem();
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> **✅ Dependencias Automáticas:** Se cargan automáticamente anime.js (animaciones) y Boxicons (iconos). No necesitas hacer nada especial.
|
|
21
|
+
|
|
22
|
+
## Características
|
|
23
|
+
|
|
24
|
+
- Estilo moderno y limpio
|
|
25
|
+
- Animaciones fluidas con anime.js (se carga automáticamente)
|
|
26
|
+
- Centrado en pantalla con overlay
|
|
27
|
+
- 4 tipos: success, error, warning, info
|
|
28
|
+
- Completamente reutilizable
|
|
29
|
+
- Auto-cierre opcional
|
|
30
|
+
- Callbacks personalizados
|
|
31
|
+
- **Soporte completo de Dark Mode**
|
|
32
|
+
- Respeta el tema del usuario en tu web
|
|
33
|
+
|
|
34
|
+
## Uso Básico
|
|
35
|
+
|
|
36
|
+
### 1. Notificación Rápida (solo mensaje)
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
// Success
|
|
40
|
+
notify.success('Operación completada exitosamente');
|
|
41
|
+
|
|
42
|
+
// Error
|
|
43
|
+
notify.error('Ocurrió un error inesperado');
|
|
44
|
+
|
|
45
|
+
// Warning
|
|
46
|
+
notify.warning('Esta acción no se puede deshacer');
|
|
47
|
+
|
|
48
|
+
// Info
|
|
49
|
+
notify.info('Hay una nueva actualización disponible');
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Con Título Personalizado
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
notify.success(
|
|
56
|
+
'Tu perfil ha sido actualizado correctamente',
|
|
57
|
+
'¡Cambios Guardados!'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
notify.error(
|
|
61
|
+
'No tienes permisos para realizar esta acción',
|
|
62
|
+
'Acceso Denegado'
|
|
63
|
+
);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Con Opciones Avanzadas
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
notify.success(
|
|
70
|
+
'Tu mensaje ha sido enviado correctamente',
|
|
71
|
+
'¡Enviado!',
|
|
72
|
+
{
|
|
73
|
+
buttonText: 'Entendido',
|
|
74
|
+
timer: 5000, // Auto-cerrar en 5 segundos
|
|
75
|
+
onClose: () => {
|
|
76
|
+
console.log('Notificación cerrada');
|
|
77
|
+
// Hacer algo después de cerrar
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. Configuración Completa
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
notify.show({
|
|
87
|
+
type: 'warning',
|
|
88
|
+
title: 'Sesión por Expirar',
|
|
89
|
+
message: 'Tu sesión expirará en 2 minutos. ¿Deseas continuar?',
|
|
90
|
+
buttonText: 'Renovar Sesión',
|
|
91
|
+
timer: null, // No auto-cerrar
|
|
92
|
+
onClose: () => {
|
|
93
|
+
// Renovar token o redirigir
|
|
94
|
+
renewSession();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Ejemplos de Uso Real
|
|
100
|
+
|
|
101
|
+
### Validación de Formulario
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
const form = document.getElementById('myForm');
|
|
105
|
+
|
|
106
|
+
form.addEventListener('submit', (e) => {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
|
|
109
|
+
const email = document.getElementById('email').value;
|
|
110
|
+
|
|
111
|
+
if (!email) {
|
|
112
|
+
notify.warning('Por favor ingresa tu email');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!isValidEmail(email)) {
|
|
117
|
+
notify.error('El formato del email no es válido', 'Email Inválido');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Enviar formulario...
|
|
122
|
+
notify.success('Formulario enviado correctamente', '¡Éxito!');
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Petición AJAX
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
async function loadData() {
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch('/api/data');
|
|
132
|
+
const data = await response.json();
|
|
133
|
+
|
|
134
|
+
if (data.success) {
|
|
135
|
+
notify.success('Datos cargados correctamente');
|
|
136
|
+
updateUI(data);
|
|
137
|
+
} else {
|
|
138
|
+
notify.error(data.error || 'Error al cargar datos');
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
notify.error(
|
|
142
|
+
'No se pudo conectar con el servidor',
|
|
143
|
+
'Error de Conexión'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Confirmación de Eliminación
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
function deleteItem(id) {
|
|
153
|
+
notify.show({
|
|
154
|
+
type: 'warning',
|
|
155
|
+
title: '¿Estás seguro?',
|
|
156
|
+
message: 'Esta acción no se puede deshacer. El registro será eliminado permanentemente.',
|
|
157
|
+
buttonText: 'Sí, eliminar',
|
|
158
|
+
onClose: async () => {
|
|
159
|
+
// Usuario confirmó, proceder con eliminación
|
|
160
|
+
await performDelete(id);
|
|
161
|
+
notify.success('Registro eliminado correctamente');
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Login Exitoso
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
async function login(email, password) {
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch('/api/login', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
body: JSON.stringify({ email, password })
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const result = await response.json();
|
|
178
|
+
|
|
179
|
+
if (result.success) {
|
|
180
|
+
notify.success(
|
|
181
|
+
`Bienvenido de nuevo, ${result.user.name}`,
|
|
182
|
+
'¡Login Exitoso!',
|
|
183
|
+
{
|
|
184
|
+
timer: 2000,
|
|
185
|
+
onClose: () => {
|
|
186
|
+
window.location.href = '/dashboard';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
} else {
|
|
191
|
+
notify.error(
|
|
192
|
+
'Credenciales incorrectas. Por favor verifica tus datos.',
|
|
193
|
+
'Error de Autenticación'
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
notify.error('Error de conexión', 'Error');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Auto-guardado
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
let autoSaveTimer;
|
|
206
|
+
|
|
207
|
+
function autoSave(content) {
|
|
208
|
+
clearTimeout(autoSaveTimer);
|
|
209
|
+
|
|
210
|
+
autoSaveTimer = setTimeout(async () => {
|
|
211
|
+
try {
|
|
212
|
+
await saveToServer(content);
|
|
213
|
+
notify.info('Cambios guardados automáticamente', null, {
|
|
214
|
+
timer: 2000
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
notify.warning('No se pudo guardar automáticamente');
|
|
218
|
+
}
|
|
219
|
+
}, 3000);
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Cargas Asincrónicas (NEW) 🆕
|
|
224
|
+
|
|
225
|
+
### Usando `notify.loading()` para Operaciones Async
|
|
226
|
+
|
|
227
|
+
El método `notify.loading()` is perfecto para mostrar un spinner mientras se realiza una operación asincrónica:
|
|
228
|
+
|
|
229
|
+
#### Carga de Datos (API)
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
async function fetchUserData(userId) {
|
|
233
|
+
// Mostrar spinner
|
|
234
|
+
notify.loading('Obteniendo datos del usuario...', 'Cargando');
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
238
|
+
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const userData = await response.json();
|
|
244
|
+
|
|
245
|
+
// Cerrar spinner
|
|
246
|
+
notify.closeLoading();
|
|
247
|
+
|
|
248
|
+
// Mostrar resultado exitoso
|
|
249
|
+
notify.success(
|
|
250
|
+
`Usuario ${userData.name} cargado correctamente`,
|
|
251
|
+
'¡Éxito!'
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
return userData;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// Cerrar spinner
|
|
257
|
+
notify.closeLoading();
|
|
258
|
+
|
|
259
|
+
// Mostrar error
|
|
260
|
+
notify.error(
|
|
261
|
+
'No se pudieron cargar los datos del usuario',
|
|
262
|
+
'Error de Conexión'
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
console.error('Error:', error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Uso
|
|
270
|
+
fetchUserData(123);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Subida de Archivo
|
|
274
|
+
|
|
275
|
+
```javascript
|
|
276
|
+
async function uploadFile(file) {
|
|
277
|
+
// Validar archivo
|
|
278
|
+
if (!file) {
|
|
279
|
+
notify.warning('Por favor selecciona un archivo');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Mostrar spinner con mensaje personalizado
|
|
284
|
+
notify.loading(
|
|
285
|
+
'Subiendo tu archivo...',
|
|
286
|
+
`${file.name}`,
|
|
287
|
+
{ timer: null } // No auto-cerrar
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const formData = new FormData();
|
|
292
|
+
formData.append('file', file);
|
|
293
|
+
|
|
294
|
+
const response = await fetch('/api/upload', {
|
|
295
|
+
method: 'POST',
|
|
296
|
+
body: formData
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const result = await response.json();
|
|
300
|
+
|
|
301
|
+
// Cerrar loading y mostrar éxito
|
|
302
|
+
notify.closeLoading();
|
|
303
|
+
notify.success(
|
|
304
|
+
'Archivo subido correctamente',
|
|
305
|
+
'¡Completado!'
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
return result;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
// Cerrar loading y mostrar error
|
|
311
|
+
notify.closeLoading();
|
|
312
|
+
notify.error(
|
|
313
|
+
'Error al subir el archivo. Intenta nuevamente.',
|
|
314
|
+
'Error de Carga'
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Uso con input file
|
|
320
|
+
document.getElementById('fileInput').addEventListener('change', (e) => {
|
|
321
|
+
uploadFile(e.target.files[0]);
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### Descarga de Archivo
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
async function downloadFile(fileId, fileName) {
|
|
329
|
+
// Mostrar spinner de descarga
|
|
330
|
+
notify.loading(
|
|
331
|
+
'Preparando descarga...',
|
|
332
|
+
'Por favor espera'
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const response = await fetch(`/api/files/${fileId}/download`);
|
|
337
|
+
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
throw new Error('Error en la descarga');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const blob = await response.blob();
|
|
343
|
+
|
|
344
|
+
// Crear URL temporal para descargar
|
|
345
|
+
const url = window.URL.createObjectURL(blob);
|
|
346
|
+
const link = document.createElement('a');
|
|
347
|
+
link.href = url;
|
|
348
|
+
link.download = fileName;
|
|
349
|
+
document.body.appendChild(link);
|
|
350
|
+
|
|
351
|
+
// Cerrar loading antes de descargar
|
|
352
|
+
notify.closeLoading();
|
|
353
|
+
|
|
354
|
+
// Iniciar descarga
|
|
355
|
+
link.click();
|
|
356
|
+
|
|
357
|
+
// Limpiar
|
|
358
|
+
document.body.removeChild(link);
|
|
359
|
+
window.URL.revokeObjectURL(url);
|
|
360
|
+
|
|
361
|
+
// Confirmación
|
|
362
|
+
notify.success('Archivo descargado correctamente');
|
|
363
|
+
} catch (error) {
|
|
364
|
+
notify.closeLoading();
|
|
365
|
+
notify.error('Error al descargar el archivo');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### Procesamiento de Formulario
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
async function processForm(formData) {
|
|
374
|
+
// Validar datos
|
|
375
|
+
if (!formData.email || !formData.message) {
|
|
376
|
+
notify.warning('Por favor completa todos los campos');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Mostrar spinner
|
|
381
|
+
notify.loading('Procesando tu solicitud...', 'Enviando');
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const response = await fetch('/api/contact', {
|
|
385
|
+
method: 'POST',
|
|
386
|
+
headers: {
|
|
387
|
+
'Content-Type': 'application/json',
|
|
388
|
+
},
|
|
389
|
+
body: JSON.stringify(formData)
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
throw new Error('Error en el servidor');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const result = await response.json();
|
|
397
|
+
|
|
398
|
+
// Cerrar loading
|
|
399
|
+
notify.closeLoading();
|
|
400
|
+
|
|
401
|
+
// Mostrar éxito y limpiar formulario
|
|
402
|
+
notify.success(
|
|
403
|
+
'Tu solicitud ha sido enviada correctamente',
|
|
404
|
+
'¡Gracias!',
|
|
405
|
+
{
|
|
406
|
+
timer: 3000,
|
|
407
|
+
onClose: () => {
|
|
408
|
+
document.getElementById('contactForm').reset();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
notify.closeLoading();
|
|
414
|
+
notify.error(
|
|
415
|
+
'Hubo un error al procesar tu solicitud',
|
|
416
|
+
'Error'
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Uso
|
|
422
|
+
document.getElementById('contactForm').addEventListener('submit', (e) => {
|
|
423
|
+
e.preventDefault();
|
|
424
|
+
|
|
425
|
+
processForm({
|
|
426
|
+
email: document.getElementById('email').value,
|
|
427
|
+
message: document.getElementById('message').value
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### Auto-guardado Mejorado
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
let autoSaveTimer;
|
|
436
|
+
let isSaving = false;
|
|
437
|
+
|
|
438
|
+
async function autoSaveDocument(content, documentId) {
|
|
439
|
+
// Evitar guardar múltiples veces simultáneamente
|
|
440
|
+
if (isSaving) return;
|
|
441
|
+
|
|
442
|
+
clearTimeout(autoSaveTimer);
|
|
443
|
+
|
|
444
|
+
autoSaveTimer = setTimeout(async () => {
|
|
445
|
+
isSaving = true;
|
|
446
|
+
|
|
447
|
+
// Mostrar spinner silencioso (sin mensaje, solo indicador visual)
|
|
448
|
+
notify.loading('Guardando cambios...', null, {
|
|
449
|
+
buttonText: 'OK',
|
|
450
|
+
timer: null
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const response = await fetch(`/api/documents/${documentId}`, {
|
|
455
|
+
method: 'PUT',
|
|
456
|
+
headers: {
|
|
457
|
+
'Content-Type': 'application/json',
|
|
458
|
+
},
|
|
459
|
+
body: JSON.stringify({ content })
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
throw new Error('Error al guardar');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Cerrar loading silenciosamente
|
|
467
|
+
notify.closeLoading();
|
|
468
|
+
|
|
469
|
+
// Mostrar confirmación breve
|
|
470
|
+
notify.success(
|
|
471
|
+
'Cambios guardados',
|
|
472
|
+
null,
|
|
473
|
+
{
|
|
474
|
+
timer: 1500 // Desaparece automáticamente
|
|
475
|
+
}
|
|
476
|
+
);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
notify.closeLoading();
|
|
479
|
+
notify.warning(
|
|
480
|
+
'No se pudieron guardar los cambios',
|
|
481
|
+
'Error de Guardado'
|
|
482
|
+
);
|
|
483
|
+
} finally {
|
|
484
|
+
isSaving = false;
|
|
485
|
+
}
|
|
486
|
+
}, 3000); // Guardar 3 segundos después del último cambio
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Uso
|
|
490
|
+
document.getElementById('editor').addEventListener('input', (e) => {
|
|
491
|
+
autoSaveDocument(e.target.value, 'doc-123');
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Con Timer Auto-cierre
|
|
496
|
+
|
|
497
|
+
Si necesitas que el loading se cierre automáticamente después de cierto tiempo:
|
|
498
|
+
|
|
499
|
+
```javascript
|
|
500
|
+
notify.loading('Procesando...', 'Espera', {
|
|
501
|
+
timer: 3000 // Auto-cerrar en 3 segundos
|
|
502
|
+
}).then(() => {
|
|
503
|
+
console.log('Loading cerrado automáticamente');
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
## Dark Mode y Temas
|
|
508
|
+
|
|
509
|
+
El sistema de notificaciones **detecta automáticamente** el tema activo en tu web y ajusta sus colores en consecuencia.
|
|
510
|
+
|
|
511
|
+
### Cómo Funciona
|
|
512
|
+
|
|
513
|
+
El sistema utiliza la clase `.dark` en el elemento `<html>` para determinar el tema actual:
|
|
514
|
+
|
|
515
|
+
```html
|
|
516
|
+
<!-- Modo Claro (default) -->
|
|
517
|
+
<html lang="es">
|
|
518
|
+
<!-- Notificaciones se muestran con fondo blanco y texto oscuro -->
|
|
519
|
+
</html>
|
|
520
|
+
|
|
521
|
+
<!-- Modo Oscuro -->
|
|
522
|
+
<html lang="es" class="dark">
|
|
523
|
+
<!-- Notificaciones se muestran con fondo oscuro y texto claro -->
|
|
524
|
+
</html>
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Integración con Tailwind CSS
|
|
528
|
+
|
|
529
|
+
Si usas Tailwind CSS con `darkMode: 'class'`, las notificaciones funcionarán automáticamente:
|
|
530
|
+
|
|
531
|
+
```html
|
|
532
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
533
|
+
<script>
|
|
534
|
+
tailwind.config = {
|
|
535
|
+
darkMode: 'class' // ← Configuración necesaria
|
|
536
|
+
}
|
|
537
|
+
</script>
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Implementar Toggle de Tema
|
|
541
|
+
|
|
542
|
+
Ejemplo de botón para cambiar entre modo claro y oscuro:
|
|
543
|
+
|
|
544
|
+
```javascript
|
|
545
|
+
const themeToggle = document.getElementById('theme-toggle');
|
|
546
|
+
|
|
547
|
+
themeToggle.addEventListener('click', () => {
|
|
548
|
+
const isDark = document.documentElement.classList.toggle('dark');
|
|
549
|
+
|
|
550
|
+
// Guardar preferencia del usuario
|
|
551
|
+
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
552
|
+
|
|
553
|
+
// Las notificaciones cambiarán automáticamente
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Cargar tema guardado al iniciar
|
|
557
|
+
const savedTheme = localStorage.getItem('theme');
|
|
558
|
+
if (savedTheme === 'dark') {
|
|
559
|
+
document.documentElement.classList.add('dark');
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Colores del Dark Mode
|
|
564
|
+
|
|
565
|
+
**Modo Claro:**
|
|
566
|
+
- Fondo del modal: `#ffffff` (blanco)
|
|
567
|
+
- Texto principal: `#111827` (gris muy oscuro)
|
|
568
|
+
- Texto secundario: `#6b7280` (gris medio)
|
|
569
|
+
- Overlay: `rgba(0, 0, 0, 0.4)` (negro semi-transparente)
|
|
570
|
+
|
|
571
|
+
**Modo Oscuro:**
|
|
572
|
+
- Fondo del modal: `#0f1724` (azul oscuro)
|
|
573
|
+
- Texto principal: `#e6eef8` (blanco-azulado)
|
|
574
|
+
- Texto secundario: `#cbd5e1` (gris claro)
|
|
575
|
+
- Overlay: `rgba(0, 0, 0, 0.6)` (negro más opaco)
|
|
576
|
+
- Inputs: `#0b1220` (azul muy oscuro)
|
|
577
|
+
|
|
578
|
+
### Ejemplo Completo con Dark Mode
|
|
579
|
+
|
|
580
|
+
```html
|
|
581
|
+
<!DOCTYPE html>
|
|
582
|
+
<html lang="es">
|
|
583
|
+
<head>
|
|
584
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
585
|
+
<script>
|
|
586
|
+
tailwind.config = { darkMode: 'class' }
|
|
587
|
+
</script>
|
|
588
|
+
</head>
|
|
589
|
+
<body class="bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100">
|
|
590
|
+
|
|
591
|
+
<!-- Botón de toggle -->
|
|
592
|
+
<button id="theme-toggle" class="p-2 rounded bg-gray-200 dark:bg-gray-700">
|
|
593
|
+
Cambiar Tema
|
|
594
|
+
</button>
|
|
595
|
+
|
|
596
|
+
<!-- Botón de notificación -->
|
|
597
|
+
<button onclick="notify.success('Tema aplicado correctamente')">
|
|
598
|
+
Mostrar Notificación
|
|
599
|
+
</button>
|
|
600
|
+
|
|
601
|
+
<!-- Scripts -->
|
|
602
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
|
|
603
|
+
<script src="notification-system.js"></script>
|
|
604
|
+
<script>
|
|
605
|
+
// Toggle de tema
|
|
606
|
+
document.getElementById('theme-toggle').addEventListener('click', () => {
|
|
607
|
+
document.documentElement.classList.toggle('dark');
|
|
608
|
+
});
|
|
609
|
+
</script>
|
|
610
|
+
</body>
|
|
611
|
+
</html>
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Sin Tailwind CSS
|
|
615
|
+
|
|
616
|
+
Si no usas Tailwind, solo necesitas agregar/quitar la clase `.dark` del `<html>`:
|
|
617
|
+
|
|
618
|
+
```javascript
|
|
619
|
+
// Activar modo oscuro
|
|
620
|
+
document.documentElement.classList.add('dark');
|
|
621
|
+
|
|
622
|
+
// Activar modo claro
|
|
623
|
+
document.documentElement.classList.remove('dark');
|
|
624
|
+
|
|
625
|
+
// Toggle
|
|
626
|
+
document.documentElement.classList.toggle('dark');
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Las notificaciones **siempre respetarán** el tema de tu web, ignorando la preferencia del sistema operativo del usuario.
|
|
630
|
+
|
|
631
|
+
### Prioridad de Temas
|
|
632
|
+
|
|
633
|
+
1. **Clase `.dark` en `<html>`** ← Máxima prioridad (tu web decide)
|
|
634
|
+
2. ~~Preferencia del sistema operativo~~ ← Ignorada intencionalmente
|
|
635
|
+
|
|
636
|
+
Esto asegura que los usuarios vean notificaciones consistentes con el tema que eligieron en tu aplicación web.
|
|
637
|
+
|
|
638
|
+
## Personalización de Tipos
|
|
639
|
+
|
|
640
|
+
Cada tipo tiene su propio estilo:
|
|
641
|
+
|
|
642
|
+
- **success** - Verde, para operaciones exitosas
|
|
643
|
+
- **error** - Rojo, para errores críticos
|
|
644
|
+
- **warning** - Amarillo, para advertencias
|
|
645
|
+
- **info** - Azul, para información general
|
|
646
|
+
|
|
647
|
+
## API Completa
|
|
648
|
+
|
|
649
|
+
### `notify.show(options)`
|
|
650
|
+
|
|
651
|
+
```javascript
|
|
652
|
+
{
|
|
653
|
+
type: 'success' | 'error' | 'warning' | 'info', // Requerido
|
|
654
|
+
title: 'Título', // Opcional
|
|
655
|
+
message: 'Mensaje detallado', // Requerido
|
|
656
|
+
buttonText: 'OK', // Opcional (default: 'OK')
|
|
657
|
+
timer: 5000, // Opcional (ms, null = sin timer)
|
|
658
|
+
onClose: () => { } // Opcional (callback)
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### `notify.loading(message, title?, options?)` (NEW) 🆕
|
|
663
|
+
|
|
664
|
+
Muestra un spinner para operaciones asincrónicas. Perfecto para cargas de datos, subidas de archivos, procesamiento de formularios, etc.
|
|
665
|
+
|
|
666
|
+
```javascript
|
|
667
|
+
// Uso básico
|
|
668
|
+
notify.loading('Cargando datos...');
|
|
669
|
+
|
|
670
|
+
// Con título y opciones
|
|
671
|
+
notify.loading(
|
|
672
|
+
'Procesando solicitud...',
|
|
673
|
+
'Por favor espera',
|
|
674
|
+
{
|
|
675
|
+
buttonText: 'OK',
|
|
676
|
+
timer: null, // null = sin auto-cerrar
|
|
677
|
+
onClose: () => { } // Callback opcional
|
|
678
|
+
}
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
// Con auto-cierre
|
|
682
|
+
notify.loading('Guardando...', null, {
|
|
683
|
+
timer: 3000, // Auto-cierra en 3 segundos
|
|
684
|
+
onClose: () => {
|
|
685
|
+
console.log('Loading completado');
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Parámetros:**
|
|
691
|
+
- `message` (string, requerido): Texto del loading
|
|
692
|
+
- `title` (string, opcional): Título del loading
|
|
693
|
+
- `options` (object, opcional): Configuración adicional
|
|
694
|
+
- `buttonText`: Texto del botón (default: 'OK')
|
|
695
|
+
- `timer`: Milisegundos antes de auto-cerrar (default: null)
|
|
696
|
+
- `onClose`: Función callback cuando se cierre
|
|
697
|
+
|
|
698
|
+
**Retorna:** Promise que se resuelve cuando se cierra
|
|
699
|
+
|
|
700
|
+
### `notify.closeLoading()`
|
|
701
|
+
|
|
702
|
+
Cierra el loading activo y muestra la siguiente notificación en la cola (si la hay).
|
|
703
|
+
|
|
704
|
+
```javascript
|
|
705
|
+
// En tu función async
|
|
706
|
+
notify.loading('Cargando datos...');
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
const data = await fetch('/api/data');
|
|
710
|
+
notify.closeLoading(); // Cerrar loading
|
|
711
|
+
notify.success('Datos cargados!');
|
|
712
|
+
} catch (error) {
|
|
713
|
+
notify.closeLoading(); // Cerrar loading
|
|
714
|
+
notify.error('Error en la carga');
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Métodos de Acceso Rápido
|
|
719
|
+
|
|
720
|
+
```javascript
|
|
721
|
+
notify.success(message, title?, options?)
|
|
722
|
+
notify.error(message, title?, options?)
|
|
723
|
+
notify.warning(message, title?, options?)
|
|
724
|
+
notify.info(message, title?, options?)
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Cerrar Programáticamente
|
|
728
|
+
|
|
729
|
+
```javascript
|
|
730
|
+
notify.close(); // Cierra la notificación actual
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
## Características Técnicas
|
|
734
|
+
|
|
735
|
+
- **Responsive**: Se adapta a móviles y tablets
|
|
736
|
+
- **Accessible**: Puede cerrarse con ESC o click en overlay
|
|
737
|
+
- **Solo una a la vez**: Cierra automáticamente la anterior
|
|
738
|
+
- **Animaciones suaves**: Entrada/salida con anime.js
|
|
739
|
+
- **Sin dependencias extras**: Solo requiere anime.js
|
|
740
|
+
- **Ligero**: ~10KB total
|
|
741
|
+
|
|
742
|
+
## Integración en Otros Proyectos
|
|
743
|
+
|
|
744
|
+
```html
|
|
745
|
+
<!-- Incluir Boxicons para los iconos -->
|
|
746
|
+
<link href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css" rel="stylesheet">
|
|
747
|
+
|
|
748
|
+
<!-- Incluir notification-system.js (anime.js se carga automáticamente) -->
|
|
749
|
+
<script src="https://cdn.jsdelivr.net/gh/Fernandocabal/fernotify@latest/dist/notification-system.js"></script>
|
|
750
|
+
|
|
751
|
+
<!-- Usar en tu código -->
|
|
752
|
+
<script>
|
|
753
|
+
notify.success('¡Sistema listo!');
|
|
754
|
+
</script>
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
¡Listo para usar!
|