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.
@@ -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!